登录
  • 人们都希望被别人需要 却往往事与愿违
  • 优秀软件的作用是让复杂的东西看起来简单@Grady Booch (UML创始人之一)

[Telegram bot 系列]1:requests库、Inline Keyboard、Reply Keyboard与其他细节

编程 Benny小土豆 14239次浏览 8238字 8个评论
文章目录[显示]
这篇文章在 2019年10月31日20:06:58 更新了哦~

又到了一年一度写“新年首博”的时候!很高兴,本博客依然坚挺,正努力向着【三周年】庆典一天天地迈进。

关于俺近期的静默

最近一个多月,俺确实比较忙(在前几篇博文中也多次提及这点),导致博客更新频率下降。在此,先向各位读者表示抱歉 :(

为了避免大伙儿无谓的担心,俺尽量把静默的时间跨度控制在【14天】之内,以表明俺自己是安全的(这是前几年在博客中做的约定)。

上一篇博文是在2017年最后几天发出的,昨天(1月31日)俺特地上博客回复了一些评论——所以这次静默的时间【没有】超过14天。

小土豆机器人课程又开课啦

自从三个月前发出第一篇《[Telegram bot 系列]0:用 Python 写一个 Telegram Bot 简单的回话 bot》,这一系列就好像是史上最不受待见的系列一样:没人看,也没有啥想写的欲望,更重要的是也没时间。

于是乎,随着ExpressBot越来越好玩,趁着这几天有时间,我决定给这一系列注入新鲜的血液,再次开课水贴,说不定本系列就这么短命的被我在此篇中终结啦♪(^∇^*)。

机器人

几天前被告知我的机器人上了少数派:《查快递、收邮件、订阅 RSS……除了聊天 Telegram 还有这些实用功能》, 然后用户数量大增,导致被快递100封IP,然后……这几天抽出来时间,悄悄的给机器人加了一些改进限制。

内联键盘(Inline Keyboard)与回复键盘(Reply Keyboard)

用过BotFather的人可能记得,创建机器人的时候我们能看到这样的按钮:

[Telegram bot 系列]1:requests库、Inline Keyboard、Reply Keyboard与其他细节

咱可以通过点击按钮来和服务器进行交互,有些按钮还可以是链接:

[Telegram bot 系列]1:requests库、Inline Keyboard、Reply Keyboard与其他细节

上图这种方式我们称之为Inline Keyboard。点击Inline Keyboard的按钮不会造成客户端向机器人发送消息

还有一种与Inline Keyboard比较接近的是custom Reply Keyboard,像下图这样:

[Telegram bot 系列]1:requests库、Inline Keyboard、Reply Keyboard与其他细节

用户点击文本编辑框下面的按钮,客户端就会自动为我们发送对应的消息,实际上Reply Keyboard更像是快捷回复。

这两种keyboard非常适合与用户进行交互(比如说游戏),并且用户不用一次性完整的输入命令、服务端一次性完成解析命令。

观察仔细的小伙伴们可能发现,ExpressBot 的查询美剧功能突然变得更好用一些了。

[Telegram bot 系列]1:requests库、Inline Keyboard、Reply Keyboard与其他细节

Reply Keyboard数据结构及简单示例

数据结构

[Telegram bot 系列]1:requests库、Inline Keyboard、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_messagereply_markup中传递markup实例。如果需要点击一次就隐藏的键盘(但是依旧可以点击按钮显示),那么可以给markup构造时加入one_time_keyboard=True的参数。

关于row_width的注意事项:
对于ReplyKeyboard和下面即将提到的InlineKeyboard来说,如果你的程序是通过循环动态生成的button,并且在循环中迭代调用markup.add(btn),那么最终只会一个按钮一排。这是因为markup.add(btn1,btn2,btn3)这三个才会被认为一组放到一排,而markup.add(btn)迭代三次会被认为是三组。在这种情况下如果想要遵循row_width的设置,那么需要将按钮append到一个列表里,然后对列表按照row_width进行切片,之后判断执行N个参数的markup.add()。详细的代码可以参考这段commit

[Telegram bot 系列]1:requests库、Inline Keyboard、Reply Keyboard与其他细节

如果我们需要点击按钮分享电话号码、地理位置,那么可以构造按钮的时候加入对应的参数,如 itembtn1 = types.KeyboardButton('a', request_contact=True),此时点击按钮,会有类似如下的提示

[Telegram bot 系列]1:requests库、Inline Keyboard、Reply Keyboard与其他细节

简单示例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)

[Telegram bot 系列]1:requests库、Inline Keyboard、Reply Keyboard与其他细节

Inline Keyboard之数据结构

[Telegram bot 系列]1:requests库、Inline Keyboard、Reply Keyboard与其他细节

我猜大家也都能看懂。InlineKeyboardMarkup的构造比较简单,InlineKeyboardButton的参数要稍微复杂一点,其中比较重要的有:

  • text 按钮上显示的文字
  • url 点击按钮时打开的url
  • callback_data 回调数据,1-64字节。

Inline Keyboard之CallbackQuery与answerCallbackQuery

