iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 14
0

第 14 天:LINE BOT SDK:Heroku 夜未眠(二)

我從星期一開始做了整整五天的工作直到周末。了不起的 120 個小時。我並不是因為喜歡工作或是怎樣所以才決定這麼搞的。更多的原因大概是是我厭倦了入睡然後又被搖醒然後又入睡然後又被搖醒的這個過程。有些時候人們就是會突然無法忍耐睡眠被強迫打斷。當那個時候到了,你就會覺得與其如此,那還不如一直工作,不論有多少工作、不論要花多久時間。

~節錄自《賴田捕手》第十三章

  由於 Heroku 的免費方案會讓 dyno 在 30 分鐘無人打擾之後陷入沉睡狀態,造成下次呼叫時需要較長的喚醒時間,而導致較差的使用者體驗➀。為了能夠讓 Heroku 保持清醒狀態,我們用 APScheduler 讓我們的免費 dyno 在快要睡著的時候,呼叫 "https://你-APP-的名字.herokuapp.com/" ,自己喚醒自己。不僅不發薪水,還要 Heroku 全天候工作,我們還真是個慣老闆啊。

發布鬧鐘(clock.py)

  於是乎,我們昨天寫了個 HTML 5 的檔案,將該檔案放在 "https://你-APP-的名字.herokuapp.com/" 上面。另外,除了用來運作 LINE 聊天機器人的 Python 檔案之外,我們根據 APScheduler 提供的使用說明書➁,寫了另一個 Python 檔案clock.py。今天就來講講,怎麼讓 Heroku 認識這個clock.py並且按照裡面的指令執行工作。
  下面是我們昨天寫的clock.py,因為我沒有信用卡,一個月最多只能有 550 小時的免費 dyno 時間,所以我讓他從星期一工作到星期五,六日放假休息:

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()

  將這個檔案放到我們要推向 Heroku 的資料夾中:

D:\alpaca_fighting>tree /F
Folder PATH listing
Volume serial number is 9C33-6XXD
D:.
│   runtime.txt
│   app_pixabay.py
│   clock.py
│   config.ini
│   Procfile
│   requirements.txt
│
└───templates
        home.html

  並且將Procfilerequirements.txt修改一下:

web: gunicorn app_pixabay:app --preload
clock: python clock.py
Flask==1.1.1
gunicorn==19.9.0
line-bot-sdk==1.14.0
APScheduler==3.6.1

  所以我們的 Heroku 應用程式現在有了兩個功能,第一個是提供 web 服務的app_pixabay.py,第二個則是鬧鐘功能clock.py。沒問題之後,就這些檔案推向 Heroku,然後從瀏覽器登入我們的 Heroku 帳戶做些設定。一進入主畫面(dashboard)可以看到各種資料,包括我們目前選擇的 dyno 方案、資費、擴充功能、上傳紀錄等等,如圖一。在 dyno 組成(Dyno formation)那一欄,赫然發現了clock python clock.py,表示我們成功讓 Heroku 認識了新增加的鬧鐘功能。那麼,還需要做點小設定的地方是在哪裡呢?大家可以看到clock python clock.py的右手邊,又赫然發現居然是代表 turn off 的OFF。沒錯,我們就是要把這個OFF調整成ON

https://ithelp.ithome.com.tw/upload/images/20190922/20120178WVYvWOwQN7.png
圖一、Heroku 的主畫面(dashboard)

  首先,看到 dyno 組成的右手邊,有一個 "Configure Dynos",點下去,來到如圖二,看到好多東西可以選喔,要選什麼呢?大家看到 "Free Dynos" 的右手邊,有一個 "Change Dyno Type",那是更改資費的地方。如果大家想要替自己的 dyno 選擇較好的規格,並且付錢的話,就可以點進去進行升級。就算沒打算升級,也可以進去看看 Heroku 提供了哪些高級的規格,並與我們目前採用的免費 dyno 比較比較➂!

https://ithelp.ithome.com.tw/upload/images/20190922/20120178pQGQBKUtIQ.png
圖二、Configure Dynos

  我們要按的,是clock python clock.py最右手邊代表編輯的鉛筆,點了之後,就可以將OFF改成ON啦!如圖三。如此一來便大功告成了!

