- 关于俺近期的静默
小土豆机器人课程又开课啦
机器人
内联键盘(Inline Keyboard)与回复键盘(Reply Keyboard)
Reply Keyboard数据结构及简单示例
Inline Keyboard之数据结构
Inline Keyboard之CallbackQuery与answerCallbackQuery
Inline Keyboard 及回调简单示例
更新消息:编辑消息(editMessageText)、删除消息(deleteMessage)与编辑markup(editMessageReplyMarkup)
格式化与正在输入的特效
操作数据库
集成requests库并解析json
最后
又到了一年一度写“新年首博”的时候!很高兴,本博客依然坚挺,正努力向着【三周年】庆典一天天地迈进。
关于俺近期的静默
最近一个多月,俺确实比较忙(在前几篇博文中也多次提及这点),导致博客更新频率下降。在此,先向各位读者表示抱歉
为了避免大伙儿无谓的担心,俺尽量把静默的时间跨度控制在【14天】之内,以表明俺自己是安全的(这是前几年在博客中做的约定)。
上一篇博文是在2017年最后几天发出的,昨天(1月31日)俺特地上博客回复了一些评论——所以这次静默的时间【没有】超过14天。
小土豆机器人课程又开课啦
自从三个月前发出第一篇《[Telegram bot 系列]0:用 Python 写一个 Telegram Bot 简单的回话 bot》,这一系列就好像是史上最不受待见的系列一样:没人看,也没有啥想写的欲望,更重要的是也没时间。
于是乎,随着ExpressBot越来越好玩,趁着这几天有时间,我决定给这一系列注入新鲜的血液,再次开课水贴,说不定本系列就这么短命的被我在此篇中终结啦♪(^∇^*)。
机器人
几天前被告知我的机器人上了少数派:《查快递、收邮件、订阅 RSS……除了聊天 Telegram 还有这些实用功能》, 然后用户数量大增,导致被快递100封IP,然后……这几天抽出来时间,悄悄的给机器人加了一些改进限制。
内联键盘(Inline Keyboard)与回复键盘(Reply Keyboard)
用过BotFather的人可能记得,创建机器人的时候我们能看到这样的按钮:
咱可以通过点击按钮来和服务器进行交互,有些按钮还可以是链接:
上图这种方式我们称之为Inline Keyboard。点击Inline Keyboard的按钮不会造成客户端向机器人发送消息
还有一种与Inline Keyboard比较接近的是custom Reply Keyboard,像下图这样:
用户点击文本编辑框下面的按钮,客户端就会自动为我们发送对应的消息,实际上Reply Keyboard更像是快捷回复。
这两种keyboard非常适合与用户进行交互(比如说游戏),并且用户不用一次性完整的输入命令、服务端一次性完成解析命令。
观察仔细的小伙伴们可能发现,ExpressBot 的查询美剧功能突然变得更好用一些了。
Reply Keyboard数据结构及简单示例
数据结构
图片截图自Telegram官网
我觉得大家都能看懂这是什么意思,那么就写一个简单的Reply Keyboard吧。
简单示例1:Reply Keyboard
为了应用Reply Keyboard,我们需要在send_xyz中给参数reply_markup传递我们自定义的键盘,其中reply_markup必须是ReplyKeyboardMarkup(自定义的键盘)、ReplyKeyboardRemove(移除键盘)或ForceReply(强制用户回复)的实例。
首先我们需要引入types
from telebot import types
之后直接开始写对应命令的代码就可以了:
@bot.message_handler(commands=['start']) def send_welcome(message): markup = types.ReplyKeyboardMarkup(row_width=2) itembtn1 = types.KeyboardButton('a') itembtn2 = types.KeyboardButton('v') itembtn3 = types.KeyboardButton('d') markup.add(itembtn1, itembtn2, itembtn3) bot.send_message(message.chat.id, "Choose one letter:", reply_markup=markup)
我们先定义了一个markup实例,设置行宽为2,然后添加avd三个按钮,之后在send_message
的reply_markup
中传递markup实例。如果需要点击一次就隐藏的键盘(但是依旧可以点击按钮显示),那么可以给markup构造时加入one_time_keyboard=True
的参数。
对于ReplyKeyboard和下面即将提到的InlineKeyboard来说,如果你的程序是通过循环动态生成的button,并且在循环中迭代调用
markup.add(btn)
,那么最终只会一个按钮一排。这是因为markup.add(btn1,btn2,btn3)
这三个才会被认为一组放到一排,而markup.add(btn)
迭代三次会被认为是三组。在这种情况下如果想要遵循row_width的设置,那么需要将按钮append到一个列表里,然后对列表按照row_width进行切片,之后判断执行N个参数的markup.add()
。详细的代码可以参考这段commit如果我们需要点击按钮分享电话号码、地理位置,那么可以构造按钮的时候加入对应的参数,如 itembtn1 = types.KeyboardButton('a', request_contact=True)
,此时点击按钮,会有类似如下的提示
简单示例2:ReplyKeyboardRemove
markup = types.ReplyKeyboardRemove(selective=False) tb.send_message(chat_id, message, reply_markup=markup)
发送这个markup会移除自定义键盘
简单示例3:ForceReply
ForceReply是强制回复,就好像用户回复机器人的消息一样,使用方法也很简单:
markup = types.ForceReply() bot.send_message(message.chat.id, "Choose one letter:", reply_markup=markup)
Inline Keyboard之数据结构
我猜大家也都能看懂。InlineKeyboardMarkup的构造比较简单,InlineKeyboardButton的参数要稍微复杂一点,其中比较重要的有:
- text 按钮上显示的文字
- url 点击按钮时打开的url
- callback_data 回调数据,1-64字节。
Inline Keyboard之CallbackQuery与answerCallbackQuery
由于点击InlineKeyboard时并不是发送常规的命令(/start),所以我们需要处理Callback,Telegram使用CallbackQuery来处理此类请求,其数据结构如下:
其中比较重要的有:
- id 唯一标识此次按钮点击
- message 触发此次callback的消息,其数据结构为Message类型,因此可以使用message.chat.id等方法
- data callback_data中的字符串
当用户点击按钮时,按钮旁边会显示一个加载中的图标,此时建议使用answerCallbackQuery来给用户提示信息,其数据结构如下所示:
Inline Keyboard 及回调简单示例
说了那么多数据结构可能会有点懵?,那么还是直接上代码更爽吧~
首先,我们需要像往常一样,创建InlineKeyboard实例,创建键盘实例,然后在send_xyz中把markup发送过去:
markup = types.InlineKeyboardMarkup() btn1 = types.InlineKeyboardButton('Google', url='https://www.google.com/ncr') btn2 = types.InlineKeyboardButton('Action', callback_data='Act Now') markup.add(btn1,btn2) bot.send_message(message.chat.id, "InlineKeyboard: ", reply_markup=markup)
按钮1是打开Google,按钮2需要我们处理回调,可以这么玩:
@bot.callback_query_handler(func=lambda call: True) def callback_handle(call): bot.answer_callback_query(call.id, '哎哟') bot.send_message(call.message.chat.id, 'You clicked %s' % call.data)
把这两段结合起来,运行的话效果大概是酱紫的,我们可以看到回调函数中call.data
就是我们在实例化InlineKeyboardButton中参数callback_data所设置的值:
如果AnswerCallbackQuery开始了showAlert的话,那么会如下弹窗:
更新消息:编辑消息(editMessageText)、删除消息(deleteMessage)与编辑markup(editMessageReplyMarkup)
我们知道,Telegram的消息是可以编辑、删除的,机器人也同样可以做到。当然了,reply_markup其实也可以被编辑的,我们就来简单的做一个示例吧。
说到编辑、删除消息,我们只需要chat_id和message_id就可以了,那么编辑、删除大概就可以这么写:
@bot.message_handler(commands=['start']) def edit_message(message): msg_id = bot.send_message(message.chat.id, '这里是原始消息').message_id time.sleep(2) # The below comment is wrong!!! # bot.edit_message_text('更新了耶', chat_id=message.chat.id, message_id=message.message_id) bot.edit_message_text('更新了耶', message.chat.id, msg_id) @bot.message_handler(commands=['help']) def delete_message(message): msg_id = bot.send_message(message.chat.id, '这里是要被删除的消息').message_id time.sleep(2) bot.delete_message(message.chat.id, msg_id)
效果如下图:
需要注意的是,很多人最开始会想当然的给message_id传递message_id参数,实际上这是错误的!如果你这么做,那么你会收到Bad Request: message can't be edited的异常。实际上,你所传递的message实例是用户发送过来的消息(包括/start /help等命令),所以获取到的message_id其实也是用户消息的id,机器人怎么可能编辑用户消息嘛!所以正确的姿势是去找send_message的返回值,通过返回值就可以获取到机器人发送的message_id了。
当然了,另外一种hack可以这么做,只要你确定好你要编辑的消息相对于用户消息的偏移量:
bot.edit_message_text('更新了耶', message.chat.id, message.message_id + 1)
还剩下一种editReplyMessageMarkup,这是用来更新reply_markup的,包括InlineKeyboard和ReplyKeyboard,基本逻辑就是构造新的markup,然后调用,以InlineKeyboard为例:
def edit_reply_markup(message): markup = types.InlineKeyboardMarkup() btn1 = types.InlineKeyboardButton('Apple', url='https://www.apple.com') btn2 = types.InlineKeyboardButton('Microsoft', url='https://www.microsoft.com') markup.add(btn1, btn2) # use hack bot.edit_message_reply_markup(message.chat.id, message.message_id - 1, reply_markup=markup)
这里引用了message_id计算偏移量,实际上我们有些时候会在callbackQuery中编辑InlineKeyboard,此时可以应用call.message.message_id
详细的API可以参考这里
格式化与正在输入的特效
有些时候,我们想让消息有加粗、斜体等效果,其实这是非常简单的,只需要在send_message时指定parse_mode即可。Telegram支持Markdown与HTML两种模式,具体的信息就参考官方文档吧。
如果想加入“正在输入”的特效,那就更简单了,使用send_action就可以了。示例代码如下:
bot.send_chat_action(message.chat.id, 'typing') bot.send_message(message.chat.id, '这个是_斜体_哦', parse_mode='Markdown') bot.send_chat_action(message.chat.id, 'record_video') bot.send_message(message.chat.id, "`print('hello world')`", parse_mode='Markdown')
操作数据库
Python支持很多很多数据库,对于关系型数据库来说,大部分库的设计都是遵循PEP 249 Python Database API Specification,所以咱的使用往往就是确定用哪种库,更改连接字符串,其他的基本不用改变太多。
Python内建sqlite的支持,所以在机器人这类应用中,用sqlite是非常方便的。
sqlite的使用方法也很简单,基本遵循如下步骤:
import sqlite3 # 使用内存型数据库,实际应用时应该指定文件路径 con = sqlite3.connect(':memory:') # 创建游标 cur = con.cursor() create_table = '''CREATE TABLE IF NOT EXISTS job( date DATETIME, done TINYINT) ''' # 执行查询 cur.execute(create_table) # 提交事务,否则不会写入数据库 con.commit() # 关闭连接 con.close()
防止注入的正确姿势
首先需要知道的是,拼接SQL命令是不可以的,这样很容易导致注入。:Python不像PHP、Java等存在名为PreparedStatement这类用法,Python防止注入的方法比较奇特,很简单,给cur.execute()
传入占位符和参数列表就可以了,具体来说(对于sqlite而言):
cursor.execute("SELECT spam FROM eggs WHERE lumberjack = ?", (lumberjack,))
对于MySQL而言,使用%s作为占位符(管他啥类型的数据,数字还是什么的,用%s就对了)
cursor.execute("SELECT spam FROM eggs WHERE lumberjack = %s", (lumberjack,))
更多详细信息可以参考StackOverflow: Python best practice and securest to connect to MySQL and execute queries
集成requests库并解析json
Requests 唯一的一个非转基因的 Python HTTP 库,人类可以安全享用。
现如今REST API非常流行。REST API说白了就是使用各种HTTP请求(GET,POST,PUT啊什么的),返回某种资源(大部分情况下是json),所以用requests是非常好的选择。
而json格式与Python的字典基本相同,简直是不要太方便。
requests的使用方法非常简单,差不多就是三行代码的事:
import requests r = requests.get('https://www.google.com/ncr') print r.text
如果需要POST方法,就把get换成post,如果需要更改headers等,就写作r = requests.get('https://www.google.com/ncr', headers={'User-Agent': 'Mozilla', })
更多的高级用法还是参考requests的官方手册吧
不如试试接入淘宝IP库,实现一个查IP地址的机器人吧?(当然了,这只是一段示例代码,其实还是有些bug的啦……)
@bot.message_handler(commands=['start']) def edit_message(message): if len(message.text.split(' ')) != 2: bot.send_chat_action(message.chat.id, 'typing') bot.send_message(message.chat.id, '参数错误') else: location = get_location(message.text.split(' ')[1]) bot.send_chat_action(message.chat.id, 'typing') bot.send_message(message.chat.id, u"你查询的IP地址:`%s`\n%s" % (message.text.split(' ')[1], location),parse_mode='Markdown') def get_location(ip): r = requests.get('http://ip.taobao.com/service/getIpInfo.php?ip=' + ip).text res = json.loads(r) if res.get('code') == 0: return res.get('data').get('region') + res.get('data').get('country') + res.get('data').get('isp') else: return '请求失败'
大概就是这么几行……
今天的教程就到这里了。本篇文章涉及到的代码可以在这里获取:
最后
说太多也难以表达我的心情,可能已经彻底失去希望了吧。
梁惠王曰:“寡人之于国也,尽心焉耳矣。河内凶,则移其民于河东,移其粟于河内。河东凶亦然。察邻国之政,无如寡人之用心者。邻国之民不加少,寡人之民不加多,何也?”
孟子对曰:“王好战,请以战喻。填然鼓之,兵刃既接,弃甲曳兵而走。或百步而后止,或五十步而后止。以五十步笑百步,则何如?”
曰:“不可,直不百步耳,是亦走也。”
曰:“王如知此,则无望民之多于邻国也。不违农时,谷不可胜食也;数罟不入洿池,鱼鳖不可胜食也;斧斤以时入山林,材木不可胜用也。谷与鱼鳖不可胜食,材木不可胜用,是使民养生丧死无憾也。养生丧死无憾,王道之始也。五亩之宅,树之以桑,五十者可以衣帛矣;鸡豚狗彘之畜,无失其时,七十者可以食肉矣;百亩之田,勿夺其时,数口之家可以无饥矣;谨庠序之教,申之以孝悌之义,颁白者不负戴于道路矣。七十者衣帛食肉,黎民不饥不寒,然而不王者,未之有也。
狗彘食人食而不知检,涂有饿莩而不知发;人死,则曰:‘非我也,岁也。’是何异于刺人而杀之,曰:‘非我也,兵也。’王无罪岁,斯天下之民至焉。”