轉眼間兩個星期就快過了,我們已經從 Python 的基礎,每天多認識一點點,直到利用 Heroku 提供的伺服器,發佈了我們的第一個 LINE 聊天機器人,到現在對於 LINE 提供的應用程式編程介面也越來越熟悉了!如果大家手邊也有發佈在 Heroku 的聊天機器人,不知道有沒有注意到一個情況:當你很久沒發訊息給你的聊天機器人,突然心血來潮想找他聊個天,這時卻發現他不理你了。喔不,不是他不理你了,而是他對你的訊息回覆得特別慢。這是什麼情況呢?他鬧彆扭了嗎?別擔心,問題不在於你,也不在於你的聊天機器人,而是在 Heroku 上。怎麼說呢?這就要從 Heroku 的免費方案開始說起。
Heroku 將我們放置應用程式的空間叫做 dyno。由於不同的使用者會有不同的需求,從應用程式的檔案大小、運算效能、檔案流量等等,並不是每一位使用者都需要非常高規格的 dyno,因此 Heroku 將 dyno 依照使用者的需求做了分類➀。我們目前用的,就是 Heroku 裡面規格最簡單,價格也最便宜的免費 dyno。雖說是最簡單的規格,但也提供了 512 MB 的檔案空間,以我們目前設計的聊天機器人來說,已經相當夠用(到今天的比賽為止,我設計的聊天機器人檔案大小不到 50 MB)。免費 dyno 唯一的缺點,就是容易睡著,變成沉睡的 Heroku。具體來說是什麼意思呢?就是當發佈在 Heroku 上的應用程式,若都無人使用, 30 分鐘過後會自動進入睡眠狀態。進入睡眠狀態的應用程式若被再次觸發,則需花費大約 20 秒的喚醒時間。
以聊天機器人為例。若聊天機器人是發佈在 Heroku 上,那我們只要傳訊息給聊天機器人,Heroku 上的應用程式就會運作。如果 30 分鐘內都無人發送訊息,Heroku 的應用程式就會進入睡眠狀態,直到聊天機器人又收到訊息為止。而且此時聊天機器人沒辦法馬上處理訊息,必須花個約 20 秒的時間,讓 Heroku 喚醒應用程式,之後聊天機器人才會開始處理訊息。因此你的聊天機器人才會看起來像是慢了半拍,或是愛理不理的樣子。
Heroku 免費 dyno 所提供的規格:
這之中最讓人覺得棘手的,就是入睡的限制。
怎麼辦呢?憑我照顧草泥馬多年的經驗,我發現:歷史是人們最好的老師,往往可以藉鏡效法。我翻開《漢書》,看到古有蘇秦,發憤苦讀,懸梁刺股。我打開電視,看到今有瑤瑤,白馬馬力夯,不讓你睡。我有了參考,兩相激盪,腦中靈感猶如長江流水,滔滔不絕,嘴角不禁露出一絲笑容:有了!讓我先賣個關子。
首先,我們要做一個網頁出來。怎麼做呢?其實不難,Heroku 已經給我們網址了,我們只要寫一個 HTML 5 的檔案並推向 Heroku 就可以了。Heroku 給我們的網址在哪裡呢?還記得 LINE Developers 裡面聊天機器人的 Channel settings 下,我們用來放置 Webhook URL 的地方,是不是填上了 "https://你-APP-的名字.herokuapp.com/callback" 呢?其中 "https://你-APP-的名字.herokuapp.com/" 就是 Heroku 給我們的第一個網址。不信你試著連過去看看,是不是看到跟我圖一一樣的情形呢?沒錯,Heroku 是給了我們網址,但我們根本還沒動手建構他呢,當然是囉!
圖一、Not Found
要怎麼做一個 HTML 5 的網頁呢?這也不難,開一個空白記事本,打上如下文字:
<!doctype html>
<html>
<head>
<title>Simple is BEST</title>
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>
然後存檔時打出副檔名html
,比如說直接存成home.html
,那麼從今天起,這就是一個 HTML 5 的檔案了!如果不喜歡用記事本,白紙黑字太乏味,或是覺得這樣會讓你看不清楚段落,那麼可以試著利用 Codepen。網址點進去如圖二,按左邊 "Start Coding" 如圖三,彩色的來囉:
圖二、Codepen,Start Coding!
圖三、Codepen 編輯系統
Codepen 是個很方便的工具,它會立即顯示我們設計過後的 HTML 5 檔案放上瀏覽器後的真實樣貌供你參考。以圖三為例,下方就是我們胡亂製造出來的 HTML 5 放到 Heroku 上之後的樣子。簡潔有力,對吧?因為今天重點不在設計網頁,而是在懸梁刺股、不讓你睡,所以這邊我們暫時先這樣帶過。
好啦,我們現在有一個 Simple is BEST 的 HTML 5 的檔案了,要怎麼讓 Heroku 知道當使用者嘗試瀏覽 "https://你-APP-的名字.herokuapp.com/" 的時候,就將這個檔案呈現給使用者看呢?這就要使用到我們在製作 LINE 聊天機器人的 Webhook URL 時的相同技巧了。
還記的我們在製作 Webhook URL 的時候,在 Python 檔案裡面打出了一段:
# 接收 LINE 的資訊
@app.route("/callback", methods=['POST'])
def callback():
signature = request.headers['X-Line-Signature']
body = request.get_data(as_text=True)
app.logger.info("Request body: " + body)
try:
handler.handle(body, signature)
except InvalidSignatureError:
abort(400)
return 'OK'
而我對@app.route("/callback", methods=['POST'])
這句程式碼的解釋是,我們利用 Python 套件 flask 的幫助,告訴 Heorku,只要有人(以這個例子來說,是 LINE 送來資訊)呼叫 "https://你-APP-的名字.herokuapp.com/callback" ,就執行下列程式碼。太簡單了!以此類推,我們只要用同樣的手段,告訴 Heroku,只要有人呼叫 "https://你-APP-的名字.herokuapp.com/" ,就傳我們熱騰騰剛出爐的 HTML 5 檔案給他看。來試試吧:
from flask import render_template
@app.route("/")
def home():
return render_template("home.html")
第三行:@app.route("/")
當有人呼叫 "https://你-APP-的名字.herokuapp.com/" ,就執行下述程式碼。仔細看看,跟當初我們在製作 "/callback" 時好像有些不同,少了methods
這個參數。methods
是做什麼用的參數呢?它定義了使用者請求(request)該網址時可以使用的方法。什麼意思呢?當 LINE 試著連絡我們的 "/callback" 時,它是帶著一份資料過來的,帶著一份「誰傳了怎麼樣的信息給我們的聊天機器人」過來的,所以要用POST
。不過呢,這次我們設計 "/" 的時候,使用者是空著手來的,他們反而是來跟我們要資料,要代表網址為 "/" 的資料,所以是用GET
➂。一般我們在瀏覽網頁,用的多半是GET
,所以當我們宣告@app.route("/")
,而沒有特別表明methods
是哪一種方法的時候,就是默認要用GET
了。
第四行:def home():
定義一個函數,每次有人呼叫 "https://你-APP-的名字.herokuapp.com/" ,就執行這個函數。
第五行: return render_template("home.html")
每次有人呼叫 "https://你-APP-的名字.herokuapp.com/" ,就利用 flask 的方法render_template
傳回(return
)我們寫好的home.html
。因為在這裡我們用到了render_template
,所以一開始記得宣告from flask import render_template
喔!
好了,太棒了,我們新的 Python 檔案應該會長這樣:
from __future__ import unicode_literals
import os
# 增加了 render_template
from flask import Flask, request, abort, render_template
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent, TextMessage, TextSendMessage, ImageSendMessage
import configparser
import urllib
import re
import random
app = Flask(__name__)
# LINE 聊天機器人的基本資料
config = configparser.ConfigParser()
config.read('config.ini')
line_bot_api = LineBotApi(config.get('line-bot', 'channel_access_token'))
handler = WebhookHandler(config.get('line-bot', 'channel_secret'))
# 增加的這段放在下面
@app.route("/")
def home():
return render_template("home.html")
# 增加的這段放在上面
# 接收 LINE 的資訊
@app.route("/callback", methods=['POST'])
def callback():
signature = request.headers['X-Line-Signature']
body = request.get_data(as_text=True)
app.logger.info("Request body: " + body)
try:
handler.handle(body, signature)
except InvalidSignatureError:
abort(400)
return 'OK'
…以下略
if __name__ == "__main__":
app.run()
把新的 Python 檔案推向 Heroku 之前,還有一件事,那就是home.html
的檔案位置。我習慣會把我要推向 Heroku 的資料夾布置成如圖四,而把home.html
放在template
這個資料夾裡:
圖四、將home.html
放在template
資料夾裡
再詳細一點:
D:\alpaca_fighting>tree /F
Folder PATH listing
Volume serial number is 9C33-6XXD
D:.
│ runtime.txt
│ app_pixabay.py
│ config.ini
│ Procfile
│ requirements.txt
│
└───templates
home.html
大概像這樣,那麼接著我們就可以把資料夾推向 Heroku 囉!完成之後後連連看 "https://你-APP-的名字.herokuapp.com/" !
好,終於進到今天的重頭戲 APScheduler 了!這是可是 Heroku 官方推薦的鬧鐘➃!輕薄短小,但保證叫醒你。APScheduler 其實是 Python 的第三方套件,主要的用途就是按表操課,按照規定的時間執行 Python 程式碼。而這套件的用法在 Heroku 官方上寫得相當清楚,首先,弄一個空白的 Python 檔案出來,檔案名稱取什麼都行,不過既然這是鬧鐘,我們就用clock.py
?然後在裡面寫下下列程式碼:
from apscheduler.schedulers.blocking import BlockingScheduler
sched = BlockingScheduler()
@sched.scheduled_job('interval', minutes=3)
def timed_job():
print('This job is run every three minutes.')
@sched.scheduled_job('cron', day_of_week='mon-fri', hour=17)
def scheduled_job():
print('This job is run every weekday at 5pm.')
sched.start()
讓我來慢慢解釋:
from apscheduler.schedulers.blocking import BlockingScheduler
BlockingScheduler
。總而言之照著做。 sched = BlockingScheduler()
BlockingScheduler()
物件,並貼上sched
的標籤方便後續操作。除非你希望換個標籤,不然照著做。@sched.scheduled_job('interval', minutes=3)
scheduled_job()
這個函數的第一個參數'interval'
,告訴 Python,請每隔多少時間,就執行下述程式碼。以這行程式碼為例,就是希望每隔 3 分鐘執行一次。@sched.scheduled_job('cron', day_of_week='mon-fri', hour=17)
scheduled_job()
這個函數的第一個參數'cron'
,告訴 Python,當幾年幾月幾日幾點幾分幾秒的時候,總而言之就是特定時間,執行下述程式碼。以這行程式碼為例,就是希望星期一到星期五的下午 5 點,都請執行一次。 夠直覺,夠簡單了吧!說到這裡,大家應該也猜到我要怎麼不讓 Heroku 睡了吧?沒錯,我要用 APScheduler 每隔一段時間就用我們學過的urllib
去呼叫 "https://你-APP-的名字.herokuapp.com/" 。自己叫醒自己,沒亂說話,真的是從《漢書》學來的呢。不過讓 Heroku 一直這麼醒著,稍微有個問題:那就是我沒有信用卡啊,沒辦法做信用卡認證,用免費 dyno 的話一個月只能讓 Heroku 清醒 550 小時。超過 550 小時之後會怎麼樣呢?Heroku 的官方說法是,超過之後,你所有採用免費 dyno 的應用程式都會進入睡眠狀態,直到下個月來臨。我沒試過,但也不想試,所以我決定換個方法,讓 Heroku 只在工作天(星期一到星期五)醒來。所幸強大的 APScheduler 可以輕易幫我們做到這件事!寫下程式碼之前,大家可以參考 APScheduler 提供的一些參數和符號,如表一、表二➄。
表一、cron常用的時間參數➄
參數 | 可接受之參數類型 | 範例 |
---|---|---|
year |
整數或字串 | 4-digit year |
month |
整數或字串 | month (1-12) |
day |
整數或字串 | day of the (1-31) |
week |
整數或字串 | ISO week (1-53) |
day_of_week |
整數或字串 | number or name of weekday (0-6 or mon,tue,wed,thu,fri,sat,sun) |
hour |
整數或字串 | hour (0-23) |
minute |
整數或字串 | minute (0-59) |
second |
整數或字串 | second (0-59) |
start_date |
整數或字串 | earliest possible date/time to trigger on (inclusive) |
end_date |
整數或字串 | latest possible date/time to trigger on (inclusive) |
表二、常用於時間參數的特殊符號➄
符號 | 可用於 | 執行方式 |
---|---|---|
* |
任何參數 | 每單位間隔執行一次 |
*/a |
任何參數 | 每 a 個單位間隔執行一次 |
a-b |
任何參數 | 從 a 到 b 每單位間隔執行一次 |
a-b/c |
任何參數 | 從 a 到 b 每 c 個單位間隔執行一次 |
以我而言,為了避免免費 dyno 超過半小時就開始打瞌睡的問題,我希望在星期一到星期五的時候,每隔 20 分鐘就呼喊一次 "https://你-APP-的名字.herokuapp.com/" 。下面是以此為目標寫出的程式碼:
from apscheduler.schedulers.blocking import BlockingScheduler
sched = BlockingScheduler()
@sched.scheduled_job('cron', day_of_week='mon-fri', minute='*/20')
def scheduled_job():
url = "https://你-APP-的名字.herokuapp.com/"
conn = urllib.request.urlopen(url)
for key, value in conn.getheaders():
print(key, value)
sched.start()
跟大家說個抱歉,原本想今天一天拚完 APScheduler 的,不過時間好像快來不及了。有興趣的可以先參考 Heroku 官網的說明這裡➃ 。我明天會把剩下的部分做個完整的說明,謝謝大家的包涵!若今天的內容有哪些呈現方式不太清楚的,也歡迎大家留言,我會在明天一併分享一次。
➀ Heroku dyno type 使用說明
➁ Heroku 信用卡認證
➂ 卡米狗 request 方法說明
➃ Heroku APScheduler 使用說明
➄ APScheduler cron 使用說明書
註:對於此系列文有興趣的讀者,歡迎參考由此系列文擴編成書的 LINE Bot by Python,以及最新的系列文《賴田捕手:追加篇》
第 31 天 初始化 LINE BOT on Heroku
第 32 天 快速回覆 QuickReply 介紹
第 33 天 妥善運用 Heroku APP 暫存空間
第 34 天 妥善運用 LINE Notify 免費推播
第 35 天 製造 Deploy to Heroku 按鈕