https://ithelp.ithome.com.tw/upload/images/20190922/20120178EFtJfgpuvd.png
圖三、將clock.py調整為ON

  咦,所以說今天就到這結束了嗎?還早還早呢。

Heroku 時區(time zone)

  不知道大家有沒有注意到,Heroku 住在一個跟我們不一樣的時區。比如說,我們可以試著用 Python 內建的模組datetime來查詢一下:

In [1]: import datetime
In [2]: datetime.datetime.now().ctime()
Out[2]: 'Sun Sep 22 10:41:49 2019'

  利用datetime.datetime.now()可以查詢現在的時間,再利用ctime()將時間的表示方式做個美化(大家可以自己試試看有沒有ctime()的差異,說不定有些人寧願用datetime.datetime.now())。將這行程式碼寫到我們的clock.py裡面:

@sched.scheduled_job('cron', minute='*/2')
def scheduled_job():
    print('========== APScheduler CRON =========')
    # 馬上讓我們瞧瞧
    print('This job runs every day */2 min.')
    # 利用datetime查詢時間
    print(f'{datetime.datetime.now().ctime()}')
    print('========== APScheduler CRON =========')

    url = "https://你-APP-的名字.herokuapp.com/"
    conn = urllib.request.urlopen(url)
        
    for key, value in conn.getheaders():
        print(key, value)

  為了要立即看到效果,我把 APScheduler 的工作設定為@sched.scheduled_job('cron', minute='*/2'),每天每兩分鐘執行一次。那這個 Python 執行的print會送到哪邊呢?答案是 Heroku 應用程式的工作日誌裡。從 Heroku 主畫面的右上角點 "More" ,選 "View logs"。好囉我們來看看吧!

2019-09-22T02:15:00.027962+00:00 app[clock.1]: ========== APScheduler CRON =========
2019-09-22T02:15:00.028271+00:00 app[clock.1]: This job is run every day */2 min
2019-09-22T02:15:00.028314+00:00 app[clock.1]: Sun Sep 22 02:15:00 2019
2019-09-22T02:15:00.028321+00:00 app[clock.1]: ========== APScheduler CRON =========
2019-09-22T02:15:00.048610+00:00 app[clock.1]: Connection close
2019-09-22T02:15:00.048671+00:00 app[clock.1]: Server gunicorn/19.9.0
2019-09-22T02:15:00.048713+00:00 app[clock.1]: Date Sun, 22 Sep 2019 02:15:00 GMT
2019-09-22T02:15:00.048750+00:00 app[clock.1]: Content-Type text/html; charset=utf-8
2019-09-22T02:15:00.048789+00:00 app[clock.1]: Content-Length 3429
2019-09-22T02:15:00.048879+00:00 app[clock.1]: Via 1.1 vegur

  恩,我們現在是早上 10 點,但 Heroku 卻說他現在是凌晨 2 點。真的是活在不同時區裡啊!這會影響到我們應用程式真正的清醒時間。當然,你可以自己算時差,並在clock.py中做出相對應的更動。但我數學不好,只想直接告訴 Heroku 我住在台灣,請他幫我算時差。做得到嗎?

  Heroku: As you wish!

  有兩種方法,第一種是從命令提示字元裡,利用 Heroku CLI 下達指令。相信大家都用命令提示字元用得相當上手了,這點簡單的操作誰也難不倒。當然是要記得先從命令提示字元登入 Heroku,接著輸入:

D:\alpaca_fighting>heroku config:add TZ="Asia/Taipei"
Setting TZ and restarting ⬢ 你-APP-的名字... done, v23
TZ: Asia/Taipei

  如此一來就搞定了。不過我還是想說一下第二種方法,就是從 Heroku 美輪美奐的主畫面(dashboard)去做更改。怎麼做呢?
  先將顯示的資料從 "Overview" 切換成 "Settings"。接著點擊 "Reveal Config Vars",如圖四。然後在 "Config Vars" 的 "KEY" 欄位輸入代表時區的TZ,"VALUE" 欄位輸入代表台灣的 "Asia/Taipei",如圖五。按下 "Add" 之後就設定完成囉!再回來看看我們工作日誌的訊息吧。

https://ithelp.ithome.com.tw/upload/images/20190922/20120178ucE1Hv99MJ.png
圖四、查看 Config vars

