當了一年半的副總務股長的我,已經厭煩每天點開 Line 統計訂單,於是設計一個 Line Bot 能夠自動統計大家的訂單,並在尋找登入方法時意外找到學校系統的漏洞,現在已經能夠自動登入並訂購了!
使用工具
- Line SDK
- Selenium
- Heroku
程式碼
使用說明
- 輸入
![隨便一個東西]
即可獲得店家選單(e.g.!菜單
)- 每天第一次使用可能會需要等一下
- 直接點選選單內容即可訂餐(也可自行打字,但內容須跟點選選單所送出的內容相同)
- 若不慎訂錯,可輸入
del [剛剛訂錯的內容]
即可取消先前訂單(e.g.del 米寶$65
)- 此處的
[剛剛訂錯的內容]
亦須與點選選單送出的內容相同
- 此處的
- 每次執行操作後,機器人都會回覆一則訊息,可由此訊息確認操作是否有效
運作邏輯
因為原先班上就是用 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 連接,就完成了!!