Fancy‘s Blog

Fancy's Blog,技術Blog在SC語言
tc sc en

使用Flask創建微信公眾號

2018-11-26 Code Fancy

基於Flask搭建微信公眾號後台

這次先用Flask為微信公眾號做個後台。微信公眾號後台一般對性能各方面要求並不高,這裏我們以新浪SAE為例,其他已解析域名的服務器同理。整個過程比較簡單,算是個快速的小項目吧

  • 部署環境為python3+pipenv+flask+uwsgi/gunicorn+supervisor+nginx 其中我們uwsgi和gunicorn我會同步對比部署,篇幅太長主題不明,主要內容留在下篇,這裏先說實現的問題吧

部署服務器


可以點擊鏈接進行註冊 說起新浪SAE(SinaAppEngine)類屬Paas,這個還是比較不穩定的,好在前期不花錢,自帶二級域名和HTTPS,控制台直接操作ssh秘鑰,不支持系統內ssh密鑰相比之下很適合做測試和微信這些。

註冊地址 新浪SAE有提供免費的基於GIT的Python2.7共享環境,這裏不用這個,實在是太難受了,我們選擇自定義一個,選擇手工部署Ubuntu,倘若Centos的弄nginx這些有點煩。

img

新浪雲的Ubuntu新建用戶後是無法ssh秘鑰登陸的,在控制台改ssh秘鑰。就忍受著用root用戶吧。其他服務器用戶還是新建用戶比較安全。寫文章雖然用的新浪雲示範,往後的代碼裏為了區別權限都加上sudo

如果是新浪SAE記下自定義的二級域名,等下會用到

img

管理可以點控制台/應用/容器管理。這裏我們註意框住的這句話,無論80還是443,新浪只開放了5050端口給我們。HTTPS也通用。這裏後面都會用到

img

接入微信公眾號


進入微信公眾平台,如果沒有公眾號的話按流程申請即可,從主頁中下滑,在左導航欄中最下面進入到 / 開發 / 基本配置界面。勾選協議成為開發者,

img

點擊“修改配置”按鈕,填寫服務器地址(URL)、Token和EncodingAESKey,URL是與服務器通信接口的接口URL,我們填入服務器的解析域名(SAE的二級域名)後跟一個自定義路由,http和https都可以,新浪雲自帶有https這是沒問題的。Token可由開發者自由填寫,用作生成簽名(該Token會和接口URL中包含的Token進行哈希值比對驗證安全性)將用作消息體加解密密鑰。我們點隨機生成,

微信後台接受一個GET請求需要的參數如下:

img

1)將token、timestamp、nonce三個參數進行字典序排序 2)將三個參數字符串拼接成一個字符串進行sha1加密 3)開發者獲得加密後的字符串可與signature對比,標識該請求來源於微信

據微信的要求,我們嘗試編寫flask後台。先部署下基本環境

$ sudo apt update & upgrade
$ sudo apt install python3-dev python3-pip
$ sudo pip3 install pipenv

不是新浪SAE強烈建議創建adduser新用戶,並usermod -aG sudo賦予權限。創建ssh密鑰登錄並關閉密碼登錄。 創建pipenv環境並進入安裝flask這些就不多說了。如果未測試的話可以先簡單編寫個測試

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World!"

 @app.route('/wechat_api/')
 def wechat():
    pass

if __name__ == "__main__":
    app.run(debug=True)
    # app.run(host='0.0.0.0', port=5050)

新浪SAE需要指定5050端口,Flask運行app.run()是默認的127.0.0.1:5000, 我們使用註釋裏的指定host參數即可外網訪問。這是我們輸入域名進入,可以看到Hello World!即可。

創建一個接口,參考微信提供的php編寫flask,填入token和自定義的路由,

base.py:

import hashlib

from flask import Flask, request, make_response
import xml.etree.ElementTree as ET

WX_TOKEN = 'fancy'
# 這裏填寫公眾號配置的token

app = Flask(__name__)
app.debug = True


@app.route("/")
def hello():
    return "Hello World!"

@app.route('/wechat_api/', methods=['GET', 'POST'])
# 定義路由地址請與URL後的保持一致
def wechat():
    if request.method == 'GET':
    token = WX_TOKEN
    data = request.args
    signature = data.get('signature', '')
    timestamp = data.get('timestamp', '')
    nonce = data.get('nonce', '')
    echostr = data.get('echostr', '')
    s = sorted([timestamp, nonce, token])
    # 字典排序
    s = ''.join(s)
    if hashlib.sha1(s.encode('utf-8')).hexdigest() == signature:
        # 判斷請求來源,並對接受的請求轉換為utf-8後進行sha1加密
        response = make_response(echostr)
        # response.headers['content-type'] = 'text' 
        # 新浪SAE未實名用戶加上上面這句
        return response

if __name__ == '__main__':
app.run()
# app.run(host='0.0.0.0', port=5050)

然後回到微信服務器配置的地方選擇明文模式或者兼容模式。點選提交,

