前言:

這是參加六角鼠年全馬的第一篇,主要是希望能夠養成寫部落格的習慣。由於我本身並沒有主要技能,因此這次參賽文章會以我最近玩的玩具、使用的套件或是遇到的問題做紀錄。

希望能夠派上用場。

目標:使用 PythonSelenium 連線到訂便當網站,自動輸入帳號密碼登入後,取回網站上的訂單資訊

(2020/12/4) 更新: 由於訂便當網站改版,所以程式碼已經不能照抄了。但有興趣的朋友還是能自己摸索做點變動,也能夠照常進行喔,加油~

最近在公司的時候有個莫大的煩惱,就是關於辦公室團購這回事兒。現在待著的公司主要是從 Dinbendon 這套系統來揪團購,舉凡品客、火鍋等都在上面訂過,據我觀察最受歡迎出現最多次的當屬雞排了。煩惱就在於,每次都會錯過雞排的團購,光在辦公室聞著四面八方傳來的雞排香味,就令人無法忍受!因此趁著這個機會,來嘗試能不能像之前的 PTT 一樣來弄出一個通知,順便玩玩最近看到的工具。這系列的文章會分成多個部分,主要是以使用的工具來分集。

由於在從團購網取得訂單的過程中需要跟網頁進行互動,因此這次要使用的工具是 Selenium

Selenium 是一個對網頁做自動化測試的工具,但我個人比較常在爬蟲的時候用到XD。它能夠經由腳本或錄製的方式對瀏覽器進行操作,並且也支援相當多語言可以使用,例如我同事便使用 C# 和 Hangfire 來完成訂便當的目標(對,這麼無聊的人不只我一個),而我則用相對比較熟悉的 Python 來實作。

關於本篇主要的操作和步驟,主要參考 在 Windows 上安裝 Python & Selenium 簡易教學 這篇文章,在此感謝;而各語言的語法等等,可以翻閱 教學文檔

準備工作

開始寫腳本之前,確保 Python 已經安裝完畢,並且先下載好 Selenium 套件包

另外 Selenium 是使用各個 Web Driver 來對瀏覽器做操作的,因此這邊也需要先下載 Chrome 的 Driver 來使用。進入 ChromeDriver 的下載頁面 ,通常挑選最新版的下載,如果 Chrome 版本有需求再選擇對應的版本即可。

(4/2) 補充:關於其他瀏覽器的 Driver,可以參考 iT 邦幫忙的 鼠年全馬鐵人挑戰 WEEK 06:Selenium 自動化測試工具 這篇,裡面有詳細的介紹以及各瀏覽器的 Driver 下載整理。

此外除了用腳本控制 Driver 的用法以外,Selenium 也提供了 IDE 可以直接使用,需要先安裝 Chrome 和 Firefox 的擴充套件,詳情可以參閱同系列的 鼠年全馬鐵人挑戰 WEEK 07:Selenium IDE 內有使用說明。

發完之後才看到這個系列,對測試的種類和 Selenium 的操作說明得清楚多了,值得推薦,故在此補上。

下載完解壓縮應該會有一個 chromedriver.exe 檔案,這個檔案的用法有兩種

  • 放置於 Python.exe 所在的位置,即當初的安裝位置,如此所有的腳本都可以使用
  • 放置於現在專案的 py 檔同一個資料夾,就只有這個資料夾中的腳本可以使用。

當然前者放一次就都可以用比較方便,不過這邊只打算迅速地讓這個腳本動起來,因此可以直接放置在等等要寫 Python 檔的資料夾就可以了。

那麼準備工作完成之後,就可以開始來寫 Code 讓它動起來囉!

取得訂單

首先測試是否能夠順利連線上便當網,這邊先撰寫最簡單的連線。

from selenium import webdriver

url = 'https://dinbendon.net/do/login'
driver = webdriver.Chrome()
driver.get(url)  # 連線到訂便當頁面

執行之後應該就能看到 Chrome 自動開啟連線到指定的網頁,同時也可以注意到 Chrome 上有標明「正在受到自動測試軟體控制」

接著想要看到訂單內容,還必須要輸入帳號密碼和驗證碼才行,這也就是前面提到的需要互動的部分。先使用 F12 的使用者工具觀察欄位的名稱,以利後續 Selenium 的抓取 ,爬蟲的基本就在於拆人家的房子

確認名稱之後就可以添加指令,讓 Selenium 幫我們輸入看看。這邊要注意我們加上了 sleep() 來暫停一下,因為在 Selenium 的操作之間,建議要加上些許延遲,避免畫面動作都還沒完成,指令就一股腦丟完了囧。

time.sleep(2) # 演一下

username = "Hello"
password = "password"

# 輸入帳密
driver.find_element_by_name("username").send_keys(username)
driver.find_element_by_name("password").send_keys(password)

可以看到它會自動幫我們輸入內容,看著帳密自己跳出來實在是相當療癒

在這一步去抓取網頁上的元素時,可以看見我使用了 driver.find_element_by_name 去按照網頁上 HTML 標籤的 name 去抓到目標的元件。這就是 Selenium 的定位器,它提供了許多方法去取得目標元件,例如 Id、Name 等等。

