Python实现微信订阅号开发

发布 : 2017-06-19 分类 : Python 浏览 :
1
此微信订阅号开发全程使用python语言进行开发

1.搭建服务

1
以web.py为网络框架,web.py提供了一种简单方法去制作一个网站

环境准备

1
2
3
4
5
6
7
(1).安装python2.7版本以上
(2).安装libxml2, libxslt, lxml库

pip install -i https://pypi.douban.com/simple/ lxml
pip install -i https://pypi.douban.com/simple/ web.py
pip install -i https://pypi.douban.com/simple/ libxml2
pip install -i https://pypi.douban.com/simple/ libxslt

main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# -*- coding: utf-8 -*-
# filename:main.py

# 导入web.py模块
import web

urls = (
'/wx','Handle',
)

class Handle(object):
def GET(self):
return "hello,this is a test"

if __name__ == '__main__':
app = web.application(urls,globals())
app.run()
1
2
如果出现"socket.error: No socket could be created"错误信息
可能为80端口号被占用,可能是没有权限

使用5002端口,启动Main.py

Markdown

Markdown

2.申请订阅号

Markdown

Markdown

Markdown

Markdown

Markdown

Markdown

3.开发者基本配置

Markdown

Markdown

Markdown

1
现在选择提交肯定是验证token失败,因为还需要完成代码逻辑。改动原先main.py文件,新增handle.py

main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# -*- coding: utf-8 -*-
# filename:main.py

# 导入web.py模块
import web
from handle import Handle

urls = (
'/wx','Handle',
)

# class Handle(object):
# def GET(self):
# return "hello,this is a test"

if __name__ == '__main__':
app = web.application(urls,globals())
app.run()

handle.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# -*- coding: utf-8 -*-
# filename: handle.py
# author: Haodi Wang

import hashlib
import web


class Handle(object):
def GET(self):
try:
data = web.input()
if len(data) == 0:
return "hello,this is handle view"
signature = data.signature
timestamp = data.timestamp
nonce = data.nonce
echostr = data.echostr
token = "sparsematrix"

list = [token, timestamp, nonce]
list.sort()
print "list:",list
sha1 = hashlib.sha1()
map(sha1.update, list)
hashcode = sha1.hexdigest()
print "hashcode",hashcode
print "handle/GET func: hashcode,signature:", hashcode, signature
if hashcode == signature:
return echostr
else:
return ""
except Exception, Argument:
return Argument
1
2
3
重新启动成功后(python main.py 80),点击提交按钮。
若提示"token验证失败", 请认真检查代码或网络链接等。
若token验证成功,会自动返回基本配置的主页面,点击启动按钮

Markdown

4.搭建一个安全稳定高效的公众号框架

Markdown

1
2
3
4
5
主要有三个部分:

负责业务逻辑部分的服务器,
负责对接微信API的API-Proxy服务器
以及唯一的AccessToken中控服务器

(1).AccessToken中控服务器:

1
2
3
负责: 提供主动刷新和被动刷新机制来刷新accessToken并存储(为了防止并发刷新,注意加并发锁),
提供给业务逻辑有效的accessToken。
优点: 避免业务逻辑方并发获取access_token,避免AccessToken互相覆盖,提高业务功能的稳定性。

(2).API-Proxy服务器:

1
2
3
负责:专一与微信API对接,不同的服务器可以负责对接不同的业务逻辑,更可进行调用频率、权限限制。
优点:某台API-proxy异常,还有其余服务器支持继续提供服务,提高稳定性,
避免直接暴漏内部接口,有效防止恶意攻击,提高安全性。

5.实现”你问我答”

目的

1
2
理解被动消息的含义
理解收\发消息机制

预实现功能

1
粉丝给公众号一条文本消息,公众号立马回复一条文本消息给粉丝,不需要通过公众平台网页操作

5.1.接受文本消息

1
2
3
粉丝给公众号发送的文本消息。官方wiki链接:消息管理/接收消息-接受普通消息
粉丝给公众号发送文本消息:"欢迎开启公众号开发者模式",在开发者后台,收到公众平台发送的xml
如下:(下文均隐藏了ToUserName 及 FromUserName 信息)
1
2
3
4
5
6
7
8
<xml>
<ToUserName><![CDATA[公众号]]></ToUserName>
<FromUserName><![CDATA[粉丝号]]></FromUserName>
<CreateTime>1460537339</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[欢迎开启公众号开发者模式]]></Content>
<MsgId>6272960105994287618</MsgId>
</xml>
1
2
3
4
createTime 是微信公众平台记录粉丝发送该消息的具体时间
text: 用于标记该xml 是文本消息,一般用于区别判断
欢迎开启公众号开发者模式: 说明该粉丝发给公众号的具体内容是欢迎开启公众号开发者模式
MsgId: 是公众平台为记录识别该消息的一个标记数值, 微信后台系统自动产生

