12306购票流程全解析

顺利的用自制程序抢到回家的火车票,也不枉费我这两周从一个服务器端程序员伪装成客户端程序员利用Chrome DevTools一段一段的挖12306的协议,同时满足了我今年的一个小心愿 – 自制12306程序并且可以实现简单刷票功能。%e8%bd%a6%e7%a5%a8

代码编写的核心使用了 PHP – Client URL Library(Curl)

未研究验证码自动识别,言外之意就是说在Login时需要手动点击验证码,在购票时也需要手动点击输入;

理论上来说这里没有使用什么黑科技(验证码自动识别之类的),只是简化了购票流程;

文章最后更新与2017-01-04,因为12306协议变动比较频繁,若未来协议流程无法与本文对应,请见谅(会尽力保证更新,同时更新日期)。

Step 1 获取cookie并保存,Get

https://kyfw.12306.cn/otn/login/init

唯一需要注意的就是时区,会导致购票异常。一般不会失败,失败了就重试,后续每一步都需要带上cookie内容。

 

Step 2 带上cookie抓取Login验证码,Get

https://kyfw.12306.cn/otn/passcodeNew/getPassCodeNew?module=login&rand=sjrand&0.321423424

sjrand后面是一个0-1的随机数,防缓存,建议传上,php一行代码的事情:mt_rand()/mt_getrandmax()。

一般不会失败,失败了就重试。

 

Step 3 验证Login验证码,Post

https://kyfw.12306.cn/otn/passcodeNew/checkRandCodeAnsyn?rand=sjrand&randCode=111,117,31,40

HTTPHEADER设置(“application/x-www-form-urlencoded; charset=utf-8”)

这里麻烦之处是randCode的格式,现在的12306验证码点击区是一个大约290×150像素点的合成图片(未精确测算),由8张子图组成,图片的左上角是坐标0.0,右下角的坐标290×150。

至于这个验证码的点击数据怎么获取,网上很多JS小程序。

回包是Json格式{“validateMessagesShowId”:”_validatorMessage”,”status”:true,”httpstatus”:200,”data”:{“result”:”1″,”msg”:”TRUE”},”messages”:[],”validateMessages”:{}}

建议判断”msg”:”TRUE”,失败跳回step 2。

 

Step 4 验证用户名\密码,Post

https://kyfw.12306.cn/otn/login/loginAysnSuggest?loginUserDTO.user_name=111&userDTO.password=222&randCode=111,117,31,40

HTTPHEADER设置(“application/x-www-form-urlencoded; charset=utf-8”)

接下来是验证用户密码,这里用户名和密码分别使用111、222代替;

回包是Json格式{“validateMessagesShowId”:”_validatorMessage”,”status”:true,”httpstatus”:200,”data”:{“otherMsg”:””,”loginCheck”:”Y”},”messages”:[],”validateMessages”:{}};

建议判断”loginCheck”:”Y”,失败跳回step 2;

这里若提示存在”msg”:”系统繁忙,请稍后再试”有一定几率是IP被封了,需要大约1个小时解封,导致IP被封的原因很多,请求过于频繁,传递了异常参数都有可能。

 

Step 5 正式Login,Post

https://kyfw.12306.cn/otn/login/userLogin?_json_att=

HTTPHEADER设置(“application/x-www-form-urlencoded)

返回的是一个html页面,一般不会失败。

 

Step 6 模拟跳转initMy12306,Get

https://kyfw.12306.cn/otn/index/initMy12306

HTTPHEADER设置(“Content-type:text/html”)

返回的是一个html页面,一般不会失败。

 

Step 7 拉乘客买票验证码,Get(个人觉得这一步无用,但是从协议流程来看12306确实是有拉取)

https://kyfw.12306.cn/otn/passcodeNew/getPassCodeNew??module=passenger&rand=randp&0.878574764745

 

Step 8 查询余票第一步,Get

https://kyfw.12306.cn/otn/leftTicket/log?leftTicketDTO.train_date=2017-01-21&leftTicketDTO.from_station=SZQ&leftTicketDTO.to_station=WHN&purpose_codes=ADULT

leftTicketDTO.train_date是购票车次日期,格式为yyyy-mm-dd;

leftTicketDTO.from_station出发地编号,比如:深圳 => SZQ,武汉 => WHN;

leftTicketDTO.to_station到达地编号;

purpose_codes,ADULT表示成人乘客,学生票用什么定义未挖掘。

