如何用 Python 和 Flask 建立部署一個(gè) Facebook Messenger 機(jī)器人
這是我建立一個(gè)簡(jiǎn)單的 Facebook Messenger 機(jī)器人的記錄。功能很簡(jiǎn)單,它是一個(gè)回顯機(jī)器人,只是打印回用戶寫(xiě)了什么。
回顯服務(wù)器類似于服務(wù)器的“Hello World”例子。
這個(gè)項(xiàng)目的目的不是建立***的 Messenger 機(jī)器人,而是讓你了解如何建立一個(gè)小型機(jī)器人和每個(gè)事物是如何整合起來(lái)的。
技術(shù)棧
使用到的技術(shù)棧:
- Heroku 做后端主機(jī)。免費(fèi)級(jí)足夠這個(gè)等級(jí)的教程?;仫@機(jī)器人不需要任何種類的數(shù)據(jù)持久,所以不需要數(shù)據(jù)庫(kù)。
- Python 是我們選擇的語(yǔ)言。版本選擇 2.7,雖然它移植到 Pyhton 3 很容易,只需要很少的改動(dòng)。
- Flask 作為網(wǎng)站開(kāi)發(fā)框架。它是非常輕量的框架,用在小型工程或微服務(wù)是非常***的。
- *** Git 版本控制系統(tǒng)用來(lái)維護(hù)代碼和部署到 Heroku。
- 值得一提:Virtualenv。這個(gè) python 工具是用來(lái)創(chuàng)建清潔的 python 庫(kù)“環(huán)境”的,這樣你可以只安裝必要的需求和最小化應(yīng)用的大小。
機(jī)器人架構(gòu)
Messenger 機(jī)器人是由一個(gè)響應(yīng)兩種請(qǐng)求的服務(wù)器組成的:
- GET 請(qǐng)求被用來(lái)認(rèn)證。他們與你注冊(cè)的 FaceBook 認(rèn)證碼一同被 Messenger 發(fā)出。
- POST 請(qǐng)求被用來(lái)實(shí)際的通信。典型的工作流是,機(jī)器人將通過(guò)用戶發(fā)送帶有消息數(shù)據(jù)的 POST 請(qǐng)求而建立通信,然后我們將處理這些數(shù)據(jù),并發(fā)回我們的 POST 請(qǐng)求。如果這個(gè)請(qǐng)求完全成功(返回一個(gè) 200 OK 狀態(tài)碼),我們也將響應(yīng)一個(gè) 200 OK 狀態(tài)碼給初始的 Messenger請(qǐng)求。
這個(gè)教程應(yīng)用將托管到 Heroku,它提供了一個(gè)優(yōu)雅而簡(jiǎn)單的部署應(yīng)用的接口。如前所述,免費(fèi)級(jí)可以滿足這個(gè)教程。
在應(yīng)用已經(jīng)部署并且運(yùn)行后,我們將創(chuàng)建一個(gè) Facebook 應(yīng)用然后連接它到我們的應(yīng)用,以便 Messenger 知道發(fā)送請(qǐng)求到哪,這就是我們的機(jī)器人。
機(jī)器人服務(wù)器
基本的服務(wù)器代碼可以在 Github 用戶 hult(Magnus Hult) 的 Chatbot 項(xiàng)目上獲取,做了一些只回顯消息的代碼修改和修正了一些我遇到的錯(cuò)誤。最終版本的服務(wù)器代碼如下:
- from flask import Flask, request
- import json
- import requests
- app = Flask(__name__)
- ### 這需要填寫(xiě)被授予的頁(yè)面通行令牌(PAT)
- ### 它由將要?jiǎng)?chuàng)建的 Facebook 應(yīng)用提供。
- PAT = ''
- @app.route('/', methods=['GET'])
- def handle_verification():
- print "Handling Verification."
- if request.args.get('hub.verify_token', '') == 'my_voice_is_my_password_verify_me':
- print "Verification successful!"
- return request.args.get('hub.challenge', '')
- else:
- print "Verification failed!"
- return 'Error, wrong validation token'
- @app.route('/', methods=['POST'])
- def handle_messages():
- print "Handling Messages"
- payload = request.get_data()
- print payload
- for sender, message in messaging_events(payload):
- print "Incoming from %s: %s" % (sender, message)
- send_message(PAT, sender, message)
- return "ok"
- def messaging_events(payload):
- """Generate tuples of (sender_id, message_text) from the
- provided payload.
- """
- data = json.loads(payload)
- messaging_events = data["entry"][0]["messaging"]
- for event in messaging_events:
- if "message" in event and "text" in event["message"]:
- yield event["sender"]["id"], event["message"]["text"].encode('unicode_escape')
- else:
- yield event["sender"]["id"], "I can't echo this"
- def send_message(token, recipient, text):
- """Send the message text to recipient with id recipient.
- """
- r = requests.post("https://graph.facebook.com/v2.6/me/messages",
- params={"access_token": token},
- data=json.dumps({
- "recipient": {"id": recipient},
- "message": {"text": text.decode('unicode_escape')}
- }),
- headers={'Content-type': 'application/json'})
- if r.status_code != requests.codes.ok:
- print r.text
- if __name__ == '__main__':
- app.run()
讓我們分解代碼。***部分是引入所需的依賴:
- from flask import Flask, request
- import json
- import requests
接下來(lái)我們定義兩個(gè)函數(shù)(使用 Flask 特定的 app.route 裝飾器),用來(lái)處理到我們的機(jī)器人的 GET 和 POST 請(qǐng)求。
- @app.route('/', methods=['GET'])
- def handle_verification():
- print "Handling Verification."
- if request.args.get('hub.verify_token', '') == 'my_voice_is_my_password_verify_me':
- print "Verification successful!"
- return request.args.get('hub.challenge', '')
- else:
- print "Verification failed!"
- return 'Error, wrong validation token'
當(dāng)我們創(chuàng)建 Facebook 應(yīng)用時(shí),verify_token 對(duì)象將由我們聲明的 Messenger 發(fā)送。我們必須自己來(lái)校驗(yàn)它。***我們返回“hub.challenge”給 Messenger。
處理 POST 請(qǐng)求的函數(shù)更有意思一些:
- @app.route('/', methods=['POST'])
- def handle_messages():
- print "Handling Messages"
- payload = request.get_data()
- print payload
- for sender, message in messaging_events(payload):
- print "Incoming from %s: %s" % (sender, message)
- send_message(PAT, sender, message)
- return "ok"
當(dāng)被調(diào)用時(shí),我們抓取消息載荷,使用函數(shù) messaging_events 來(lái)拆解它,并且提取發(fā)件人身份和實(shí)際發(fā)送的消息,生成一個(gè)可以循環(huán)處理的 python 迭代器。請(qǐng)注意 Messenger 發(fā)送的每個(gè)請(qǐng)求有可能多于一個(gè)消息。
- def messaging_events(payload):
- """Generate tuples of (sender_id, message_text) from the
- provided payload.
- """
- data = json.loads(payload)
- messaging_events = data["entry"][0]["messaging"]
- for event in messaging_events:
- if "message" in event and "text" in event["message"]:
- yield event["sender"]["id"], event["message"]["text"].encode('unicode_escape')
- else:
- yield event["sender"]["id"], "I can't echo this"
對(duì)每個(gè)消息迭代時(shí),我們會(huì)調(diào)用 send_message 函數(shù),然后我們使用 Facebook Graph messages API 對(duì) Messenger 發(fā)回 POST 請(qǐng)求。在這期間我們一直沒(méi)有回應(yīng)我們阻塞的原始 Messenger請(qǐng)求。這會(huì)導(dǎo)致超時(shí)和 5XX 錯(cuò)誤。
上述情況是我在解決遇到錯(cuò)誤時(shí)發(fā)現(xiàn)的,當(dāng)用戶發(fā)送表情時(shí)實(shí)際上是發(fā)送的 unicode 標(biāo)識(shí)符,但是被 Python 錯(cuò)誤的編碼了,最終我們發(fā)回了一些亂碼。
這個(gè)發(fā)回 Messenger 的 POST 請(qǐng)求將永遠(yuǎn)不會(huì)完成,這會(huì)導(dǎo)致給初始請(qǐng)求返回 5xx 狀態(tài)碼,顯示服務(wù)不可用。
通過(guò)使用 encode('unicode_escape') 封裝消息,然后在我們發(fā)送回消息前用 decode('unicode_escape') 解碼消息就可以解決。
- def send_message(token, recipient, text):
- """Send the message text to recipient with id recipient.
- """
- r = requests.post("https://graph.facebook.com/v2.6/me/messages",
- params={"access_token": token},
- data=json.dumps({
- "recipient": {"id": recipient},
- "message": {"text": text.decode('unicode_escape')}
- }),
- headers={'Content-type': 'application/json'})
- if r.status_code != requests.codes.ok:
- print r.text
部署到 Heroku
一旦代碼已經(jīng)建立成我想要的樣子時(shí)就可以進(jìn)行下一步。部署應(yīng)用。
那么,該怎么做?
我之前在 Heroku 上部署過(guò)應(yīng)用(主要是 Rails),然而我總是遵循某種教程做的,所用的配置是創(chuàng)建好了的。而在本文的情況下,我就需要從頭開(kāi)始。
幸運(yùn)的是有官方 Heroku 文檔來(lái)幫忙。這篇文檔很好地說(shuō)明了運(yùn)行應(yīng)用程序所需的***限度。
長(zhǎng)話短說(shuō),我們需要的除了我們的代碼還有兩個(gè)文件。***個(gè)文件是“requirements.txt”,它列出了運(yùn)行應(yīng)用所依賴的庫(kù)。
需要的第二個(gè)文件是“Procfile”。這個(gè)文件通知 Heroku 如何運(yùn)行我們的服務(wù)。此外這個(gè)文件只需要一點(diǎn)點(diǎn)內(nèi)容:
- web: gunicorn echoserver:app
Heroku 對(duì)它的解讀是,我們的應(yīng)用通過(guò)運(yùn)行 echoserver.py 啟動(dòng),并且應(yīng)用將使用 gunicorn 作為 Web 服務(wù)器。我們使用一個(gè)額外的網(wǎng)站服務(wù)器是因?yàn)榕c性能相關(guān),在上面的 Heroku 文檔里對(duì)此解釋了:
Web 應(yīng)用程序并發(fā)處理傳入的 HTTP 請(qǐng)求比一次只處理一個(gè)請(qǐng)求的 Web 應(yīng)用程序會(huì)更有效利地用測(cè)試機(jī)的資源。由于這個(gè)原因,我們建議使用支持并發(fā)請(qǐng)求的 Web 服務(wù)器來(lái)部署和運(yùn)行產(chǎn)品級(jí)服務(wù)。
Django 和 Flask web 框架提供了一個(gè)方便的內(nèi)建 Web 服務(wù)器,但是這些阻塞式服務(wù)器一個(gè)時(shí)刻只能處理一個(gè)請(qǐng)求。如果你部署這種服務(wù)到 Heroku 上,你的測(cè)試機(jī)就會(huì)資源利用率低下,應(yīng)用會(huì)感覺(jué)反應(yīng)遲鈍。
Gunicorn 是一個(gè)純 Python 的 HTTP 服務(wù)器,用于 WSGI 應(yīng)用。允許你在單獨(dú)一個(gè)測(cè)試機(jī)內(nèi)通過(guò)運(yùn)行多 Python 進(jìn)程的方式來(lái)并發(fā)的運(yùn)行各種 Python 應(yīng)用。它在性能、靈活性和配置簡(jiǎn)易性方面取得了***的平衡。回到我們之前提到過(guò)的“requirements.txt”文件,讓我們看看它如何結(jié)合 Virtualenv 工具。
很多情況下,你的開(kāi)發(fā)機(jī)器也許已經(jīng)安裝了很多 python 庫(kù)。當(dāng)部署應(yīng)用時(shí)你不想全部加載那些庫(kù),但是辨認(rèn)出你實(shí)際使用哪些庫(kù)很困難。
Virtualenv 可以創(chuàng)建一個(gè)新的空白虛擬環(huán)境,以便你可以只安裝你應(yīng)用所需要的庫(kù)。
你可以運(yùn)行如下命令來(lái)檢查當(dāng)前安裝了哪些庫(kù):
- kostis@KostisMBP ~ $ pip freeze
- cycler==0.10.0
- Flask==0.10.1
- gunicorn==19.6.0
- itsdangerous==0.24
- Jinja2==2.8
- MarkupSafe==0.23
- matplotlib==1.5.1
- numpy==1.10.4
- pyparsing==2.1.0
- python-dateutil==2.5.0
- pytz==2015.7
- requests==2.10.0
- scipy==0.17.0
- six==1.10.0
- virtualenv==15.0.1
- Werkzeug==0.11.10
注意:pip 工具應(yīng)該已經(jīng)與 Python 一起安裝在你的機(jī)器上。如果沒(méi)有,查看官方網(wǎng)站如何安裝它。
現(xiàn)在讓我們使用 Virtualenv 來(lái)創(chuàng)建一個(gè)新的空白環(huán)境。首先我們給我們的項(xiàng)目創(chuàng)建一個(gè)新文件夾,然后進(jìn)到目錄下:
- kostis@KostisMBP projects $ mkdir echoserver
- kostis@KostisMBP projects $ cd echoserver/
- kostis@KostisMBP echoserver $
現(xiàn)在來(lái)創(chuàng)建一個(gè)叫做 echobot 的新環(huán)境。運(yùn)行下面的 source 命令激活它,然后使用 pip freeze 檢查,我們能看到現(xiàn)在是空的。
- kostis@KostisMBP echoserver $ virtualenv echobot
- kostis@KostisMBP echoserver $ source echobot/bin/activate
- (echobot) kostis@KostisMBP echoserver $ pip freeze
- (echobot) kostis@KostisMBP echoserver $
我們可以安裝需要的庫(kù)。我們需要是 flask、gunicorn 和 requests,它們被安裝后我們就創(chuàng)建 requirements.txt 文件:
- (echobot) kostis@KostisMBP echoserver $ pip install flask
- (echobot) kostis@KostisMBP echoserver $ pip install gunicorn
- (echobot) kostis@KostisMBP echoserver $ pip install requests
- (echobot) kostis@KostisMBP echoserver $ pip freeze
- click==6.6
- Flask==0.11
- gunicorn==19.6.0
- itsdangerous==0.24
- Jinja2==2.8
- MarkupSafe==0.23
- requests==2.10.0
- Werkzeug==0.11.10
- (echobot) kostis@KostisMBP echoserver $ pip freeze > requirements.txt
上述完成之后,我們用 python 代碼創(chuàng)建 echoserver.py 文件,然后用之前提到的命令創(chuàng)建 Procfile,我們最終的文件/文件夾如下:
- (echobot) kostis@KostisMBP echoserver $ ls
- Procfile echobot echoserver.py requirements.txt
我們現(xiàn)在準(zhǔn)備上傳到 Heroku。我們需要做兩件事。***是如果還沒(méi)有安裝 Heroku toolbet,就安裝它(詳見(jiàn) Heroku)。第二是通過(guò) Heroku 網(wǎng)頁(yè)界面創(chuàng)建一個(gè)新的 Heroku 應(yīng)用。
點(diǎn)擊右上的大加號(hào)然后選擇“Create new app”。
為你的應(yīng)用選擇一個(gè)名字,然后點(diǎn)擊“Create App”。
你將會(huì)重定向到你的應(yīng)用的控制面板,在那里你可以找到如何部署你的應(yīng)用到 Heroku 的細(xì)節(jié)說(shuō)明。
- (echobot) kostis@KostisMBP echoserver $ heroku login
- (echobot) kostis@KostisMBP echoserver $ git init
- (echobot) kostis@KostisMBP echoserver $ heroku git:remote -a <myappname>
- (echobot) kostis@KostisMBP echoserver $ git add .
- (echobot) kostis@KostisMBP echoserver $ git commit -m "Initial commit"
- (echobot) kostis@KostisMBP echoserver (master) $ git push heroku master
- ...
- remote: https://<myappname>.herokuapp.com/ deployed to Heroku
- ...
- (echobot) kostis@KostisMBP echoserver (master) $ heroku config:set WEB_CONCURRENCY=3
如上,當(dāng)你推送你的修改到 Heroku 之后,你會(huì)得到一個(gè)用于公開(kāi)訪問(wèn)你新創(chuàng)建的應(yīng)用的 URL。保存該 URL,下一步需要它。
創(chuàng)建這個(gè) Facebook 應(yīng)用
讓我們的機(jī)器人可以工作的***一步是創(chuàng)建這個(gè)我們將連接到其上的 Facebook 應(yīng)用。Facebook 通常要求每個(gè)應(yīng)用都有一個(gè)相關(guān)頁(yè)面,所以我們來(lái)創(chuàng)建一個(gè)。
接下來(lái)我們?nèi)?Facebook 開(kāi)發(fā)者專頁(yè),點(diǎn)擊右上角的“My Apps”按鈕并選擇“Add a New App”。不要選擇建議的那個(gè),而是點(diǎn)擊“basic setup”。填入需要的信息并點(diǎn)擊“Create App Id”,然后你會(huì)重定向到新的應(yīng)用頁(yè)面。
在 “Products” 菜單之下,點(diǎn)擊“+ Add Product” ,然后在“Messenger”下點(diǎn)擊“Get Started”。跟隨這些步驟設(shè)置 Messenger,當(dāng)完成后你就可以設(shè)置你的 webhooks 了。Webhooks 簡(jiǎn)單的來(lái)說(shuō)是你的服務(wù)所用的 URL 的名稱。點(diǎn)擊 “Setup Webhooks” 按鈕,并添加該 Heroku 應(yīng)用的 URL (你之前保存的那個(gè))。在校驗(yàn)元組中寫(xiě)入 ‘myvoiceismypasswordverifyme’。你可以寫(xiě)入任何你要的內(nèi)容,但是不管你在這里寫(xiě)入的是什么內(nèi)容,要確保同時(shí)修改代碼中 handle_verification 函數(shù)。然后勾選 “messages” 選項(xiàng)。
點(diǎn)擊“Verify and Save” 就完成了。Facebook 將訪問(wèn)該 Heroku 應(yīng)用并校驗(yàn)它。如果不工作,可以試試運(yùn)行:
- (echobot) kostis@KostisMBP heroku logs -t
然后看看日志中是否有錯(cuò)誤。如果發(fā)現(xiàn)錯(cuò)誤, Google 搜索一下可能是最快的解決方法。
***一步是取得頁(yè)面訪問(wèn)元組(PAT),它可以將該 Facebook 應(yīng)用于你創(chuàng)建好的頁(yè)面連接起來(lái)。
從下拉列表中選擇你創(chuàng)建好的頁(yè)面。這會(huì)在“Page Access Token”(PAT)下面生成一個(gè)字符串。點(diǎn)擊復(fù)制它,然后編輯 echoserver.py 文件,將其貼入 PAT 變量中。然后在 Git 中添加、提交并推送該修改。
- (echobot) kostis@KostisMBP echoserver (master) $ git add .
- (echobot) kostis@KostisMBP echoserver (master) $ git commit -m "Initial commit"
- (echobot) kostis@KostisMBP echoserver (master) $ git push heroku master
***,在 Webhooks 菜單下再次選擇你的頁(yè)面并點(diǎn)擊“Subscribe”。
現(xiàn)在去訪問(wèn)你的頁(yè)面并建立會(huì)話:
成功了,機(jī)器人回顯了!
注意:除非你要將這個(gè)機(jī)器人用在 Messenger 上測(cè)試,否則你就是機(jī)器人唯一響應(yīng)的那個(gè)人。如果你想讓其他人也試試它,到Facebook 開(kāi)發(fā)者專頁(yè)中,選擇你的應(yīng)用、角色,然后添加你要添加的測(cè)試者。
總結(jié)
這對(duì)于我來(lái)說(shuō)是一個(gè)非常有用的項(xiàng)目,希望它可以指引你找到開(kāi)始的正確方向。官方的 Facebook 指南有更多的資料可以幫你學(xué)到更多。
你可以在 Github 上找到該項(xiàng)目的代碼。
如果你有任何評(píng)論、勘誤和建議,請(qǐng)隨時(shí)聯(lián)系我。