https://ithelp.ithome.com.tw/upload/images/20190922/20120178mPwaQOBZOX.png
圖五、新增 Config vars

2019-09-22T02:18:00.006470+00:00 app[clock.1]: ========== APScheduler CRON =========
2019-09-22T02:18:00.006494+00:00 app[clock.1]: This job is run every weekday */2 min
2019-09-22T02:18:00.006500+00:00 app[clock.1]: Sun Sep 22 10:18:00 2019
2019-09-22T02:18:00.006523+00:00 app[clock.1]: ========== APScheduler CRON =========
2019-09-22T02:18:00.088197+00:00 app[clock.1]: Connection close
2019-09-22T02:18:00.088604+00:00 app[clock.1]: Server gunicorn/19.9.0
2019-09-22T02:18:00.088644+00:00 app[clock.1]: Date Sun, 22 Sep 2019 02:18:00 GMT
2019-09-22T02:18:00.088679+00:00 app[clock.1]: Content-Type text/html; charset=utf-8
2019-09-22T02:18:00.088716+00:00 app[clock.1]: Content-Length 3429
2019-09-22T02:18:00.088752+00:00 app[clock.1]: Via 1.1 vegur

看看上面那段訊息的第三行:
2019-09-22T02:18:00.006500+00:00 app[clock.1]: Sun Sep 22 10:18:00 2019

  這是我們利用datetime.datetime.now().ctime()得到的訊息。喔喔,我們幫 Heroku 對時啦!若想更改時區到其他位置的,可以參考 wiki 時區➃。

LINE 聊天機器人主動推送信息(push_message())

  現在有了知道我們住在台灣的 Heroku 應用程式,還能做什麼呢,當然是用 LINE 聊天機器人叫我們起床囉!之前我們的 LINE 聊天機器人只能被動地用reply_message()回覆信息,現在我們要教 LINE 聊天機器人用push_message()來主動的傳一些信息。不過話說在前面,因為我們目前也是用免錢的,所以有些限制,如圖六。輕用量的 LINE 聊天機器人,每個月只能主動推送 500 則信息。被動回覆信息則沒有限制。

https://ithelp.ithome.com.tw/upload/images/20190922/201201784ZXK0oo7Bn.png
圖六、LINE 輕用量規範

  主動推送的程式碼是這麼寫的➄:

line_bot_api.push_message(to, TextSendMessage(text='Good Morning!'))

  第一個參數to是主動推送信息時,推送對象的user_id。第二個則是你要推送哪種信息。以上面的例子來說,我們是要推送文字信息,信息內容則是Good Morning!',很簡單吧?但是等等,我怎麼知道我的user_id呢?給大家一點提示,讓 LINE 聊天機器人回覆這些資料給我們吧(或是你想印在 Heroku 工作日誌上也行)!
  回到屬於 LINE 聊天機器人的 Python 檔案:

line_bot_api.reply_message(
    event.reply_token,
    TextSendMessage(text=str(event))
)

  還有一個地方要注意一下,如果各位像我一樣,是把reply_message()包在一個負責回應的函數裡,如下:

@handler.add(MessageEvent, message=TextMessage)
def pixabay_isch(event):
    
    if event.source.user_id != "Udeadbeefdeadbeefdeadbeefdeadbeef":
    
        # 先找圖
        try:
            # 用來找圖的程式碼以下略
            
        # 找不到圖就傳str(event)
        except:
            line_bot_api.reply_message(
                event.reply_token,
                TextSendMessage(text=str(event))
            )

  我之前用tryexcept來讓 LINE 機器人先找圖,找不到圖才回str(event)。我想要快速看到我的user_id要怎麼做呢?傳一堆奇怪的文字給 LINE 聊天機器人,看到底在怎樣的極限情況下會找不到圖,然後回傳user_id給我?當然是個方法。但其實我們只要用放個#用來找圖的程式碼裡面,讓那段程式碼崩潰,這也是一個可行的方法。
  再給大家參考一下,LINE 的 event 物件是長這樣:

