12306之下单流程解析

前言

本套教程共分3章:

本套内容主要用于分析12306购票流程,意在编写一套自动购票小程序。12306接口 api 经常变动,但是流程分析是固定的。因此,本套教程主要记录12306 相关购票流程分析过程,以作记录。

下单流程分析

查询到余票后,点击预定(假设用户已经登录),那么就会走下单流程:

  1. 下单第一步:submitOrderRequest,提交订单请求。
    我们来细看下这个请求:
    submitOrderRequest_headers

可以看到,submitOrderRequestPOST请求,其请求地址为:

https://kyfw.12306.cn/otn/leftTicket/submitOrderRequest

既然是POST请求,那么我们就来看下它的请求体内容:

submitOrderRequest_params

图中对参数的解释已经很清楚了,这里唯一要注意的是:我们在查询时获取得到的secretStr是已经被urlencode后的内容,因此,当我们在发送请求时,首先要把这个secretStr先解码回来,这样做的原因是大部分网络请求框架对于form data数据都会自动进行urlencode(比如requests对于dict数据),因此,这里需要先解码回原来的数据,后面网络请求框架编码后才能得到正确的值。

最后,来看下服务器给我们返回的内容:


submitOrderRequest_response

这里我们只需看下status的内容即可,如果statustrue,说明我们的请求时成功的;如果statusfalse,说明我们请求失败了,失败原因可以在messages中获取。

  1. 接下来,如果我们在12306官网进行预订这个操作后,我们的页面就会跳转到:https://kyfw.12306.cn/otn/confirmPassenger/initDc(对于返程票,则跳转到:https://kyfw.12306.cn/otn/confirmPassenger/initWc),这个界面是非常重要的,后续的请求中携带的参数信息很多都是存在于这个页面源码中。
    因此,我们先来看下这个网页的源代码:

    initDc_reqeatSubmitToken

    initDc_ticketInfoForPassengerForm

