我也記不清啥時候動了寫bot刷票這個念頭的。原因很簡單,我一直認為作為一個以代碼謀生的不合格程序員,只有把生產(chǎn)工具用好,才能增加自己存在的價值。
首先說明一下主要開發(fā)環(huán)境:Windows 7,PHP 5.3,php_curl。
翻到了 第一條關于刷票的微博,附了圖

很不低調(diào)地炫耀。
要刷票,首先自然得熟悉目標系統(tǒng),所謂踩點。firefox+firebug,抓了一個標準流程的請求:登錄、查票、訂票。確認訂單一開始沒敢點,怕會有什么影響,后來去注冊了幾個測試號,然后嘗試了確認訂單的操作。流程本身不復雜,但是提交參數(shù)有點太多,一步一步來。
回到圖1,登錄,其實核心在驗證碼。
1 驗證碼識別
登錄的驗證碼處理起來很簡單,圖在 https://dynamic.12306.cn/otsweb/passCodeAction.do?rand=lrand,提供一個示例
。
這個驗證碼結(jié)構(gòu)一直沒變,字體比較規(guī)則,無變形,直接就想到了tesseract,上一次用這個的時候是2010年12月初為了刷順豐快遞的訂單追蹤。
流程很簡單:
1 下載圖片;
2 轉(zhuǎn)tiff(使用了ImageMagick的convert);
3 tesseract識別;
4 讀取識別文件;
5 簡單判斷識別輸出是否合法;
6 拼登錄請求。
代碼里留了個 decaptcha_valid()的方法,不過只判斷了是不是4位數(shù)字+字母。
寫完第一版代碼后,清理了一些個人信息,把代碼發(fā)給了@也云,不過當時畢竟比較早,他沒怎么看,但是后期驗證碼識別這塊提供了一些改進,包括:
1 replace識別輸出里的空字符,因為由于字符間距不確定,tesseract偶爾會識別出空格;
2 訓練tesseract,優(yōu)化識別效果;
3 這次tesseract用的是v3,已經(jīng)支持多種圖片格式,而我還是按上次使用的經(jīng)驗convert jpeg 2 tiff,這次直接省去這個過程;
4 最關鍵的,登錄驗證碼在登錄請求提交后并不會刪除上次訪問驗證碼圖片生成的session字段,也就是說,只需要識別一次驗證碼,后續(xù)只需要暴力提交即可。
按4調(diào)整了驗證碼識別方法,增加了人肉輸入的支持。使用system(‘out.jpg’)直接利用Windows 7 CMD的高級功能,同時發(fā)現(xiàn)了Win7自帶的圖片查看器system執(zhí)行后程序可以繼續(xù)往下走而不必關掉圖片查看器,這樣可以alt+tab切換cmd和圖片查看器,確認人肉識別的輸入是否正確。
上述邏輯在1月19號之前也同樣適用于確認訂單的驗證碼,那天早起發(fā)現(xiàn)這塊兒驗證碼需要每次請求了,幸好之前人肉識別的改造增加了配置開關,很輕松地就完成了改動(雖然19號刷票無一成功,但是20號回家客車上刷5張票輕松到手)。
2 查票
查票的過程其實沒啥特別可以說的,簡單拼個請求,查票就是了。不過返回值是需要處理的,處理的依據(jù),從頁面看,就是查票結(jié)果輸出里的“預訂”按鈕。

預訂的參數(shù)就是頁面輸出里的getSelected()這個js調(diào)用的參數(shù),最初的結(jié)構(gòu)我從另外一條微博里找到了例子。

正如頁面上所示,結(jié)構(gòu)是“車次號#歷時(分鐘單位)#發(fā)車時間#某個ID#始發(fā)站編號#目標站編號”,這個參數(shù)由頁面解析后帶入實際的預訂請求。
不過后來某一天,這個參數(shù)變了,一個輸出示例是“Z67#11:26#20:06#2400000Z6705#BXP#NCG#07:32#北京西#南昌#10175000003030800001404860000160895000001017503001”。同樣按上邊的解釋,結(jié)構(gòu)是“車次號#歷時#發(fā)車時間#某個ID#始發(fā)站編號#目標站編號#到站時間#某個帶入ypInfoDetail字段的參數(shù)”,@也云同學研究了一下最后這個字段,認定是“余票信息”的意思,對我們沒有特別的意義,可無視(后續(xù)也證實了)。
這里可能有用的信息包括上述的“某個ID”。而查票的時候也需要輸出里的始發(fā)站和目標站編號,這個編號可以從 這個地址里找到 。
3 訂票 && 提交訂單
訂票使用了查票的輸出參數(shù),提交url是 https://dynamic.12306.cn/otsweb/order/querySingleAction.do?method=submutOrderRequest。明顯有個Typo,而且這里名字是“提交訂單”。不管那么多,說技術(shù)細節(jié)。
這個使用查票參數(shù)構(gòu)造了一個POST請求,如果提交成功,服務器端會發(fā)一個302,重定向到一個新的頁面 https://dynamic.12306.cn/otsweb/order/confirmPassengerAction.do?method=init。
流程很簡單,curl實現(xiàn)只需要開 CURLOPT_FOLLOWLOCATION 即可。但是這個地方卡了我很久,直到12月初的某天才查出來為什么每次提交在這里總是出現(xiàn) HTTP 500,雖然還是不知道原因,但是補上了幾個http頭,一切ok了(具體看代碼)。
訂票輸出的頁面,會有formtoken,參數(shù)名是 org.apache.struts.taglib.html.TOKEN。有點Web編程經(jīng)驗就應該能知道是干啥的。不多說,參數(shù)請求必須帶上,所以正則匹配,無技術(shù)含量。
確認訂單頁只是拼參數(shù),補上驗證碼識別就ok,無技術(shù)含量。
第一個細節(jié),這個頁面是填身份證的頁面,入口有兩個,訂票 和 確認訂單失?。ê笃诖_認訂單失敗不再直接回到該頁面,不過驗證碼錯誤的應該還是會回來),所以確認訂單的錯誤處理是可以考慮增加的。這個一開始代碼就加上了,不過后來@也云改的lite版把這個流程改為 提交訂單 + 確認訂單,兩步走,確認失敗直接重訂票。
第二個細節(jié),驗證碼URL不一樣了,https://dynamic.12306.cn/otsweb/passCodeAction.do?rand=randp,不過輸出還是一樣的。
第三個細節(jié),最關鍵的,某個晚上睡前突然意識到的問題?;氐絝orm token上,我們可以觀察到這個系統(tǒng)很多地方都會有form token,包括但不限于確認訂單、支付訂單、撤銷訂單等等。但是我意識到一個問題,提交訂單這個過程可能會很慢,慢到一個不能忍的狀態(tài),尤其是高峰期。而,用戶中心的表單里,也有form token,而且參數(shù)名完全一樣,那么,這兩個token會是同一個session字段嗎?
接下來做的事情是改造form token的獲取方法,改從用戶中心的頁面匹配,僅作測試。測試結(jié)果很樂觀,從用戶中心匹配到的token能作為確認訂單token,而且這個請求自然會比從提交訂單拿到的頁面匹配要快,于是匹配token的方法可以優(yōu)化了。(但是16號早上,訂單系統(tǒng)似乎出問題了,各種慢,未支付訂單頁面刷不出來,支付訂單也失敗了,3張票扣了我1.3k RMB,沒出票,極度想粗口?。。。?/p>
說到第三細節(jié),如果你順著讀下來,可能會覺得有疑問:不是有很多參數(shù)要從頁面匹配嗎?如果不去拿提交訂單后的頁面,匹配變量,怎么確認訂單?
答案是:你所需要的參數(shù)從你拿到查票輸出的時候就已經(jīng)足夠了。你所需要的車次相關的信息,查票的輸出已經(jīng)能滿足所有字段的需求;你所需要的訂票人信息,以及所需坐席,你自己是已經(jīng)知道了的。而,經(jīng)過觀察,查票輸出里除了余票信息字段會變,其他的都不變?。?!所以,你只需要在確定要刷票的前一天晚上,做一下準備工作,查查你要刷票的車次的信息,第二天拿著刷票便是了?。?!
具體不多說,看代碼便知。
到此,基本能說的都說了。
后邊還有一些其他的細節(jié)優(yōu)化。
支付
最初的支付表單是在一個獨立的頁面上,從未完成訂單里選擇訂單,點支付,彈出的一個新頁面匹配幾個參數(shù)再submit,輸出頁面只有一個簡單的form,body onload的時候觸發(fā)了submit操作。也就是說,只需要拿下這個頁面完成支付即可,這個最早@也云實驗成功了,后來我有幾次支付也用這個方法完成。過了幾天,支付的表單在未完成訂單點支付后的頁面里直接寫死了,于是新的lite版bot直接匹配了頁面表單,存了個獨立頁面。
完后,彈出IE,支付。
本來想做招行的手機支付自動化的,后來實在懶了,自己訂票的需求滿足了,沒心做這種優(yōu)化。
登錄
登錄是件苦力活,經(jīng)常就人滿了,頻率高了IP也會被封。所以登錄成功一次得好好珍惜。
做法是: CURLOPT_COOKIEFILE + CURLOPT_COOKIEJAR。
腳本執(zhí)行結(jié)束后,手工改 cookie jar文件,把expiration時間改長點。
但是由于經(jīng)常性在登錄完成后ctrl+c,沒法觸發(fā)__destruct()的調(diào)用,按@也云的建議,改成了登錄完成后觸發(fā)一次curl close操作并改expiration,執(zhí)行結(jié)束后的繼續(xù)保留。
源碼
/bot.php
/config/test.php
執(zhí)行參數(shù) php bot.php 0 test 2>err.txt
只保證win下可用,soff改過一個mac下可用的版本,沒找他要。
test 是 config/ 下的 test.php
0 是 test.php 里的tickets_info字段的索引,其實叫train_info更合適
然后為了騷擾自己,加了個vbs文件,內(nèi)容很簡單,取名messagebox.vbs,這段內(nèi)容感謝@linxinsnow
' Message box Script
Set objArgs = WScript.Arguments
For I = 0 to objArgs.Count - 1
msgbox objArgs(I)
Next
寫在最后
雖然我經(jīng)常在微博XY,但是一直沒公開源碼。最終可用的代碼也只有也云和soff拿到了。
一直不開源或者不發(fā)布的原因很簡單,我不希望我的代碼成為別人系統(tǒng)“有壓力”的理由。我也看到了幾個網(wǎng)上放出來的刷票工具或者js解決方案,但是,越多人知道這個,就意味著越多人會用你的“刀”“殺”鐵道部。
或許我想的有點多。
感謝也云同學對本年度刷票的支持。感謝soff提供的驗證碼識別優(yōu)化方案,雖然效果還不如我們的。