創建監理站考照報名預約搶名額的 BOT 程式
( 2025/08/21 此文章成功搬移到新網站 ! 備註:先廢話一小段,不知不覺已經過1年了啊 真還念 寫這篇已經是17歲的事情 [目前是19歲的少年 老了...],內容我基本上是原封不動的搬過來沒有重新審閱只有改設計,所以內文有誤還請見諒 畢竟很久之前寫的東西,那時候的寫法應該是比照寫學習歷程的方式。 )
前言
靈感源自於本人暑假即將年滿18歲,想先預約報名機車駕照考試,發現名額很少想要手動搶取非常困難,於是萌生了開發一個幫我搶名額的BOT程式的想法。
功能需求分析
在開發這個BOT程式之前,首先我對監理站的網站進行了詳細的觀察,整理出了報名過程的詳細步驟:
- 選擇報考照類(Type of Test):在報名頁面上,首先遇到的是一個下拉選單,需要選擇考試的類型。這是個固定的選項列表,只能從中選擇一項。
- 設定預計考試日期(Date of Test):考試日期的選擇依據特定的規則:系統每天在午夜0:00開放下一個月同一天的考試名額。日期必須按特定格式輸入(例如:民國78年7月2日,請填0780702)。
- 選擇考試地點(Place of Test):接著選擇考試地點,這部分由兩個下拉式選單組成:首先選擇地區,然後才能選擇具體的監理所。
- 查詢場次:完成地點選擇後,需要點擊「查詢場次」按鈕。系統會提示需先完成危險感知體驗並登錄體驗紀錄才能進行預約。
- 選擇場次繼續報名:如果已完成體驗登錄,則點擊「選擇場次繼續報名」按鈕。此時會顯示考試日期、場次組別說明、可報名人數等資訊。
- 報名確認和資料填寫:選擇合適的場次後,點擊報名按鈕進入填寫個人資料的界面。這裡會有一個確認的彈窗,需確認已瞭解相關的預約提醒和注意事項,點擊我已悉知即可輸入身分證字號、出生年月日、姓名、聯絡電話/手機、Email等資訊,確認無誤後,點擊提交即可完成報名。
依據整理的流程分析,BOT的功能需求細節包括:自動選擇報考照類(Type of Test),在報名頁面的下拉選單中選擇所需的考試類型;自動填寫考試日期(Date of Test),按照特定格式輸入日期(例如:民國78年7月2日,請填0780702);選擇考試地點(Place of Test),需要BOT依次操作兩個下拉選單來選擇地區和具體的監理所;查詢場次,BOT需自動點擊「查詢場次」按鈕並能處理危險感知體驗的提示彈窗;選擇場次報名,BOT在場次資訊頁面中自動選擇「報名」按鈕進入報名流程;最後,報名確認和資料填寫,BOT需要處理任何確認彈窗,自動填入個人資訊(如身分證字號、出生年月日、姓名等),最後提交完成報名。
技術選擇
我選擇使用 Python 與 Selenium 模組,主要基於幾個考量。Python 是一種廣泛使用的高級編程語言,開發快速且高效。此外擁有豐富的庫支持,特別是在自動化和網路爬蟲領域表現相當出色。
Selenium 是一個開源的自動化測試工具,它允許開發者模擬用戶對網頁的各種操作,像是點擊、滾動、填寫表單等。這對於需要模擬真實用戶行為的自動化程式來說是非常關鍵的。Selenium 能夠直接與瀏覽器驅動程式(例如 Chromedriver)交互,讓它能在實際的瀏覽器環境中運行,確保自動化操作與真實用戶的操作無異。
設計與實現
我主要的想法是使用 Selenium 的 find_element
方法來定位網頁上的重要元素,並使用了 Select
、click()
和 send_keys()
這些方法來交互和填寫資料。透過瀏覽器開發者工具(按F12),首先分析網頁的 HTML 結構,確定操作目標的 ID、類名或其他特定屬性。讓程式可以準確地操作下拉選單(使用 Select
)、點擊按鈕(使用 click()
)、填寫表單(使用 send_keys()
)。讓自動化腳本能夠有效地模擬用戶行為,從而高效的完成線上報名流程。
程式流程
(我們先拿可以點擊報名的玉里監理站做測試 預約報名8/12的駕照考試)
首先設置ChromeDriver的路徑與初始化WebDriver
# 設置ChromeDriver的路徑
s = Service('C:\\Path\\to\\your\\driver\\chromedriver.exe')
# 初始化WebDriver
driver = webdriver.Chrome(service=s)
一開始先處理預約報名頁面
確定預約報名頁面上各個關鍵元素的定位。透過瀏覽器的開發者工具,確定以下幾個重要元素的 ID 和選擇器
- 報考照類下拉選單 ID:
licenseTypeCode
- 預計考試日期輸入框 ID:
expectExamDateStr
- 第一個考試地點選單 ID:
dmvNoLv1
- 第二個考試地點選單 ID:
dmvNo
- 查詢場次按鈕 選擇器:
a[onclick='query();']
找到後可以來撰寫我們的主程式,為了確保網站在高流量情況下不會導致程序出錯,我們的自動化腳本首先確保「報考照類」的下拉選單完全加載並變為可互動狀態。通過使用 WebDriverWait
來等待直到元素可操作。一旦元素準備好,使用 Select
函數從下拉選單中選擇「普通重型機車」作為考試類型。
接著,對於考試日期的輸入,程式首先清空預計考試日期輸入框中的任何已有內容,然後使用 send_keys
函數輸入所需的日期。再來進行地點選擇,我們先操作第一個地點相關的下拉選單選擇考試大區。為防止因網頁卡頓而未能精確定位到第二個選單,我們會等待系統更新並顯示具體的監理站選項再進行選擇。提交所有選擇的條件並查詢可選的考試場次,腳本會定位「查詢場次」並使用 click()
函數點擊進行場次查詢。
# 打開網頁
driver.get("https://www.mvdis.gov.tw/m3-emv-trn/exm/locations#gsc.tab=0")
# 等待「報考照類」下拉選單可點擊
WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.ID, 'licenseTypeCode'))
)
# 定位「報考照類」並選擇
select_license_type = Select(driver.find_element(By.ID, 'licenseTypeCode'))
select_license_type.select_by_visible_text('普通重型機車') # 選擇普通重型機車
# 定位預計考試日期輸入框並填寫
date_input = driver.find_element(By.ID, 'expectExamDateStr')
date_input.clear() # 清除已有的內容(保險起見 如果有)
date_input.send_keys('1130812') # 填入預計考試日期
# 定位第一個考試地點選單並選擇臺北區監理所(北宜花)
select_region = Select(driver.find_element(By.ID, 'dmvNoLv1'))
select_region.select_by_visible_text('臺北區監理所(北宜花)') # 這裡使用中文名稱選擇大區
# 等待第二個下拉選單(具體的監理站)更新並包含特定選項
WebDriverWait(driver, 10).until(
EC.text_to_be_present_in_element((By.ID, 'dmvNo'), '玉里監理分站(花蓮縣玉里鎮中華路427號)')
)
# 定位第二個考試地點選單並選擇具體的監理站
select_station = Select(driver.find_element(By.ID, 'dmvNo'))
select_station.select_by_visible_text('玉里監理分站(花蓮縣玉里鎮中華路427號)') # 選擇具體的監理站
來到處理彈窗和按下報名按鈕的部分,這裡也使用 WebDriverWait
等待彈出視窗的元素完全加載,確保彈窗中的所有元素都可互動。這個彈窗通過 CSS 類名 "blockUI"
來標識。但由於直接通過 Selenium 我嘗試無數方法都無法互動到彈窗中的按鈕,所以我選擇使用 JavaScript直接移除彈窗的遮罩層,包括 '.blockUI.blockOverlay'
和 '.blockUI.blockMsg.blockPage'
。雖然是暴力破解,但也有效解決無法觸達彈窗按鈕的問題。
關於點擊特定時段的報名按鈕,仔細觀察不同時段報名按鈕的 HTML 結構發現每個按鈕都具有特定的格式,例如,玉里監理站的8月12日上午場次, 組別 1的報名按鈕為://a[@onclick="preAdd('2024-08-12', '1', '1')"]
,其中日期 '2024-08-12'
表示考試日期,第二個參數 '1'
表示上午場次(2 表示下午場),最後的 '1'
表示組別。這種格式能夠精確定位到所需的報名按鈕進行點擊操作。
# 等待彈出視窗元素加載完成
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "blockUI"))
)
# 使用JavaScript移除遮罩層
driver.execute_script("""
var overlay = document.querySelector('.blockUI.blockOverlay');
if (overlay) {
overlay.style.display = 'none';
}
var messageBox = document.querySelector('.blockUI.blockMsg.blockPage');
if (messageBox) {
messageBox.style.display = 'none';
}
""")
# 點擊特定時段的報名按鈕
# 假設你要選擇第二個時段
signup_button = driver.find_element(By.XPATH, "//a[@onclick=\"preAdd('2024-08-12', '1', '2')\"]")
signup_button.click()
進入到填寫個人資料的頁面後,會遇到一個彈窗,這裡一樣使用 JavaScript 直接移除所有與 blockUI
相關的遮罩層。通過 WebDriverWait
確保所有相關元素完全加載後,再執行 JavaScript 來移除它們。移除遮罩層後,設了一個 0.5 秒的延遲,確保彈窗被完全移除可以順利進行後續的資料填寫。接下來程式會定位到各個資料輸入框使用 send_keys
方法填入所需資料。
最後,定位到「報名」按鈕(使用 CSS 選擇器 a[onclick='add()']
),並點擊它以提交所有填寫的資料,完成報名過程。整套流程確保即使在許多用戶同時訪問的情況下,自動化腳本也能穩定運行有效完成所有必要的操作。
# 等待彈出視窗元素加載完成
WebDriverWait(driver, 10).until(
EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".blockUI"))
)
# 使用JavaScript強制移除所有遮罩層
driver.execute_script("""
Array.from(document.querySelectorAll('.blockUI')).forEach(el => el.remove());
""")
time.sleep(0.5)
# 身分證字號 ID No.
id_input = driver.find_element(By.ID, 'idNo')
id_input.send_keys('F000000000') # 填入身分證字號
# 出生年月日 Birthday
birthday_input = driver.find_element(By.ID, 'birthdayStr')
birthday_input.send_keys('0950230') # 民國出生年月日
# 姓名 Name
name_input = driver.find_element(By.ID, 'name')
name_input.send_keys('陳小明') # 填入姓名
# 聯絡電話/手機 Phone No.
phone_input = driver.find_element(By.ID, 'contactTel')
phone_input.send_keys('09XXXXXXXX') # 填入手機號碼
# Email
email_input = driver.find_element(By.ID, 'email')
email_input.send_keys('xxxxxxxx@email.com') # 填入電子郵件地址
# 點擊最後的報名按鈕
final_signup_button = driver.find_element(By.CSS_SELECTOR, "a[onclick='add()']")
final_signup_button.click()
完整程式碼
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select, WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
# 設置ChromeDriver的路徑
s = Service('C:\\Path\\to\\your\\driver\\chromedriver.exe')
# 初始化WebDriver
driver = webdriver.Chrome(service=s)
# 打開網頁
driver.get("https://www.mvdis.gov.tw/m3-emv-trn/exm/locations#gsc.tab=0")
# 等待「報考照類」下拉選單可點擊
WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.ID, 'licenseTypeCode'))
)
# 定位「報考照類」並選擇
select_license_type = Select(driver.find_element(By.ID, 'licenseTypeCode'))
select_license_type.select_by_visible_text('普通重型機車') # 選擇普通重型機車
# 定位預計考試日期輸入框並填寫
date_input = driver.find_element(By.ID, 'expectExamDateStr')
date_input.clear() # 清除已有的內容(保險起見 如果有)
date_input.send_keys('1130812') # 填入預計考試日期
# 定位第一個考試地點選單並選擇臺北區監理所(北宜花)
select_region = Select(driver.find_element(By.ID, 'dmvNoLv1'))
select_region.select_by_visible_text('臺北區監理所(北宜花)') # 這裡使用中文名稱選擇
# 等待第二個下拉選單(具體的監理站)更新並包含特定選項
WebDriverWait(driver, 10).until(
EC.text_to_be_present_in_element((By.ID, 'dmvNo'), '玉里監理分站(花蓮縣玉里鎮中華路427號)')
)
# 定位第二個考試地點選單並選擇具體的監理站
select_station = Select(driver.find_element(By.ID, 'dmvNo'))
select_station.select_by_visible_text('玉里監理分站(花蓮縣玉里鎮中華路427號)') # 選擇具體的監理站
# 定位「查詢場次」按鈕並點擊
search_button = driver.find_element(By.CSS_SELECTOR, "a[onclick='query();']")
search_button.click()
# 等待彈出視窗元素加載完成
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "blockUI"))
)
# 使用JavaScript移除遮罩層
driver.execute_script("""
var overlay = document.querySelector('.blockUI.blockOverlay');
if (overlay) {
overlay.style.display = 'none';
}
var messageBox = document.querySelector('.blockUI.blockMsg.blockPage');
if (messageBox) {
messageBox.style.display = 'none';
}
""")
# 點擊特定時段的報名按鈕
# 假設你要選擇第一個時段
signup_button = driver.find_element(By.XPATH, "//a[@onclick=\"preAdd('2024-08-12', '1', '1')\"]")
signup_button.click()
# 等待彈出視窗元素加載完成
WebDriverWait(driver, 10).until(
EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".blockUI"))
)
# 使用JavaScript強制移除所有遮罩層
driver.execute_script("""
Array.from(document.querySelectorAll('.blockUI')).forEach(el => el.remove());
""")
time.sleep(0.5)
# 身分證字號 ID No.
id_input = driver.find_element(By.ID, 'idNo')
id_input.send_keys('F000000000') # 填入身分證字號
# 出生年月日 Birthday
birthday_input = driver.find_element(By.ID, 'birthdayStr')
birthday_input.send_keys('0950230') # 民國出生年月日
# 姓名 Name
name_input = driver.find_element(By.ID, 'name')
name_input.send_keys('陳小明') # 填入姓名
# 聯絡電話/手機 Phone No.
phone_input = driver.find_element(By.ID, 'contactTel')
phone_input.send_keys('09XXXXXXXX') # 填入手機號碼
# Email
email_input = driver.find_element(By.ID, 'email')
email_input.send_keys('xxxxxxxx@email.com') # 填入電子郵件地址
# 點擊最後的報名按鈕
final_signup_button = driver.find_element(By.CSS_SELECTOR, "a[onclick='add()']")
final_signup_button.click()
# 等待一些時間以便觀察
time.sleep(70)
# 完成後 關閉瀏覽器
driver.quit()
使用方法
程式中你可以找到用於選擇報考類型的部分,這裡可以根據想要報考的類型填入下拉選單的關鍵字,填寫考試日期的欄位需要根據計劃的考試日期進行更新。特定的監理所偏好也需填入相對應的關鍵字。針對想要報名的具體時段,需要觀察並調整按鈕的選擇器匹配網頁上的特定時段。最後填寫個人資料時,包含身分證字號、出生日期、姓名、聯絡方式以及電子郵件等都應確保輸入正確,這些資料將直接影響報名的成功與否。
實際測試

BOT可以成功運行並自動選擇場次填入相關資料,而且非常迅速。
反思 持續優化
雖然BOT目前能夠高效地完成任務,但還是有些問題需要探討。目前使用了 JavaScript 強制移除彈窗的做法在短期內有效解決彈窗的問題。若未來網站更新以檢測彈窗按鈕的實際點擊,現在的作法很可能會失效。因此尋找一個能夠直接與彈窗按鈕互動的可靠方法變得至關重要。
另外進一步的改進方向可以包括開發一個用戶友好的可視化界面,讓非技術用戶也能輕鬆使用此BOT。提供一個直觀的操作界面,提升整體的使用體驗。
結論
本自動化腳本成功實現了使用 Selenium 來自動完成線上報名過程,顯著提高報名的效率。透過自動化選擇報考類型、填寫考試日期、選擇考試地點,以及輸入個人資料等步驟,程式能夠在短時間內完成原本需手動輸入的繁瑣流程。程式還有處理網頁彈窗的機制,即使遇到阻礙進程的彈窗時也能保持流程的順利執行。