讓LINE幫忙提醒PChome口罩可購買狀態 – 樹莓派篇

樹莓派定時關注商品是否開始販售或特價,並利用LINE的即時推撥功能隨時通知,讓我們能順利買到特價品


觀念篇(如果沒有看過請務必前往後再回到這篇教學)有提到整個運作的原理跟流程

  1. 用flask建立網站建立與linebot API間的通訊
  2. 收到指令後,使用selenium開啟PChome的網頁,再利用BeautifulSoup爬蟲資料
  3. 比對網頁內容是否有變動,若是有變動即代表商品正在開賣或價格有變動
  4. 利用linebot的 REPLY_MESSAGE 與 PUSH_MESSAGE 在LINE端進行推播提醒

在樹莓派(以下簡稱RPI)的設定也是依照上述,而為了讓LINE API能夠透過網際網路連線到RPI,就必須要增加一項設定:

5. 使用ngrok 建立RPI的SSL連線

在開始之前要在RPI上確認已經安裝Python3的版本,而後續 flask建立網站與linebot API的設定步驟可以直接參考觀念篇的STEP 1說明。

完成Flask與linebot API的安裝設定後,接下來的 selenium與BeautifulSoup設定也是直接參考觀念篇的STEP 2說明,而其中在Chromium WebDriver的安裝上須要從launchpad.net下載,而下載版本要跟安裝在RPI的Chromium瀏覽器的版本相同並選擇 armhf (Updates) 的版本,以下教學用的是65版。

1. 安裝 Chromium 瀏覽器 WebDriver

首先下載chromium-chromedriver,在RPI的終端機或用遠端SSH輸入下方指令:

wget http://launchpadlibrarian.net/361669488/chromium-chromedriver_65.0.3325.181-0ubuntu0.14.04.1_armhf.deb

並執行安裝:

sudo dpkg -i chromium-chromedriver_65.0.3325.181-0ubuntu0.14.04.1_armhf.deb

完成安裝後的 WebDriver 會放在 /usr/lib/chromium-browser/chromedriver目錄內,這個位址後續在程式中會用到。

如果後續chromium版本有更新,WebDriver也必須同步更新以免在無法執行,除了手動更新WebDriver版本外,也可以參考這篇教學文達到自動更新。

2. 安裝並設定 ngrok

雖然我們在RPI上建立Flask的網頁伺服器可以來接收 LINE 傳來的訊息,但是RPI的網路連線設定通常會是內網與虛擬IP,為了能夠與網際網路連線這時就需要ngrok的協助,ngrok 提供讓內網伺服器與外界溝通的一個服務,也就是會提供一個HTTPS的網址讓RPI可以與LINE服務溝通。

先到ngrok的網站註冊一個帳號,完成註冊後它會給你一個token,在使用ngrok時是不需要帳號密碼而是使用token跟帳號做連結的。另外ngrok很貼心的直接將使用指令也附上讓我們可以複製使用,這個稍後就會用到。

完成註冊後接下就要安裝ngrok,在RPI的終端機或用遠端SSH輸入下方指令:

sudo wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-arm.zip

下載後解壓縮,請輸入以下指令:

sudo unzip ngrok-stable-linux-arm.zip

3. 讓程式動起來

程式的部分基本上還是使用在觀念篇中的完整程式碼,但是要留意以下的設定並做調整:

  • 第38行的WebDriver的位置設定
  • 第95行的host IP與PORT改為app.run(host=’0.0.0.0′, port=os.environ.get(‘PORT’, ‘5000’))

適用於RPI的完整範例程式碼如下:

from flask import Flask, request, abort
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from bs4 import BeautifulSoup
import time, os
from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import *
 
# "PaymentContainer" => 價格有無變動 / 或可用折價券
# "fieldset_box orignbutton" => 有無貨
HTMLs=[ 
    ["https://24h.pchome.com.tw/prod/DABCE5-1900AQKMT","ul","class","fieldset_box orignbutton",""],
    ["https://24h.pchome.com.tw/prod/DYAM2M-A900AHXUD","div","id","PaymentContainer",""]
]
 
# Channel Access Token
line_bot_api = LineBotApi('YOUR_CHANNEL_ACCESS_TOKEN')
# Channel Secret
handler = WebhookHandler('YOUR_CHANNEL_SECRET')
 
