优雅地处理机器人指令

为了将内容维护工作转接到通过向 Bot 下达指令完成,一开始打算准备采用 Hubot 作为中转机器人,后来因为安装过程中的种种不适以及对 CoffeeScript 的莫名恐惧,最终放弃这一方案。其实之前分别基于 Flask 和 Tornado 实现过 Telegram 和微信公众号的简单自动回复机器人,其中 API 响应环节一般都是非常简单明确的,比较乱的反而是对指令的解析。例如:

def parse(cmd):
    cmds = cmd.split(' ')
    if cmds[0] == 'share':
        print("Share {} to Hub".format(cmds[1]))
    elif cmds[0] == 'update':        
        if cmds[1] == 'title':
            print("Update title of {} as {}".format(cmds[2], cmds[3]))
    elif cmds[0] == 'unpublic':
        print("Unpublic link {}".format(cmds[1]))
    else:
        print("Commands Help.")

cmd1 = "share https://pyhub.cc"
cmd2 = "update title https://pyhub.cc Python头条"
cmd3 = "unpublic https://pyhub.cc"

parse(cmd1)
parse(cmd2)
parse(cmd3)

"""
Share https://pyhub.cc to Hub
Update title of https://pyhub.cc as Python头条
Unpublic link https://pyhub.cc
"""

这其中还省略了很多指令准确性的检查,随着指令数量、形式的增加,可读性、可维护性都会变得非常糟糕。刚好在查看 Hubot 文档的时候找到了一个 Python 版本的 slackbot,采用修饰器和正则作为指令解析、分配的方法,看起来非常简洁:

from slackbot.bot import respond_to
import re
@respond_to('hi', re.IGNORECASE)
def hi(message):
    message.reply('I can understand hi or HI!')
    # react with thumb up emoji
    message.react('+1')
@respond_to('Give me (.*)')
def giveme(message, something):
    message.reply('Here is {}'.format(something))

根据这一思路,写了一个简单的指令处理工具:

import reclass Jarvis(object):
    dispatcher = {}
    max_patt_len = 0

    @classmethod
    def cmd(cls, pattern, flags=0):
        def wrapper(func):
            cls.max_patt_len = max(cls.max_patt_len, len(pattern))
            cls.dispatcher[pattern] = (func, re.compile(pattern, flags))
            return func
        return wrapper
    def resolve(self, msg):
        for pattern in self.dispatcher:
            m = self.dispatcher[pattern][1].match(msg)
            if m:                
                return self.dispatcher[pattern][0](self, *(m.groups()))
                break
        else:            
            return self.help()

通过 dispatcher 记录每条指令绑定的 handler 函数,这样做的还可以同时将几个同义指令绑定到同一个 handler 函数上:

class M(Javis):
  @Javis.cmd(r'hello')
  @Javis.cmd(r'hi')
  def hello(self):
    return "Hello"m = M()
print(m.resolve('hi'))
print(m.resolve('hello'))

为了方便查看全部指令,还可以根据 handler 函数的文档或函数明自动生成所有指令的说明文档:

def help(self):
  body = ""
    for patt in sorted(self.dispatcher.keys()):
      body += "{:.<{max_patt_len}}{}\n".format(patt,
        self.dispatcher[patt][0].__doc__ or self.dispatcher[patt][0].__name__, max_patt_len=self.max_patt_len+4)
  return "\nCommands:\n{0}\n{1}{0}\n".format("-"*40, body)

class M(Jarvis):
  @Jarvis.cmd(r'hello')
  @Jarvis.cmd(r'hi')
  def hello(self):
    return "Hello"

  @Jarvis.cmd(r'del (.*)')
  def delete(self, lid):
    """根据lid删除记录"""
    return "Record {} Deleted!".format(lid)
print(m.help())
"""
Commands:
----------------------------------------
del (.*)....根据lid删除记录
hello.......hello
hi..........hello
----------------------------------------
"""