Messense Lv

wechatpy 项目的前世今生

wechatpy 是什么?

wechatpy 项目是一个 Python 语言实现的微信公众号开发 SDK,代码托管在 GitHub 上:https://github.com/wechatpy/wechatpy

wechatpy 前世

wechatpy 是我 2014 年大学还没毕业在杭州一家使用 Python 开发的创业公司实习的时候创建的项目。当时微信公众号 Python SDK 应该也已经出现了好些个了,一开始公司要做微信公众号开发项目的时候,我选择的是当时网友 @whtsky 开发的 WeRobot 框架(之前也和 @whtsky 合作开发过 Catsup 项目)。

当时使用的 Django 框架,在使用 WeRobot 接入完被动响应功能之后,觉得我想要的微信公众号开发 SDK 更多的是一个库(library)而不是一个框架(framework),库和框架的主要区别我认为在于:比如实现同一个功能,库是用户调用库提供的 API,而框架则是框架调用用户提供的 API。就实现微信公众号被动响应功能来说,使用库的方式就已经足够简单了,框架当然也很简单但对维护框架的开发者来说需要为各种 Web 框架再做适配。

而之后接入主动调用接口的时候,因为微信开放的接口数量繁多,WeRobot 把所有的接口都组织在 werobot.Client 类下,我个人不是很喜欢这种做法,并且当时也看了 nameko 的代码,我很喜欢它的那种命名空间的 API 组织方式,便想尝试按自己的想法写一个微信公众号 Python SDK,由此 wechatpy 项目诞生。

wechatpy 接口设计

wechatpy 项目初期,复用了 WeRobot 的一些代码,非常感谢 @whtsky 和 WeRobot 项目贡献者(也曾在 WeRobot 项目发起过使用 wechatpy 来做解析消息等工作,WeRoBot 专注 robot 层面的东西 的提议,只是被否了)。除此之外,wechatpy 有着几个受到 Django ORM 和 nameko 影响的接口设计。

一、被动响应消息

微信被动响应消息的格式是 XML,解析本质上就是把 XML 里面的字段转换成 Python 的 dict,这个使用 xmltodict 库可以很方便地实现。但是直接使用 dict 代码中很难知道有哪些字段可以使用,要是有 schema 就好了,当时还没了解过 marshmallow,dataclasses 和 pydantic 都还没有面世,基于对 Django ORM model 实现代码的学习,设计了一套类似的机制。

定义了 BaseField 提供 from_xmlto_xml 抽象 XML 字段序列化和反序列化,再继承 BaseField 实现具体的字段类型,比如 StringField 代码如下:

class StringField(BaseField):
def __to_text(self, value):
return to_text(value)
converter = __to_text
def to_xml(self, value):
value = self.converter(value)
return f"<{self.name}><![CDATA[{value}]]></{self.name}>"
@classmethod
def from_xml(cls, value):
return value

f-strings 的用法是最近 @LKI 贡献的,:-)

有了这些字段类型的定义之后,就可以进行消息 model 的定义了,wechatpy 使用 Python 的 metaclass 特性实现了字段类型到数据的序列化和反序列化操作,具体可以参考 MessageMetaClass 代码。

被动响应消息的回复实现也很类似,在 MessageMetaClass 的基础上,BaseReply 增加了 render() 方法,调用字段的 to_xml 方法将回复 model 序列化为 XML,调用示例:

from wechatpy.replies import TextReply
reply = TextReply(content='text reply', message=msg)
# 或者
reply = TextReply(message=msg)
reply.content = 'text reply'
# 转换成 XML
xml = reply.render()

二、主动调用接口

主动调用接口主要是实现了接口的分组,比如主动消息接口都在 WeChatClient.message 实例下,调用方法如下:

from wechatpy import WeChatClient
client = WeChatClient('appid', 'secret')
# 发送图片消息
res = client.message.send_image('openid', 'media_id')
# 查询自定义菜单
menu = client.menu.get()

这种接口的设计也是基于 Python 的 metaclass 特性实现的,主要代码在 BaseWeChatClient 类的 __new__ 方法上。

import inspect
from wechatpy.client.api.base import BaseWeChatAPI
def _is_api_endpoint(obj):
return isinstance(obj, BaseWeChatAPI)
class BaseWeChatClient:
def __new__(cls, *args, **kwargs):
self = super().__new__(cls)
api_endpoints = inspect.getmembers(self, _is_api_endpoint)
for name, api in api_endpoints:
api_cls = type(api)
api = api_cls(self)
setattr(self, name, api)
return self
  1. 使用 inspect.getmembers 获取到类成员中的继承自 BaseWeChatAPI 的实例
  2. 通过 type 内置函数获取继承自 BaseWeChatAPI 的实例成员的类型后重新初始化并传入当前 WeChatClient 实例作为构造函数的参数,用 setattr 内置函数替换原来的成员

之后需要实现具体的微信主动调用接口,只需要集成 BaseWeChatAPI 并实例为 WeChatClient 的一个成员变量就行了,以菜单接口为例:

from wechatpy.client.base import BaseWeChatClient
from wechatpy.client.api.base import BaseWeChatAPI
class WeChatMenu(BaseWeChatAPI):
def get(self):
# 实现代码省略
class WeChatClient(BaseWeChatClient):
API_BASE_URL = "https://api.weixin.qq.com/cgi-bin/"
menu = WeChatMenu()

就可以愉快地使用 client.menu.get() 方法调用了。

wechatpy 今生

wechatpy 项目从创建到今天,已经获得了 2400 多 GitHub 星标,有 600 多 forks,活跃的代码贡献者 3~4 个(感谢!),QQ 群约 500 成员(经常清理不活跃的成员方便新人进群),依然是个比较小的项目。wechatpy 当前支持的微信开放平台功能有:

  1. 公众号平台被动响应和主动调用 API
  2. 企业微信 API
  3. 微信支付 API
  4. 第三方平台代公众号调用接口 API
  5. 小程序和小程序云开发 API

也有一些基于 wechatpy 做的周边项目,如:

  1. Odoo 的微信模块
  2. Odoo WeChat API
  3. flask-wechatpy

等。

wechatpy 未来

目前 wechatpy 项目已经在 2.0 版本的迭代中,最大的改变是放弃了 Python 2 的支持,并使用 Poetry 进行依赖管理。

非常欢迎参与 wechatpy 项目开发的讨论和代码贡献,谢谢。