大三上的時(shí)候,對(duì)微信公眾號(hào)開發(fā)淺嘗輒止的玩了一下审轮,感覺還是挺有意思的损敷。http://blog.csdn.net/marksinoberg/article/details/54235271 后來(lái)服務(wù)器到期了,也就擱置了屹逛。由于發(fā)布web程序础废,使用PHP很順手,就使用了PHP作為開發(fā)語(yǔ)言罕模。但是其實(shí)微信公眾號(hào)的開發(fā)和語(yǔ)言關(guān)聯(lián)并不大评腺,流程,原理上都是一致的淑掌。
快要做畢設(shè)了蒿讥,想著到時(shí)候應(yīng)該會(huì)部署一些代碼到服務(wù)器上,進(jìn)行長(zhǎng)期的系統(tǒng)構(gòu)建。所以趁著還是學(xué)生芋绸,就買了阿里云的學(xué)生機(jī)媒殉。買了之后,就想著玩點(diǎn)什么摔敛,于是微信公眾號(hào)的開發(fā)廷蓉,就又提上了日程。但是這次马昙,我不打算使用PHP了桃犬,感覺局限性相對(duì)于Python而言,稍微有點(diǎn)大行楞。
使用Python的話攒暇,可以靈活的部署一些爬蟲類程序,和用戶交互起來(lái)也會(huì)比較方便敢伸〕度模可拓展性感覺也比較的高,于是就選它了池颈。
服務(wù)器配置這部分屬于是比較基礎(chǔ)的尾序,不太明白的可以看看我之前的那個(gè)博客,還算是比較的詳細(xì)躯砰。今天就只是對(duì)核心代碼做下介紹好了每币。
項(xiàng)目目錄
root@aliyun:/var/www/html/wx/py# ls *.py
api.py dispatcher.py robot.py
root@aliyun:/var/www/html/wx/py#
api.py
這個(gè)文件相當(dāng)于是一個(gè)關(guān)卡,涉及token的驗(yàn)證琢歇,和服務(wù)的支持兰怠。
# -*- coding:utf-8 -*- #中文編碼
import sys
reload(sys) # 不加這部分處理中文還是會(huì)出問題
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('/') # 默認(rèn)網(wǎng)址
def index():
return 'Index Page'
@app.route('/wx', methods=['GET', 'POST'])
def wechat_auth(): # 處理微信請(qǐng)求的處理函數(shù),get方法用于認(rèn)證李茫,post方法取得微信轉(zhuǎn)發(fā)的數(shù)據(jù)
if request.method == 'GET':
token = '你自己設(shè)置好的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
這個(gè)文件是整個(gè)服務(wù)的核心揭保,用于識(shí)別用戶發(fā)來(lái)的消息類型,然后交給不同的handler來(lái)處理魄宏,并將運(yùn)行的結(jié)果反饋給前臺(tái)秸侣,發(fā)送給用戶。消息類型這塊宠互,在微信的開發(fā)文檔上有詳細(xì)的介紹味榛,因此這里就不再過多的贅述了。
#! /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):
"""
用于解析從微信公眾平臺(tái)傳遞過來(lái)的參數(shù)予跌,并進(jìn)行解析
"""
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 ""
# 語(yǔ)音信息字段
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):
"""
根據(jù)消息的類型葛躏,獲取不同的處理返回值
"""
def __init__(self, data):
parser = MsgParser(data).parse()
self.msg = parser
self.handler = MsgHandler(parser)
def dispatch(self):
self.result = "" # 統(tǒng)一的公眾號(hào)出口數(shù)據(jù)
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):
"""
針對(duì)type不同咪鲜,轉(zhuǎn)交給不同的處理函數(shù)神得。直接處理即可
"""
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>
"""
# 對(duì)用戶發(fā)過來(lái)的數(shù)據(jù)進(jìn)行解析市咽,并執(zhí)行不同的路徑
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)
# 這里還可以添加更多的拓展內(nèi)容
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
這個(gè)文件屬于那種畫龍點(diǎn)睛性質(zhì)的航邢。
#!/usr/bin python
#coding: utf8
import requests
import json
def get_turing_response(req=""):
url = "http://www.tuling123.com/openapi/api"
secretcode = "嘿嘿集币,這個(gè)就不說(shuō)啦"
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 ""
# 簡(jiǎn)單做下。后面慢慢來(lái)
def get_response_by_keyword(keyword):
if '團(tuán)建' 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":"有個(gè)男歌手姓巴翠忠,他的女朋友姓萬(wàn),于是這首歌叫80000", "url": musicurl, "hqurl": musicurl}}
elif '關(guān)于' in keyword:
items = [{"title": "關(guān)于我", "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": "副標(biāo)題有點(diǎn)奇怪秽之,不知道要怎么設(shè)置比較好","picurl": "https://www.baidu.com/img/bd_logo1.png","url": "http://www.baidu.com"}
]
result = {"type": "news", "content": items}
else:
result = {"type": "text", "content": "可以自由進(jìn)行拓展"}
return result
其實(shí)這看起來(lái)是一個(gè)文件,其實(shí)可以拓展為很多的方面吃既。
如果想通過公眾號(hào)來(lái)監(jiān)控服務(wù)器的運(yùn)行情況考榨,就可以添加一個(gè)對(duì)服務(wù)器負(fù)載的監(jiān)控的腳本;
如果想做一些爬蟲鹦倚,每天抓取一些高質(zhì)量的文章河质,然后通過公眾號(hào)進(jìn)行展示。
不方便使用電腦的情況下震叙,讓公眾號(hào)調(diào)用一些命令也可以算是曲線救國(guó)的一種方式掀鹅。
等等吧,其實(shí)有多少想法媒楼,就可以用Python進(jìn)行事先乐尊。然后通過公眾號(hào)這個(gè)平臺(tái)進(jìn)行展示。
易錯(cuò)點(diǎn)
在從PHP重構(gòu)為Python的過程中划址,我其實(shí)也是遇到了一些坑的扔嵌。下面總結(jié)下,如果恰好能幫助到遇到同樣問題的你夺颤,那我這篇文章也算是沒有白寫了痢缎。
微信公眾號(hào)的開發(fā),其實(shí)關(guān)鍵就在于理解這個(gè)工作的模式世澜。大致有這么兩條路独旷。
- 用戶把消息發(fā)送到微信公眾平臺(tái)上,平臺(tái)把信息拼接組裝成XML發(fā)到我們自己的服務(wù)器宜狐。(通過一系列的認(rèn)證势告,校驗(yàn),讓平臺(tái)知道抚恒,我們的服務(wù)是合法的)咱台,然后服務(wù)器將XML進(jìn)行解析,處理俭驮。
- 我們的服務(wù)器解析處理完成后回溺,將數(shù)據(jù)再次拼接組裝成XML春贸,發(fā)給微信公眾平臺(tái),平臺(tái)幫我們把數(shù)據(jù)反饋給對(duì)應(yīng)的用戶遗遵。
這樣萍恕,一個(gè)交互就算是完成了。在這個(gè)過程中车要,有下面幾個(gè)容易出錯(cuò)的地方允粤。
token校驗(yàn): token的校驗(yàn)是一個(gè)get方式的請(qǐng)求。通過代碼我們也可以看到翼岁,就是對(duì)singature的校驗(yàn)类垫,具體看代碼就明白了。
XML數(shù)據(jù)的解析琅坡,對(duì)于不同的消息悉患,記得使用不同的格式。其中很容易出錯(cuò)的就是格式不規(guī)范榆俺。
<!CDATA[[]]>
中括號(hào)之間最好不要有空格售躁,不然定位起錯(cuò)誤還是很麻煩的。服務(wù)的穩(wěn)定性茴晋。這里用的web框架是flask陪捷,小巧精良。但是對(duì)并發(fā)的支持性不是很好晃跺,對(duì)此可以使用uwsgi和Nginx來(lái)實(shí)現(xiàn)一個(gè)更穩(wěn)定的服務(wù)揩局。如果就是打算自己玩一玩,通過命令行啟用(如python api.py)就不是很保險(xiǎn)了掀虎,因?yàn)楹苡锌赡軙?huì)因?yàn)橛脩舻囊粋€(gè)奇怪的輸入導(dǎo)致整個(gè)服務(wù)垮掉凌盯,建議使用nohup的方式,來(lái)在一定程度上保證服務(wù)的質(zhì)量烹玉。
結(jié)果演示
目前這個(gè)公眾號(hào)支持文字驰怎,語(yǔ)音,圖片二打,圖文等消息類型县忌。示例如下。
總結(jié)
在將公眾號(hào)從PHP重構(gòu)為Python的過程中继效,遇到了一些問題症杏,然后通過不斷的摸索,慢慢的也把問題解決了瑞信。其實(shí)有時(shí)候就是這樣厉颤,只有不斷的發(fā)現(xiàn)問題,才能不斷的提升自己凡简。
這里其實(shí)并沒有深入的去完善逼友,重構(gòu)后的微信公眾號(hào)其實(shí)能做的還有很多精肃,畢竟就看敢不敢想嘛。好了帜乞,就先扯這么多了司抱,后面如果有好的思路和實(shí)現(xiàn),再回來(lái)更新好了黎烈。