前言
本套教程共分3章:
本套内容主要用于分析12306购票流程,意在编写一套自动购票小程序。12306接口 api 经常变动,但是流程分析是固定的。因此,本套教程主要记录12306 相关购票流程分析过程,以作记录。
下单流程分析
查询到余票后,点击预定(假设用户已经登录),那么就会走下单流程:
- 下单第一步:
submitOrderRequest
,提交订单请求。
我们来细看下这个请求:
可以看到,submitOrderRequest
是POST
请求,其请求地址为:
https://kyfw.12306.cn/otn/leftTicket/submitOrderRequest
既然是POST
请求,那么我们就来看下它的请求体内容:
图中对参数的解释已经很清楚了,这里唯一要注意的是:我们在查询时获取得到的secretStr
是已经被urlencode
后的内容,因此,当我们在发送请求时,首先要把这个secretStr
先解码回来,这样做的原因是大部分网络请求框架对于form data
数据都会自动进行urlencode
(比如requests
对于dict
数据),因此,这里需要先解码回原来的数据,后面网络请求框架编码后才能得到正确的值。
最后,来看下服务器给我们返回的内容:
这里我们只需看下status
的内容即可,如果status
为true
,说明我们的请求时成功的;如果status
为false
,说明我们请求失败了,失败原因可以在messages
中获取。
- 接下来,如果我们在12306官网进行预订这个操作后,我们的页面就会跳转到:
https://kyfw.12306.cn/otn/confirmPassenger/initDc
(对于返程票,则跳转到:https://kyfw.12306.cn/otn/confirmPassenger/initWc
),这个界面是非常重要的,后续的请求中携带的参数信息很多都是存在于这个页面源码中。
因此,我们先来看下这个网页的源代码:
我们主要获取的就是globalRepeatSubmitToken
和ticketInfoForPassengerForm
这两个变量内容,其中globalRepeatSubmitToken
就是一个字符串,可以很容易由正则得到它的内容;而ticketInfoForPassengerForm
里面我们需要的内容较多,用正则获取会繁琐很多,仔细看一下,其实它就是一个json
数据,所以我们可以先用正则获取json
字符串内容,再将其转成json
就可以了,但是这里需要注意一下,ticketInfoForPassengerForm
的格式不是严格符合json
格式,因此这里获取到字符串后,需要把里面的单引号('
)转换成双引号("
),这样才能正确的用json
来解析(小细节,注意一下)。
- 接下来,就到
getPassengerDTOs
,即获取乘客信息:
这里也是一个POST
请求,请求网址为:
https://kyfw.12306.cn/otn/confirmPassenger/getPassengerDTOs
参数信息也比较简单:
参数内容见上图。
那么我们下面我们来看下服务器给我返回的数据:
可以看到服务给我呢返回的是一个json
数据。这里同样要先看下status
的状态,为true
,请求成功,为false
,请求失败,失败内容从messages
中获取。
如果我们的请求成功了(status:true
),那么我们就可以打开data
字段,就可以看到如下图所示内容:
可以看到,在data
字段里面的normal_passengers
字段就存储了我们账号中所有乘客的信息。因此,我们这里最主要的就是提取出这些乘客的信息,为后续提交订单做准备(乘客信息)。
- 接下来,会对订单信息进行检查:
checkOrderInfo
,对应于网页操作 提交订单 这个过程:
首先,这是一个POST
请求,网址为:
https://kyfw.12306.cn/otn/confirmPassenger/checkOrderInfo
然后,看下请求体:
这里的cancel_flag
和bed_level_order_num
应该是固定值,但是这两个键在ticketInfoForPassengerForm
里面也能查找到,只是为null
,这里应该是12306预留的一些键值,建议在程序中首先从ticketInfoForPassengerForm
获取,当其为null
时,才采用上面的默认值(鬼知道12306什么时候就用上这些预留的键讷!!!)
passengerTicketStr
这个参数的组合方式为: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
这个参数的组合方式为:张三(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
数据(包含status
,message
用来判别请求是否成功,以及另一个键内部拥有一个标志的键值),那么,在请求成功,动作失败时,一定会伴随有一个失败信息的键值返回,即errMsg
。
- 接下来的一步,就是请求:
getQueueCount
,获取余票和排队信息。
这里同样还是一个POST
请求,请求网址为:
https://kyfw.12306.cn/otn/confirmPassenger/getQueueCount
这里 Firefox 获取的参数train_date
的格式是错误的,因此这里放上 Chrome 上截取到的最新参数信息(内容可能与上面的信息不同,但是参数获取原来一致):
最后来看下返回体内容吧:
可以看到返回结果也是一个json
,返回结果的参数内容见上图,至于其它参数是什么意思,笔者暂时也未能分析出来!哈哈。
- 获取到剩余票数和排队信息后,接下来就是要将我们的订单入队确认过程:
这里对应网页操作的 选择座位确定 这个过程,如下图所示:
注:此处,根据票的类型,向不同的网址发送确认信息:
- 如果是单程票(
dc
):则订单入队确认过程为:confirmSingleForQueue
。 - 如果是往返票(
wc
):则订单入队确认过程为:confirmGoForQueue
。
还是按照我们上面的分析过程来:
首先,这是一个POST
请求,请求网址为:
- 单程票(
dc
)
https://kyfw.12306.cn/otn/confirmPassenger/confirmSingleForQueue
- 往返票(
wc
)
https://kyfw.12306.cn/otn/confirmPassenger/confirmGoForQueue
然后,来看下请求体:
参数详情见上图。
这里要讲一下:choose_seats
,这里的值可以为空,表示随机座位;也可以按照上面所示的输入座位号,具体的座位号应该是按下图所示方式进行排序:
最后,看下返回结果:
这里看到其返回的json
格式跟我们上面分析checkOrderInfo
格式是一致的,所以这里大胆猜测一下,当submitStatus
返回false
时,会同时返回errMsg
字段。
- 上面已经入队成功了,那么接下来就等待下单成功:
queryOrderWaitTime
:
这个过程需要我们自己不断的对服务器进行轮询,获取下单结果。因此,这是一个GET
操作,其 URL 为:
https://kyfw.12306.cn/otn/confirmPassenger/queryOrderWaitTime
那么,我们来看下其发送的参数内容:
参数详情请查看上图。
最后,来看下服务器返回的结果:
这里的操作步骤是,我们需要死循环轮询订单结果,直到当waitTime
为负数并且返回的订单号(orderId
)不为null
,那么就表明我们下单成功了。如果waitTime
为负数,但是orderId
为null
,那么就需要从data[msg]
提取出失败信息,12306官方请求轮询时间为3秒。(笔者测试发现,程序中请求时,waitTime
的值经常为-100,因此,这里用waitTime
进行判断可能存在误区,必须用orderId
进行判断,当orderId
为空,并且data[msg]
不为空,则失败,退出程序)
注: 上述接口中的waitTime
其实就是订单排队时间,单位:秒。
所以我们可以通过waitTime
数值,大概估计出排队时间,既可以给出友好的提示,又可以根据这个预估时间动态调整再次查询请求,减少请求次数。
- 此时,我们还剩下最后一步,就是查询下我们这个订单号是不是最终成功下单了:
resultOrderForDcQueue
(resultOrderForWcQueue
):
首先,这是一个POST
请求,网址为:
- 单程票(
dc
)
https://kyfw.12306.cn/otn/confirmPassenger/resultOrderForDcQueue
- 往返票(
wc
)
https://kyfw.12306.cn/otn/confirmPassenger/resultOrderForWcQueue
然后,看下请求体:
参数含义见上图。
最后,看下返回结果:
参数含义见上图。
以上,其实就已经算是完成了下单流程。
但是,如果还想获取订单详细信息,则请看第9步。
- 获取订单详细信息:
queryMyOrderNoComplete
:
这个接口也是一个POST
请求,其api为:
https://kyfw.12306.cn/otn/queryOrder/queryMyOrderNoComplete
其参数为:
最重要的是其返回值:
{
"validateMessagesShowId": "_validatorMessage",
"status": true,
"httpstatus": 200,
"data": {
"orderDBList": [
{
"sequence_no": "EH66031825", //订单号
"order_date": "2018-01-31 17:56:12", //下单时间
"ticket_totalnum": 2, //车票总张数
"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": "深圳北", //出发站
"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", //出发日期
"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": "待支付", //支付状态
"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]
中,这是一个数组,数组的长度就是下单的票数(就像上面下单数为2张,所以长度为2),每个数组内包含了车票的详细信息,我们可以从中取出所需信息进行展示。
至此,我们终于完成12306下单整个流程。
总结
到这里,我们对12306的登录流程,余票查询流程和下单流程都已经详细的解析完毕了,读者根据我们这3篇的内容就可以编写出一套自动购票小程序了 ^-^ 。
最后,附上笔者自己写的一套自动抢票小软件:EasyTrain