chrome_options = Options()
chrome_options.add_argument('--disable-gpu')
chrome_options.add_argument('--headless')
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument("--disable-notifications")
chrome_options.add_argument('--disable-extensions')
chrome_options.add_argument('start-maximized')
chrome_options.add_argument('disable-infobars')
chrome_options.add_argument('disable-blink-features=AutomationControlled')
chrome_options.add_argument('user-agent=Type user agent here')
chrome_options.add_experimental_option("excludeSwitches", ["enable-logging"])
 
chrome = webdriver.Chrome('/usr/lib/chromium-browser/chromedriver', options=chrome_options)
 
doGrap = 0
 
app = Flask(__name__)
 
def pageview(url, view_item, view_ID, view_contain):
    global chrome
    chrome.get(url)
    time.sleep(3)
     
    chrome.execute_script("window.scrollTo(0,document.body.scrollHeight)")
    soup = BeautifulSoup(chrome.page_source, 'html.parser')
    mainBlock = soup.find(view_item,{view_ID: view_contain})
    return mainBlock
 
# 監聽所有來自 /callback 的 Post Request
@app.route("/callback", methods=['POST'])
def callback():
    # get X-Line-Signature header value
    signature = request.headers['X-Line-Signature']
    # get request body as text
    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)
     
    # handle webhook body
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        print("Invalid signature. Please check your channel access token/channel secret.")
        abort(400)        
    return 'OK'
 
# 處理訊息
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    global doGrap
    msg = event.message.text
    if msg == "Go":     
        doGrap = 1
        line_bot_api.reply_message(event.reply_token, TextSendMessage(text='start grapping!\n(with '+str(len(HTMLs))+' websites...)'))
        while doGrap:
            for thePage in HTMLs:
                print(thePage[0])
                hpage = pageview(thePage[0], thePage[1], thePage[2], thePage[3])
                if thePage[4]!='' and hpage != thePage[4]:
                    line_bot_api.push_message('YOUR_USER_ID', TextSendMessage(text='Page changed!\n'+str(thePage[0])))
                    print("change!!!!!")
                thePage[4] = hpage
                time.sleep(1)
            time.sleep(30)
    elif msg == "Stop":
        line_bot_api.reply_message(event.reply_token, TextSendMessage(text='stop!'))
        print("stop!")
        doGrap = 0     
 
if __name__ == "__main__":
    app.run(host='0.0.0.0', port=os.environ.get('PORT', '5000'))

再來我們考量一下使用情境,使用RPI不外乎可以利用它省電輕巧的優點,因此在運作時是可以不需要外接螢幕跟鍵盤滑鼠,反之我們可以用SSH的連線方式去做控制,像是用putty做遠端連線,以下的操作都是使用SSH進行,而且會需要同時使用到2個SSH。

接下來我們要做3件事讓RPI與LINE可以連線:1.啟動ngrok2.設定LINE API的 Webhook URL3.執行python程式

1. 啟動ngrok:請開啟SSH的連線輸入以下兩個指令(第一個指令可以直接複製ngrok網頁提供的使用指令):

./ngrok authtoken XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

./ngrok http 5000

如果執行成功,就會出現以下畫面,裡面會有一個HTTPS網址,這個就是可以連結到RPI的網址。

2.設定LINE API的 Webhook URL:來到LINE Developers網頁,在Messaging API裡面可以找到Webhook settings裡的Webhook URL 如下圖,將ngrok給的網址填入並勾選下方Use webhook的設定,這裡要注意一點,ngrok網址後面請加上/callback,以免出現404 NOT FOUND的錯誤。

3.執行python程式:這裡將上面的python碼存成app_RPI.py檔,然後開啟第2個SSH的連線,並輸入指令:

python3 app_RPI.py

這樣就完成整個RPI與LINE之間的通訊功能,這時在LINE端的聊天室輸入”Go”跟”Stop”就會出現類似下列訊息:

接下來我們只要找個地方放置RPI模組,設定好網路連線後,RPI跟LINE就可以隨時幫我們監看PChome購物有沒有商品上架或是價格變動,讓我們能順利的買到東西。

ref: G. T. Wang,