event = {"reply_token":"就是代表reply_token的一串亂碼", 
         "type":"message",
         "timestamp":"1462629479859", 
         "source":{"type":"user",
                   "user_id":"就是代表user的一串亂碼"}, 
         "message":{"id":"就是代表這次message的一串代碼", 
                    "type":"text", 
                    "text":"使用者傳來的文字信息內容"}}

  所以如果我們不想看那麼多,只想看user_id,那麼將TextSendMessage(text=str(event))改成TextSendMessage(text=str(event.source.user_id))也是可以的。

  把這個查詢user_id的 LINE 聊天機器人推向 Heroku 之後,再有禮貌的去請教他,如圖七

https://ithelp.ithome.com.tw/upload/images/20190922/20120178QNI3JbHA6b.png
圖七、透過 LINE 查詢user_id

  這樣就拿到我們的user_id囉。

  回到我們想要主動推送信息的那段程式碼,改成這樣

line_bot_api.push_message('你的user_id', TextSendMessage(text='Good Morning!'))

  就會主動發送信息到我們的帳號囉!

查詢免費 dyno 時間(free dyno hours)

  最後我來說明一下清醒/睡眠的運作機制,以及如何查詢我們還剩下多少免費 dyno 時間。
  首先,不要以為我們擺了一個clock.py在 dyno 裡面,該 dyno 就會在特定時間從睡眠轉為清醒,然後執行程式碼。並不是這樣子。dyno 睡著就是睡著了,誰管你還在什麼時間安排工作給誰啊?以我為例,我利用 APScheduler 在 dyno 還清醒的時候,每 20 分鐘呼叫它一次,不讓它睡,藉此令 dyno 在星期一到星期五不間斷的工作。然後六日休息,然後下星期一也休息。什麼,不是說好星期一開工嗎?因為 dyno 已經睡著了,睡到翻掉了,根本就不知道還有一個clock.py在等它。怎麼辦呢?星期一再利用 LINE 聊天機器人,或是呼叫 "https://你-APP-的名字.herokuapp.com/" 叫醒它就可以了。dyno 醒了以後,就會看到clock.py,就會繼續開始工作了。
  所以說,如果大家真的想讓 LINE 聊天機器人叫你起床,記得要先確認 dyno 是清醒的啊。不然到時候它也睡了,你也睡了,大家都睡了,你就遲到了。

https://ithelp.ithome.com.tw/upload/images/20190922/20120178WfsZA35ZYv.png
圖八、睡著的 dyno

https://ithelp.ithome.com.tw/upload/images/20190922/201201789SDjquxqxm.png
圖九、清醒的 dyno

  免費 dyno 每個月的免費工作時間有 550 小時。我要怎麼知道 dyno 是不是快要超時工作了呢?

D:\alpaca_fighting>heroku ps -a 你-APP-的名字
Free dyno hours quota remaining this month: 535h 9m (97%)
Free dyno usage for this app: 12h 26m (2%)
For more information on dyno sleeping and how to upgrade, see:
https://devcenter.heroku.com/articles/dyno-sleeping

=== clock (Free): python clock.py (1)
clock.1: up 2019/09/22 11:52:53 +0800 (~ 46m ago)

=== web (Free): gunicorn app_pixabay:app --preload (1)
web.1: up 2019/09/22 11:52:57 +0800 (~ 46m ago)

  在命令提示字元中登入 Heroku 帳號,輸入heroku ps -a 你-APP-的名字,就會找到我想要的答案了。原來我這個月才用了 2% 而已呀?Heroku 的免費 dyno 真的很厲害!
  今天的內容就真正到此為止了。相關的程式碼我一樣放在 Github 上了(往這邊走),有興趣的可以過去看看。如果覺得內容有不清楚的地方,或是使用 APScheduler 還不是那麼熟悉,都歡迎在下面留言,我會盡可能地回答的。謝謝大家!

參考資料

➀ Heroku 免費 dyno 時間 說明
➁ APScheduler cron 使用說明書
➂ Heroku dyno 價格一覽表
➃ 時區 標準格式
➄ LINE BOT SDK 推送信息使用說明


上一篇
第 13 天:LINE BOT SDK:Heroku 夜未眠(一)
下一篇
第 15 天:LINE BOT SDK:Heroku Postgres 資料庫
系列文
從LINE BOT到資料視覺化:賴田捕手30

尚未有邦友留言

立即登入留言