在花東跨院支援門診,最花心神的其實不是看診,是回程的火車票。下診時間不固定,玉里、關山、臺東各有各的班次窗口,加上夏天傍晚的花東線,坐錯邊就是整路被太陽曬🥵

這種事就該交給 Claude。這幾天我們把「訂票祕書」做起來了,今天真的用它訂了回花蓮的票。

這篇的重點

  • 為什麼一張車票會需要一個系統:門診表、停診公告、台鐵訂票紀錄三份資料要對得起來
  • 門診行程 × 車票總覽:哪一天缺哪一段車票,打開一眼看出來,勾一勾批次訂完
  • 座位側別是推算出來的:環島路網的海側是不變量,配上各車種的座號餘數表就能判定曬不曬
  • 不只避曬:邊緣列、輪椅座正後方的桌板位、到站出口最近的車廂,全部進同一個評分
  • 建置用到的工具:本機瀏覽器自動化、交通部 TDX 開放資料、Cloudflare Tunnel、DPAPI、ntfy
  • 有人問了一句「這樣用可以嗎」,於是我把台鐵條款、鐵路法修法沿革和 32 件相關判決全部讀完,並據此改掉了訂票邏輯
  • 為什麼這個專案不開源:自我約束只存在於我這台電腦上,工具放出去就管不到別人拿去做什麼了

一張車票,為什麼會需要一個系統

我的支援門診固定在幾條路線上:週一玉里來回,隔週三花蓮到關山、關山再到臺東、傍晚回花蓮。看起來規律,實際訂票時要同時記住三件事:

  1. 哪幾天要去哪裡:門診是規則排的,但會有停診
  2. 哪幾段已經訂了、哪幾段還沒:台鐵的訂票紀錄頁面預設只顯示最近幾筆,一次要訂一個月的票就得自己數
  3. 那一班的座位坐哪邊:這是最後才想得到、卻最影響搭車體驗的一件事

以前是靠記憶加上翻訂票 App。做成工具之後,這三件事變成一個畫面。

門診行程 × 車票:先對帳,再訂票

打開祕書站的第一個畫面不是班次表,是未來四週的門診行程,每一段旅程旁邊掛著它的車票狀態

門診行程與車票對帳:每個門診日下列出去程回程,已訂的顯示車次與付款狀態,沒訂的是空的勾選框

這一頁背後接了三份資料:

  • 門診日由排班規則生成(週一玉里、隔週三關山與東基,錨定在一個已知日期往後推)
  • 停診直接讀我自己門診資訊頁背後的同一個 API。同一份資料兩個用途:對外公告停診,對內就不會排出一段不存在的行程
  • 已訂車票去台鐵會員的訂票紀錄掃,把每筆訂單拆成「日期+起站+迄站」,跟行程比對

對得上的顯示 ✅ 加車次、訂票代碼與付款狀態;對不上的就是一個空的勾選框,寫著「未訂(預設 441 次)」。

缺的票勾一勾,丟進購物車,按一次全部訂完。

購物車:四筆待訂車票,按「依序訂 4 筆」一筆跑完才跑下一筆

批次訂票的順序是嚴格排隊的:後端有一把工作鎖,一筆訂完才開始下一筆。原因很單純,訂票是操作同一個瀏覽器分頁,兩筆並行只會互相踩到。

要自己挑班次時,還有一個班次看板:選日期、看時刻、點「訂」,跳出張數、靠窗或走道、要不要避曬的選單。

訂票面板:張數、座位偏好、避曬開關(訂到曬側就退掉重訂,最多 4 次,鎖同班次)

座位:曬不曬是可以算出來的

這是整個專案最好玩的部分。訂票網站不會告訴你這個座位在哪一側,但這件事其實可以推。

海側與山側是不變量

台鐵環島路網是一個環。固定編組的列車兩端都有駕駛室,中途從不調頭,而環島線的「海」永遠在外圈。所以一個座位是海側還是山側,全線、雙向都不會變

會變的是方位:花東線的海側朝東,南迴線的海側朝南,屏東與縱貫線南段的海側朝西。所以只要知道「海側」,再知道現在走到哪一段、幾點,就能判斷太陽在不在你這一邊。

座號的餘數決定一切

台鐵的座位編號有規律,座號除以 4 的餘數是 1 或 2 就是靠窗,3 或 0 是走道(例外:EMU3000 第 6 車騰雲座艙是 2+1 配置,餘數 0 才是那排單人靠窗座)。再往下一層,餘數也決定左右:

  • EMU3000:1 到 8 車餘數 2 是海側,9 到 12 車換成餘數 1(後半編組物理上是反向的)
  • 普悠瑪與太魯閣(TEMU):1 到 4 車餘數 2 是海側,5 到 8 車餘數 1

這些規則我們是一班一班實際搭車對出來的,網路上找得到的版本彼此矛盾的地方,以實際坐到的座位為準。

太陽在哪,由時間和路段決定

早上太陽在東、中午偏南、傍晚在西。祕書會把整段行程沿著環島鏈切成小段,每一段配上通過的大概時刻與海側朝向,逐段判定會不會曬。

天黑就不判了。發車時間超過 18:30 或早於 06:00 的班次,整段跳過曬側判定,介面上直接顯示 🌙 並把避曬開關鎖起來。晚上的車沒有曬側問題,多這一道只會白白多退幾張票。

不只是曬:一張票有四個評分項

實際搭下來,影響體驗的不只有太陽:

  • 每節車廂的第一排與最後一排不坐:空間感差,桌板也不好用
  • 輪椅車廂的殘障座位與其正後方不坐:那幾排的桌板很難用(EMU3000 是第 3、7 車;普悠瑪與太魯閣是第 1、8 車,座號 1 到 8)
  • 到站出口最近的車廂優先:往關山、臺東坐 4 到 5 車出來就是電梯;往花蓮是 6、7 車交界或 9 車;玉里是 6 車對到月台樓梯

這四項合成一個分數:背陽 4 分、非邊緣 2 分、靠窗 1 分,再扣掉離出口的車廂距離。

訂法:訂到符合條件就收手

這一段我繞了一圈,而且中途走錯過一次,值得完整寫下來。

最早的做法是「訂到不喜歡的就退掉重訂」。缺點很明顯:最後一張永遠獲勝,就算第一張其實更好。於是我改成「同一班次連續訂幾筆、每筆算分、留最高分的、其餘退掉」。理由是台鐵規定每人每個乘車日可以訂 9 張,期限內取消也不列入未付款紀錄,看起來額度綽綽有餘。

後來我發現這個想法本身就是錯的。

那 9 張的上限不是給我拿來試座位用的。它是台鐵用來防止車票被少數人壟斷的機制。這一點在法院判決裡講得很白:網路訂票系統要求以本人或經授權的身分證字號訂票,目的是「驗證身分、限制訂票張數,俾防止車票遭大量壟斷」。把它當成重試預算,等於把一個防濫用的設計當成我的配額。就算每一張最後都退掉了,那段期間我確實同時佔著同一班車的好幾個位子。

所以現在的流程是任何時候只有一筆訂單活著

  1. 訂一張,算分
  2. 符合硬條件(避開曬側、避開每節的第一排與最後一排、避開輪椅座及其正後方)就留下,結束
  3. 不符合,先退掉這一張,再訂下一張,絕不會有兩筆同時存在
  4. 同一班次最多訂 4 次(也就是最多重訂 3 次),用完就留手上這張

代價很誠實:因為退掉就拿不回來了,有可能後面訂到的比先前退掉的差。程式會把這件事講出來(「先前退掉的那張分數較高」),要更好就自己再訂一次。「離出口最近的車廂」也從硬條件降級成只影響分數。為了走幾步路多訂一輪票,並不划算。

重訂一律鎖同一班次,絕不換車,搭到目標班次比座位好壞重要得多。

訂完之後的事,也一起包了

會員點數倍數送

設籍花東的台鐵會員,搭乘花蓮和平到臺東大武之間的車,可以申請點數倍數送,平日每 50 元回饋 8 點、假日 5 點。很多人不知道有這個。

它的入口藏得有點深:按鈕在會員專區訂票紀錄的那一列上,不在訂單詳情頁,而且只有未付款的訂單才有。所以一定要訂完、付款前申請。祕書會在下訂後自動跑這一段,填上乘客身分證再送出。

申請後車票會變成實名制

實名制的票不能線上換票,也不能用 App 取票,只能到車站窗口或多功能售票機取。這是拿點數的代價,訂之前要想清楚。

付款期限看門狗

台鐵的規則是訂票之後隔天 24:00 前要付款,當日票更緊,是發車前 20 分鐘。而一個月累積 6 次「訂票未付款」會被停權一個月。

所以當天要搭的票,祕書會自己排兩個檢查點:

  • 發車前 60 分鐘:還沒付款就推播提醒我(走自架的 ntfy,手機馬上收到)
  • 發車前 35 分鐘:還沒付款就自動收尾。如果當天不小心有多筆未付款的單(例如手機上另外訂過一筆),用上面那套評分留下最好的一張、其他退掉,免得整批逾期變成未付款紀錄。退不掉會在通知裡大聲標警告

誤點與停駛

班次時刻、即時位置、誤點分鐘、停駛公告都走交通部的 TDX 運輸資料流通服務,免費申請就有,查時刻表完全不用開瀏覽器。颱風天的停駛公告會直接變成頁面最上方的橫幅,花東線這件事真的很重要。

TDX 唯一給不了的是剩餘座位,所以「這班還有沒有票」仍然得回官網查。

核銷

搭完車的收尾也接上了:自動下載乘車證明、簽上名,寄給助理報帳。支援醫院的交通費核銷有金額上限與憑證形式的規定,這部分照著跑就好。

建置用到的工具

操作的是我自己電腦上真的登入著的瀏覽器

我的自動化原則向來是 API 優先,瀏覽器點擊只當備援。台鐵訂票是那個例外。

新版訂票網站掛的是隱形的 reCAPTCHA Enterprise,沒有圖形驗證碼可以解,訂票流程本身也綁著一連串前端狀態。與其去逆向它的私有介面,不如就用我自己的瀏覽器:同一個帳號、同一個登入狀態,訂的也只是我們自己要搭的票。

具體是透過 kimi-webbridge 驅動本機的 Edge,跟我平常買票是同一個瀏覽器分頁。

其他零件

  • 後端:FastAPI 跑在自己電腦的本機埠上,訂票與退票都是開子行程跑同一支 Python 腳本,工作鎖序列化
  • 前端:單一檔案的 PWA,手機加到桌面就是一個 App
  • 對外:Cloudflare Tunnel 加上 Cloudflare Access,只有我的信箱過得去。這套配方跟我的個人儀表板完全一樣,第二次用就很快
  • 祕密:會員密碼與身分證存在 Windows 的 DPAPI 加密檔裡,在程式內解密直接填入表單,永遠不寫進 log、不進聊天視窗
  • 通知:自架的 ntfy
  • 開機自啟:Windows 工作排程器

真正的訂票邏輯全部集中在一支腳本裡,網頁只是它的殼。CLI 跟網頁用的是同一份程式碼,這樣不會出現「網頁上可以但指令列不行」的分裂。

讓自動化不出事的幾條線

自動化真金白銀的東西,錯的代價跟寫寫筆記完全不同。幾個實際踩到、後來變成硬規則的地方:

「送出之後沒反應」不等於「沒訂到」

台鐵的送出按鈕按下去,有時候超過 30 秒畫面完全不動:網址不變、沒有錯誤、沒有轉圈圈。早期的判斷是等三秒沒動靜就重按,結果是訂單其實已經成立了,程式卻以為失敗,重按又疊了一筆

修法有三層。第一層是等待時間放寬到每輪 45 秒;第二層是在宣告失敗之前,先去訂票紀錄掃一遍有沒有剛成立的未付款訂單,找到就把訂票代碼放進錯誤訊息裡。寧可報一個「疑似成功」,也不要讓一筆真的訂單消失在「失敗」兩個字後面。

第三層是這篇文章發出去之後才補上的。我請另一個 AI 用完全獨立的角度把程式和文章一起重看一遍,它指出一個我自己沒看到的漏洞:等不到畫面變化之後,程式仍然會重新送出一次。可是「畫面凍住」有兩種完全不同的可能,從畫面上看起來一模一樣:

  • 表單被打回(例如跳出「尚未選擇搭乘車次」):訂單沒有成立,重送是安全的
  • 只是卡住:訂單其實可能已經在台鐵那邊成立了

舊的寫法對這兩種情況一視同仁地重送,而第二種再送一次,就會真的多出一筆訂單。諷刺的是程式裡早就有一行註解寫著「re-clicking mid-flight is how orders stack」,我卻沒把它跟重試迴圈連起來看。

現在只有明確看到被打回才會重送;只是卡住就直接停下來,去訂票紀錄查有沒有剛成立的單,把代碼交出來。這一段修完,「絕不會有兩筆同時存在」才真的是程式的保證,而不只是我的說法。

查到沒位子要立刻停,不能空轉

查詢結果是「查無可售座位」的時候,頁面上沒有任何班次列。程式如果只是傻等班次列出現,會空轉五分鐘才倒,而且倒在迴圈中間。現在偵測到這幾種訊息就立即中止,不再空轉。

測試紅線

  • 絕不拿當天的車測試。發車前一小時訂票,付款期限只剩幾分鐘,逾期自動取消還會列入未付款紀錄
  • 測試一律訂三四週後的遠期車票,跑完立刻退
  • 訂票是真的訂單,這句話要一直放在心裡

有人問了一句條款,我就去查了

貼文發出去大概一小時,有位朋友在底下留言:「等等…要看一下台鐵的使用條款,能不能這樣用喔。」

這句話問得對,而且我發現自己其實沒有認真查過。所以我把台鐵的訂票須知、鐵路法的修法沿革、立法院的議題研析,以及所有援引過這條罪的公開判決都翻了一遍。以下是查到的東西。我不是律師,這不是法律意見,只是一個當事人把公開資料讀完之後的整理。

台鐵自己怎麼寫

訂票須知裡有一句話非常直白:

程式訂票行為係屬違法,若經查獲本公司將取消成功訂票之紀錄,並將所有訂票紀錄送交檢調單位偵辦。

這句話沒有區分自用或轉售。所以就台鐵的規範來說,答案是清楚的:這樣用不符合它的規定。 這一點我沒有要辯解。

法條與它的來歷

法源是鐵路法第 65 條第 2 項:

以不正方法將虛偽資料或不正指令輸入電腦或其相關設備而購買車票、取得訂票或取票憑證者,處五年以下有期徒刑或科或併科新臺幣三百萬元以下罰金。

這項刑責是民國 105 年增訂的,提案理由講得很明確:前一次修法之後,還是擋不住黃牛與業者「以代理方式壟斷車票」。同條在 115 年 1 月剛修過一次,但只調高了第 1 項黃牛轉售的罰鍰(改成運價的十倍到五十倍),第 2 項一個字都沒動。

判決長什麼樣子

