顺利的用自制程序抢到回家的火车票,也不枉费我这两周从一个服务器端程序员伪装成客户端程序员利用Chrome DevTools一段一段的挖12306的协议,同时满足了我今年的一个小心愿 – 自制12306程序并且可以实现简单刷票功能。
代码编写的核心使用了 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
第九步secretStr获取不到
之前使用Chrome可以看到response数据,最近不行了,就得用Fiddler来抓包了
第17步一直是扣票失败,会是哪里的问题?
我也是一直扣票失败。。。。。
我也是
我的扣票失败解决了,因为cookie不全
楼主好啊, 用Chrome DevTools怎么抓到 step2,3,4的包, 奇怪了, 我只抓到step1的, 2,3,4的没有.
可以再仔细看看,区分一下Get/Post。
也可能是12306把协议更新了,我最近并没有跟进协议更新。
之前使用Chrome可以看到response数据,最近不行了,就得用Fiddler来抓包了
预提交订单一直返回这个包:
{‘validateMessagesShowId’: ‘_validatorMessage’, ‘status’: False, ‘httpstatus’: 200, ‘messages’: [‘提交失败,请重试…’], ‘validateMessages’: {}}
_jc_save_这种cookies没法自动获取, 我是手动加进去。 然后在请求的headers中能看到。与浏览器对比,发现Cookie的顺序不对。 前面几步都是正确的
我和你一样的, 你怎么解决的?
在第三步一直返回:
{“validateMessagesShowId”:”_validatorMessage”,”status”:true,”httpstatus”:200,”data”:{“result”:”0″,”msg”:””},”messages”:[],”validateMessages”:{}}
msg没消息,楼主这个有遇到过吗?
应该是12306的协议变化了,我最近没有空来更新这个东西,十分抱歉。
我卡在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
请求楼主帮忙看一下。
把train_date的内容使用php的urlencode转换一次
解决了吗?我也卡这了
遇到一个奇怪的想象,在登陆成功后,通过借口/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……;
好东西,正在学习研究中….
我这里也卡在第四步,出现,“系统繁忙,请稍后重试!”。我用了代理也是一样的情况。能不能把第四步的代码放出来看一下,灰常感谢
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相关的。
卡在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= 这个前面有一个’_’得小心。