Python 微信公眾號(hào) 開發(fā)

演示1

大三上的時(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è)工作的模式世澜。大致有這么兩條路独旷。

  1. 用戶把消息發(fā)送到微信公眾平臺(tái)上,平臺(tái)把信息拼接組裝成XML發(fā)到我們自己的服務(wù)器宜狐。(通過一系列的認(rèn)證势告,校驗(yàn),讓平臺(tái)知道抚恒,我們的服務(wù)是合法的)咱台,然后服務(wù)器將XML進(jìn)行解析,處理俭驮。
  2. 我們的服務(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ǔ)音,圖片二打,圖文等消息類型县忌。示例如下。

演示1
演示2

總結(jié)

在將公眾號(hào)從PHP重構(gòu)為Python的過程中继效,遇到了一些問題症杏,然后通過不斷的摸索,慢慢的也把問題解決了瑞信。其實(shí)有時(shí)候就是這樣厉颤,只有不斷的發(fā)現(xiàn)問題,才能不斷的提升自己凡简。

這里其實(shí)并沒有深入的去完善逼友,重構(gòu)后的微信公眾號(hào)其實(shí)能做的還有很多精肃,畢竟就看敢不敢想嘛。好了帜乞,就先扯這么多了司抱,后面如果有好的思路和實(shí)現(xiàn),再回來(lái)更新好了黎烈。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末习柠,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子照棋,更是在濱河造成了極大的恐慌津畸,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,888評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件必怜,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡后频,警方通過查閱死者的電腦和手機(jī)梳庆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)卑惜,“玉大人膏执,你說(shuō)我怎么就攤上這事÷毒茫” “怎么了更米?”我有些...
    開封第一講書人閱讀 168,386評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)毫痕。 經(jīng)常有香客問我征峦,道長(zhǎng),這世上最難降的妖魔是什么消请? 我笑而不...
    開封第一講書人閱讀 59,726評(píng)論 1 297
  • 正文 為了忘掉前任栏笆,我火速辦了婚禮,結(jié)果婚禮上臊泰,老公的妹妹穿的比我還像新娘蛉加。我一直安慰自己,他們只是感情好缸逃,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,729評(píng)論 6 397
  • 文/花漫 我一把揭開白布针饥。 她就那樣靜靜地躺著,像睡著了一般需频。 火紅的嫁衣襯著肌膚如雪丁眼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,337評(píng)論 1 310
  • 那天贺辰,我揣著相機(jī)與錄音户盯,去河邊找鬼嵌施。 笑死,一個(gè)胖子當(dāng)著我的面吹牛莽鸭,可吹牛的內(nèi)容都是我干的吗伤。 我是一名探鬼主播,決...
    沈念sama閱讀 40,902評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼硫眨,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼足淆!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起礁阁,我...
    開封第一講書人閱讀 39,807評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤巧号,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后姥闭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體丹鸿,經(jīng)...
    沈念sama閱讀 46,349評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,439評(píng)論 3 340
  • 正文 我和宋清朗相戀三年棚品,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了靠欢。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,567評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡铜跑,死狀恐怖门怪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情锅纺,我是刑警寧澤掷空,帶...
    沈念sama閱讀 36,242評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站囤锉,受9級(jí)特大地震影響坦弟,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜官地,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,933評(píng)論 3 334
  • 文/蒙蒙 一减拭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧区丑,春花似錦拧粪、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至宴杀,卻和暖如春癣朗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背旺罢。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工旷余, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留绢记,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,995評(píng)論 3 377
  • 正文 我出身青樓正卧,卻偏偏與公主長(zhǎng)得像蠢熄,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子炉旷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,585評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容