這一段是我覺得最值得分享的。我用司法院的裁判書系統做了全文檢索,把援引這條的判決全部抓下來讀完,總共 32 件。結果出乎意料地整齊:

  • 涉及自動化程式、外掛或搶票軟體的:0 件
  • 經論罪的案件,犯罪事實一律是身分證字號的冒用或造假:用網路上的身分證字號產生器造一組(很多判決裡都是那個經典的 Z 開頭號碼)、冒用別人的、或是用已經過世的親人的
  • 其餘幾件則是無罪(證據不足)、詐欺案中這部分未獲證明、或行為發生在 105 年修法之前而不適用
  • 更有意思的是,32 件的論罪一律寫成「以不正方法將虛偽資料輸入電腦」。條文裡那個「不正指令」,從頭到尾沒有被單獨拿來用過
  • 幾乎每一件都跟「行使偽造準私文書罪」一起成立,從一重處斷。之所以會構成偽造文書,正是因為輸入了別人或不存在的人的身分

換句話說,這條罪在實務上運作起來,核心是假冒身分,不是自動化。

法院反覆出現一句定型的說法,說訂票系統要求以本人或經授權的身分證字號訂票,目的是「驗證身分、限制訂票張數,俾防止車票遭大量壟斷」。

這趟查下來,我改了什麼

就是前面那一節寫的:每天 9 張的上限是防壟斷用的,不是我的重試預算。 我原本把它當額度在用,這個想法本身就是錯的,所以連訂比較的做法整個拿掉,改成任何時候只有一筆訂單活著。

至於這樣算不算已經到了刑事處罰的門檻,那是律師和法院的事,我不會在自己的部落格上幫自己下結論。我能做的是:用自己的帳號、自己的身分證、訂自己要搭的那班車,不轉售、不代訂、不碰熱門票,把最說不過去的那一段改掉。另外我打算把「送出訂票」這最後一步交回自己按

這篇文章也因此從「做了一個好玩的工具」變成「做了一個工具,然後被問了一句,於是去把功課補完」。後面這個版本我覺得比較誠實。

為什麼不開源

自動化訂票工具很容易被聯想到搶票程式,那是台鐵明確在抓的紅線。

我自己這一套的用法是:自己的帳號、自己與太太的身分證、我們自己要搭的那班車,不轉售、不替外人代訂,操作的是我電腦上真的登入著的瀏覽器,跟我手動點按鈕同一個節奏。但這些自我約束全都只存在於我這一台電腦上。工具放出去,就管不到別人拿去做什麼了。所以這個專案留在我自己的電腦上。

最後回到那句留言。有人願意在你貼文底下說一句「等等,這樣可以嗎」,其實是幫了大忙。被問到的時候,去查比去辯有用。

有 AI agent 訂閱的話,可以請它代做

概念都在上面了。有訂閱 Claude 或 Codex 的朋友,可以把這篇餵給你的 AI,說「我也要一個這樣的」,再補上你自己的路線、班次與座位偏好,應該一兩個晚上就能長出你的版本。

動手之前請先讀這段

台鐵的訂票須知明文禁止程式訂票,鐵路法第 65 條第 2 項對「將虛偽資料或不正指令輸入電腦而購買車票」定有刑責(五年以下有期徒刑或併科三百萬元以下罰金)。 這篇分享的是查詢、對帳、時刻表與座位判斷這些資訊整理的作法,以及我讀完條款和判決之後怎麼收斂自己的做法。最後那一下送出訂票,請自己按。 絕對不要拿去做的事:用產生器造的或別人的身分證字號訂票、代訂、加價轉售、搶熱門票。這些在判決裡是真的有人被判刑的。

你出判斷(哪幾天要出門、座位在意什麼、什麼情況不能自動退票),它出機械勞動。還不熟悉 AI agent 的話,可以從入門系列開始:從零開始怎麼跟 AI agent 講話,再看看工作流怎麼長大

結語

回頭看,這個祕書真正解決的不是「按幾下按鈕」,而是把散在門診表、停診公告、台鐵訂票紀錄裡的資訊,收成一個看一眼就知道還缺什麼的畫面。訂票只是最後那一下。

座位的規則還在累積,每搭一次車就多一筆觀察。有走花東線的朋友,也歡迎跟我分享你們判斷座位的方法~

🧋請我喝飲料