最近用python寫了一個聊天機器人的微信公衆號,網上找的開發文檔參差不齊,官方文檔也比較老舊,還有部分小問題。於是,分享一下我的思路。
開發環境
windows sever 2008+python3.6+Flask
1.搭建服務器
服務器:隨便租個什麼服務器,或者用弄個能連外網的機子。
1.1 在服務器上裝python3.6開發環境,個人喜歡裝Anaconda,省得手動裝各種包。
1.2 新建一個test.py文件,寫如下代碼:
from flask import Flask, request, make_response
app = Flask(__name__)
app.debug = True
@app.route('/') # 默認網址
def index():
return '測試頁面'
if __name__ == '__main__':
app.run(host="0.0.0.0", port=80,debug=True)
打開終端,cd到目錄下,運行這個文件
python test.py
彈出 * Running on http://0.0.0.0:80/ (Press CTRL+C to quit) 說明成功了。
注意這裏的端口必須是80或443,微信公衆號只支持這兩個端口。
1.3 在另一臺機子上打開瀏覽器,輸入服務器的ip地址,彈出‘測試頁面’表示服務器搭建成功。
注意:使用阿里雲服務器顯示連接超時,需要配置安全組規則,具體方法百度。
2. 配置微信公衆號
2.1 首先當然是申請一個公衆號啦。注意:素材和菜單的開發權限必須要通過認證,未認證只能發發文字,具體可以去接口權限查看。
2.2 開發者基本配置
點擊修改配置後,選擇提交肯定是驗證token失敗,因爲還需要完成代碼邏輯。
2.3 改動 test.py 爲app.py.代碼如下:
# -*- coding: utf-8 -*-
# filename: app.py
from flask import Flask, request, make_response
import hashlib
import handle
app = Flask(__name__)
app.debug = True
@app.route('/') # 默認網址
def index():
return '測試頁面'
@app.route('/wx', methods=['GET', 'POST'])
def wechat_auth(): # 處理微信請求的處理函數,get方法用於認證,post方法取得微信轉發的數據
if request.method == 'GET':
token = 'xxxx' # 這裏填公衆號裏設置的token。
data = request.args
signature = data.get('signature', '')
timestamp = data.get('timestamp', '')
# print(timestamp)
nonce = data.get('nonce', '')
echostr = data.get('echostr', '')
s = [timestamp, nonce, token]
s.sort()
s = ''.join(s)
s = s.encode(encoding='utf-8')
#加密後的token匹配上signature
if (hashlib.sha1(s).hexdigest() == signature):
return make_response(echostr)
else:
return 'signature is error'
else:
rec = request.stream.read() # 接收消息
#print(rec)
dispatcher = handle.MsgHandler(rec)
data = dispatcher.dispatch()
with open("./debug.log", "a") as file:
file.write(data)
response = make_response(data)
response.content_type = 'application/xml'
return response #發送消息
if __name__ == '__main__':
app.run(host="0.0.0.0", port=80,debug=True)
這裏只用到wechat_auth函數中的get請求部分,else後面是post請求,暫且不管,import handle是用來處理消息的,先不要看。
運行app.py python app.py
2.4 運行app.py成功後,點擊公衆號界面的提交按鈕,token驗證成功即可。
3. 消息回覆流程
核心文件
app.py : 服務器主程序,接收和發送消息。
handle.py:消息處理樞紐。
receive.py:解析接收到的消息
reply.py:回覆消息。(可以添加複雜的回覆功能)
流程圖
有了以上幾個模塊就可以完成簡單的文字消息回覆了。
4.access_token
官方文檔:access_token是公衆號的全局唯一接口調用憑據,公衆號調用各接口時都需使用access_token。開發者需要進行妥善保存。access_token的存儲至少要保留512個字符空間。access_token的有效期目前爲2個小時,需定時刷新,重複獲取將導致上次獲取的access_token失效。
可以看到獲取一次access_token的有效期是2小時,那麼我們需要寫一個定時程序,每2小時獲取一次access_token。保存在文本文檔access_token.txt中。
basic.py :定時獲取access_token
# -*- coding: utf-8 -*-
# filename: basic.py
import urllib.request
import time
import json
class Basic():
def __init__(self):
self.__accessToken = ''
def __real_get_access_token(self):
appId = "xxxxx" #配置公衆號時得到的id和secret
appSecret = "xxxxxx"
postUrl = ("https://api.weixin.qq.com/cgi-bin/token?grant_type="
"client_credential&appid=%s&secret=%s" % (appId, appSecret))
urlResp = urllib.request.urlopen(postUrl)
urlResp = json.loads(urlResp.read())
self.__accessToken = urlResp['access_token']
def get_access_token(self):
self.__real_get_access_token()
return self.__accessToken
if __name__ == "__main__":
basic = Basic()
while True:
token = basic.get_access_token()
with open('access_token.txt','w',encoding='utf-8') as f:
f.write(token)
time.sleep(7000)
運行basic.py就能持續不斷的獲取access_token啦
5.臨時素材添加與獲取
如果你向公衆號發送語音、圖片、視頻等非文本消息,app.py只會接收到一個這樣的消息(消息模板),例如圖片消息:
<xml>
<ToUserName><![CDATA[接收人]]></ToUserName>
<FromUserName><![CDATA[發送人]]></FromUserName>
<CreateTime>創建時間</CreateTime>
<MsgType><![CDATA[image]]></MsgType>
<Image>
<MediaId><![CDATA[素材id]]></MediaId>
</Image>
</xml>
這樣一段消息經過receive.py的解析後,我們拿到“素材id”,然後向微信服務器發送一個請求,才能下載到對應的素材。
同理,如果公衆號向用戶發送圖片也需要先上傳圖片到微信服務器,服務器會返回一個“素材id”,然後公衆號再發送一段類似上面的消息,填入返回的”素材id“就可以了,其它類型消息模板可以去微信公衆號官方文檔查看。
media.py:素材上傳和下載。
# -*- coding: utf-8 -*-
# filename: media.py
import requests
import json
import os
class Media(object):
def __init__(self):
with open('access_token.txt','r',encoding='utf-8') as f: #獲取access_token
token = f.read()
self.accessToken = token
def get(self, mediaId, filepath):
postUrl = "https://api.weixin.qq.com/cgi-bin/media/get?access_token=%s&media_id=%s" % (self.accessToken, mediaId)
urlResp = requests.post(postUrl)
headers = urlResp.headers
if ('Content-Type: application/json\r\n' in headers) or ('Content-Type: text/plain\r\n' in headers):
jsonDict = json.loads(urlResp.content)
print(jsonDict)
else:
# print(urlResp.content.decode('ascii'))
buffer = urlResp.content #素材的二進制
with open(filepath, "wb") as f:
f.write(buffer)
print("get successful")
def _add(self, type,filepath):
with open(filepath,"rb") as f:
img = f.read()
filename = os.path.split(filepath)[-1]
url = 'https://api.weixin.qq.com/cgi-bin/media/upload?access_token={}&type={}'
files = {'media':(filename,img)}
res = requests.post(url.format(self.accessToken,type),files=files)
return json.loads(res.content.decode('utf-8'))['media_id']
5.自定義菜單
自定義菜單能夠幫助公衆號豐富界面,讓用戶更好更快地理解公衆號的功能。開啓自定義菜單後,公衆號界面如圖所示:
menu.py :自定義菜單增刪改查
# -*- coding: utf-8 -*-
# filename: menu.py
import urllib.request
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.request.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.request.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.request.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.request.urlopen(url=postUrl)
print(urlResp.read())
if __name__ == '__main__':
myMenu = Menu()
postJson = """
{
"button":
[
{
"name": "小迪互動",
"sub_button":
[
{
"type": "click",
"name": "圖片文字識別",
"key": "ocr"
},
{
"type": "click",
"name": "語音聊天",
"key": "speech"
},
{
"type": "click",
"name": "文字聊天",
"key": "text"
}
]
},
{
"name": "精選欄目",
"sub_button":
[
{
"type": "view",
"name": "往期文章",
"url": "https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=MzU4ODcxOTE2OA==&scene=124#wechat_redirect"
}
]
},
{
"name": "關於我們",
"sub_button":
[
{
"type": "click",
"name": "DepthsData",
"key": "depth"
},
{
"type": "click",
"name": "RPA",
"key": "rpa"
}
]
}
]
}
"""
with open('access_token.txt','r',encoding='utf-8') as f:
accessToken = f.read()
myMenu.create(postJson, accessToken)
運行menu.py一次性創建自定義菜單,有些許延遲。
創建成功後,點擊菜單會向公衆號發送一個“事件消息”,根據你自定義的click-key,判斷該事件屬於哪個菜單。
此外,新用戶的訂閱會發送一個“subscribe”的“事件消息”,可以利用它給新用戶打招呼噢。
6. 總結
到此,一個功能還算齊全的公衆號開發完畢。
1.補充完整的流程圖:
圖中紅色字體的文件是需要運行的。
2.文件結構:
3.剩餘代碼
handle.py
# -*- coding: utf-8 -*-
# filename: handle.py
from receive import parse_xml
from reply import event_reply,text_reply,ocr_reply
from media import Media
from helpfunc import speech2text,text2speech
class MsgHandler(object):
"""
針對type不同,轉交給不同的處理函數。直接處理即可
"""
def __init__(self, msg):
self.msg = parse_xml(msg)
self.time = int(time.time())
self.temp_text = """
<xml>
<ToUserName><![CDATA[{}]]></ToUserName>
<FromUserName><![CDATA[{}]]></FromUserName>
<CreateTime>{}</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[{}]]></Content>
</xml>
"""
self.temp_image = """
<xml>
<ToUserName><![CDATA[{}]]></ToUserName>
<FromUserName><![CDATA[{}]]></FromUserName>
<CreateTime>{}</CreateTime>
<MsgType><![CDATA[image]]></MsgType>
<Image>
<MediaId><![CDATA[{}]]></MediaId>
</Image>
</xml>
"""
self.temp_voice = """
<xml>
<ToUserName><![CDATA[{}]]></ToUserName>
<FromUserName><![CDATA[{}]]></FromUserName>
<CreateTime>{}</CreateTime>
<MsgType><![CDATA[voice]]></MsgType>
<Voice>
<MediaId><![CDATA[{}]]></MediaId></Voice>
</xml>
"""
def dispatch(self):
self.result = "" # 統一的公衆號出口數據
if self.msg.MsgType == "text":
self.result = self.textHandle()
elif self.msg.MsgType== "voice":
self.result = self.voiceHandle()
elif self.msg.MsgType == 'image':
self.result = self.imageHandle()
elif self.msg.MsgType == 'video':
self.result = self.videoHandle()
elif self.msg.MsgType == 'shortvideo':
self.result = self.shortVideoHandle()
elif self.msg.MsgType == 'location':
self.result = self.locationHandle()
elif self.msg.MsgType == 'link':
self.result = self.linkHandle()
elif self.msg.MsgType == 'event':
self.result = self.eventHandle()
return self.result
def textHandle(self):
result = text_reply(self.msg.Content,self.msg.FromUserName)
response = self.temp_text.format(self.msg.FromUserName, self.msg.ToUserName, self.time, result)
return response
def voiceHandle(self):
# template = self.temp_voice
mediaid = self.msg.MediaId
filepath = r'voice/' + str(time.time()) + '.mp3'
Media().get(mediaid, filepath)#下載素材
text = speech2text(filepath)
result = text_reply(text,self.msg.FromUserName)
voice_path = text2speech(result)
r_mediaid = Media()._add('voice', voice_path)#上傳素材
response = self.temp_voice.format(self.msg.FromUserName, self.msg.ToUserName, self.time, r_mediaid)
return response
def imageHandle(self):
mediaid = self.msg.MediaId
style = get_state(self.msg.FromUserName)
if style == 'img-ocr':
filepath = r'pic/' + str(time.time()) + '.jpg'
Media().get(mediaid, filepath)
try:
result = ocr_reply(imgpath=filepath)
except:
result = '圖上好像沒有字吧'
respnse = self.temp_text.format(self.msg.FromUserName, self.msg.ToUserName, self.time, result)
else:
respnse = self.temp_image.format(self.msg.FromUserName, self.msg.ToUserName, self.time, mediaid)
return respnse
def videoHandle(self):
return self.temp_text.format(self.msg.FromUserName, self.msg.ToUserName, self.time, '不支持的消息類型!')
def shortVideoHandle(self):
return self.temp_text.format(self.msg.FromUserName, self.msg.ToUserName, self.time, '不支持的消息類型!')
def locationHandle(self):
return self.temp_text.format(self.msg.FromUserName, self.msg.ToUserName, self.time, '不支持的消息類型!')
def linkHandle(self):
return self.temp_text.format(self.msg.FromUserName, self.msg.ToUserName, self.time, '不支持的消息類型!')
def eventHandle(self):
result = event_reply(self.msg,self.msg.FromUserName)
response = self.temp_text.format(self.msg.FromUserName, self.msg.ToUserName, self.time, result)
return response
# -*- coding: utf-8 -*-
# filename: receive.py
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
print(msg_type)
if msg_type == 'event':
event_type = xmlData.find('Event').text
if event_type == 'CLICK':
return Click(xmlData)
elif event_type in ('subscribe', 'unsubscribe'):
return Subscribe(xmlData)
#elif event_type == 'VIEW':
#return View(xmlData)
#elif event_type == 'LOCATION':
#return LocationEvent(xmlData)
#elif event_type == 'SCAN':
#return Scan(xmlData)
elif msg_type == 'text':
return TextMsg(xmlData)
elif msg_type == 'image':
return ImageMsg(xmlData)
elif msg_type == 'voice':
return VoiceMsg(xmlData)
else:
return Msg(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
class Click(Msg):
def __init__(self, xmlData):
Msg.__init__(self, xmlData)
self.Event = xmlData.find('Event').text
self.Eventkey = xmlData.find('EventKey').text
class Subscribe(Msg):
def __init__(self, xmlData):
Msg.__init__(self, xmlData)
self.Event = xmlData.find('Event').text
class TextMsg(Msg):
def __init__(self, xmlData):
Msg.__init__(self, xmlData)
self.Content = xmlData.find('Content').text
class ImageMsg(Msg):
def __init__(self, xmlData):
Msg.__init__(self, xmlData)
self.PicUrl = xmlData.find("PicUrl").text
self.MediaId = xmlData.find("MediaId").text
class VoiceMsg(Msg):
def __init__(self, xmlData):
Msg.__init__(self, xmlData)
self.MediaId = xmlData.find("MediaId").text
放心吧,這個我是不會放出來的,畢竟我是一個“稱職”的員工。
好了,以上就是用python開發微信公衆平臺的全部了,覺得好看點個贊再走吧!