由于点击InlineKeyboard时并不是发送常规的命令(/start),所以我们需要处理Callback,Telegram使用CallbackQuery来处理此类请求,其数据结构如下:

[Telegram bot 系列]1:requests库、Inline Keyboard、Reply Keyboard与其他细节

其中比较重要的有:

  • id 唯一标识此次按钮点击
  • message 触发此次callback的消息,其数据结构为Message类型,因此可以使用message.chat.id等方法
  • data callback_data中的字符串

当用户点击按钮时,按钮旁边会显示一个加载中的图标,此时建议使用answerCallbackQuery来给用户提示信息,其数据结构如下所示:

[Telegram bot 系列]1:requests库、Inline Keyboard、Reply Keyboard与其他细节

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所设置的值:

[Telegram bot 系列]1:requests库、Inline Keyboard、Reply Keyboard与其他细节

如果AnswerCallbackQuery开始了showAlert的话,那么会如下弹窗:

[Telegram bot 系列]1:requests库、Inline Keyboard、Reply Keyboard与其他细节

更新消息:编辑消息(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)

效果如下图:

[Telegram bot 系列]1:requests库、Inline Keyboard、Reply Keyboard与其他细节

需要注意的是,很多人最开始会想当然的给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

[Telegram bot 系列]1:requests库、Inline Keyboard、Reply Keyboard与其他细节

详细的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')

[Telegram bot 系列]1:requests库、Inline Keyboard、Reply Keyboard与其他细节

操作数据库

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 库,人类可以安全享用。

警告:非专业使用其他 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 '请求失败'

[Telegram bot 系列]1:requests库、Inline Keyboard、Reply Keyboard与其他细节

大概就是这么几行……

今天的教程就到这里了。本篇文章涉及到的代码可以在这里获取:

开源地址

最后

说太多也难以表达我的心情,可能已经彻底失去希望了吧。

梁惠王曰:“寡人之于国也,尽心焉耳矣。河内凶,则移其民于河东,移其粟于河内。河东凶亦然。察邻国之政,无如寡人之用心者。邻国之民不加少,寡人之民不加多,何也?”

孟子对曰:“王好战,请以战喻。填然鼓之,兵刃既接,弃甲曳兵而走。或百步而后止,或五十步而后止。以五十步笑百步,则何如?”

曰:“不可,直不百步耳,是亦走也。”

曰:“王如知此,则无望民之多于邻国也。不违农时,谷不可胜食也;数罟不入洿池,鱼鳖不可胜食也;斧斤以时入山林,材木不可胜用也。谷与鱼鳖不可胜食,材木不可胜用,是使民养生丧死无憾也。养生丧死无憾,王道之始也。五亩之宅,树之以桑,五十者可以衣帛矣;鸡豚狗彘之畜,无失其时,七十者可以食肉矣;百亩之田,勿夺其时,数口之家可以无饥矣;谨庠序之教,申之以孝悌之义,颁白者不负戴于道路矣。七十者衣帛食肉,黎民不饥不寒,然而不王者,未之有也。

狗彘食人食而不知检,涂有饿莩而不知发;人死,则曰:‘非我也,岁也。’是何异于刺人而杀之,曰:‘非我也,兵也。’王无罪岁,斯天下之民至焉。”


文章版权归原作者所有丨本站默认采用CC-BY-NC-SA 4.0协议进行授权|
转载必须包含本声明,并以超链接形式注明原作者和本文原始地址:
https://dmesg.app/tgbot1.html
喜欢 (25)
分享:-)
关于作者:
If you have any further questions, feel free to contact me in English or Chinese.
发表我的评论(代码和日志请使用Pastebin或Gist)
取消评论

                     

去你妹的实名制!

  • 昵称 (必填)
  • 邮箱 (必填,不要邮件提醒可以随便写)
  • 网址 (选填)
(8)个小伙伴在吐槽
  1. 链接多了一个"</span>",导致打开不正确哦....刚开始没注意看还以为项目改名了.看了url才发现这个问题.
    崔先森2021-07-23 00:05 回复
  2. 行王道,施仁政
    阿布2021-05-18 14:00 回复
  3. 小土豆,我写了一个py程序需要一直运行,我想时不时通过telegram bot问问运行状态, 你说是不是应该把一些重要的运行结果保存在数据库里面(我之前是保存在logging),然后写一个telegram bot来询问?还是有什么更简便的思路?运行结果不多,就是几个数
    少爷小老刘2018-06-26 00:10 回复
    • 结果不多的话存文件其实也是可以的,数据库也没问题啦。另外,一直运行可以考虑下systemd, supervisor
      Benny小土豆2018-06-27 16:08 回复
  4. 小土豆,我写了个微信公众号KiosSecPush 欢迎使用~
    Kios2018-02-06 14:08 回复
    • 咦~~~好玩吗,可以吃吗~!
      Benny小土豆2018-02-06 16:03 回复
  5. 来水一贴 :grin:
    Andy2018-02-02 12:30 回复
    • 签到成功!签到时间:2018-02-02 16:39:49,每日打卡,生活更精彩哦~
      Benny小土豆2018-02-02 16:39 回复