5.2. 被动回复文本消息

1
2
3
4
5
6
7
即公众号给粉丝发送的文本消息,官方wiki链接: 消息管理/接收消息-被动回复消息
特别强调:
1) 被动回复消息,即发送被动响应消息,不同于客服消息接口
2) 它其实并不是一种接口,而是对微信服务器发过来消息的一次回复
3) 收到粉丝消息后不想或者不能5秒内回复时,需回复“success”字符串(下文详细介绍)
4) 客服接口在满足一定条件下随时调用
公众号想回复给粉丝一条文本消息,内容为“test”, 那么开发者发送给公众平台后台的xml 内容如下:
1
2
3
4
5
6
7
<xml>
<ToUserName><![CDATA[粉丝号]]></ToUserName>
<FromUserName><![CDATA[公众号]]></FromUserName>
<CreateTime>1460541339</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[test]]></Content>
</xml>
1
2
3
4
5
特别备注:
1)ToUserName(接受者)、FromUserName(发送者) 字段请实际填写。
2)createtime 只用于标记开发者回复消息的时间,微信后台发送此消息都是不受这个字段约束。
3)text : 用于标记 此次行为是发送文本消息 (当然可以是image/voice等类型)。
4)文本换行 ‘\n’。

5.3.回复success问题

1
2
3
4
5
6
7
查询官方wiki 开头强调: 假如服务器无法保证在五秒内处理回复,则必须回复"success"或者""(空串),否则微信后台会发起三次重试。
解释一下为何有这么奇怪的规定。
发起重试是微信后台为了尽可以保证粉丝发送的内容开发者均可以收到。
如果开发者不进行回复,微信后台没办法确认开发者已收到消息,只好重试。
真的是这样子吗?尝试一下收到消息后,不做任何回复。在日志中查看到微信后台发起了三次重试操作,日志截图如下:

三次重试后,依旧没有及时回复任何内容,系统自动在粉丝会话界面出现错误提示“该公众号暂时无法提供服务,请稍后再试”。

5.4.流程图

Markdown

代码实现

1
main.py文件不改变,handle.py 需要增加一下代码,增加新的文件receive.py, reply.py

main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# -*- coding: utf-8 -*-
# filename:main.py

# 导入web.py模块
import web
from handle import Handle

urls = (
'/wx', 'Handle',
)

if __name__ == '__main__':
app = web.application(urls, globals())
app.run()

handle.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# -*- coding: utf-8 -*-
# filename: handle.py
# author: Haodi Wang
import hashlib
import reply
import receive
import web


class Handle(object):
def POST(self):
try:
webData = web.data()
print "Handle Post webdata is ", webData # 后台打日志
recMsg = receive.parse_xml(webData)
if isinstance(recMsg, receive.Msg) and recMsg.MsgType == 'text':
toUser = recMsg.FromUserName
fromUser = recMsg.ToUserName
content = "test"
replyMsg = reply.TextMsg(toUser, fromUser, content)
return replyMsg.send()
else:
print "暂且不处理"
return "success"
except Exception, Argment:
return Argment

receive.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# -*- coding: utf-8 -*-
# filename: receive.py
# author: Haodi Wang
import xml.etree.ElementTree as ET

def parse_xml(web_data):
if len(web_data) == 0:
return None
xmlData = ET.fromstring(web_data)
msg_type = xmlData.find('MsgType').text
if msg_type == 'text':
return TextMsg(xmlData)
elif msg_type == 'image':
return ImageMsg(xmlData)

class Msg(object):
def __init__(self, xmlData):
self.ToUserName = xmlData.find('ToUserName').text
self.FromUserName = xmlData.find('FromUserName').text
self.CreateTime = xmlData.find('CreateTime').text
self.MsgType = xmlData.find('MsgType').text
self.MsgId = xmlData.find('MsgId').text

class TextMsg(Msg):
def __init__(self, xmlData):
Msg.__init__(self, xmlData)
self.Content = xmlData.find('Content').text.encode("utf-8")