img

我遇到問題。由於我新浪SAE未實名還未審核通過,新郎SAE對未實名的在返回值內會帶上奇怪的的html內容信息,從而導致Token驗證失敗。我們在返回值的頭部帶上'content-type' = 'text'即可。

到此,點擊提交,應該就沒問題了,如果顯示Token驗證失敗,請回頭再次檢查一遍

接入後的應答

我們查看微信的文檔,微信是這樣說的:

微信收到的消息類型結構mp.weixin.qq.com微信被動回覆用戶消息結構mp.weixin.qq.com

還有一個接收事件推送,介於篇幅問題,這個留作下下次再說。 微信提供給開發者的通信接口是xml格式的,確實一直處理json,用xml還是感覺比較難受。這裏使用 xml.etree.ElementTree 來解析。

分類有文本text,圖片image,語音voice,視頻video,小視頻shortvideo,地理位置location,鏈接link。 這裏由於沒有太多硬性的功能,我希望簡單化用戶的操作,將功能全部先融合在輸入框,根據用戶輸入的內容做出相應的判斷,如果無法判斷時或者沒實際意義時,判斷你可能只想聊天,再調用聊天機器人。

思路是初步判斷消息類型,然後再逐個if-elif篩選下來。 - 首先判斷文字模塊,微信POST過來的XML數據包結構:

img

判斷一個MsgType,主要用到的ToUserNameFromUserNameContent,我們先看如何回覆文本消息:

img

那麽好了,ToUserNameFromUserName實際在收發過程中是調轉的,利用time()生成整型時間,我們先把結構起好 微信這裏原來的GET請求驗證不能刪除,除此之外的POST我們直接else處理即可。

最開始和最後——圖靈機器人

main.py:

from tuling import get_response
import xml.etree.ElementTree as ET
# ...
@app.route('/wechat_api/', methods=['GET', 'POST'])
def wechat():
    if request.method == 'GET':
    #...
    else:
        xml = ET.fromstring(request.data)
        toUser = xml.find('ToUserName').text
        fromUser = xml.find('FromUserName').text
        msgType = xml.find("MsgType").text

        if msgType == 'text':
            content = xml.find('Content').text
            return reply_text(
                fromUser, toUser, get_response(
                    fromUser, content))
        else:
            return reply_text(fromUser, toUser, "嗯?我聽不太懂")

微信要求必須回覆success(建議)或者空字符串防止輪詢,我這裏先將未分類的消息回覆text這樣感覺不會不理人。get_response 先判斷一個text消息,接收Content再做判斷是否為關鍵語句再針對回覆。倘若沒有即調用圖靈機器人。 我們用的v2的api,用的是post請求一個json,json比較簡單,這邊不多說了,按API V2.0接入文檔的示例即可

tuling.py:

import os
import json
import requests

TULING_KEY = os.getenv('TULING_KEY')


def get_response(openid, msg):
    api = 'http://openapi.tuling123.com/openapi/api/v2'
    dat = {
        "perception": {
            "inputText": {
                "text": msg
            },
            "inputImage": {
                "url": "imageUrl"
            },
            "selfInfo": {
                "location": {
                    "city": "北京",
                    "province": "北京",
                    "street": "信息路"
                }
            }
        },
        "userInfo": {
            "apiKey": TULING_KEY,
            "userId": openid
        }
    }
    dat = json.dumps(dat)
    r = requests.post(api, data=dat).json()

    mesage = r['results'][0]['values']['text']
    print(r['results'][0]['values']['text'])
    return mesage

輸出我們暫時只要results裏的values輸出值,取值範圍是文本text,後期再一點點補充。 APIkey可以在這裏找到

img

- 圖靈機器人支持直接接入公眾號,這件事就當我不知道 - 更新:圖靈機器人支持關鍵詞回覆,可以添加各種語料庫。這裏目前沒有回覆覆雜消息的需求就先簡化代碼。

回覆微信的xml格式按照要求處理即可, main.py:

import time
from flask import  make_response
# ...
def reply_text(to_user, from_user, content):
    reply = """
    <xml><ToUserName><![CDATA[%s]]></ToUserName>
    <FromUserName><![CDATA[%s]]></FromUserName>
    <CreateTime>%s</CreateTime>
    <MsgType><![CDATA[text]]></MsgType>
    <Content><![CDATA[%s]]></Content>
    <FuncFlag>0</FuncFlag></xml>
    """
    response = make_response(reply % (to_user, from_user,
                                      str(int(time.time())), content))
    response.content_type = 'application/xml'
    return response

至此本地測試通過後用git方式什麽都好推送到服務器端,運行python main.py應該手機端發送就沒問題的。

img

附:官方提供的

微信公眾平台接口調試工具mp.weixin.qq.com

這才剛剛開始,下一篇裏我針對性的嘗試探究下程序必須的措施pipenv+uwsgi/gunicorn+supervisor+nginx的部署,部署幾次都沒好好總結

comments powered by Disqus