# 微信公众号开发
# 前期准备
# 具有公网IP的服务器
只有具有公网IP的服务器,微信服务器才能与其建立连接,作为中介转发请求。同时为具有公网IP的服务器绑定域名,该域名随后会作为参数填入微信公众号的后台。
# 申请微信公众号
微信公众号网址: https://mp.weixin.qq.com 我使用的是未认证的订阅号。
# 配置应用服务器与数据库
# 应用服务器
微信公众号开发只能使用80或443端口,也就是我们常说的http或https,但是我部署的博客也使用的这个端口。不过公众号配置的时候,url路径可以进行灵活的配置。这样,我们就可以配置代理服务器,根据不同的路径,将请求转发到相应的应用服务器上。 本节以nginx + uwsgi + supervisor为例,进行介绍。其中nginx为代理服务器,uwsgi为应用服务器,supervisor为进程管理程序。
# nginx
关于nginx的安装,可以在博客中搜索关键字"nginx"。
nginx的配置,主要是添加一个新的访问路径,进行对应请求的转发:
location /mpserver/ {
include uwsgi_params;
uwsgi_pass 127.0.0.1:9090;
}
有关nginx的配置,参考自如下链接:
关于nginx,建议引入https,提升传输数据的安全性。关于https的配置,可以采用certbot进行自动化配置。具体过程可以在博客内搜索关键词"certbot",或者自行参考网上资料。
# uwsgi
uwsgi的安装,可以通过pip进行。首先进入虚拟环境,然后:
pip install uwsgi
uwsgi,作为应用服务器,其主要功能就是将相关请求转发到对应的处理逻辑上,并将结果返回,配置文件可以放在/var/www/mpserver/下,并命名为uwsgi.ini:
[uwsgi]
socket = 127.0.0.1:9090
chdir = /var/www/mpserver
virtualenv = /var/www/mpserver/venv/
manage-script-name
mount = /mpserver=mpserver:app
processes = 4
threads = 2
由于引入了虚拟环境,所以需要添加virtualenv这个配置选项。为每一个项目创建一个虚拟环境,有利于进行项目的包管理。
此处: nginx中的uwsgi_pass参数,要和uwsgi中的socket保持一致; chdir为公众号项目的根目录; mount = /mpserver=mpserver:app,/mpserver为挂载的路径,对应nginx的location,mpserver:app中的mpserver为flask的启动文件名称,app为flask启动文件中初始化的Flask对象名称,例如:app = Flask(name)。此处需要结合flask的官方文档,新建一个最小的测试demo,用于验证配置的正确性。
有关uwsgi的配置,参考自如下链接:
- uwsgi官方文档 (opens new window)
- flask官方文档 (opens new window) 截止目前,flask官方文档服务器还没有引入合适的https,所以如果flask官方文档点击以后出现错误,请自行输入以下链接进行访问:
http://flask.pocoo.org/docs/0.12/deploying/uwsgi/#starting-your-app-with-uwsgi
# supervisor
supervisor的安装,可以通过pip进行。首先进入虚拟环境,然后:
pip install supervisor
supervisor作为进程管理程序,主要用于监控uwsgi的运行,如果uwsgi意外退出,可以自动启动uwsgi,配置文件可以放置在/var/www/mpserver/venv/下,并命名为supervisord.conf:
[program:mpserver]
command = /var/www/mpserver/venv/bin/uwsgi --ini /var/www/mpserver/uwsgi.ini
autorestart = true
stopasgroup = true
redirect_stderr=true
stdout_logfile=/tmp/mpserver.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10
由于我们之前配置了uwsgi,每次启动4个进程,所以在使用supervisor进行管理的时候,要设置stopasgroup为true。这样在终止supervisor进程的时候,四个uwsgi子进程都可以被终止,否则只会有一个子进程被终止。
此时,我们只能通过指定supervisor的路径进行启动,所以,还需要将其放入systemctl系统服务中,这样,就可以通过sudo systemctl start/stop/restart supervisor对supervisor进行控制。
自定义服务配置可以通过github进行下载,然后将其放入/lib/systemd/system/下,并命名为supervisor.service。对自定义服务配置文件的编辑如下:
# supervisord service for systemd (CentOS 7.0+)
# by ET-CS (https://github.com/ET-CS)
[Unit]
Description=Supervisor daemon
[Service]
Type=forking
ExecStart=/var/www/mpserver/venv/bin/supervisord
ExecStop=/var/www/mpserver/venv/bin/supervisorctl $OPTIONS shutdown
ExecReload=/var/www/mpserver/venv/bin/supervisorctl $OPTIONS reload
KillMode=process
Restart=on-failure
RestartSec=42s
[Install]
WantedBy=multi-user.target
此时,建议输入 sudo systemctl daemon-reload 重新加载一遍所有的配置文件 如果输入 sudo systemctl start supervisor 可以启动supervisor,则配置无误。此时可以通过sudo systemctl enable supervisor设置supervisor为开机自动启动。 然后,继续输入ps -aux | grep uwsgi查看,是否有4个uwsgi的进程,如果有,则输入sudo killall uwsgi,然后再次通过ps -aux | grep uwsgi查看是否有4个uwsgi进程,如果有,则配置完成。出错的话,重点盘查supervisord.conf和uwsgi.ini文件。
有关supervisor的配置,参考自如下链接:
- supervisor官方文档 (opens new window)
- supervisor配置为systemctl服务 (opens new window)
- CSDN博客-supervisor-1 (opens new window)
- CSDN博客-supervisor-2 (opens new window)
- CSDN博客-/lib与/usr/lib (opens new window)
- 阮一峰的网络日志-systemctl相关命令 (opens new window) 截至目前,“supervisor官方文档”服务器与“阮一峰的网络日志-systemctl相关命令”服务器都没有引入合适的https,如果点击以上文字出现错误,请自行输入以下网址:
http://www.supervisord.org/
http://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-commands.html
# 数据库
关于数据库的配置,除了安装,就是为项目新建数据库用户,避免直接使用root账户连接数据库。
安装的话,可以通过sudo yum install mariadb-server mariadb-client。执行该命令前,记得提前配置好mariadb的仓库。
安装结束以后,输入mysql -u root -p,以root账户登录mariadb; 执行以下命令,新建数据库用户:
create user 'username'@'%' identified by 'password';
grant all on mpserver.* to 'username'@'%';
以上步骤新建了用户username,其密码为password,能够操作的数据库为mpserver。
关于数据库的安装和配置,可以参考如下链接:
# 框架引入
注意:所有的pip操作都要提前进入虚拟环境。
# 安装
通过pip安装flask,flask-restplus,flask-sqlalchemy,pymysql,werobot:
pip install flask flask-restplus flask-sqlalchemy pymysql werobot
# 配置
在项目文件夹根目录新建两个空文件,如mpserver.py、config.py;然后在根目录下新建文件夹,如mpcode;然后在mpcode文件夹中,分别新建空文件__init__.py、models.py、weapis.py、robot.py。
mpserver.py内容如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from flask import Flask
from mpcode.robot import myrobot
from mpcode.weapis import api
from mpcode.models import db
from werobot.contrib.flask import make_view
app = Flask(__name__)
# 配置flask-sqlalchemy使用pymysql作为连接数据库的中间件
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://user:[email protected]:3306/dbname'
# 将flask-sqlalchemy与flask整合
db.init_app(app)
# 在数据库创建models中定义的表
db.create_all(app = app)
# 将werobot与flask整合
app.add_url_rule(
rule = '/',
endpoint = 'werobot',
view_func = make_view(myrobot),
methods = ['GET', 'POST'],
)
api.init_app(app)
config.py内容如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
TOKEN = 'yourtoken'
APP_ID = 'yourappid'
APP_SECRET = 'yourappsecret'
ENCODING_AES_KEY = 'yourencodingaeskey'
ENABLE_SESSION = False
SESSION_STORAGE = False
models.py内容如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
weapis.py内容如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from flask_restplus import Api, Resource
api = Api(doc = False)
robot.py内容如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from werobot import WeRoBot
myrobot = WeRoBot()
myrobot.config.from_pyfile('config.py')
# myrobot.add_filter(func=article, rules=[re.compile(u"[A-Za-z]|[0-9]|[\u4e00-\u9fa5]|[\uFB00-\uFFFD]")])
至此,几个框架已经得到整合,只需要按照需求在对应的文件中填充代码即可。
关于框架的整合,包括公众号相关参数的填写参考自以下文档:
- Flask官方文档 (opens new window)
- Flask-Restplus官方文档 (opens new window)
- Flask-SQLAlchemy官方文档 (opens new window)
- PyMySQL官方文档 (opens new window)
- WeRoBot官方文档 (opens new window)
- 微信公众号官方文档 (opens new window) 截至目前,“Flask官方文档”服务器与“Flask-SQLAlchemy官方文档”服务器都没有引入合适的https,如果点击以上文字出现错误,请自行输入以下网址:
http://flask.pocoo.org/
http://flask-sqlalchemy.pocoo.org
# 相关知识
# HTTP常用请求类型
未认证的订阅号,开放的接口有限,本文主要利用的是公众号被动回复的功能。这个在微信官方文档-消息管理-被动回复用户消息中提到,“严格来说,发送被动响应消息其实并不是一种接口,而是对微信服务器发过来消息的一次回复”。所以,对于只利用到被动消息功能的应用,发现数据库中没有写入accesstoken是正常的,因为调用接口的时候才会进入获取accesstoken并保存的逻辑。
本公众号的多数功能都是基于接口。用户在公众号内主动发送消息,微信服务器将消息转发给自建服务器,自建服务器根据过滤条件,触发不同的处理逻辑,处理逻辑通过参数向相关的接口发送请求,然后将返回值返回给微信服务器,最后由微信服务器将消息回复给用户。
因此,有必要了解常用的几种HTTP请求类型。本文只介绍四种请求类型,分别为get、post、put、delete,其含义如下: get:获取 post:提交 put:更新 delete:删除
关于HTTP请求的具体信息,参考如下链接:
# python decorator
在执行某些函数之前,都要执行某段代码,无疑增加了整个项目的冗余,而且对于提前执行的代码段,维护量也比较大、比较繁琐。但是python的装饰器可以很好的解决这个问题。对于需要提前执行的代码段,抽离出来,形成装饰器。然后在每一个需要执行它的函数之前,通过@“装饰器”的方法实现提前执行特定代码段的目的。
decorator的关键就是将A函数作为参数传入B函数,在B函数中调用A函数。
示例代码如下:
def tagcheck(func):
def wrapper(args):
url4p = "https://yoururl/weapis/user/" + weapitoken
url4g = url4p + '?uid=' + args.source
r = requests.get(url4g).json()
if (r['tag'] > 0):
r['tag'] += 1
r = requests.put(url4p, json = {'uid': args.source, 'count': r['count'], 'tag': r['tag']})
else:
return func(args)
return wrapper
def apiauth(func):
def wrapper(*args, **kwargs):
if (kwargs['weapitoken'] == weapistoken):
return func(*args, **kwargs)
else:
return {'code': '403'}
return wrapper
# 使用方法:
@tagcheck
funcA(argA)
@apiauth
funcB(argb, argc)
其中,tagcheck传入的func参数即为需要在tagcheck中调用的函数,本例为funcA;wrapper传入的args参数即为funcA要接收的参数argA。 apiauth说明了如果函数的参数个数不固定,应该如何写对应的装饰器。此处需要注意*args与**kwagrs的区别。*args表示任意多个无名参数,是一个tuple,**kwargs表示有关键字的参数,是一个dictionary。
关于python的装饰器,可以参考以下链接:
- CSDN博客-*args与**kwargs (opens new window)
- 廖雪峰python教程-装饰器 (opens new window)
- 博客园文章 (opens new window)
# 功能点示例
# 事件过滤
代码示例:
# 当用户关注公众号以后,会触发一个subscribe事件
# 然后,用户会在公众号内收到“欢迎关注”的消息
@myrobot.subscribe
def subscribe(message):
return "欢迎关注"
# 关键词过滤
代码示例:
@tagcheck
def count(message):
url4p = "https://yoururl/weapis/user/" + weapitoken
url4g = url4p + '?uid=' + message.source
r = requests.get(url4g).json()
if (r['uid'] == 'null'):
r = requests.post(url4p, json = {'uid': message.source}).json()
return "你在Yunerself已留言%s次,有你的回应,我很幸福!" % r['count']
# 触发关键词为:C, c, count
myrobot.add_filter(func=count, rules=["C", "c", "count"])
在我的公众号内,有一个功能是统计用户在公众号里面留言的次数,而这个功能的实现就是依赖于上面的函数。函数将用户的OpenID(message.source)作为参数,向接口请求该用户的留言次数。tagcheck用于检查用户是否处于留言模式,当用户进入留言模式以后,count对应的触发关键词会当做留言内容,不会执行count函数。
对于werobot,基于文字的过滤有一个filter装饰器,例:
@robot.filter("a")
def a():
return "正文为 a "
# 如果用户在公众号内输入“a”,则会收到“正文为a”的回复
当我需要在函数内部调用message参数的时候,使用filter始终获取不到值,即使是在a()中传入参数名message,但是通过上面的add_filter即可。
# 获取语音识别内容
代码示例:
@myrobot.voice
def voicedispatch(message):
# 公众号开启语音识别以后,微信服务器会将识别结果一并传入自建服务器
# 但是微信的识别结果最后会加一个标点符号,貌似还能基于语义来判断
# “哈喽、只识别中文啊、哎呦”,这些词,都会加感叹号!,而“微信、帮助、我”,这些词,都会加句号。
# 因此,我们需要将最后的标点符号位截取掉,然后进行匹配
keyword = message.recognition[:-1]
if (keyword == u"简介"):
return about(message)
elif (keyword == u"统计"):
return count(message)
elif (keyword == u"帮助"):
return help(message)
elif (keyword == u"魔法"):
return magic(message)
elif (keyword == u"结束留言"):
return over(message)
elif (keyword == u"我要留言"):
return wish(message)
else:
return article(message)
关于语音识别的处理,重点关注上面注释提及的部分即可。貌似微信的语音识别只支持中文和数字,但是也没有在官方文档中看到明确的内容。
我的项目源码已经上传到了github,但是对部分细节做了处理,隐藏了密码、路径之类的信息,部署测试的时候请注意将其修改为正确的内容。