您當前位置>首頁 » 新聞資(zī)訊 » 公衆号相關(guān) >
使用雲函數SCF+COS免費運營微信公衆号
發表時間:2020-9-30
發布人:葵宇科技
浏覽次數:57
是的,你(nǐ)沒聽錯,這一次我來帶大家直接上手運營微信公衆号。
震驚,Awesome,哼,我才不信捏,所謂無圖無真相 ~
效果展示
更多的體驗,可(kě)以關(guān)注我的微信公衆号:乂乂又又 (僅供測試,不要亂搞哈~)
嗯,這次我信了,快點教一下(xià)我吧,嘤嘤嘤~
操作步驟
在上一篇《使用SCF+COS快速開發全棧應用》教程中(zhōng),我們用騰訊雲無服務器(qì)雲函數 SCF 和(hé)對象存儲 COS 實現了一個(gè)後端雲函數,這個(gè)雲函數可(kě)以根據我們的請求返回對應的結果。
現在我們将嘗試在這個(gè)雲函數的基礎上解析微信 XML 消息,實現公衆号消息的自動(dòng)回複,關(guān)鍵詞回複,文(wén)字菜單等功能。
01
添加相關(guān)依賴
為了快速完成開發,這裡我們選擇 python 第三方開源庫 wechatpy 來接入微信公衆平台。
wechatpy 支持以下(xià)功能:
普通(tōng)公衆平台被動(dòng)響應和(hé)主動(dòng)調用 API
企業(yè)微信 API
微信支付 API
第三方平台代公衆号調用接口 API
小程序雲開發 API
可(kě)見功能是十分完整的,不僅支持普通(tōng)公衆平台主被動(dòng)調用,企業(yè)微信和(hé)微信支付,甚至還支持第三方平台代公衆号調用接口,拿來運營微信公衆号是十分綽綽有餘的~
由于騰訊雲函數的運行環境中(zhōng)缺少(shǎo)第三方庫,需要我們自己手動(dòng)上傳添加依賴,這裡我們需要添加的第三方依賴有:wechatpy
、optionaldict
、xmltodict
以及 timeout_decorator
其中(zhōng) wechatpy
需要依賴 optionaldict
、xmltodict
,timeout_decorator
是用來限制函數運行時長的,具體的依賴文(wén)件可(kě)以自行 pip 安裝後 copy 到雲函數項目根目錄,如(rú)上圖。
02
接入微信公衆号
這裡需要記下(xià)自己的 AppID、Token 和(hé) EncodingAESKey,消息加密方式建議選為安全模式。這個(gè)頁面先不要關(guān),一會兒上線發布好雲函數還需要過來再次修改配置。
03
編寫雲函數解析并回複微信公衆号消息
這一步可(kě)以直接參考 wechatpy 的官方文(wén)檔
地址:http://docs.wechatpy.org/zh_CN/master/quickstart.html#id2
Life is short, show me the code.
這裡我就直接上代碼了(原始業(yè)務代碼已略去,可(kě)以按照自己的需求開發)
import json
import timeout_decorator
from wechatpy.replies import ArticlesReply
from wechatpy.utils import check_signature
from wechatpy.crypto import WeChatCrypto
from wechatpy import parse_message, create_reply
from wechatpy.exceptions import InvalidSignatureException, InvalidAppIdException
# 是否開啟本地debug模式
debug = False
# 騰訊雲對象存儲依賴
if debug:
from qcloud_cos import CosConfig
from qcloud_cos import CosS3Client
from qcloud_cos import CosServiceError
from qcloud_cos import CosClientError
else:
from qcloud_cos_v5 import CosConfig
from qcloud_cos_v5 import CosS3Client
from qcloud_cos_v5 import CosServiceError
from qcloud_cos_v5 import CosClientError
# 配置存儲桶
appid = '66666666666'
secret_id = u'xxxxxxxxxxxxxxx'
secret_key = u'xxxxxxxxxxxxxxx'
region = u'ap-chongqing'
bucket = 'name'+'-'+appid
# 對象存儲實例
config = CosConfig(Secret_id=secret_id, Secret_key=secret_key, Region=region)
client = CosS3Client(config)
# cos 文(wén)件讀寫
def cosRead(key):
try:
response = client.get_object(Bucket=bucket, Key=key)
txtBytes = response['Body'].get_raw_stream()
return txtBytes.read().decode()
except CosServiceError as e:
return ""
def cosWrite(key, txt):
try:
response = client.put_object(
Bucket=bucket,
Body=txt.encode(encoding="utf-8"),
Key=key,
)
return True
except CosServiceError as e:
return False
def getReplys():
replyMap = {}
replyTxt = cosRead('Replys.txt') # 讀取數據
if len(replyTxt) > 0:
replyMap = json.loads(replyTxt)
return replyMap
def addReplys(reply):
replyMap = getReplys()
if len(replyMap) > 0:
replyMap[reply]='我是黑名單'
return cosWrite('Replys.txt', json.dumps(replyMap, ensure_ascii=False)) if len(replyMap) > 0 else False
def delReplys(reply):
replyMap = getReplys()
if len(replyMap) > 0:
replyMap.pop(reply)
return cosWrite('Replys.txt', json.dumps(replyMap, ensure_ascii=False)) if len(replyMap) > 0 else False
# 微信公衆号對接
wecaht_id = 'xxxxxxxxxxxxxxx'
WECHAT_TOKEN = 'xxxxxxxxxxxxxxxxxxx'
encoding_aes_key = 'xxxxxxxxxxxxxxxxxxxxxx'
crypto = WeChatCrypto(WECHAT_TOKEN, encoding_aes_key, wecaht_id)
# api網關(guān)響應集成
def apiReply(reply, txt=False, content_type='application/json', code=200):
return {
"isBase64Encoded": False,
"statusCode": code,
"headers": {'Content-Type': content_type},
"body": json.dumps(reply, ensure_ascii=False) if not txt else str(reply)
}
def replyMessage(msg):
txt = msg.content
ip = msg.source
print('請求信息--->'+ip+'%'+txt) # 用來在騰訊雲控制台打印請求日志
replysTxtMap = getReplys() # 獲取回複關(guān)鍵詞
if '@' in txt:
keys = txt.split('@')
if keys[0] == '電影': #do something
return
if keys[0] == '音樂(yuè)': #do something
return
if keys[0] == '下(xià)架': #do something
return
if keys[0] == '上架': #do something
return
if keys[0] == '回複': #do something
return
if keys[0] == '删除': #do something
return
elif txt in replysTxtMap.keys(): # 如(rú)果消息在回複關(guān)鍵詞内則自動(dòng)回複
return create_reply(replysTxtMap[txt], msg)
return create_reply("喵嗚 ?'ω'?", msg)
def wechat(httpMethod, requestParameters, body=''):
if httpMethod == 'GET':
signature = requestParameters['signature']
timestamp = requestParameters['timestamp']
nonce = requestParameters['nonce']
echo_str = requestParameters['echostr']
try:
check_signature(WECHAT_TOKEN, signature, timestamp, nonce)
except InvalidSignatureException:
echo_str = 'error'
return apiReply(echo_str, txt=True, content_type="text/plain")
elif httpMethod == 'POST':
msg_signature = requestParameters['msg_signature']
timestamp = requestParameters['timestamp']
nonce = requestParameters['nonce']
try:
decrypted_xml = crypto.decrypt_message(
body,
msg_signature,
timestamp,
nonce
)
except (InvalidAppIdException, InvalidSignatureException):
return
msg = parse_message(decrypted_xml)
if msg.type == 'text':
reply = replyMessage(msg)
elif msg.type == 'image':
reply = create_reply('哈? ???\n好端端的,給我發圖片幹啥~', msg)
elif msg.type == 'voice':
reply = create_reply('哈? ???\n好端端的,給我發語音幹啥~', msg)
else:
reply = create_reply('哈? ???\n搞不明白你(nǐ)給我發了啥~', msg)
reply = reply.render()
print('返回結果--->'+str(reply)) # 用來在騰訊雲控制台打印請求日志
reply = crypto.encrypt_message(reply, nonce, timestamp)
return apiReply(reply, txt=True, content_type="application/xml")
else:
msg = parse_message(body)
reply = create_reply("喵嗚 ?'ω'?", msg)
reply = reply.render()
print('返回結果--->'+str(reply)) # 用來在騰訊雲控制台打印請求日志
reply = crypto.encrypt_message(reply, nonce, timestamp)
return apiReply(reply, txt=True, content_type="application/xml")
@timeout_decorator.timeout(4, timeout_exception=StopIteration)
def myMain(httpMethod, requestParameters, body=''):
return wechat(httpMethod, requestParameters, body=body)
def timeOutReply(httpMethod, requestParameters, body=''):
msg_signature = requestParameters['msg_signature']
timestamp = requestParameters['timestamp']
nonce = requestParameters['nonce']
try:
decrypted_xml = crypto.decrypt_message(
body,
msg_signature,
timestamp,
nonce
)
except (InvalidAppIdException, InvalidSignatureException):
return
msg = parse_message(decrypted_xml)
reply = create_reply("出了點小問(wèn)題,請稍後再試", msg).render()
print('返回結果--->'+str(reply)) # 用來在騰訊雲控制台打印請求日志
reply = crypto.encrypt_message(reply, nonce, timestamp)
return apiReply(reply, txt=True, content_type="application/xml")
def main_handler(event, context):
body = ''
httpMethod = event["httpMethod"]
requestParameters = event['queryString']
if 'body' in event.keys():
body = event['body']
try:
response = myMain(httpMethod, requestParameters, body=body)
except:
response = timeOutReply(httpMethod, requestParameters, body=body)
return response
請求參數解析和(hé) COS 讀寫部分可(kě)參考上一篇《使用 SCF+COS 快速開發全棧應用》教程
下(xià)面我來捋一下(xià)整個(gè)雲函數的思路(lù)
def main_handler(event, context):
body = ''
httpMethod = event["httpMethod"]
requestParameters = event['queryString']
if 'body' in event.keys():
body = event['body']
try:
response = myMain(httpMethod, requestParameters, body=body)
except:
response = timeOutReply(httpMethod, requestParameters, body=body)
return response
我們先從main_handler
入手。
這裡我們通(tōng)過 API 網關(guān)觸發雲函數在 event 裡拿到了微信公衆号請求的方法、頭部和(hé)請求體,然後傳給 myMain 函數做處理,需要注意的是 myMain 是通(tōng)過timeout_decorator
包裝的限時運行函數。
@timeout_decorator.timeout(4, timeout_exception=StopIteration)
def myMain(httpMethod, requestParameters, body=''):
return wechat(httpMethod, requestParameters, body=body)
當 myMain 函數運行市場超過設定的 4 秒後,就會抛出異常。
然後我們可(kě)以通(tōng)過設置一個(gè) timeOutReply
函數來處理超時後的微信公衆号消息回複,可(kě)是為什麼要這麼做呢(ne)?
可(kě)以看到,當雲函數運行超時後,微信這邊就會顯示「該公衆号提供的服務器(qì)出現故障,請稍後再試」
這對用戶體驗是極不友好的,所以我們需要一個(gè)函數超時後的回複來兜底。
那麼對于一次微信公衆号後台消息請求多長時間算是超時呢(ne)?答案是 5 秒左右,從雲函數後台的調用日志我們可(kě)以得到這個(gè)結果。
不過需要注意的是對于用戶的一次消息請求,微信可(kě)能會每隔 1 秒左右重撥一次請求,直到收到服務器(qì)第一次響應。另外,超過 3 次應該就不會再重撥了,并且在 5 秒超時後即使雲函數調用成功并返回了數據,用戶也不會再接收到消息了~
所以我們就很有必要将自己的雲函數的運行時長限制在 5 秒之内了!
當然隻通(tōng)過配置雲函數超時時長得方式來處理是不正确的,因為這樣做雲函數超時後就被系統停掉了,并不會向微信返回消息。所以從一開始我就導入了 timeout_decorator
庫來限制主函數的運行時長,并用一個(gè)超時後回複函數來兜底。
另外值得一提的是,在我原始的業(yè)務代碼中(zhōng)是有一些爬蟲,這些爬蟲本來我是單線程順序執行的,考慮到超時問(wèn)題,我在微信雲函數版這裡全部改成了多線程運行來壓縮時間,所以如(rú)果你(nǐ)也有一些比較耗時的小任務話,也可(kě)以嘗試通(tōng)過多線程的方式來壓縮雲函數的運行時長。
我們接着向下(xià)看:
def wechat(httpMethod, requestParameters, body=''):
if httpMethod == 'GET':
signature = requestParameters['signature']
timestamp = requestParameters['timestamp']
nonce = requestParameters['nonce']
echo_str = requestParameters['echostr']
try:
check_signature(WECHAT_TOKEN, signature, timestamp, nonce)
except InvalidSignatureException:
echo_str = 'error'
return apiReply(echo_str, txt=True, content_type="text/plain")
elif httpMethod == 'POST':
msg_signature = requestParameters['msg_signature']
timestamp = requestParameters['timestamp']
nonce = requestParameters['nonce']
try:
decrypted_xml = crypto.decrypt_message(
body,
msg_signature,
timestamp,
nonce
)
except (InvalidAppIdException, InvalidSignatureException):
return
msg = parse_message(decrypted_xml)
if msg.type == 'text':
reply = replyMessage(msg)
elif msg.type == 'image':
reply = create_reply('哈? ???\n好端端的,給我發圖片幹啥~', msg)
elif msg.type == 'voice':
reply = create_reply('哈? ???\n好端端的,給我發語音幹啥~', msg)
else:
reply = create_reply('哈? ???\n搞不明白你(nǐ)給我發了啥~', msg)
reply = reply.render()
print('返回結果--->'+str(reply)) # 用來在騰訊雲控制台打印請求日志
reply = crypto.encrypt_message(reply, nonce, timestamp)
return apiReply(reply, txt=True, content_type="application/xml")
else:
msg = parse_message(body)
reply = create_reply("喵嗚 ?'ω'?", msg)
reply = reply.render()
print('返回結果--->'+str(reply)) # 用來在騰訊雲控制台打印請求日志
reply = crypto.encrypt_message(reply, nonce, timestamp)
return apiReply(reply, txt=True, content_type="application/xml")
這裡的 wechat 函數就是整個(gè)微信消息的解析過程,首先判斷請求方法是 GET 還是 POST,GET 方法隻在第一次綁定微信後台時會用到,這時我們會從微信服務器(qì)推送的請求參數中(zhōng)拿到 signature
, timestamp
, echostr
和(hé) nonce
參數,
check_signature(WECHAT_TOKEN, signature, timestamp, nonce)
我們隻需根據自己的公衆号 token 和(hé)來生成簽名與微信服務器(qì)傳過來的 signature 對比看是否一緻,若一緻就說明我們的消息加解密驗證是OK的,然後再将 echostr 原樣返回即可(kě)接入微信公衆号後台。
接入好微信公衆号後,如(rú)果有用戶在後台給我們發送消息,這裡雲函數收到的就是 POST 方法,
elif httpMethod == 'POST':
msg_signature = requestParameters['msg_signature']
timestamp = requestParameters['timestamp']
nonce = requestParameters['nonce']
try:
decrypted_xml = crypto.decrypt_message(
body,
msg_signature,
timestamp,
nonce
)
except (InvalidAppIdException, InvalidSignatureException):
return
msg = parse_message(decrypted_xml)
if msg.type == 'text':
reply = replyMessage(msg)
elif msg.type == 'image':
reply = create_reply('哈? ???\n好端端的,給我發圖片幹啥~', msg)
elif msg.type == 'voice':
reply = create_reply('哈? ???\n好端端的,給我發語音幹啥~', msg)
else:
reply = create_reply('哈? ???\n搞不明白你(nǐ)給我發了啥~', msg)
reply = reply.render()
print('返回結果--->'+str(reply)) # 用來在騰訊雲控制台打印請求日志
reply = crypto.encrypt_message(reply, nonce, timestamp)
return apiReply(reply, txt=True, content_type="application/xml")
然後我們根據前面在微信公衆号後台拿到的 id,token 和(hé) aes 加密 key 來初始化消息加解密實例并解密還原用戶發送的消息
# 微信公衆号對接
wecaht_id = 'xxxxxxxxxxxxxxx'
WECHAT_TOKEN = 'xxxxxxxxxxxxxxxxxxx'
encoding_aes_key = 'xxxxxxxxxxxxxxxxxxxxxx'
crypto = WeChatCrypto(WECHAT_TOKEN, encoding_aes_key, wecaht_id)
接着判斷一下(xià)消息類型,不同類型的消息可(kě)自行處理
msg = parse_message(decrypted_xml)
if msg.type == 'text':
reply = replyMessage(msg)
elif msg.type == 'image':
reply = create_reply('哈? ??? 好端端的,給我發圖片幹啥~', msg)
elif msg.type == 'voice':
reply = create_reply('哈? ??? 好端端的,給我發語音幹啥~', msg)
else:
reply = create_reply('哈? ??? 搞不明白你(nǐ)給我發了啥~', msg)
需要注意的是當一個(gè)用戶新關(guān)注自己的公衆号時,我們收到的是一個(gè)其他類型的消息,也就是上面的最後一個(gè)判斷項,這裡你(nǐ)可(kě)以自己設置新關(guān)注用戶的歡迎語
reply = create_reply('哈? ???\n搞不明白你(nǐ)給我發了啥~', msg)
reply = reply.render()
print('返回結果--->'+str(reply)) # 用來在騰訊雲控制台打印請求日志
reply = crypto.encrypt_message(reply, nonce, timestamp)
return apiReply(reply, txt=True, content_type="application/xml")
之後我們通(tōng)過 create_reply
來快速創建一個(gè)文(wén)本回複,并通(tōng)過 render() 來生成 xml 回複消息文(wén)本。因為我之前在後台設置的是安全模式,所以還需要把 xml 重新通(tōng)過 crypto.encrypt_message
方法加密,然後才能把加密後的回複消息返回給微信服務器(qì)。
上一篇文(wén)章我有提到我們不能直接返回消息,需要按照特定的格式返回數據(API 網關(guān)需要開啟響應集成)
# api網關(guān)響應集成
def apiReply(reply, txt=False, content_type='application/json', code=200):
return {
"isBase64Encoded": False,
"statusCode": code,
"headers": {'Content-Type': content_type},
"body": json.dumps(reply, ensure_ascii=False) if not txt else str(reply)
}
04
上線發布雲函數、添加 API 網關(guān)觸發器(qì)、啟用響應集成
參考上一篇教程 《使用 SCF+COS 快速開發全棧應用》
05
修改微信公衆号後台服務器(qì)配置
終于到最後一步了,如(rú)果你(nǐ)已經上線發布了好自己的雲函數,那麼快去微信公衆号後台綁定一下(xià)自己的後台服務器(qì)配置吧~
呼~ 大功告成
點擊閱讀原文(wén),領取 COS 限時1元禮包!