回包Json格式{“validateMessagesShowId”:”_validatorMessage”,”status”:true,”httpstatus”:200,”messages”:[],”validateMessages”:{}}

判断”status”:true,失败了就跳到Step 8(还是这一步)

 

Step 9 查询余票第二步,Get

https://kyfw.12306.cn/otn/leftTicket/queryA?leftTicketDTO.train_date=2017-01-21&leftTicketDTO.from_station=SZQ&leftTicketDTO.to_station=WHN&purpose_codes=ADULT

参数和Step 8是一样的;

回包Json格式所有车次的列表train_info(太长略),没有余票就跳到Step 8,建议sleep1~3秒再跳转;

注意:这步有可能回包为空,导致Json解析异常,若异常就跳到Step 8;

这个url会偶尔变更,开发第一周url是https://kyfw.12306.cn/otn/leftTicket/queryX?……

当station_train_code符合我们车次,并且有票,从Json结果中挖取我们需要的数据:

secretStr,train_no,start_station_telecode,end_station_telecode,yp_info,location_code

其中yp_info需要做urlencode。

 

Step 10 验证登录,Post

https://kyfw.12306.cn/otn/login/checkUser?_json_att=

HTTPHEADER设置(“application/x-www-form-urlencoded; charset=utf-8”)

回包Json格式{“validateMessagesShowId”:”_validatorMessage”,”status”:true,”httpstatus”:200,”data”:{“flag”:true},”messages”:[],”validateMessages”:{}}

建议同时验证”status”:true,”flag”:true,否则返回step 1,需要重新登录

 

Step 11 预提交订单,Post

https://kyfw.12306.cn/otn/leftTicket/submitOrderRequest?secretStr=secretStr&train_date=2017-01-21&back_train_date=2016-12-23&tour_flag=dc&purpose_codes=ADULT&query_from_station_name=深圳&query_to_station_name=武汉&undefined=

HTTPHEADER设置(“application/x-www-form-urlencoded; charset=utf-8”)

secretStr是在Step 9中Json取值secretStr;

train_date是购票车次日期,与Step 8中的leftTicketDTO.train_date一致;

back_train_date是回程日期,格式填对即可yyyy-mm-dd;

tour_flag=dc,表示单程

purpose_codes=ADULT,表示成人

回包Json格式{“validateMessagesShowId”:”_validatorMessage”,”status”:true,”httpstatus”:200,”data”:”N”,”messages”:[],”validateMessages”:{}}

验证”status”:true,失败重试3次后建议返回step 1

 

Step 12 模拟跳转页面InitDc,Post

https://kyfw.12306.cn/otn/confirmPassenger/initDc?_json_att=

HTTPHEADER设置(“application/x-www-form-urlencoded”)

回包html格式,需要利用正则挖取数据:

“/var globalRepeatSubmitToken = ‘(.*?)’;/”  => SubmitToken

“/’key_check_isChange’:'(.*?)’/” => key_check_isChange

 

Step 13 常用联系人确定,Post

https://kyfw.12306.cn/otn/confirmPassenger/getPassengerDTOs?_json_att=&REPEAT_SUBMIT_TOKEN=SubmitToken

HTTPHEADER设置(“application/x-www-form-urlencoded; charset=utf-8”)

REPEAT_SUBMIT_TOKEN就是在Step 12中挖出来的SubmitToken;

回包是Json格式的联系人列表,解析联系人的信息,在乘车人符合passenger_name时需要关心这几个数据:

passenger_id_type_code,passenger_id_no,mobile_no,passenger_type

 

Step 14 拉乘客买票验证码,Get

https://kyfw.12306.cn/otn/passcodeNew/getPassCodeNew??module=passenger&rand=randp&0.6352423422

 

Step 15 购票人确定,Post

https://kyfw.12306.cn/otn/confirmPassenger/checkOrderInfo?cancel_flag=2&bed_level_order_num=000000000000000000000000000000&passengerTicketStr=str1_str2&
oldPassengerStr=str3_str4_&tour_flag=dc&randCode=&_json_att=&REPEAT_SUBMIT_TOKEN=SubmitToken

HTTPHEADER设置(“application/x-www-form-urlencoded; charset=utf-8”)

座位编号(seatType)参考:

‘硬卧’ => ‘3’,
‘软卧’ => ‘4’,
‘二等座’ => ‘O’,
‘一等座’ => ‘M’,
‘硬座’ => ‘1’,

