iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 13
0

第 13 天:LINE BOT SDK:Heroku 夜未眠(一)

  轉眼間兩個星期就快過了,我們已經從 Python 的基礎,每天多認識一點點,直到利用 Heroku 提供的伺服器,發佈了我們的第一個 LINE 聊天機器人,到現在對於 LINE 提供的應用程式編程介面也越來越熟悉了!如果大家手邊也有發佈在 Heroku 的聊天機器人,不知道有沒有注意到一個情況:當你很久沒發訊息給你的聊天機器人,突然心血來潮想找他聊個天,這時卻發現他不理你了。喔不,不是他不理你了,而是他對你的訊息回覆得特別慢。這是什麼情況呢?他鬧彆扭了嗎?別擔心,問題不在於你,也不在於你的聊天機器人,而是在 Heroku 上。怎麼說呢?這就要從 Heroku 的免費方案開始說起。

Heroku 免費伺服器(free dyno)

  Heroku 將我們放置應用程式的空間叫做 dyno。由於不同的使用者會有不同的需求,從應用程式的檔案大小、運算效能、檔案流量等等,並不是每一位使用者都需要非常高規格的 dyno,因此 Heroku 將 dyno 依照使用者的需求做了分類➀。我們目前用的,就是 Heroku 裡面規格最簡單,價格也最便宜的免費 dyno。雖說是最簡單的規格,但也提供了 512 MB 的檔案空間,以我們目前設計的聊天機器人來說,已經相當夠用(到今天的比賽為止,我設計的聊天機器人檔案大小不到 50 MB)。免費 dyno 唯一的缺點,就是容易睡著,變成沉睡的 Heroku。具體來說是什麼意思呢?就是當發佈在 Heroku 上的應用程式,若都無人使用, 30 分鐘過後會自動進入睡眠狀態。進入睡眠狀態的應用程式若被再次觸發,則需花費大約 20 秒的喚醒時間。
  以聊天機器人為例。若聊天機器人是發佈在 Heroku 上,那我們只要傳訊息給聊天機器人,Heroku 上的應用程式就會運作。如果 30 分鐘內都無人發送訊息,Heroku 的應用程式就會進入睡眠狀態,直到聊天機器人又收到訊息為止。而且此時聊天機器人沒辦法馬上處理訊息,必須花個約 20 秒的時間,讓 Heroku 喚醒應用程式,之後聊天機器人才會開始處理訊息。因此你的聊天機器人才會看起來像是慢了半拍,或是愛理不理的樣子。

  Heroku 免費 dyno 所提供的規格:

  • dyno 會入睡。若 30 分鐘內無人使用,則進入睡眠狀態。喚醒需花約 20 秒。
  • 每個月提供免費運作時間為 550 小時,通過信用卡認證後可增加至 1000 小時➁。
  • 資料庫(database)免費可記錄 1 萬筆資料,最多同時提供 20 個連線服務。

  這之中最讓人覺得棘手的,就是入睡的限制。

  怎麼辦呢?憑我照顧草泥馬多年的經驗,我發現:歷史是人們最好的老師,往往可以藉鏡效法。我翻開《漢書》,看到古有蘇秦,發憤苦讀,懸梁刺股。我打開電視,看到今有瑤瑤,白馬馬力夯,不讓你睡。我有了參考,兩相激盪,腦中靈感猶如長江流水,滔滔不絕,嘴角不禁露出一絲笑容:有了!讓我先賣個關子。

用 Flask 做一個網頁

  首先,我們要做一個網頁出來。怎麼做呢?其實不難,Heroku 已經給我們網址了,我們只要寫一個 HTML 5 的檔案並推向 Heroku 就可以了。Heroku 給我們的網址在哪裡呢?還記得 LINE Developers 裡面聊天機器人的 Channel settings 下,我們用來放置 Webhook URL 的地方,是不是填上了 "https://你-APP-的名字.herokuapp.com/callback" 呢?其中 "https://你-APP-的名字.herokuapp.com/" 就是 Heroku 給我們的第一個網址。不信你試著連過去看看,是不是看到跟我圖一一樣的情形呢?沒錯,Heroku 是給了我們網址,但我們根本還沒動手建構他呢,當然是囉!

https://ithelp.ithome.com.tw/upload/images/20190921/20120178daGi7UKp9A.png
圖一、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" 如圖三,彩色的來囉:

https://ithelp.ithome.com.tw/upload/images/20190921/20120178d2V0H8cJbD.png
圖二、Codepen,Start Coding!

https://ithelp.ithome.com.tw/upload/images/20190921/20120178KGq2DsK9Ww.png
圖三、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這個資料夾裡:

https://ithelp.ithome.com.tw/upload/images/20190921/20120178kiKcPuj6MC.png
圖四、將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

  好,終於進到今天的重頭戲 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
      從 APScheduler 的套件裡拿出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 使用說明書


上一篇
第 12 天:LINE BOT SDK:應用程式編程介面(續)
下一篇
第 14 天:LINE BOT SDK:Heroku 夜未眠(二)
系列文
從LINE BOT到資料視覺化:賴田捕手30

尚未有邦友留言

立即登入留言