關於定位器的操作可以參閱 Selenium HTML element locator 定位器 以及 Selenium webdriver 定位物件方法比較 xpath v.s. css selector 這兩篇。接下來的介紹會以使用為主。

回到我們的便當網,這網頁的友善就在於它的驗證碼是顯示數字讓你計算,每次的變化只有中間的「+」可能會變成「加」和全形的「+」。但這並不妨礙我們去把它的值剝取出來。

# 輸入驗證碼
ques = driver.find_elements_by_class_name("alignRight")[2].text # 有點強硬地拿到整串問題
temp = re.findall(r"\d+\.?\d*", ques) # 用正規表達式把數字取出
a = int(temp[0])
b = int(temp[1])
c = a + b
driver.find_element_by_name("result").send_keys(c)

可以看見它自動幫我們輸入了計算結果

題外話:如果遇到麻煩點的驗證碼怎麼辦?

可以先用大數軟體 - 如何使用 Selenium 抓取驗證碼?

再試試看大數學堂 - 如何透過 OpenCV 破解台灣證券交易所買賣日報表的驗證碼(Captcha)

也許能有效,先記錄下來。

接著就可以測試是否能夠登入了,將帳號密碼設定為測試用的訪客帳號 guest,並在指令最後添加按下按鈕的動作

# 提交表單
driver.find_element_by_name("submit").click() 

到這一步已經順利登入,並且可以看到訂單列表了。

整理及包裝

接著流程一如前部分,觀察網頁結構並且將目標取出。

這邊先將左半部分的 Table 拿出來,接著針對表格的每一列取出該元素之後取文字。

# 取出訂單表格列
rows = driver.find_elements_by_css_selector(
    "div#inProgressBox>table>tbody>tr")

if len(rows) == 0:
    driver.close()
    return list()
    
# 取出每一列資料的文字
bandons = [list(map(getText, row.find_elements_by_css_selector(
    "td>div>a>span"))) for row in rows]  
driver.close()

# 做成一張表
tableHeader = ['人數', '發起人', '目標']
bandons_df = pandas.DataFrame(bandons, columns=tableHeader)

目前為止整體程式碼如下:

from selenium import webdriver
import re
import time
import pandas

# 自動檢查團購便當網
def main():
    url = 'https://dinbendon.net/do/login'
    order = fetch_bandon(url)
    print_order(order)

def fetch_bandon(url, username="guest", password="guest"):
    ''' 開啟瀏覽器並連線到便當網取得資料 '''

    driver = webdriver.Chrome()
    driver.get(url)  # 連線到訂便當頁面
    time.sleep(1) # 演一下

    # 輸入帳密
    driver.find_element_by_name("username").send_keys(username)
    driver.find_element_by_name("password").send_keys(password)

    # 輸入驗證碼
    ques = driver.find_elements_by_class_name("alignRight")[2].text
    temp = re.findall(r"\d+\.?\d*", ques)
    answer = int(temp[0]) + int(temp[1])
    driver.find_element_by_name("result").send_keys(answer)

    # 提交表單
    driver.find_element_by_name("submit").click()
    time.sleep(1)

    # 取出訂單表格列
    rows = driver.find_elements_by_css_selector(
        "div#inProgressBox>table>tbody>tr")

    if len(rows) == 0:
        driver.close()
        return list()
        
    # 取出每一列資料的文字
    bandons = [list(map(getText, row.find_elements_by_css_selector(
        "td>div>a>span"))) for row in rows]  
    driver.close()

    # 做成一張表
    tableHeader = ['人數', '發起人', '目標']
    bandons_df = pandas.DataFrame(bandons, columns=tableHeader)

    return bandons_df


def print_order(data):
    '''列印訂單資料,看起來整齊一點'''
    for index, row in data.iterrows():
        if row is not None:
            print('({hcount:>4s}) {orderer}: {order:<40s}'.format(
                orderer = str(row['發起人']),
                order = str(row['目標']),
                hcount = str(row['人數'])))


def getText(x):
    return x.text


if __name__ == '__main__':
    main()

抓回來的樣子如下

另外每次執行的時候都還會有瀏覽器跳出來操作,但我們在這邊已經確認可以成功取回資料了,因此瀏覽器的顯示也不是那麼必要。

這邊就可以考慮加上無頭模式讓瀏覽器不要顯示,而是在背景執行。只需要在一開始宣告瀏覽器的部分加上選項,就可以不要跳視窗囉。

options = webdriver.ChromeOptions()
options.add_argument('headless')
driver = webdriver.Chrome(options=options)

到此為止我們已經成功控制瀏覽器幫我們打開網頁,填帳號密碼登入,也取得了想要的訂單列表內容,完成了訂便當野心的第一步!

然而,接著還有相當多的部分必須處理。如何判斷有沒有新訂單?又要怎麼通知我有新訂單呢?

欲知後續如何,且待 下回 分曉!

我要訂便當系列

參考資料