passengerTicketStr组成的格式:seatType,0,票类型(成人票填1),乘客名,passenger_id_type_code,passenger_id_no,mobile_no,’N’

多个乘车人用’_’隔开

oldPassengerStr组成的格式:乘客名,passenger_id_type_code,passenger_id_no,passenger_type,’_’

多个乘车人用’_’隔开,注意最后的需要多加一个’_’。

回包是Json格式,需要关心的是”status”:true,失败返回step 8;

还需要关心”ifShowPassCode”:”Y”,若是”Y”表示后续要校验验证码,”N”则不用校验。

 

Step 16 准备进入排队,Post

https://kyfw.12306.cn/otn/confirmPassenger/getQueueCount?train_date=Sat Jan 21 2017 00:00:00 GMT+0800 (中国标准时间)&train_no=train_no&stationTrainCode=station_train_code&seatType=seatType&fromStationTelecode=start_station_telecode&
toStationTelecode=end_station_telecode&leftTicket=yp_info&purpose_codes=00&train_location=location_code&_json_att=&REPEAT_SUBMIT_TOKEN=SubmitToken

HTTPHEADER设置(“application/x-www-form-urlencoded; charset=utf-8″)

记得使用urlencode转换一次

上面很多参数都是在Step 9中取得的,可以一一对应。

至于这一步为何在输入验证码前面不得而知,但是确实官网的流程是这样。

回包是Json格式,可以看到当前的票数”ticket”:”xxx”与正在排队人数”count”:”xxx”

判断”status”:true,若失败建议返回step 1

 

若Step 15中的”ifShowPassCode”:”Y”,那么多了输入验证码这一步,Post

https://kyfw.12306.cn/otn/passcodeNew/checkRandCodeAnsyn?randCode=131,117,371,40&rand=randp&_json_att=&REPEAT_SUBMIT_TOKEN=SubmitToken

HTTPHEADER设置(“application/x-www-form-urlencoded; charset=utf-8”)

输错了就重新拉取验证码然后进入这一步

 

Step 17 确认购买,Post

https://kyfw.12306.cn/otn/confirmPassenger/confirmSingleForQueue?passengerTicketStr=str1_str2&oldPassengerStr=str3_str4_&&randCode=131,117,371,40&purpose_codes=00&key_check_isChange=key_check_isChange
&leftTicketStr=yp_info&train_location=location_code&choose_seats=&seatDetailType=000&roomType=00&dwAll=N&_json_att=&REPEAT_SUBMIT_TOKEN=SubmitToken

HTTPHEADER设置(“application/x-www-form-urlencoded; charset=utf-8″)

参数都是前面可以挖的。

randCode这个参数,若”ifShowPassCode”:”N”填空,若为”Y”填入和上一步同样的randCode。

返回是Json格式{“validateMessagesShowId”:”_validatorMessage”,”status”:true,”httpstatus”:200,”data”:{“submitStatus”:true},”messages”:[],”validateMessages”:{}}

验证”submitStatus”:true进行下一步

主要注意的是,这一步会出现各种异常“出票错误”,可能是真的没票,可能是参数传的有问题,可能是没有登录态了…我暂时的处理方式直接回到step 1

 

Step 18 快成功了!每隔4秒循环Post

https://kyfw.12306.cn/otn/confirmPassenger/queryOrderWaitTime?random=time&tourFlag=dc&_json_att=&REPEAT_SUBMIT_TOKEN=SubmitToken

random参数是当前秒数*1000+毫秒数

返回是Json格式{“validateMessagesShowId”:”_validatorMessage”,”status”:true,”httpstatus”:200,”data”:{“queryOrderWaitTimeStatus”:true,”count”:0,”waitTime”:17,”requestId”:6217964314520123645,”waitCount”:366,”tourFlag”:”dc”,”orderId”:null},”messages”:[],”validateMessages”:{}}

需要关心的是”waitCount”,如果数量小于Step 16中的”ticket”那么买到票的几率就很大了。

若”orderId”有实际值表示成功!

 

(全文结束)

 


转载文章请注明出处:漫漫路 - lanindex.com

