02.26 用Serverless快速上手Python版的公眾號開發

​傳統意義上來說,想給公眾號增加更多的功能,需要我們有一臺服務器,搭建一個公眾號的後臺服務。那麼在Serverless架構下,我們是否有超簡便的方法來實現一個簡單的公眾號後臺呢?

我說可以:我們只用一個函數,就可以初步的搭建一個公眾號後臺的雛形。


首先我們要有一個公眾號,這一段我就Pass了;

然後我們要學習看文檔:

用Serverless快速上手Python版的公眾號開發

再然後,我們還要做一個有趣的小操作,那就是為我們的函數申請固定IP:

用Serverless快速上手Python版的公眾號開發

用Serverless快速上手Python版的公眾號開發

進入到白名單之後,我們可以填寫表單,完成固定公網出口IP的申請,這裡可能要幾個工作日才能完成。最後就是我們的代碼開發。




1: 想要將函數綁定到公眾號後臺,需要我們參考文檔:

我們可以先在函數中按照文檔完成一個基本的鑑定功能:

<code>def checkSignature(param):    '''    :param param:    :return:    '''    signature = param['signature']    timestamp = param['timestamp']    nonce = param["nonce"]    tmparr = [wxtoken, timestamp, nonce]    tmparr.sort()    tmpstr = ''.join(tmparr)    tmpstr = hashlib.sha1(tmpstr.encode("utf-8")).hexdigest()    return tmpstr == signature/<code>

再定義一個基本的回覆方法:

<code>def response(body, status=200):    return {        "isBase64Encoded": False,        "statusCode": status,        "headers": {"Content-Type": "text/html"},        "body": body    }/<code>

然後在函數入口處:

<code>def main_handler(event, context):        if 'echostr' in event['queryString']:  # 接入時的校驗        return response(event['queryString']['echostr'] if checkSignature(event['queryString']) else False)/<code>

我們接著配置我們Yaml:

<code># serverless.ymlWeixin_GoServerless:  component: "@serverless/tencent-scf"  inputs:    name: Weixin_GoServerless    codeUri: ./Admin    handler: index.main_handler    runtime: Python3.6    region: ap-shanghai    description: 公眾號後臺服務器配置    memorySize: 128    timeout: 20    environment:      variables:        wxtoken: 自定義一個字符串        appid: 暫時不寫        secret: 暫時不寫    events:      - apigw:          name: Weixin_GoServerless          parameters:            protocols:              - https            environment: release            endpoints:              - path: /                method: ANY                function:                  isIntegratedResponse: TRUE/<code>

我們執行代碼,完成部署:

用Serverless快速上手Python版的公眾號開發

接下來在我們的公眾號後臺,選擇基本配置:

用Serverless快速上手Python版的公眾號開發

然後選擇修改配置:

用Serverless快速上手Python版的公眾號開發

在這裡我們要注意:

  • URL,寫我們剛才部署完成返回給我們的地址,並且在最後加一個/
  • Token,寫我們Yaml中的wxtoken,兩個地方要保持一樣的字符串
  • EncodingAESKey,可以點擊隨機生成
  • 消息加密方法可以選擇明文

完成之後,我們可以點擊提交:

用Serverless快速上手Python版的公眾號開發

看到提交成功,就說明我們已經完成了第一步驟的綁定,接下來,我們到函數的後臺:

用Serverless快速上手Python版的公眾號開發

打開這個固定出口IP,然後看到IP地址之後,複製IP地址:

用Serverless快速上手Python版的公眾號開發

點擊查看->修改,並將IP地址複製粘貼進來,保存。

同時我們查看開發者ID和密碼:

用Serverless快速上手Python版的公眾號開發

並將這兩個內容複製粘貼,放到我們環境變量中:

用Serverless快速上手Python版的公眾號開發

至此,我們完成了一個公眾號後臺服務的綁定。為了方便之後的操作,

我先獲取一下全局變量:

<code>wxtoken = os.environ.get('wxtoken')appid = os.environ.get('appid')secret = os.environ.get('secret')/<code>

接下來對各個模塊進行編輯(本文只提供部分簡單基礎的模塊,更多功能實現可以參考公眾號文檔實現)

1: 獲取AccessToken模塊:

<code>def getAccessToken():    '''    正常返回:{"access_token":"ACCESS_TOKEN","expires_in":7200}    異常返回:{"errcode":40013,"errmsg":"invalid appid"}    :return:    '''    url = "公眾號url/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s" % (appid, secret)    accessToken = json.loads(urllib.request.urlopen(url).read().decode("utf-8"))    print(accessToken)    return None if "errcode" in accessToken else accessToken["access_token"]/<code>

2: 創建自定義菜單模塊:

<code>def setMenu(menu):    '''    正確返回:{"errcode":0,"errmsg":"ok"}    異常返回:{"errcode":40018,"errmsg":"invalid button name size"}    :return:    '''    accessToken = getAccessToken()    if not accessToken:        return "Get Access Token Error"​    url = "公眾號url/cgi-bin/menu/create?access_token=%s" % accessToken    postData = urllib.parse.urlencode(menu).encode("utf-8")    requestAttr = urllib.request.Request(url=url, data=postData)    responseAttr = urllib.request.urlopen(requestAttr)    responseData = json.loads(responseAttr.read())    return responseData['errmsg'] if "errcode" in responseData else "success"/<code>

3: 常見消息回覆模塊:

<code>def textXML(body, event):    '''    :param body: {"msg": "test"}        msg: 必填,回覆的消息內容(換行:在content中能夠換行,客戶端就支持換行顯示)    :param event:    :return:    '''    return """<tousername>              <fromusername>              <createtime>{time}/<createtime>              <msgtype>              <content>""".format(toUser=event["FromUserName"],                                                                   fromUser=event["ToUserName"],                                                                   time=int(time.time()),                                                                   msg=body["msg"])​​def pictureXML(body, event):    '''    :param body:  {"media_id": 123}        media_id: 必填,通過素材管理中的接口上傳多媒體文件,得到的id。    :param event:    :return:    '''    return """<tousername>              <fromusername>              <createtime>{time}/<createtime>              <msgtype>              <image>                <mediaid>              /<image>""".format(toUser=event["FromUserName"],                                       fromUser=event["ToUserName"],                                       time=int(time.time()),                                       media_id=body["media_id"])​​def voiceXML(body, event):    '''    :param body: {"media_id": 123}        media_id: 必填,通過素材管理中的接口上傳多媒體文件,得到的id    :param event:    :return:    '''    return """<tousername>              <fromusername>              <createtime>{time}/<createtime>              <msgtype>              <voice>                <mediaid>              /<voice>""".format(toUser=event["FromUserName"],                                       fromUser=event["ToUserName"],                                       time=int(time.time()),                                       media_id=body["media_id"])​​def videoXML(body, event):    '''    :param body: {"media_id": 123, "title": "test", "description": "test}        media_id: 必填,通過素材管理中的接口上傳多媒體文件,得到的id        title::選填,視頻消息的標題        description:選填,視頻消息的描述    :param event:    :return:    '''    return """ 
<tousername> <fromusername> <createtime>{time}/<createtime> <msgtype> <video> <mediaid> <title> <description> /<video>""".format(toUser=event["FromUserName"], fromUser=event["ToUserName"], time=int(time.time()), media_id=body["media_id"], title=body.get('title', ''), description=body.get('description', ''))​​def musicXML(body, event): ''' :param body: {"media_id": 123, "title": "test", "description": "test} media_id:必填,縮略圖的媒體id,通過素材管理中的接口上傳多媒體文件,得到的id title:選填,音樂標題 description:選填,音樂描述 url:選填,音樂鏈接 hq_url:選填,高質量音樂鏈接,WIFI環境優先使用該鏈接播放音樂 :param event: :return: ''' return """<tousername> <fromusername> <createtime>{time}/<createtime> <msgtype> <music> <title> <description> <musicurl> <hqmusicurl> <thumbmediaid> /<music>""".format(toUser=event["FromUserName"], fromUser=event["ToUserName"], time=int(time.time()), media_id=body["media_id"], title=body.get('title', ''), url=body.get('url', ''), hq_url=body.get('hq_url', ''), description=body.get('description', ''))​​def articlesXML(body, event): ''' :param body: 一個list [{"title":"test", "description": "test", "picUrl": "test", "url": "test"}] title:必填,圖文消息標題 description:必填,圖文消息描述 picUrl:必填,圖片鏈接,支持JPG、PNG格式,較好的效果為大圖360*200,小圖200*200 url:必填,點擊圖文消息跳轉鏈接 :param event: :return: ''' if len(body["articles"]) > 8: # 最多隻允許返回8個 body["articles"] = body["articles"][0:8] tempArticle = """<item> <title> <description> <picurl> /<item>""" return """<tousername> <fromusername> <createtime>{time}/<createtime> <msgtype> <articlecount>{count}/<articlecount> <articles> {articles} /<articles>""".format(toUser=event["FromUserName"], fromUser=event["ToUserName"], time=int(time.time()), count=len(body["articles"]), articles="".join([tempArticle.format( title=eveArticle['title'], description=eveArticle['description'], picurl=eveArticle['picurl'], url=eveArticle['url'] ) for eveArticle in body["articles"]]))​/<code>

4: 對main_handler進行修改,使其:

  • 識別綁定功能
  • 識別基本信息
  • 識別特殊額外請求(例如通過url觸發自定義菜單的更新)

整體代碼:

<code>def main_handler(event, context):    print('event: ', event)​    if event["path"] == '/setMenu':  # 設置菜單接口        menu = {            "button": [                {                    "type": "view",                    "name": "精彩文章",                    "url": "******"                },                {                    "type": "view",                    "name": "開源項目",                    "url": "*******"                },                {                    "type": "miniprogram",                    "name": "在線編程",                    "appid": "wx453cb539f9f963b2",                    "pagepath": "/page/index"                }]        }        return response(setMenu(menu))​    if 'echostr' in event['queryString']:  # 接入時的校驗        return response(event['queryString']['echostr'] if checkSignature(event['queryString']) else False)    else:  # 用戶消息/事件        event = getEvent(event)        if event["MsgType"] == "text":            # 文本消息            return response(body=textXML({"msg": "這是一個文本消息"}, event))        elif event["MsgType"] == "image":            # 圖片消息            return response(body=textXML({"msg": "這是一個圖片消息"}, event))        elif event["MsgType"] == "voice":            # 語音消息            pass        elif event["MsgType"] == "video":            # 視頻消息            pass        elif event["MsgType"] == "shortvideo":            # 小視頻消息            pass        elif event["MsgType"] == "location":            # 地理位置消息            pass        elif event["MsgType"] == "link":            # 鏈接消息            pass        elif event["MsgType"] == "event":            # 事件消息            if event["Event"] == "subscribe":                # 訂閱事件                if event.get('EventKey', None):                    # 用戶未關注時,進行關注後的事件推送(帶參數的二維碼)                    pass                else:                    # 普通關注                    pass            elif event["Event"] == "unsubscribe":                # 取消訂閱事件                pass            elif event["Event"] == "SCAN":                # 用戶已關注時的事件推送(帶參數的二維碼)                pass            elif event["Event"] == "LOCATION":                # 上報地理位置事件                pass            elif event["Event"] == "CLICK":                # 點擊菜單拉取消息時的事件推送                pass            elif event["Event"] == "VIEW":                # 點擊菜單跳轉鏈接時的事件推送                pass​/<code> 

在上述代碼中可以看到:

<code>if event["MsgType"] == "text":    # 文本消息    return response(body=textXML({"msg": "這是一個文本消息"}, event))elif event["MsgType"] == "image":    # 圖片消息    return response(body=textXML({"msg": "這是一個圖片消息"}, event))/<code>

這裡就是說,當用戶發送了文本消息時候,我們給用戶回覆一個文本消息:這是一個文本消息,當用戶發送了一個圖片,我們給用戶返回這是一個圖片消息,用這兩個功能測試我們這個後臺的連通性:

用Serverless快速上手Python版的公眾號開發

可以看到,系統已經可以正常返回。


這樣一個簡單的小框架或者小Demo的意義是什麼呢?


  • 可以告訴大家,我們可以很輕量的,通過一個函數來實現公眾號的後端服務;
  • 這裡都是基礎能力,我們可以在這個基礎能力基礎上,“肆無忌憚”的添加創新力,例如:
    1. 用戶傳過來的是圖片消息,我們可以通過一些識圖API告訴用戶這個圖片包括了什麼(接下來的文章分享中會涉及這部分內容)
    2. 用戶傳過來的是文字消息,我們可以先設定一些幫助信息/檢索信息進行對比,如果沒找到就給用戶開啟聊天功能(這裡涉及到人工智能中的自然語言處理,例如對話、文本相似度檢測,之後分享也會和大家舉例說明)
    3. 如果用戶發送到是語音我們還可以將其轉成文本,生成對話消息,然後再轉換成語音返回給用戶
    4. 如果用戶發送了地理位置信息,我們可以返回給用戶所在經緯度的街景信息或者周邊的信息/生活服務信息等
    5. .........

這些能力都可以自行添加


當然,如果你覺得上面的實現比較Low,也沒有問題,因為這裡還有一個Werobot的框架,有的人比較疑惑:Werobot也能部署在雲函數上?Of Course!

<code>Weixin_Werobot:  component: "@serverless/tencent-werobot"  inputs:    functionName: Weixin_Werobot    code: ./test    werobotProjectName: app    werobotAttrName: robot    functionConf:      timeout: 10      memorySize: 256      environment:        variables:          wxtoken: *********          appid: *********          secret: *********      apigatewayConf:        protocols:          - http        environment: release/<code>

然後新建代碼:

<code>import osimport werobotrobot = werobot.WeRoBot(token=os.environ.get('wxtoken'))robot.config['SESSION_STORAGE'] = Falserobot.config["APP_ID"] = os.environ.get('appid')robot.config["APP_SECRET"] = os.environ.get('secret')# @robot.handler 處理所有消息@robot.handlerdef hello(message):    return 'Hello World!'if __name__ == "__main__":    # 讓服務器監聽在 0.0.0.0:80    robot.config['HOST'] = '0.0.0.0'    robot.config['PORT'] = 80    robot.run()/<code>

並且在本地安裝werobot相關依賴,完成之後,執行部署:

用Serverless快速上手Python版的公眾號開發

並把下面的這個地址複製到公眾號後臺:

用Serverless快速上手Python版的公眾號開發

開啟調用即可。參考Git:https://github.com/serverless-tencent/tencent-werobot

這裡需要注意的是,我們一定要關掉Session或者將Session改成雲數據庫,不能使用本地文件等,例如關閉Session配置:

<code>robot.config['SESSION_STORAGE'] = False/<code>


分享到:


相關文章: