前言
本套教程共分3章:
本套內容主要用于分析12306購票流程,意在編寫一套自動購票小程序。12306接口 api 經常變動,但是流程分析是固定的。因此,本套教程主要記錄12306 相關購票流程分析過程,以作記錄。
下單流程分析
查詢到余票后,點擊預定(假設用戶已經登錄),那么就會走下單流程:
- 下單第一步:
submitOrderRequest
,提交訂單請求。
我們來細看下這個請求:
submitOrderRequest_headers
可以看到,submitOrderRequest
是POST
請求,其請求地址為:
https://kyfw.12306.cn/otn/leftTicket/submitOrderRequest
既然是POST
請求,那么我們就來看下它的請求體內容:
圖中對參數(shù)的解釋已經很清楚了,這里唯一要注意的是:我們在查詢時獲取得到的secretStr
是已經被urlencode
后的內容,因此,當我們在發(fā)送請求時,首先要把這個secretStr
先解碼回來,這樣做的原因是大部分網(wǎng)絡請求框架對于form data
數(shù)據(jù)都會自動進行urlencode
(比如requests
對于dict
數(shù)據(jù)),因此,這里需要先解碼回原來的數(shù)據(jù),后面網(wǎng)絡請求框架編碼后才能得到正確的值。
最后,來看下服務器給我們返回的內容:
這里我們只需看下status
的內容即可,如果status
為true
,說明我們的請求時成功的;如果status
為false
,說明我們請求失敗了,失敗原因可以在messages
中獲取。
- 接下來,如果我們在12306官網(wǎng)進行預訂這個操作后,我們的頁面就會跳轉到:
https://kyfw.12306.cn/otn/confirmPassenger/initDc
(對于返程票,則跳轉到:https://kyfw.12306.cn/otn/confirmPassenger/initWc
),這個界面是非常重要的,后續(xù)的請求中攜帶的參數(shù)信息很多都是存在于這個頁面源碼中。
因此,我們先來看下這個網(wǎng)頁的源代碼:
initDc_reqeatSubmitToken
initDc_ticketInfoForPassengerForm
我們主要獲取的就是globalRepeatSubmitToken
和ticketInfoForPassengerForm
這兩個變量內容,其中globalRepeatSubmitToken
就是一個字符串,可以很容易由正則得到它的內容;而ticketInfoForPassengerForm
里面我們需要的內容較多,用正則獲取會繁瑣很多,仔細看一下,其實它就是一個json
數(shù)據(jù),所以我們可以先用正則獲取json
字符串內容,再將其轉成json
就可以了,但是這里需要注意一下,ticketInfoForPassengerForm
的格式不是嚴格符合json
格式,因此這里獲取到字符串后,需要把里面的單引號('
)轉換成雙引號("
),這樣才能正確的用json
來解析(小細節(jié),注意一下)。
- 接下來,就到
getPassengerDTOs
,即獲取乘客信息:
這里也是一個POST
請求,請求網(wǎng)址為:
https://kyfw.12306.cn/otn/confirmPassenger/getPassengerDTOs
參數(shù)信息也比較簡單:
參數(shù)內容見上圖。
那么我們下面我們來看下服務器給我返回的數(shù)據(jù):
可以看到服務給我呢返回的是一個json
數(shù)據(jù)。這里同樣要先看下status
的狀態(tài),為true
,請求成功,為false
,請求失敗,失敗內容從messages
中獲取。
如果我們的請求成功了(status:true
),那么我們就可以打開data
字段,就可以看到如下圖所示內容:
可以看到,在data
字段里面的normal_passengers
字段就存儲了我們賬號中所有乘客的信息。因此,我們這里最主要的就是提取出這些乘客的信息,為后續(xù)提交訂單做準備(乘客信息)。
- 接下來,會對訂單信息進行檢查:
checkOrderInfo
,對應于網(wǎng)頁操作 提交訂單 這個過程:
首先,這是一個POST
請求,網(wǎng)址為:
https://kyfw.12306.cn/otn/confirmPassenger/checkOrderInfo
然后,看下請求體:
這里的cancel_flag
和bed_level_order_num
應該是固定值,但是這兩個鍵在ticketInfoForPassengerForm
里面也能查找到,只是為null
,這里應該是12306預留的一些鍵值,建議在程序中首先從ticketInfoForPassengerForm
獲取,當其為null
時,才采用上面的默認值(鬼知道12306什么時候就用上這些預留的鍵訥!!!)
passengerTicketStr
這個參數(shù)的組合方式為:1(seatType),0,1(車票類型:ticket_type_codes),張三(passenger_name),1(證件類型:passenger_id_type_code),320xxxxxx(passenger_id_no),151xxxx(mobile_no),N
如果有多個乘客,那么各個乘客之間用一個_
分隔:seatType,0,ticket_type_codes,xxxx,mobile_no,N_seatType,0,ticket_type_codes,xxxx,mobile_no,N
oldPassengerStr
這個參數(shù)的組合方式為:張三(passenger_name),1(證件類型:passenger_id_type_code),320xxxxxx(passenger_id_no),1_
如果有多個乘客,那么直接拼接到后面就可以了:name,1,identity,1_name2,1,identity2,1_
最后看下返回結果:
各鍵值含義請看上圖。
這里有一個點可以注意的是:我們上面看到的是成功的返回信息,筆者在測試時,抓包抓到檢測失敗(即status:true
,submitStatus:false
)的信息里面data
字段里面會有一個errMsg
鍵值,其攜帶了失敗的具體信息,所以在程序中如果submitStatus
失敗了,那么可以輸出errMsg
顯示原因。
注:筆者覺得這里可以大膽假設一下,形如上面格式的json
數(shù)據(jù)(包含status
,message
用來判別請求是否成功,以及另一個鍵內部擁有一個標志的鍵值),那么,在請求成功,動作失敗時,一定會伴隨有一個失敗信息的鍵值返回,即errMsg
。
- 接下來的一步,就是請求:
getQueueCount
,獲取余票和排隊信息。
這里同樣還是一個POST
請求,請求網(wǎng)址為:
https://kyfw.12306.cn/otn/confirmPassenger/getQueueCount
這里 Firefox 獲取的參數(shù)train_date
的格式是錯誤的,因此這里放上 Chrome 上截取到的最新參數(shù)信息(內容可能與上面的信息不同,但是參數(shù)獲取原來一致):
最后來看下返回體內容吧:
可以看到返回結果也是一個json
,返回結果的參數(shù)內容見上圖,至于其它參數(shù)是什么意思,筆者暫時也未能分析出來!哈哈。
- 獲取到剩余票數(shù)和排隊信息后,接下來就是要將我們的訂單入隊確認過程:
這里對應網(wǎng)頁操作的 選擇座位確定 這個過程,如下圖所示:
confirmSigle/GoForQueue_web
注:此處,根據(jù)票的類型,向不同的網(wǎng)址發(fā)送確認信息:
- 如果是單程票(
dc
):則訂單入隊確認過程為:confirmSingleForQueue
。 - 如果是往返票(
wc
):則訂單入隊確認過程為:confirmGoForQueue
。
還是按照我們上面的分析過程來:
首先,這是一個POST
請求,請求網(wǎng)址為:
- 單程票(
dc
)
https://kyfw.12306.cn/otn/confirmPassenger/confirmSingleForQueue
- 往返票(
wc
)
https://kyfw.12306.cn/otn/confirmPassenger/confirmGoForQueue
然后,來看下請求體:
參數(shù)詳情見上圖。
這里要講一下:choose_seats
,這里的值可以為空,表示隨機座位;也可以按照上面所示的輸入座位號,具體的座位號應該是按下圖所示方式進行排序:
最后,看下返回結果:
這里看到其返回的json
格式跟我們上面分析checkOrderInfo
格式是一致的,所以這里大膽猜測一下,當submitStatus
返回false
時,會同時返回errMsg
字段。
- 上面已經入隊成功了,那么接下來就等待下單成功:
queryOrderWaitTime
:
這個過程需要我們自己不斷的對服務器進行輪詢,獲取下單結果。因此,這是一個GET
操作,其 URL 為:
https://kyfw.12306.cn/otn/confirmPassenger/queryOrderWaitTime
那么,我們來看下其發(fā)送的參數(shù)內容:
參數(shù)詳情請查看上圖。
最后,來看下服務器返回的結果:
這里的操作步驟是,我們需要死循環(huán)輪詢訂單結果,直到當waitTime
為負數(shù)并且返回的訂單號(orderId
)不為null
,那么就表明我們下單成功了。如果waitTime
為負數(shù),但是orderId
為null
,那么就需要從data[msg]
提取出失敗信息,12306官方請求輪詢時間為3秒。(筆者測試發(fā)現(xiàn),程序中請求時,waitTime
的值經常為-100,因此,這里用waitTime
進行判斷可能存在誤區(qū),必須用orderId
進行判斷,當orderId
為空,并且data[msg]
不為空,則失敗,退出程序)
注: 上述接口中的waitTime
其實就是訂單排隊時間,單位:秒。
所以我們可以通過waitTime
數(shù)值,大概估計出排隊時間,既可以給出友好的提示,又可以根據(jù)這個預估時間動態(tài)調整再次查詢請求,減少請求次數(shù)。
- 此時,我們還剩下最后一步,就是查詢下我們這個訂單號是不是最終成功下單了:
resultOrderForDcQueue
(resultOrderForWcQueue
):
首先,這是一個POST
請求,網(wǎng)址為:
- 單程票(
dc
)
https://kyfw.12306.cn/otn/confirmPassenger/resultOrderForDcQueue
- 往返票(
wc
)
https://kyfw.12306.cn/otn/confirmPassenger/resultOrderForWcQueue
然后,看下請求體:
參數(shù)含義見上圖。
最后,看下返回結果:
參數(shù)含義見上圖。
以上,其實就已經算是完成了下單流程。
但是,如果還想獲取訂單詳細信息,則請看第9步。
- 獲取訂單詳細信息:
queryMyOrderNoComplete
:
這個接口也是一個POST
請求,其api為:
https://kyfw.12306.cn/otn/queryOrder/queryMyOrderNoComplete
其參數(shù)為:
最重要的是其返回值:
{
"validateMessagesShowId": "_validatorMessage",
"status": true,
"httpstatus": 200,
"data": {
"orderDBList": [
{
"sequence_no": "EH66031825", //訂單號
"order_date": "2018-01-31 17:56:12", //下單時間
"ticket_totalnum": 2, //車票總張數(shù)
"ticket_price_all": 34200, //車票總價:342.00元
"cancel_flag": "Y",
"resign_flag": "4",
"return_flag": "N",
"print_eticket_flag": "N",
"pay_flag": "Y",
"pay_resign_flag": "N",
"confirm_flag": "N",
"tickets": [
{
"stationTrainDTO": {
"trainDTO": {
"train_no": "6i000D40920C"
},
"station_train_code": "D4092", //車次
"from_station_telecode": "IOQ",
"from_station_name": "深圳北", //出發(fā)站
"start_time": "1970-01-01 06:40:00",
"to_station_telecode": "CBQ",
"to_station_name": "潮汕", //到達站
"arrive_time": "1970-01-01 08:44:00",
"distance": "305" //距離(km)
},
"passengerDTO": {
"passenger_name": "用戶名1", //用戶名
"passenger_id_type_code": "1",
"passenger_id_type_name": "二代身份證",
"passenger_id_no": "xxxxxxxxxx", //身份證號
"total_times": "98"
},
"ticket_no": "EH66031825101003D",
"sequence_no": "EH66031825", //訂單號
"batch_no": "1",
"train_date": "2018-02-27 00:00:00", //出發(fā)日期
"coach_no": "01", //車廂號
"coach_name": "01", //車廂名
"seat_no": "003D", //座位號
"seat_name": "03D號", //座位名
"seat_flag": "0",
"seat_type_code": "M", //座位類別號
"seat_type_name": "一等座", //座位類型名
"ticket_type_code": "1", //票類型號
"ticket_type_name": "成人票", //票類別
"reserve_time": "2018-01-31 17:56:12", //票預訂時間
"limit_time": "2018-01-31 17:56:12",
"lose_time": "2018-01-31 18:26:13",
"pay_limit_time": "2018-01-31 18:26:13",//最遲付款時間
"ticket_price": 17100, //票單價(171.00元)
"print_eticket_flag": "N",
"resign_flag": "4",
"return_flag": "N",
"confirm_flag": "N",
"pay_mode_code": "Y",
"ticket_status_code": "i",
"ticket_status_name": "待支付", //支付狀態(tài)
"cancel_flag": "Y",
"amount_char": 0,
"trade_mode": "",
"start_train_date_page": "2018-02-27 06:40",
"str_ticket_price_page": "171.0", //票價字符串
"come_go_traveller_ticket_page": "N",
"return_deliver_flag": "N",
"deliver_fee_char": "",
"is_need_alert_flag": false,
"is_deliver": "N",
"dynamicProp": "",
"fee_char": "",
"insure_query_no": "",
"column_nine_msg": "",
"lc_flag": "4",
"integral_pay_flag": "N"
},
{
"stationTrainDTO": {
"trainDTO": {
"train_no": "6i000D40920C"
},
"station_train_code": "D4092",
"from_station_telecode": "IOQ",
"from_station_name": "深圳北",
"start_time": "1970-01-01 06:40:00",
"to_station_telecode": "CBQ",
"to_station_name": "潮汕",
"arrive_time": "1970-01-01 08:44:00",
"distance": "305"
},
"passengerDTO": {
"passenger_name": "用戶2",
"passenger_id_type_code": "1",
"passenger_id_type_name": "二代身份證",
"passenger_id_no": "xxxxxxxxxxxx", //身份證
"total_times": "98"
},
"ticket_no": "EH66031825101003F",
"sequence_no": "EH66031825",
"batch_no": "1",
"train_date": "2018-02-27 00:00:00",
"coach_no": "01",
"coach_name": "01",
"seat_no": "003F",
"seat_name": "03F號",
"seat_flag": "0",
"seat_type_code": "M",
"seat_type_name": "一等座",
"ticket_type_code": "1",
"ticket_type_name": "成人票",
"reserve_time": "2018-01-31 17:56:12",
"limit_time": "2018-01-31 17:56:12",
"lose_time": "2018-01-31 18:26:13",
"pay_limit_time": "2018-01-31 18:26:13",
"ticket_price": 17100,
"print_eticket_flag": "N",
"resign_flag": "4",
"return_flag": "N",
"confirm_flag": "N",
"pay_mode_code": "Y",
"ticket_status_code": "i",
"ticket_status_name": "待支付",
"cancel_flag": "Y",
"amount_char": 0,
"trade_mode": "",
"start_train_date_page": "2018-02-27 06:40",
"str_ticket_price_page": "171.0",
"come_go_traveller_ticket_page": "N",
"return_deliver_flag": "N",
"deliver_fee_char": "",
"is_need_alert_flag": false,
"is_deliver": "N",
"dynamicProp": "",
"fee_char": "",
"insure_query_no": "",
"column_nine_msg": "",
"lc_flag": "4",
"integral_pay_flag": "N"
}
],
"reserve_flag_query": "p",
"if_show_resigning_info": "N",
"recordCount": "1",
"isNeedSendMailAndMsg": "N",
"array_passser_name_page": [ //用戶列表
"用戶1",
"用戶2",
........
"用戶n"
],
"from_station_name_page": [
"深圳北"
],
"to_station_name_page": [
"潮汕"
],
"start_train_date_page": "2018-02-27 06:40",
"start_time_page": "06:40",
"arrive_time_page": "08:44",
"train_code_page": "D4092",
"ticket_total_price_page": "342.0",
"come_go_traveller_order_page": "N",
"canOffLinePay": "N",
"if_deliver": "N",
"insure_query_no": ""
}
],
"to_page": "db"
},
"messages": [],
"validateMessages": {}
}
所以,對于該接口返回內容,首先要判斷status
的值為true
,表明請求成功,如果status
為false
,那么猜測錯誤原因在messages
字段中。然后下單的車票內容在data["orderDBList"][0]["tickets"][index]
中,這是一個數(shù)組,數(shù)組的長度就是下單的票數(shù)(就像上面下單數(shù)為2張,所以長度為2),每個數(shù)組內包含了車票的詳細信息,我們可以從中取出所需信息進行展示。
至此,我們終于完成12306下單整個流程。
總結
到這里,我們對12306的登錄流程,余票查詢流程和下單流程都已經詳細的解析完畢了,讀者根據(jù)我們這3篇的內容就可以編寫出一套自動購票小程序了 ^-^ 。
最后,附上筆者自己寫的一套自動搶票小軟件:EasyTrain