25 Comments

 Add your comment
  1. 第九步secretStr获取不到

  2. 之前使用Chrome可以看到response数据,最近不行了,就得用Fiddler来抓包了

  3. 第17步一直是扣票失败,会是哪里的问题?

  4. 楼主好啊, 用Chrome DevTools怎么抓到 step2,3,4的包, 奇怪了, 我只抓到step1的, 2,3,4的没有.

  5. 预提交订单一直返回这个包:
    {‘validateMessagesShowId’: ‘_validatorMessage’, ‘status’: False, ‘httpstatus’: 200, ‘messages’: [‘提交失败,请重试…’], ‘validateMessages’: {}}

  6. 在第三步一直返回:
    {“validateMessagesShowId”:”_validatorMessage”,”status”:true,”httpstatus”:200,”data”:{“result”:”0″,”msg”:””},”messages”:[],”validateMessages”:{}}
    msg没消息,楼主这个有遇到过吗?

  7. 我卡在Step16了,总是得到“系统繁忙”,我传递的参数是:_json_att=&fromStationTelecode=VNP&leftTicket=BdovAfExH%2FRfLJC6ZFY0SpX9KAqZ6b%2Bg9QeOpeoKRUbWjn2P&purpose_codes=00&REPEAT_SUBMIT_TOKEN=440b3f138f135644a031101e52ff0a39&seatType=O&stationTrainCode=G101&toStationTelecode=AOH&train_date=Sat Feb 11 2017 00:00:00 GMT+0800(中国标准时间)&train_location=P2&train_no=240000G1010C
    请求楼主帮忙看一下。

  8. 遇到一个奇怪的想象,在登陆成功后,通过借口/otn/leftTicket/queryA进行刷新,刷新几轮之后,session会被踢掉,然后需要重新登陆,请问知道是什么原因吗?

    • 下面是出问题的返回头部,会有一个新的session的cookie
      HTTP/1.1 200 OK
      Date: Mon, 09 Jan 2017 03:04:14 GMT
      Server: Apache-Coyote/1.1
      X-Powered-By: Servlet 2.5; JBoss-5.0/JBossWeb-2.1
      Set-Cookie: JSESSIONID=E9033EC912C88EC35C44D10466D3CDEB; Path=/otn
      ct: c1_151
      Expires: Mon, 09 Jan 2017 03:08:57 GMT
      Content-Type: application/json;charset=UTF-8
      Transfer-Encoding: chunked
      Content-Encoding: gzip
      Set-Cookie: BIGipServerotn=2547253514.38945.0000; path=/
      X-Via: 1.1 tongwangtong23:3 (Cdn Cache Server V2.0)
      Connection: keep-alive
      X-Cdn-Src-Port: 50653

      • 引起被踢掉的原因很多。

        供参考:
        1.12306账号是不是在刷票的时候在别处登录了;
        2.使用的IP地址是不是同时有很多人访问12306,例如公司的网络,很容易被踢掉;
        3.刷新频率问题,建议设置线程sleep(1-4秒),如果刷新间隔一直很时间短也很容易被踢掉(原因同2,访问太频繁);
        4.执行完step 9后,进入下一次刷余票需要先执行一遍step 8,意思就是在刷票循环中是这样的step 8->step 9->step 8->step 9……;

  9. 好东西,正在学习研究中….

  10. 我这里也卡在第四步,出现,“系统繁忙,请稍后重试!”。我用了代理也是一样的情况。能不能把第四步的代码放出来看一下,灰常感谢

    • date_default_timezone_set(‘PRC’);
      $url = “https://kyfw.12306.cn/otn/login/loginAysnSuggest”;
      $data = ‘loginUserDTO.user_name=’.$user_name.’&userDTO.password=’.$pwd.’&randCode=’.$verify;
      $result = $this->SendPost($url, $data, array(“application/x-www-form-urlencoded; charset=utf-8”));

      用了代理还是一样,应该哪里参数传错了,检查一下CURLOPT_HTTPHEADER和cookie相关的。

  11. 卡在10, 11步了,
    第10步返回:
    {“validateMessagesShowId”:”_validatorMessage”,”status”:true,”httpstatus”:200,”messages”:[“系统繁忙,请稍后重试!”],”validateMessages”:{}}
    第11步返回:
    {“validateMessagesShowId”:”_validatorMessage”,”status”:false,”httpstatus”:200,”messages”:[“提交失败,请重试…”],”validateMessages”:{}}

    • 提示系统繁忙有二种可能比较大:
      1、IP被临时封了,需要等1个小时;
      2、可能在step 10之前有参数传递错误;
      一种可能性是真的“系统忙”。

      建议检查一下参数传递例如:_json_att= 这个前面有一个’_’得小心。

Leave a Comment

Your email address will not be published.