我们主要获取的就是globalRepeatSubmitTokenticketInfoForPassengerForm这两个变量内容,其中globalRepeatSubmitToken就是一个字符串,可以很容易由正则得到它的内容;而ticketInfoForPassengerForm里面我们需要的内容较多,用正则获取会繁琐很多,仔细看一下,其实它就是一个json数据,所以我们可以先用正则获取json字符串内容,再将其转成json就可以了,但是这里需要注意一下,ticketInfoForPassengerForm的格式不是严格符合json格式,因此这里获取到字符串后,需要把里面的单引号(')转换成双引号("),这样才能正确的用json来解析(小细节,注意一下)。

  1. 接下来,就到getPassengerDTOs,即获取乘客信息:
    这里也是一个POST请求,请求网址为:
https://kyfw.12306.cn/otn/confirmPassenger/getPassengerDTOs

参数信息也比较简单:


getPassengerDTOs_params

参数内容见上图。

那么我们下面我们来看下服务器给我返回的数据:


getPassengerDTOs_response01

可以看到服务给我呢返回的是一个json数据。这里同样要先看下status的状态,为true,请求成功,为false,请求失败,失败内容从messages中获取。
如果我们的请求成功了(status:true),那么我们就可以打开data字段,就可以看到如下图所示内容:

getPassengerDTOs_response02

可以看到,在data字段里面的normal_passengers字段就存储了我们账号中所有乘客的信息。因此,我们这里最主要的就是提取出这些乘客的信息,为后续提交订单做准备(乘客信息)。

  1. 接下来,会对订单信息进行检查:checkOrderInfo,对应于网页操作 提交订单 这个过程:
    首先,这是一个POST请求,网址为:
https://kyfw.12306.cn/otn/confirmPassenger/checkOrderInfo

然后,看下请求体:


checkRoderInfo_params

这里的cancel_flagbed_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_

最后看下返回结果:


checkRoderInfo_response

各键值含义请看上图。
这里有一个点可以注意的是:我们上面看到的是成功的返回信息,笔者在测试时,抓包抓到检测失败(即status:true,submitStatus:false)的信息里面data字段里面会有一个errMsg键值,其携带了失败的具体信息,所以在程序中如果submitStatus失败了,那么可以输出errMsg显示原因。

注:笔者觉得这里可以大胆假设一下,形如上面格式的json数据(包含status,message用来判别请求是否成功,以及另一个键内部拥有一个标志的键值),那么,在请求成功,动作失败时,一定会伴随有一个失败信息的键值返回,即errMsg

  1. 接下来的一步,就是请求:getQueueCount,获取余票和排队信息。
    这里同样还是一个POST请求,请求网址为:
https://kyfw.12306.cn/otn/confirmPassenger/getQueueCount

这里 Firefox 获取的参数train_date的格式是错误的,因此这里放上 Chrome 上截取到的最新参数信息(内容可能与上面的信息不同,但是参数获取原来一致):

getQueueCount_params

最后来看下返回体内容吧:


getQueueCount_response

可以看到返回结果也是一个json,返回结果的参数内容见上图,至于其它参数是什么意思,笔者暂时也未能分析出来!哈哈。

  1. 获取到剩余票数和排队信息后,接下来就是要将我们的订单入队确认过程:
    这里对应网页操作的 选择座位确定 这个过程,如下图所示:
    confirmSigle/GoForQueue_web

注:此处,根据票的类型,向不同的网址发送确认信息:

  • 如果是单程票(dc):则订单入队确认过程为:confirmSingleForQueue
  • 如果是往返票(wc):则订单入队确认过程为:confirmGoForQueue

还是按照我们上面的分析过程来:

首先,这是一个POST请求,请求网址为:

  • 单程票(dc)
https://kyfw.12306.cn/otn/confirmPassenger/confirmSingleForQueue
  • 往返票(wc)
https://kyfw.12306.cn/otn/confirmPassenger/confirmGoForQueue

然后,来看下请求体:


confirmSingle(Go)ForQueue_params

参数详情见上图。
这里要讲一下:choose_seats,这里的值可以为空,表示随机座位;也可以按照上面所示的输入座位号,具体的座位号应该是按下图所示方式进行排序:

choose_seat_code

最后,看下返回结果:


confirmSingleForQueue_response

这里看到其返回的json格式跟我们上面分析checkOrderInfo格式是一致的,所以这里大胆猜测一下,当submitStatus返回false时,会同时返回errMsg字段。

  1. 上面已经入队成功了,那么接下来就等待下单成功:queryOrderWaitTime:
    这个过程需要我们自己不断的对服务器进行轮询,获取下单结果。因此,这是一个GET操作,其 URL 为:
https://kyfw.12306.cn/otn/confirmPassenger/queryOrderWaitTime

那么,我们来看下其发送的参数内容:


queryOrderWaitTime_params

参数详情请查看上图。

最后,来看下服务器返回的结果:


queryOrderWaitTime_response

这里的操作步骤是,我们需要死循环轮询订单结果,直到当waitTime为负数并且返回的订单号(orderId)不为null,那么就表明我们下单成功了。如果waitTime为负数,但是orderIdnull,那么就需要从data[msg]提取出失败信息,12306官方请求轮询时间为3秒。(笔者测试发现,程序中请求时,waitTime的值经常为-100,因此,这里用waitTime进行判断可能存在误区,必须用orderId进行判断,当orderId为空,并且data[msg]不为空,则失败,退出程序)

: 上述接口中的waitTime其实就是订单排队时间,单位:秒。
所以我们可以通过waitTime数值,大概估计出排队时间,既可以给出友好的提示,又可以根据这个预估时间动态调整再次查询请求,减少请求次数。

  1. 此时,我们还剩下最后一步,就是查询下我们这个订单号是不是最终成功下单了:resultOrderForDcQueueresultOrderForWcQueue):
    首先,这是一个POST请求,网址为:
  • 单程票(dc)
https://kyfw.12306.cn/otn/confirmPassenger/resultOrderForDcQueue
  • 往返票(wc)
https://kyfw.12306.cn/otn/confirmPassenger/resultOrderForWcQueue

然后,看下请求体:


resultOrderForDcQueue_params

参数含义见上图。

最后,看下返回结果:


resultOrderForDcQueue_response

参数含义见上图。

以上,其实就已经算是完成了下单流程。
但是,如果还想获取订单详细信息,则请看第9步。

  1. 获取订单详细信息:queryMyOrderNoComplete:
    这个接口也是一个POST请求,其api为:
https://kyfw.12306.cn/otn/queryOrder/queryMyOrderNoComplete

其参数为:

queryMyOrderNoComplete_params

最重要的是其返回值:

{
    "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,表明请求成功,如果statusfalse,那么猜测错误原因在messages字段中。然后下单的车票内容在data["orderDBList"][0]["tickets"][index]中,这是一个数组,数组的长度就是下单的票数(就像上面下单数为2张,所以长度为2),每个数组内包含了车票的详细信息,我们可以从中取出所需信息进行展示。

至此,我们终于完成12306下单整个流程。

总结

到这里,我们对12306的登录流程余票查询流程下单流程都已经详细的解析完毕了,读者根据我们这3篇的内容就可以编写出一套自动购票小程序了 ^-^ 。

最后,附上笔者自己写的一套自动抢票小软件:EasyTrain

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335