大三上的時候,對微信公眾號開發淺嘗輒止的玩了一下,感覺還是挺有意思的。http://blog.csdn.net/marksinoberg/article/details/54235271 後來伺服器到期了,也就擱置了。由於釋出web程式,使用PHP很順手,就使用了PHP作為開發語言。但是其實微信公眾號的開發和語言關聯並不大,流程,原理上都是一致的。
快要做畢設了,想著到時候應該會部署一些程式碼到伺服器上,進行長期的系統構建。所以趁著還是學生,就買了阿里雲的學生機。買了之後,就想著玩點什麼,於是微信公眾號的開發,就又提上了日程。但是這次,我不打算使用PHP了,感覺侷限性相對於Python而言,稍微有點大。
使用Python的話,可以靈活的部署一些爬蟲類程式,和使用者互動起來也會比較方便。可擴充性感覺也比較的高,於是就選它了。
伺服器配置這部分屬於是比較基礎的,不太明白的可以看看我之前的那個部落格,還算是比較的詳細。今天就只是對核心程式碼做下介紹好了。
專案目錄
root@aliyun:/var/www/html/wx/py# ls *.py
api.py dispatcher.py robot.py
root@aliyun:/var/www/html/wx/py#
複製程式碼
api.py
這個檔案相當於是一個關卡,涉及token的驗證,和服務的支援。
# -*- coding:utf-8 -*- #中文編碼
import sys
reload(sys) # 不加這部分處理中文還是會出問題
sys.setdefaultencoding('utf-8')
import time
from flask import Flask, request, make_response
import hashlib
import json
import xml.etree.ElementTree as ET
from dispatcher import *
app = Flask(__name__)
app.debug = True
@app.route('/') # 預設網址
def index():
return 'Index Page'
@app.route('/wx', methods=['GET', 'POST'])
def wechat_auth(): # 處理微信請求的處理函式,get方法用於認證,post方法取得微信轉發的資料
if request.method == 'GET':
token = '你自己設定好的token'
data = request.args
signature = data.get('signature', '')
timestamp = data.get('timestamp', '')
nonce = data.get('nonce', '')
echostr = data.get('echostr', '')
s = [timestamp, nonce, token]
s.sort()
s = ''.join(s)
if (hashlib.sha1(s).hexdigest() == signature):
return make_response(echostr)
else:
rec = request.stream.read() # 接收訊息
dispatcher = MsgDispatcher(rec)
data = dispatcher.dispatch()
with open("./debug.log", "a") as file:
file.write(data)
file.close()
response = make_response(data)
response.content_type = 'application/xml'
return response
if __name__ == '__main__':
app.run(host="0.0.0.0", port=80)
複製程式碼
dispatcher.py
這個檔案是整個服務的核心,用於識別使用者發來的訊息型別,然後交給不同的handler來處理,並將執行的結果反饋給前臺,傳送給使用者。訊息型別這塊,在微信的開發文件上有詳細的介紹,因此這裡就不再過多的贅述了。
#! /usr/bin python
# coding: utf8
import sys
reload(sys)
sys.setdefaultencoding("utf8")
import time
import json
import xml.etree.ElementTree as ET
from robot import *
class MsgParser(object):
"""
用於解析從微信公眾平臺傳遞過來的引數,並進行解析
"""
def __init__(self, data):
self.data = data
def parse(self):
self.et = ET.fromstring(self.data)
self.user = self.et.find("FromUserName").text
self.master = self.et.find("ToUserName").text
self.msgtype = self.et.find("MsgType").text
# 純文字資訊欄位
self.content = self.et.find("Content").text if self.et.find("Content") is not None else ""
# 語音資訊欄位
self.recognition = self.et.find("Recognition").text if self.et.find("Recognition") is not None else ""
self.format = self.et.find("Format").text if self.et.find("Format") is not None else ""
self.msgid = self.et.find("MsgId").text if self.et.find("MsgId") is not None else ""
# 圖片
self.picurl = self.et.find("PicUrl").text if self.et.find("PicUrl") is not None else ""
self.mediaid = self.et.find("MediaId").text if self.et.find("MediaId") is not None else ""
# 事件
self.event = self.et.find("Event").text if self.et.find("Event") is not None else ""
return self
class MsgDispatcher(object):
"""
根據訊息的型別,獲取不同的處理返回值
"""
def __init__(self, data):
parser = MsgParser(data).parse()
self.msg = parser
self.handler = MsgHandler(parser)
def dispatch(self):
self.result = "" # 統一的公眾號出口資料
if self.msg.msgtype == "text":
self.result = self.handler.textHandle()
elif self.msg.msgtype == "voice":
self.result = self.handler.voiceHandle()
elif self.msg.msgtype == 'image':
self.result = self.handler.imageHandle()
elif self.msg.msgtype == 'video':
self.result = self.handler.videoHandle()
elif self.msg.msgtype == 'shortvideo':
self.result = self.handler.shortVideoHandle()
elif self.msg.msgtype == 'location':
self.result = self.handler.locationHandle()
elif self.msg.msgtype == 'link':
self.result = self.handler.linkHandle()
elif self.msg.msgtype == 'event':
self.result = self.handler.eventHandle()
return self.result
class MsgHandler(object):
"""
針對type不同,轉交給不同的處理函式。直接處理即可
"""
def __init__(self, msg):
self.msg = msg
self.time = int(time.time())
def textHandle(self, user='', master='', time='', content=''):
template = """
<xml>
<ToUserName><![CDATA[{}]]></ToUserName>
<FromUserName><![CDATA[{}]]></FromUserName>
<CreateTime>{}</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[{}]]></Content>
</xml>
"""
# 對使用者發過來的資料進行解析,並執行不同的路徑
try:
response = get_response_by_keyword(self.msg.content)
if response['type'] == "image":
result = self.imageHandle(self.msg.user, self.msg.master, self.time, response['content'])
elif response['type'] == "music":
data = response['content']
result = self.musicHandle(data['title'], data['description'], data['url'], data['hqurl'])
elif response['type'] == "news":
items = response['content']
result = self.newsHandle(items)
# 這裡還可以新增更多的擴充內容
else:
response = get_turing_response(self.msg.content)
result = template.format(self.msg.user, self.msg.master, self.time, response)
#with open("./debug.log", 'a') as f:
# f.write(response['content'] + '~~' + result)
# f.close()
except Exception as e:
with open("./debug.log", 'a') as f:
f.write("text handler:"+str(e.message))
f.close()
return result
def musicHandle(self, title='', description='', url='', hqurl=''):
template = """
<xml>
<ToUserName><![CDATA[{}]]></ToUserName>
<FromUserName><![CDATA[{}]]></FromUserName>
<CreateTime>{}</CreateTime>
<MsgType><![CDATA[music]]></MsgType>
<Music>
<Title><![CDATA[{}]]></Title>
<Description><![CDATA[{}]]></Description>
<MusicUrl><![CDATA[{}]]></MusicUrl>
<HQMusicUrl><![CDATA[{}]]></HQMusicUrl>
</Music>
<FuncFlag>0</FuncFlag>
</xml>
"""
response = template.format(self.msg.user, self.msg.master, self.time, title, description, url, hqurl)
return response
def voiceHandle(self):
response = get_turing_response(self.msg.recognition)
result = self.textHandle(self.msg.user, self.msg.master, self.time, response)
return result
def imageHandle(self, user='', master='', time='', mediaid=''):
template = """
<xml>
<ToUserName><![CDATA[{}]]></ToUserName>
<FromUserName><![CDATA[{}]]></FromUserName>
<CreateTime>{}</CreateTime>
<MsgType><![CDATA[image]]></MsgType>
<Image>
<MediaId><![CDATA[{}]]></MediaId>
</Image>
</xml>
"""
if mediaid == '':
response = self.msg.mediaid
else:
response = mediaid
result = template.format(self.msg.user, self.msg.master, self.time, response)
return result
def videoHandle(self):
return 'video'
def shortVideoHandle(self):
return 'shortvideo'
def locationHandle(self):
return 'location'
def linkHandle(self):
return 'link'
def eventHandle(self):
return 'event'
def newsHandle(self, items):
# 圖文訊息這塊真的好多坑,尤其是<![CDATA[]]>中間不可以有空格,可怕極了
articlestr = """
<item>
<Title><![CDATA[{}]]></Title>
<Description><![CDATA[{}]]></Description>
<PicUrl><![CDATA[{}]]></PicUrl>
<Url><![CDATA[{}]]></Url>
</item>
"""
itemstr = ""
for item in items:
itemstr += str(articlestr.format(item['title'], item['description'], item['picurl'], item['url']))
template = """
<xml>
<ToUserName><![CDATA[{}]]></ToUserName>
<FromUserName><![CDATA[{}]]></FromUserName>
<CreateTime>{}</CreateTime>
<MsgType><![CDATA[news]]></MsgType>
<ArticleCount>{}</ArticleCount>
<Articles>{}</Articles>
</xml>
"""
result = template.format(self.msg.user, self.msg.master, self.time, len(items), itemstr)
return result
複製程式碼
robot.py
這個檔案屬於那種畫龍點睛性質的。
#!/usr/bin python
#coding: utf8
import requests
import json
def get_turing_response(req=""):
url = "http://www.tuling123.com/openapi/api"
secretcode = "嘿嘿,這個就不說啦"
response = requests.post(url=url, json={"key": secretcode, "info": req, "userid": 12345678})
return json.loads(response.text)['text'] if response.status_code == 200 else ""
def get_qingyunke_response(req=""):
url = "http://api.qingyunke.com/api.php?key=free&appid=0&msg={}".format(req)
response = requests.get(url=url)
return json.loads(response.text)['content'] if response.status_code == 200 else ""
# 簡單做下。後面慢慢來
def get_response_by_keyword(keyword):
if '團建' in keyword:
result = {"type": "image", "content": "3s9Dh5rYdP9QruoJ_M6tIYDnxLLdsQNCMxkY0L2FMi6HhMlNPlkA1-50xaE_imL7"}
elif 'music' in keyword or '音樂' in keyword:
musicurl='http://204.11.1.34:9999/dl.stream.qqmusic.qq.com/C400001oO7TM2DE1OE.m4a?vkey=3DFC73D67AF14C36FD1128A7ABB7247D421A482EBEDA17DE43FF0F68420032B5A2D6818E364CB0BD4EAAD44E3E6DA00F5632859BEB687344&guid=5024663952&uin=1064319632&fromtag=66'
result = {"type": "music", "content": {"title": "80000", "description":"有個男歌手姓巴,他的女朋友姓萬,於是這首歌叫80000", "url": musicurl, "hqurl": musicurl}}
elif '關於' in keyword:
items = [{"title": "關於我", "description":"喜歡瞎搞一些指令碼", "picurl":"https://avatars1.githubusercontent.com/u/12973402?s=460&v=4", "url":"https://github.com/guoruibiao"},
{"title": "我的部落格", "description":"收集到的,瞎寫的一些部落格", "picurl":"http://avatar.csdn.net/0/8/F/1_marksinoberg.jpg", "url":"http://blog.csdn.net/marksinoberg"},
{"title": "薛定諤的?", "description": "副標題有點奇怪,不知道要怎麼設定比較好","picurl": "https://www.baidu.com/img/bd_logo1.png","url": "http://www.baidu.com"}
]
result = {"type": "news", "content": items}
else:
result = {"type": "text", "content": "可以自由進行擴充"}
return result
複製程式碼
其實這看起來是一個檔案,其實可以擴充為很多的方面。
-
如果想通過公眾號來監控伺服器的執行情況,就可以新增一個對伺服器負載的監控的指令碼;
-
如果想做一些爬蟲,每天抓取一些高質量的文章,然後通過公眾號進行展示。
-
不方便使用電腦的情況下,讓公眾號呼叫一些命令也可以算是曲線救國的一種方式。
等等吧,其實有多少想法,就可以用Python進行事先。然後通過公眾號這個平臺進行展示。
易錯點
在從PHP重構為Python的過程中,我其實也是遇到了一些坑的。下面總結下,如果恰好能幫助到遇到同樣問題的你,那我這篇文章也算是沒有白寫了。
微信公眾號的開發,其實關鍵就在於理解這個工作的模式。大致有這麼兩條路。
- 使用者把訊息傳送到微信公眾平臺上,平臺把資訊拼接組裝成XML發到我們自己的伺服器。(通過一系列的認證,校驗,讓平臺知道,我們的服務是合法的),然後伺服器將XML進行解析,處理。
- 我們的伺服器解析處理完成後,將資料再次拼接組裝成XML,發給微信公眾平臺,平臺幫我們把資料反饋給對應的使用者。
這樣,一個互動就算是完成了。在這個過程中,有下面幾個容易出錯的地方。
-
token校驗: token的校驗是一個get方式的請求。通過程式碼我們也可以看到,就是對singature的校驗,具體看程式碼就明白了。
-
XML資料的解析,對於不同的訊息,記得使用不同的格式。其中很容易出錯的就是格式不規範。
<!CDATA[[]]>
中括號之間最好不要有空格,不然定位起錯誤還是很麻煩的。 -
服務的穩定性。這裡用的web框架是flask,小巧精良。但是對併發的支援性不是很好,對此可以使用uwsgi和Nginx來實現一個更穩定的服務。如果就是打算自己玩一玩,通過命令列啟用(如python api.py)就不是很保險了,因為很有可能會因為使用者的一個奇怪的輸入導致整個服務垮掉,建議使用nohup的方式,來在一定程度上保證服務的質量。
結果演示
目前這個公眾號支援文字,語音,圖片,圖文等訊息型別。示例如下。
總結
在將公眾號從PHP重構為Python的過程中,遇到了一些問題,然後通過不斷的摸索,慢慢的也把問題解決了。其實有時候就是這樣,只有不斷的發現問題,才能不斷的提升自己。
這裡其實並沒有深入的去完善,重構後的微信公眾號其實能做的還有很多,畢竟就看敢不敢想嘛。好了,就先扯這麼多了,後面如果有好的思路和實現,再回來更新好了。