天天看點

解析12306訂票流程

20015/09/15更新:12306代碼已變化,本文代碼分析己過期,Github托管軟體也失效,感謝支援!

前言

每當春節臨近時,因為網絡的友善,通路12306購買火車票回家過年成了很多人的首選。但由于12306的種種不給力,給那些在官網刷票的人帶來了很多的不便。從2011年未12306上線起,連續幾年回家我都是靠網上購票,今年也不例外;我記得11年時我使用的是官網直接購票,到了12年則使用了新出的木魚搶票助手,而今年我用了360與獵豹兩款主流搶票浏覽器,還發動了幾位朋友一起幫忙,才買到了一張差強人意的票,現在感覺買票是越來越困難。而就在前幾天媒體還曝出了商業黃牛使用假身份證生成器10分鐘鈔殺1000多張票的新聞,讓人吃驚不已。于是就萌生了自己寫一個搶票應用的念頭,最開始設想的就是本地桌面應用,而非浏覽器插件,個人覺得本地應用始終比浏覽器插件靈活,因為本地應用可以精确穩定的請求有用的連結,過濾圖檔和CSS等前台無用請求,可以節省網絡消耗時間。于是我花了一段時間将12306的整體訂票流程解析了一遍,其間還經曆了一次12306的改版,幸好主體流程改動不是很大,終算有點收獲。

粗略的将12306的流程劃分為:登入、查詢和訂票三大子產品,下面就這三大子產品逐一說明:

1.登入

登入12306請求的URL是:https://kyfw.12306.cn/otn/login/init,可以使用Firbug抓取一下它的請求頭,得到的response響應内容如下:

解析12306訂票流程

從中可以看到Set-Cookie資訊,也就是說,如果想要登入就必須先請求https://kyfw.12306.cn/otn/login/init這個連結,以擷取服務端設定的Cookie資訊,而有了該Cookie資訊就可以将其儲存,以備下步的請求使用。

再來分析一下它的頁面HTML與其對應處理登入的Javascript腳本檔案(https://kyfw.12306.cn/otn/resources/merged/login_js.js),得到如下流程:

1.使用者點選登入送出時先要驗證請求一下:https://kyfw.12306.cn/otn/login/loginAysnSuggest連結,用于判斷目前網絡環境是否可以登入,得到JSON資料(通過Firebug抓包):

{
    "validateMessagesShowId":"_validatorMessage"
    "status":true
    "httpstatus":200,
    "data":{
        "loginCheck":"Y"
    },
    "messages":[],
    "validateMessages":{}
}
           

這裡通過判斷data.loginCheck是否為字元串Y判斷使用者是否可以登入,如不能登入,則顯示messages中的内容.

2.當使用者登入資訊檢查成功時,則POST請求https://kyfw.12306.cn/otn/login/userLogin,得到登入請求後的HTML,對應請求的參數為:

"loginUserDTO.user_name":  // 使用者名
"userDTO.password":        // 密碼
"randCode":                // 驗證碼
           

注:登入圖檔驗證碼的擷取位址可以從登入頁面的HTML中得到為: https://kyfw.12306.cn/otn/passcodeNew/getPassCodeNew?module=login&rand=sjrand

3.通過解析擷取的HTML可以根據id為login-txt的<span>标簽來判斷是否登入成功,登入成功的對應的HTML内容為:

<span class="login-txt" style="color: #666666">
    <span>意見回報:
         <a class="cursor colorA" href="mailto:[email protected]" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" >
             [email protected]
         </a>您好,
    </span>
    <a id="login_user" href="/otn/index/initMy12306" target="_blank" rel="external nofollow"  
       class="colorA" style="margin-left:-0.5px;"><span>登入成功使用者名</span></a>|
    <a id="regist_out" href="/otn/login/loginOut" target="_blank" rel="external nofollow" >退出</a>
</span>
           

失敗的内容為:

<span class="login-txt" style="color: #666666">
    <span>意見回報:
         <a class="cursor colorA" href="mailto:[email protected]" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" >
               [email protected]
          </a>您好,請
    </span>
    <a id="login_user" href="/otn/login/init" target="_blank" rel="external nofollow" 
       class="colorA" style="margin-left:-0.5px;">登入</a> |
    <a id="regist_out" href="/otn/regist/init" target="_blank" rel="external nofollow" >注冊</a>
</span>
           

如上登入成功即可進行下一步的操作:對于車次的查詢。

2,車次查詢

新版車次預訂的查詢(這裡單指單程票查詢)大大減化了請求參數,隻接收出發地編碼,到達地編碼,出發日期與旅客編碼四個參數,所有的過濾操作都扔給了前台Javascript,這也說明了車次查詢流程的簡單,隻需請求一個連結位址:

查詢車次是通過GET:https://kyfw.12306.cn/otn/leftTicket/query連結擷取的,對應的查詢參數為(GET請求注意查詢參數的順序):

leftTicketDTO.train_date=2014-01-23  // 出發日期
leftTicketDTO.from_station=BJP       // 出發站編碼
leftTicketDTO.to_station=SHH         // 到達站編碼
purpose_codes=ADULT                  // 旅客編碼:成人為ADULT,學生為:0X00
           

對應的擷取的JSON資訊格式如下:

{"validateMessagesShowId": "_validatorMessage",
    "status": true,
    "httpstatus": 200,
    "data": [
        {"queryLeftNewDTO": {
                "train_no": "240000G14104",          // 列車編号
                "station_train_code": "G141",        // 車次
                "start_station_telecode": "VNP",     // 始發站編碼
                "start_station_name": "北京南",      // 始發站名
                "end_station_telecode": "AOH",       // 終到站編碼
                "end_station_name": "上海虹橋",      // 終到站名
                "from_station_telecode": "VNP",      // 查詢輸入經過站編碼
                "from_station_name": "北京南",       // 查詢輸入經過站名
                "to_station_telecode": "AOH",        // 查詢輸入到站編碼
                "to_station_name": "上海虹橋",       // 查詢輸入到站名
                "start_time": "14:16",               // 出發時間
                "arrive_time": "19:47",              // 到站時間
                "day_difference": "0",               // 花費天數
                "train_class_name": "",
                "lishi": "05:31",                    // 曆時
                "canWebBuy": "Y",                    // 是否可以預定
                "lishiValue": "331",
                "yp_info": "O055300094M0933000999174800017",
                "control_train_day": "20301231",
                "start_train_date": "20140123",
                "seat_feature": "O3M393",
                "yp_ex": "O0M090",
                "train_seat_feature": "3",
                "seat_types": "OM9",
                "location_code": "P3",
                "from_station_no": "01",
                "to_station_no": "09",
                "control_day": 19,
                "sale_time": "1400",                // 出票時間點hhmm
                "is_support_card": "1",
                "gg_num": "--",
                "gr_num": "--",          // 進階軟卧座剩餘數
                "qt_num": "--",          // 其他座剩餘數
                "rw_num": "--",          // 軟卧座剩餘數
                "rz_num": "--",          // 軟座座剩餘數
                "tz_num": "--",          // 特等座剩餘數
                "wz_num": "--",          // 無座座剩餘數
                "yb_num": "--",
                "yw_num": "--",          // 硬卧座剩餘數
                "yz_num": "--",          // 硬座座剩餘數
                "ze_num": "有",          // 二等座剩餘數
                "zy_num": "有",          // 一等座剩餘數
                "swz_num": "17"          // 商務座剩餘數
            },
            "secretStr": "預定請求令牌字元串",
            "buttonTextInfo": "預訂或開售日期"
        },
        ..........                       // 省略其它車次,資訊同上
    ],
    "messages": [],
    "validateMessages": {}
}
           

注意這裡的 canWebBuy屬性,用于标記該趟列車是否可以預訂,還有對應列車的 secretStr字元,它用于請求預訂确認頁面的令牌,

對于其中一直提到的列車站點編碼,可以通過請求https://kyfw.12306.cn/otn/resources/js/framework/station_name.js連結,通過得到JS腳本中的station_names變量擷取,對應的站點以@字元分隔,而每一個站點資訊如下,這裡以北京北為例:

bjb|北京北|VAP|beijingbei|bjb|0
           

用于提取其中有用的資訊是:北京北與VAP,使用查詢北京北的編碼就是VAP,其它站點的解析同理。

如上即可以查詢指定出發地與到達地的車次預定資訊,緊接着進行預訂流程的分析。

3,車票預訂

在12306的解析中,就屬車票預訂的解析最為費神,也是最核心的一個流程,我現在隻掌握了成人單程票的預訂流程,其他的比如返程,學生票等都還沒有分析出來,如下講解的就是關于成人單程票的預定基本流程:

3.1,擷取預定确認頁面

車票預定首先要請求擷取車票的預訂确認頁面,如下流程圖所示:

解析12306訂票流程

分析:該流程是在使用者單擊車次的“預訂”按鈕時觸發的,如圖所示,擷取預訂确認頁面,先要判斷使用者是否登入,POST請求的位址是:https://kyfw.12306.cn/otn/login/checkUser,這個請求無參數,然後通過判斷得到的JSON資訊中的data.flag屬性是否為true判斷使用者是否已登入,接着再根據對應列車查詢時所獲得的secretStr字元與使用者輸入的查詢資訊POST請求https://kyfw.12306.cn/otn/leftTicket/submitOrderRequest,判斷使用者是否可以通路預定确認畫面,通過得到JSON資訊的status屬性判斷是否允許通路,如果為true說明可以通路,最後依據旅行類型為單程(dc)POST跳轉擷取單程車票的預訂确認畫面:https://kyfw.12306.cn/otn/confirmPassenger/initDc。如果登入使用者不進行上述判斷,直接POST請求https://kyfw.12306.cn/otn/confirmPassenger/initDc提示非法請求,隻有成功擷取預訂确認頁面後才能進行下一步的操作。

注:該流程可以檢視對應JS腳本:https://kyfw.12306.cn/otn/resources/merged/queryLeftTicket_end_js.js,function L(b4, bX)方法獲知。

從請求訂單的确認畫面還可以得到擷取目前登入使用者常用聯系人的連結位址為:https://kyfw.12306.cn/otn/confirmPassenger/getPassengerDTOs。

3.2,預訂送出

在車票的預定送出之前必先要擷取預定确認畫面的原因是因為預訂确認HTML中聲明的orderRequestDTO與ticketInfoForPassengerForm兩個Javascript變量,含有預訂送出的時的必需參數資訊,下面就預訂送出給出粗略的流程分析圖,如下:

解析12306訂票流程

注:圖檔可以右擊後檢視大圖,該流程對應的JS檔案位址為:https://kyfw.12306.cn/otn/resources/merged/passengerInfo_js.js

分析:如上圖顯示了車票預定送出的大體流程,可以依據請求的連結數将其分為四大塊:

1.檢查使用者選擇的乘客資訊的合法性,POST請求:https://kyfw.12306.cn/otn/confirmPassenger/checkOrderInfo,通過分析得到的JSON中的data.submitStatus屬性是否為true判斷,同時這一步的JSON資訊中還會包含有一個data.isCheckOrderInfo屬性将會作為下一步判斷目前使用者是否可排隊請求的參數。對應請求參數有如下5個:

cancel_flag: "2",                                         // 固定值
bed_level_order_num: "000000000000000000000000000000",    // 固定值
passengerTicketStr: getpassengerTickets(),                // 旅客資訊字元串
oldPassengerStr: getOldPassengers(),                      // 旅客資訊字元串
tour_flag: ticketInfoForPassengerForm.tour_flag,  // 從ticketInfoForPassengerForm中擷取
randCode: $("#randCode").val()                            // 前台輸入驗證碼
           

這五個參數中,有兩個參數需要注意passengerTicketStr與oldPassengersStr:

passengerTicketStr是以下劃線"_"分隔當每一個乘客資訊組成的字元串,對應每個乘客資訊字元串組成如下:

座位編号,0,票類型,乘客名,證件類型,證件号,手機号碼,儲存常用聯系人(Y或N)
           

同樣oldPassengersStr也是以下劃線"_"分隔每個乘客資訊組成的字元串,對應每個乘客資訊字元串組成如下:

乘客名,證件類型,證件号,乘客類型
           

在上面的資訊中座位編号指的是,一等座、二等座等的編碼,從 ticketInfoForPassengerForm.limitBuySeatTicketDTO.seat_type_codes屬性中選擇擷取。

票類型指的是,成人票,學生票等的編碼,可以從ticketInfoForPassengerForm.limitBuySeatTicketDTO.ticket_type_codes屬性中選擇擷取。

證件類型指的是二代身份證,學生證,簽證等的編碼,可以從ticketInfoForPassengerForm.cardTypes屬性中選擇擷取。

最後oldPassengersStr中的乘客類型主要有如下資訊:

adult: "1",
child: "2",
student: "3",
disability: "4"
           

取上面對應的數字編碼。

注意:在組合oldPassengersStr乘客資訊字元串時,未尾會多一個下劃線,送出請求是一定要補上,從上也可以看出所有的一些參數都是通過ticketInfoForPassengerForm變量擷取的,這也是為什麼要事先擷取預定确認畫面HTML的原因。

2.檢查乘合資訊合法後,接下來就會結合傳回的data.isCheckOrderInfo屬性,POST請求:https://kyfw.12306.cn/otn/confirmPassenger/confirmSingleForQueue,判斷目前乘客是否可以排隊,對應的參數如下:

train_date: new Date(orderRequestDTO.train_date.time).toString(),  // 列車日期
train_no: orderRequestDTO.train_no,                                // 列車号
stationTrainCode: orderRequestDTO.station_train_code,
seatType: limit_tickets[0].seat_type,                            // 座位類型
fromStationTelecode: orderRequestDTO.from_station_telecode,      // 發站編号
toStationTelecode: orderRequestDTO.to_station_telecode,          // 到站編号
leftTicket: ticketInfoForPassengerForm.queryLeftTicketRequestDTO.ypInfoDetail,
purpose_codes: n,         // 預設取ADULT,表成人,學生表示為:0X00
isCheckOrderInfo: m       // data.isCheckOrderInfo
           

這裡的參數要注意傳遞列車日期的方式,及座位類型編碼,這裡選擇的是第一個乘客的座位類型編碼。最後還要確定orderRequestDTO變量的準确性。

通過傳回的JSON資訊的data屬性值來判斷是否允許目前使用者進行排隊下單,并提示目前的剩餘票數。

其中的data屬性會包含有兩個重要的參數,countT與ticket,(ticket的格式為:1*****30314*****00001*****00003*****0000的形式):

countT表示的是排隊人數,而ticket指的是目前列車對應座位的剩餘票數,可以通過https://kyfw.12306.cn/otn/resources/merged/passengerInfo_js.js檔案中的function L(l, m) 函數解析擷取:

function L(l, m) {
            rt = "";
            seat_1 = -1;
            seat_2 = -1;
            i = 0;
            while (i < l.length) {
                s = l.substr(i, 10);
                c_seat = s.substr(0, 1);
                if (c_seat == m) {
                    count = s.substr(6, 4);
                    while (count.length > 1 && count.substr(0, 1) == "0") {
                        count = count.substr(1, count.length)
                    }
                    count = parseInt(count);
                    if (count < 3000) {
                        seat_1 = count
                    } else {
                        seat_2 = (count - 3000)
                    }
                }
                i = i + 10
            }
            if (seat_1 > -1) {
                rt += seat_1
            }
            if (seat_2 > -1) {
                rt += "," + seat_2
            }
            return rt
        }
           

函數中的l指的就是 ticket,而m指的是第一位乘客所選擇的座位編号。

如果計算的餘票資訊還有剩餘,則會提示使用者點選确認按進行訂單的送出請求,如果沒有充實的票,則會提示使用者選擇其它車次,處理該請求的方法詳情見https://kyfw.12306.cn/otn/resources/merged/passengerInfo_js.js檔案中的function M(n, m) 方法。

3.當提示的有充足的餘票,且使用者點選了确定按鈕,則接下來會POST請求:https://kyfw.12306.cn/otn/confirmPassenger/confirmSingleForQueue,進行單程票(dc)類型的排隊下單操作,通過判斷傳回的JSON資訊data.submitStatus屬性判斷訂單是否以成功送出至伺服器,對應的請求參數為:

passengerTicketStr: getpassengerTickets(),
oldPassengerStr: getOldPassengers(),
randCode: $("#randCode").val(),
purpose_codes: ticketInfoForPassengerForm.purpose_codes,
key_check_isChange: ticketInfoForPassengerForm.key_check_isChange,
leftTicketStr: ticketInfoForPassengerForm.leftTicketStr,
train_location: ticketInfoForPassengerForm.train_location
           

這裡的參數沒有新意,主要是注意擷取 ticketInfoForPassengerForm變量的準确性。

4.訂單送出至伺服器後不一定說明訂單已經成功了,還需要GET請求:https://kyfw.12306.cn/otn/confirmPassenger/queryOrderWaitTime,判斷系統是否已根據送出的訂單資訊為相應的乘客占位成功,并提示預估出票等待時間,這一步隻有一個參數,就是旅行類型,由于我們主要考慮的是單程票,故送出時POST dc就行了,如下:

tourFlag: "dc"
           

這一步占位的操作在12306的官網中是将其封裝在了一個名為OrderQueueWaitTime的對象中,可以解壓https://kyfw.12306.cn/otn/resources/merged/passengerInfo_js.js檔案獲知,對應的如果判斷系統占位成功,将會從返的JSON資訊中擷取data.orderId屬性,即為下單成功時的訂單号。

如上4次請求就可以準确的模拟出12306官網訂單送出的整套流程,其中其實還忽略了驗證碼的擷取與判斷操作,而這一步僅僅是判斷驗證碼的合法性,與主體流程無關。對應訂單确定頁面的驗證碼擷取連結為:https://kyfw.12306.cn/otn/passcodeNew/getPassCodeNew?module=passenger&rand=randp,從中與登入頁面的驗證碼連結對比,可知新版12306的驗證碼管理統一為了一個方法,登入與訂單确認的驗證碼連結隻是傳遞的module和rand參數不一樣而已。

4,結束語:

根據上面的操作,基本可以全程模拟官網的訂單操作,編寫出一個屬于自己的搶票助手。在寫這篇文章時,我一直在想這樣做是否有意義,因為12306随時都有可能變更,由于23:00點~07:00點的維護時間段的設定,也許今天寫出來的東西明天馬上就會失效過期。但仔細考慮後還是打算将他分享出來,就當是一種學習吧。同時在這裡公布GitHub上使用Python3編寫的一個訂票項目源碼:https://github.com/lzqwebsoft/trainticket,對應window下獨立運作exe檔案下載下傳位址為:https://code.google.com/p/lzqwebsoft-projects/source/browse/#svn%2Ftrunk,軟體運作效果如下:

解析12306訂票流程