class ImageMsg(Msg):
def __init__(self, xmlData):
Msg.__init__(self, xmlData)
self.PicUrl = xmlData.find('PicUrl').text
self.MediaId = xmlData.find('MediaId').text

reply.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# -*- coding:utf-8 -*-
# filename: reply.py
# author: Haodi Wang
import time


class Msg(object):
def __init__(self):
pass

def send(self):
return "success"


class TextMsg(Msg):
def __init__(self, toUserName, fromUserName, content):
self.__dict = dict()
self.__dict['ToUserName'] = toUserName
self.__dict['FromUserName'] = fromUserName
self.__dict['CreateTime'] = int(time.time())
self.__dict['Content'] = content

def send(self):
XmlForm = """
<xml>
<ToUserName><![CDATA[{ToUserName}]]></ToUserName>
<FromUserName><![CDATA[{FromUserName}]]></FromUserName>
<CreateTime>{CreateTime}</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[{Content}]]></Content>
</xml>
"""
return XmlForm.format(**self.__dict)


class ImageMsg(Msg):
def __init__(self, toUserName, fromUserName, mediaId):
self.__dict = dict()
self.__dict['ToUserName'] = toUserName
self.__dict['FromUserName'] = fromUserName
self.__dict['CreateTime'] = int(time.time())
self.__dict['MediaId'] = mediaId

def send(self):
XmlForm = """
<xml>
<ToUserName><![CDATA[{ToUserName}]]></ToUserName>
<FromUserName><![CDATA[{FromUserName}]]></FromUserName>
<CreateTime>{CreateTime}</CreateTime>
<MsgType><![CDATA[image]]></MsgType>
<Image>
<MediaId><![CDATA[{MediaId}]]></MediaId>
</Image>
</xml>
"""
return XmlForm.format(**self.__dict)

在线测试

微信在线测试的平台

1
2
3
4
5
6
微信公众平台有提供一个在线测试的平台,方便开发者模拟场景测试代码逻辑。

正如被动回复文本消息 交代此被动回复接口不同于客服接口,测试时也要注意区别。

在线测试目的在于测试开发者代码逻辑是否有误、是否符合预期。
即便测试成功也不会发送内容给粉丝。所以可以随意测试。

Markdown

Markdown

真实体验

1
拿出手机,微信扫描公众号二维码,成为自己公众号的第一个粉丝。公众号二维码位置如下图:

Markdown

Markdown

6.实现”图”尚往来

目的

1
2
(1).引入素材管理
(2).以文本消息,图片消息为基础,可自行理解剩余的语音消息、视频消息、地理消息等

预实现功能:

1
接受粉丝发送的图片消息,并立马回复相同的图片给粉丝

6.1.接收图片消息

