Home 就是一個訂餐機器人(By Python)
文章
取消

就是一個訂餐機器人(By Python)

當了一年半的副總務股長的我,已經厭煩每天點開 Line 統計訂單,於是設計一個 Line Bot 能夠自動統計大家的訂單,並在尋找登入方法時意外找到學校系統的漏洞,現在已經能夠自動登入並訂購了!

使用工具

  • Line SDK
  • Selenium
  • Heroku

程式碼

Github

使用說明

  1. 輸入 ![隨便一個東西] 即可獲得店家選單(e.g. !菜單
    • 每天第一次使用可能會需要等一下
  2. 直接點選選單內容即可訂餐(也可自行打字,但內容須跟點選選單所送出的內容相同)
  3. 若不慎訂錯,可輸入 del [剛剛訂錯的內容] 即可取消先前訂單(e.g. del 米寶$65
    • 此處的 [剛剛訂錯的內容] 亦須與點選選單送出的內容相同
  4. 每次執行操作後,機器人都會回覆一則訊息,可由此訊息確認操作是否有效

運作邏輯

因為原先班上就是用 Line 中的記事本訂餐,於是我打算不換平台,同樣利用 Line 的機器人。幸運的是,網路上 Line Bot 的教學非常多,官方文件也講得很清楚,加快了不少開發速度

1
2
3
4
5
6
7
8
9
10
menu = {"大家鐵路$70":0, "大家鐵路$75":0, "太師傅$75":0, "太師傅$70":0, "太師傅$65":0, "正園A$60":0, "正園B$60":0, "正園羊肉$60":0, "吉樂米$65":0, "吉樂米$75":0, "吉樂米$85":0, "吉樂米素$65":0, "米寶$65":0, "米寶$75":0, "米寶素$65":0,  "彩鶴$50":0}
.
.
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    message = event.message.text
    # 訂餐訊息
    if message in menu:
        menu[message] += 1
        line_bot_api.reply_message(event.reply_token, TextSendMessage(text = "已收到您的訂單,您訂購的是" + message))

首先,利用官方文件所教,先處理收到的訊息,確定為要訂餐的訊息,並在該品項數量 +1(此處利用字典 menu 儲存)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
botton = TemplateSendMessage(
    alt_text = "店家",
    template = CarouselTemplate( 
        columns = [
            CarouselColumn( 
                title = "米寶", 
                text = "請點選訂購內容", 
                actions = [
                    MessageAction( 
                        label = "米寶$65",
                        text = "米寶$65"
                    ),
                    MessageAction( 
                        label = "米寶$75",
                        text = "米寶$75"
                    ),
                    MessageAction(
                        label = "米寶素$65",
                        text = "米寶素$65"
                    )
                ]
            ),
            .
            .
        ]
    )
)
.
.
elif message[0] == '' or message[0] == '!':
        line_bot_api.reply_message(event.reply_token, botton)

接著,為了讓大家更好傳出機器人能辨認的詞,我在設計了一個按鈕選單,按下即可傳出訊息,如此也不用揣摩大家的使用習慣設定關鍵字。(這裡也藏了一點小彩蛋 ww)


1
2
3
4
5
6
7
8
9
10
11
12
13
elif (message[0] == 'D' or message[0] == 'd') and (message[1] == 'E' or message[1] == 'e') and (message[2] == 'L' or message[2] == 'l'):
    if(message[3] == ' '):
        if message[4:] in menu:
            menu[message[4:]] -= 1
            line_bot_api.reply_message(event.reply_token, TextSendMessage(text = "已刪除您 " + message[4:] + " 的訂單"))
        else:
            line_bot_api.reply_message(event.reply_token, TextSendMessage(text = "無法辨識輸入的品項,請確認後再打一次"))
    else:
        if message[3:] in menu:
                menu[message[3:]] -= 1
            line_bot_api.reply_message(event.reply_token, TextSendMessage(text = "已刪除您 " + message[3:] + " 的訂單"))
        else:
            line_bot_api.reply_message(event.reply_token, TextSendMessage(text = "無法辨識輸入的品項,請確認後再打一次"))

這段程式碼處理了當有人打錯或想後悔時的情況,並且考慮到各種因手機打字或使用者輸入會出現的情況,例如開頭大寫、del 後是否加空格等

開始爬蟲
1
2
3
4
5
6
7
8
9
10
11
12
13
elif message == "截止":
    if userID == "[my userid]":
        order()
        m = ""
        j = 0
        for i in menu:
            m += (i + '\t\t' + typesetting[j] + str(menu[i]) + '\n')
            menu[i] = 0
            j += 1
        m += "\n內訂已截止!記得繳錢!"
        line_bot_api.reply_message(event.reply_token, TextSendMessage(text=m))
    else:
        line_bot_api.reply_message(event.reply_token, TextSendMessage(text="還敢亂搞阿,以為我沒有修這個bug?"))

當大家都訂好後,從我的帳號傳 截止,系統就會利用 Selenium 上網訂購,並且在群組回傳總數


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
def order():
    # setups
    options = webdriver.ChromeOptions()
    options.binary_location = os.environ.get("GOOGLE_CHROME_BIN")
    options.add_argument("--headless")
    options.add_argument("--disable-dev-shm-usage")
    options.add_argument("--no-sandbox")
    driver = webdriver.Chrome(executable_path=os.environ.get("CHROMEDRIVER_PATH"), options=options)
    driver.get("https://webap1.kshs.kh.edu.tw/kshsSSO/")

    # login
    waitforbrowser = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.ID, "ContentPlaceHolder1_txtID"))
    )
    stid = driver.find_element(By.ID, "ContentPlaceHolder1_txtID")
    stid.send_keys("[這裡填學號]")
    pw = driver.find_element(By.ID, "ContentPlaceHolder1_txtPassword")
    pw.send_keys("[這裡填密碼]")
    ckcd = driver.find_element(By.ID, "ContentPlaceHolder1_txtChkCode")
    ckcd.send_keys(driver.get_cookie("CheckCode")["value"])  # check code
    submit_1 = driver.find_element(By.ID, "ContentPlaceHolder1_lbLogin")
    submit_1.click()
    waitforbrowser = WebDriverWait(driver, 10).until(  # wait login
        EC.url_changes("https://webap1.kshs.kh.edu.tw/kshsSSO/")
    )
    driver.get("https://webap1.kshs.kh.edu.tw/kshsSSO/runAspx.aspx?url=fi9jb29wL2xpc3QuYXNweA==&progParent=c3R1ZGVudENvb3A=")

    # show menu
    waitforbrowser = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.LINK_TEXT, "合作社功能→→訂購午餐便當"))
    )
    bt1 = driver.find_element(By.LINK_TEXT, "合作社功能→→訂購午餐便當")
    bt1.click()
    waitforbrowser = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.LINK_TEXT, "訂購午餐便當"))
    )
    bt2 = driver.find_element(By.LINK_TEXT, "訂購午餐便當")
    bt2.click()
    waitforbrowser = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.ID, "ContentPlaceHolder1_btnLoad"))
    )
    showmenu = driver.find_element(By.ID, "ContentPlaceHolder1_btnLoad")
    showmenu.click()

    # order
    j=0
    waitforbrowser = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.ID, "ContentPlaceHolder1_gvLunch_ddlAmount_0"))
    )
    for i in menu:
        item = driver.find_element(By.ID, f'ContentPlaceHolder1_gvLunch_ddlAmount_{j}')
        Select(item).select_by_value(str(menu[i]))
        j+=1
    submit_2 = driver.find_element(By.ID, "ContentPlaceHolder1_btnUpdate")
    submit_2.click()
    waitforbrowser = WebDriverWait(driver, 10).until(
        EC.alert_is_present()
    )
    driver.switch_to.alert.accept()
    driver.quit()

這是詳細的 order() 函式,有趣的是,系統其實有設圖形驗證碼,因此當初我也很對此非常困擾,試過網路上別人寫好的 OCR 程式,但效果不佳,錯誤率很高,正當我開始思考「該不會得自己訓練一個模型了吧…」,我試著利用當初在 SCIST 學到的網頁鑑識,翻了翻後發現,阿呀原來他直接把驗證碼寫到 cookie 裡面啦,那就簡單啦,畢竟 cookie 很好爬,也因此我就繞過的驗證碼,成功登入了。

此外,程式在模擬登入時,常因瀏覽器速度過慢,跟不上程式運行速度而抓不到定位點,最初我是利用 sleep() 讓程式等待,但我發現這樣不是很穩定,而且每個動作之間的延遲也都不同,等待統一的時間實在太浪費時間,於是我上網搜尋了有沒有替代方案,結果發現 WebDriverWait().until(),搭配 selenium 中的 expected_conditions,便可以讓程式等待某定位點出現再進行下一步。

Heroku

最後上傳到 Heroku 並跟 Line Bot 連接,就完成了!!

參考資料

本文章以 CC BY 4.0 授權