1
即粉丝给公众号发送的图片消息。粉丝给公众号发送一张图片消息,在公众号开发者后台接收到的xml如下:
1
2
3
4
5
6
7
8
9
<xml>
<ToUserName><![CDATA[公众号]]></ToUserName>
<FromUserName><![CDATA[粉丝号]]></FromUserName>
<CreateTime>1460536575</CreateTime>
<MsgType><![CDATA[image]]></MsgType>
<PicUrl><![CDATA[http://mmbiz.qpic.cn/xxxxxx /0]]></PicUrl>
<MsgId>6272956824639273066</MsgId>
<MediaId><![CDATA[gyci5a-xxxxx-OL]]></MediaId>
</xml>

特别说明

1
2
PicUrl: 这个参数是微信系统把"粉丝"发送的图片消息自动转化成url。 这个url可用浏览器打开查看到图片。
MediaId: 是微信系统产生的id用于标记该图片

6.2.被动回复图片消息

1
即公众号给粉丝发送的图片消息

特别说明

1
2
3
4
5
(1).被动回复消息,即发送被动响应消息,不同于客服消息接口
(2).它其实并不是一种接口,而是对微信服务器发过来消息的一次回复
(3).收到粉丝消息后不想或者不能5秒内回复时,需回复"success"字符串
(4).客服接口在满足一定条件下随时调用
开发者发送给微信后台的xml 如下:
1
2
3
4
5
6
7
8
9
<xml>
<ToUserName><![CDATA[粉丝号]]></ToUserName>
<FromUserName><![CDATA[公众号]]></FromUserName>
<CreateTime>1460536576</CreateTime>
<MsgType><![CDATA[image]]></MsgType>
<Image>
<MediaId><![CDATA[gyci5oxxxxxxv3cOL]]></MediaId>
</Image>
</xml>
1
2
3
4
5
这里填写的MediaId的内容,其实就是粉丝的发送图片的原MediaId,所以粉丝收到了一张一模一样的原图。
如果想回复粉丝其它图片怎么呢?

(1).新增素材,请参考 新增临时素材 或者 新增永久素材
(2).获取其MediaId,请参考获取临时素材MediaID 或者 获取永久素材MediaID

流程图

Markdown

handle.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# -*- coding: utf-8 -*-
# filename: handle.py
# author: Haodi Wang
import hashlib
import reply
import receive
import web


class Handle(object):
def POST(self):
try:
webData = web.data()
print "Handle Post webdata is ", webData #后台打日志
recMsg = receive.parse_xml(webData)
if isinstance(recMsg, receive.Msg):
toUser = recMsg.FromUserName
fromUser = recMsg.ToUserName
if recMsg.MsgType == 'text':
content = "test"
replyMsg = reply.TextMsg(toUser, fromUser, content)
return replyMsg.send()
if recMsg.MsgType == 'image':
mediaId = recMsg.MediaId
replyMsg = reply.ImageMsg(toUser, fromUser, mediaId)
return replyMsg.send()
else:
return reply.Msg().send()
else:
print "暂且不处理"
return reply.Msg().send()
except Exception, Argment:
return Argment

7.AccessToken

7.1.查看appid及appsecret

1
2
3
4
5
6
7
公众平台官网查看, 其中AppSecret 不点击重置时候,则一直保持不变

access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。

开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。

access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。

f813e764eb892f1ef6e3e2b2ebc6bddb

7.2.获取accessToken

临时方法获取

1
为了方便先体验其他接口,可以临时通过 在线测试 或者 浏览器获取accessToken

Markdown

Markdown

7.3.接口获取

特别强调

1
2
(1).第三方需要一个access_token获取和刷新的中控服务器。
(2).并发获取access_token会导致AccessToken互相覆盖,影响具体的业务功能

basic.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# -*- coding: utf-8 -*-
# filename: basic.py
# Author: Haodi Wang
import urllib
import time
import json


class Basic:
def __init__(self):
self.__accessToken = ''
self.__leftTime = 0

def __real_get_access_token(self):
appId = "xxx"
appSecret = "xxx"

postUrl = ("https://api.weixin.qq.com/cgi-bin/token?grant_type="
"client_credential&appid=%s&secret=%s" % (appId, appSecret))
urlResp = urllib.urlopen(postUrl)
urlResp = json.loads(urlResp.read())

self.__accessToken = urlResp['access_token']
self.__leftTime = urlResp['expires_in']

def get_access_token(self):
if self.__leftTime < 10:
self.__real_get_access_token()
return self.__accessToken

def run(self):
while (True):
if self.__leftTime > 10:
time.sleep(2)
self.__leftTime -= 2
else:
self.__real_get_access_token()

8. 临时素材

1
2
3
4
5
6
7
8
9
10
11
公众号经常有需要用到一些临时性的多媒体素材的场景

例如在使用接口特别是发送消息时,对多媒体文件、多媒体消息的获取和调用等操作,是通过MediaID来进行的。

譬如实现"图"尚往来中,粉丝给公众号发送图片消息,便产生一临时素材。

因为永久素材有数量的限制,但是公众号又需要临时性使用一些素材,因而产生了临时素材。

这类素材不在微信公众平台后台长期存储,所以在公众平台官网的素材管理中查询不到,但是可以通过接口对其操作。

其他详情请以公众平台官网wiki介绍为依据。

8.1.新建临时素材

1
2
3
接口详情请依据wiki介绍。提供参考代码如何上传素材作为临时素材,供其它接口使用。

vim media.py 编写完成之后,直接运行media.py 即可上传临时素材。

media.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# -*- coding: utf-8 -*-
# filename: meida.py
from basic import Basic
import urllib2
import poster.encode
import json
from poster.streaminghttp import register_openers

class Media(object):
def __init__(self):
register_openers()
#上传图片
def uplaod(self, accessToken, filePath, mediaType):
openFile = open(filePath, "rb")
param = {'media': openFile}
postData, postHeaders = poster.encode.multipart_encode(param)

postUrl = "https://api.weixin.qq.com/cgi-bin/media/upload?access_token=%s&type=%s" % (accessToken, mediaType)
request = urllib2.Request(postUrl, postData, postHeaders)
urlResp = urllib2.urlopen(request)
print urlResp.read()

def get(self, accessToken, mediaId):
postUrl = "https://api.weixin.qq.com/cgi-bin/media/get?access_token=%s&media_id=%s" % (accessToken, mediaId)
urlResp = urllib2.urlopen(postUrl)

headers = urlResp.info().__dict__['headers']
if ('Content-Type: application/json\r\n' in headers) or ('Content-Type: text/plain\r\n' in headers):
jsonDict = json.loads(urlResp.read())
print jsonDict
else:
buffer = urlResp.read() # 素材的二进制
mediaFile = file("test_media.jpg", "wb")
mediaFile.write(buffer)
print "get successful"

if __name__ == '__main__':
myMedia = Media()
accessToken = Basic().get_access_token()
print "accessToken:",accessToken
filePath = "/data/agv机器人.png" #请安实际填写
mediaType = "image"
# myMedia.uplaod(accessToken, filePath, mediaType)
mediaId = "8iw9i7wp7gq2QdXgEeUbULLAbGgH-fpWIdFqvTPtHUPoaxmg6V9YeaCLVkXxomaV"
myMedia.get(accessToken, mediaId)

8.2.获取临时素材MediaID

1
2
3
临时素材的MediaID 没有提供特定的接口进行统一查询,因此有俩种方式
(1).通过接口上次的临时素材,在调用成功的情况下,从返回JSON数据中提取MediaID,可临时使用
(2).粉丝互动中的临时素材,可从xml 数据提取MediaID,可临时使用

8.3.下载临时素材

手工体验

1
2
开发者如何保存粉丝发送的图片呢?
接口文档:获取临时素材接口,为方便理解,从最简单浏览器获取素材的方法入手,根据实际情况,浏览器输入网址:

https://api.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID (自行替换数据)
ACCESS_TOKEN 如 “AccessToken”章节讲解
MEDIA_ID 如 图尚往来/接受图片消息xml中的MediaId 讲解
只要数据正确,则会下载图片到本地,如下图:

9.自定义菜单

目的

1
2
3
三个菜单栏,体验click、view、media_id 三种类型的菜单按钮

订阅号需要微信认证才可以,使用自定义菜单接口、个性化菜单接口

menu.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# -*- coding: utf-8 -*-
# filename: menu.py
# author: Haodi Wang
import urllib
from basic import Basic


class Menu(object):
def __init__(self):
pass

def create(self, postData, accessToken):
postUrl = "https://api.weixin.qq.com/cgi-bin/menu/create?access_token=%s" % accessToken
if isinstance(postData, unicode):
postData = postData.encode('utf-8')
urlResp = urllib.urlopen(url=postUrl, data=postData)
print urlResp.read()

def query(self, accessToken):
postUrl = "https://api.weixin.qq.com/cgi-bin/menu/get?access_token=%s" % accessToken
urlResp = urllib.urlopen(url=postUrl)
print urlResp.read()

def delete(self, accessToken):
postUrl = "https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=%s" % accessToken
urlResp = urllib.urlopen(url=postUrl)
print urlResp.read()

# 获取自定义菜单配置接口
def get_current_selfmenu_info(self, accessToken):
postUrl = "https://api.weixin.qq.com/cgi-bin/get_current_selfmenu_info?access_token=%s" % accessToken
urlResp = urllib.urlopen(url=postUrl)
print urlResp.read()


if __name__ == '__main__':
myMenu = Menu()
postJson = """
{
"button":
[
{
"type": "click",
"name": "开发指引",
"key": "mpGuide"
},
{
"name": "公众平台",
"sub_button":
[
{
"type": "view",
"name": "更新公告",
"url": "http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1418702138&token=&lang=zh_CN"
},
{
"type": "view",
"name": "接口权限说明",
"url": "http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1418702138&token=&lang=zh_CN"
},
{
"type": "view",
"name": "返回码说明",
"url": "http://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1433747234&token=&lang=zh_CN"
}
]
}
]
}
"""
accessToken = Basic().get_access_token()
print accessToken
# myMenu.delete(accessToken)
myMenu.create(postJson, accessToken)

使用图灵微信第三方平台接管公众号

Markdown

Markdown

本文作者 : Matrix
原文链接 : https://matrixsparse.github.io/2017/06/19/Python实现微信订阅号开发/
版权声明 : 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!

知识 & 情怀 | 二者兼得

微信扫一扫, 向我投食

微信扫一扫, 向我投食

支付宝扫一扫, 向我投食

支付宝扫一扫, 向我投食

留下足迹