*** 说明***:
这不是黑科技,并不是自动购票,请根据自己的需求使用,建议使用抢票 APP 靠谱一些,360 抢票也比这个好,至少它能智能识图!!!
只是自动查询你想要的车次,并自动点击预订,自动填写用户名和密码,但是图片验证码还需要自己点击(这个无解)。
本文先说使用方法,再讲解。
使用方法
首先,打开 12306 的 车票预定页面
-
摁 F12 键打开浏览器控制台(Chrome 浏览为例),选择 console,如下图所示:
将配置好的代码粘贴到图中区域,按 Enter 键回车就行。可以在 Network 里面看到已经在刷了:
- 建议运行时关闭控制台 或者 将控制台放到右侧之类,不然小屏幕下登陆框会看不全。点击控制台右侧的三个竖点选择:
可能的结果
第一种情况是需要重新登录一次,这种很常见。用户名密码都自动帮你填好了,然后自己再填这个坑爹的验证码吧(ˉ▽ˉ;)...,示意图如下:
另一种情况是直接跳到购买页面,你需要自己勾选乘客和点击提交订单即可:
无论是哪种结果,都需要重新运行代码。在控制台按向上箭头,再按回车键就行(当然重新粘贴也行)。
配置代码
先粘代码:
var WISH = {
train_date: '2017-01-24', // 乘车日期
from_station_telecode: 'HGH', // HGH - 杭州东
to_station_telecode: 'NXG', // NXG - 南昌西
purpose_codes: 'ADULT', // ADULT - 成年人,0X00 - 学生
station_train_code: [ // 想买的车次,排前面的优先
'G2365',
'G1417',
'G1341'
],
setType: [ // 座位类型,不填 - 不限制; yz_num - 硬座; rz_num - 软座; yw_num - 硬卧; rw_num - 软卧; gr_num - 高级软卧; zy_num - 一等座; ze_num - 二等座; tz_num - 特等座; wz_num - 无座; qt_num - 其它; swz_num - 商务座
"zy_num", // 一等座
"ze_num", // 二等座
"wz_num" // 无座
]
},
USER = {
name: '[email protected]', // 用户名称
password: 'xxx' // 用户密码
},
SEARCH_RATE = 5000, // 刷新频率,5000 毫秒
timer = null,
matchTicket = {},
availableTicketsMap = {};
/**
* Ajax
* @param {Object} data 搜索参数
* @param {Function} callback 回调函数,用于处理返回的数据
*/
function queryAjax(data, callback) {
var ajaxData = {
'leftTicketDTO.train_date': data.train_date,
'leftTicketDTO.from_station': data.from_station_telecode,
'leftTicketDTO.to_station': data.to_station_telecode,
'purpose_codes': data.purpose_codes
};
// log, it's no use for me
$.ajax({
type: "GET",
isTakeParam: false,
beforeSend: function(xhr) {
xhr.setRequestHeader("If-Modified-Since", "0");
xhr.setRequestHeader("Cache-Control", "no-cache");
},
url: "/otn/leftTicket/log",
data: ajaxData,
timeout: 15000,
success: function(res) {}
});
// query
$.ajax({
type: 'GET',
isTakeParam: false,
beforeSend: function(xhr) {
xhr.setRequestHeader('If-Modified-Since', '0');
xhr.setRequestHeader('Cache-Control', 'no-cache');
},
url: '/otn/leftTicket/queryA',
data: ajaxData,
timeout: 10000,
success: function(res) {
if (res.status) {
callback(res.data);
}
}
});
}
/**
* 处理返回的数据
* @param {Array} data 返回的所有车次信息
* @return {Object} 可购买的车次 map
*/
function getAvailableTicketsMap(data) {
var i = 0,
ticket = {},
result = {};
for (i = 0; i < data.length; i++) {
ticket = {
secretStr: data[i].secretStr,
train_no: data[i].queryLeftNewDTO.train_no,
start_time: data[i].queryLeftNewDTO.start_time,
station_train_code: data[i].queryLeftNewDTO.station_train_code,
to_station_telecode: data[i].queryLeftNewDTO.to_station_telecode,
from_station_telecode: data[i].queryLeftNewDTO.from_station_telecode
};
if (ticket.secretStr !== '' && // or data[i].queryLeftNewDTO.canWebBuy === 'Y'
ticket.to_station_telecode === WISH.to_station_telecode &&
ticket.from_station_telecode === WISH.from_station_telecode &&
hasSiteType(data[i].queryLeftNewDTO) ) {
result[ticket.station_train_code] = {
train_no: ticket.train_no,
secretStr: ticket.secretStr,
start_time: ticket.start_time,
station_train_code: ticket.station_train_code,
to_station_telecode: ticket.to_station_telecode,
from_station_telecode: ticket.from_station_telecode
};
}
}
return result;
}
/**
* 是否有想要的座位
* @param {[type]} queryLeftNewDTO [description]
* @return {Boolean} true-有,false-无
*/
function hasSiteType( queryLeftNewDTO ) {
var i = 0,
wantSetTypeArr = WISH.setType;
if ( wantSetTypeArr.length === 0 ) {
return true;
}
for (i = 0; i < wantSetTypeArr.length; i++) {
if ( /有|[1-9]/.test(queryLeftNewDTO[ wantSetTypeArr[i] ]) ) {
return true;
}
}
return false;
}
/**
* 匹配想要购买的车次
* @param {Object} ticketsMap 可购买的所有车次
* @return {Boolean} true-匹配到,false-没匹配到
*/
function matchYourTickets(ticketsMap) {
var i = 0,
ticket = {},
wantTrains = WISH.station_train_code;
console.log(ticketsMap);
for (i = 0; i < wantTrains.length; i++) {
ticket = ticketsMap[wantTrains[i]];
if (typeof ticket !== 'undefined') {
clearInterval(timer); // 清除定时器
reserveTicket(ticket); // 预订车票
setFormData(USER); // 填写用户信息
return true;
}
}
return false;
}
/**
* 预订车票
* @param {Object} ticket 改车次信息
*/
function reserveTicket(ticket) {
checkG1234(ticket.secretStr, ticket.start_time, ticket.train_no, ticket.from_station_telecode, ticket.to_station_telecode);
}
/**
* 填写表单信息
* @param {Object} user 用户信息对象
*/
function setFormData(user) {
$('#username').val(user.name);
$('#password').val(user.password);
}
/**
* 初始化
*/
function init() {
// 一开始执行就查询匹配一次
queryAjax(WISH, function(data) {
availableTicketsMap = getAvailableTicketsMap(data);
matchYourTickets(availableTicketsMap);
});
// 定时器
timer = setInterval(function() {
queryAjax(WISH, function(data) {
availableTicketsMap = getAvailableTicketsMap(data);
matchYourTickets(availableTicketsMap);
});
}, SEARCH_RATE);
}
init();
// 用于下一个页面
// function buy() {
// $( '#normalPassenger_0' ).click();
// $( '#submitOrder_id' ).click();
// }
// buy();
需要配置的信息就是代码开头的 WISH 部分。
城市 code 查询
至于城市 code 怎么查询,请在官方代码中查找,代码很长很长...链接地址
使用浏览器的 Ctrl + F 查找你所想查找的城市,比如 “杭州东”(注意:杭州和杭州东的 code 不一样),紧接着杭州东后面的字母就是对应的 code :
建议使用可搜索的城市区间,在官网测试过的;注意大小写是区分的
思路分析——搜索功能
好了,接下来是思路分析。
先填写搜索条件,点击搜索,查看控制台 Network 里面的 XHR 记录,也就是发送的 Ajax 了:
可以发现,每次点击搜索,都会发送两个 Ajax 请求:
- /otn/leftTicket/log:这个看名字像是记录搜索日志,不是很清楚
- /otn/leftTicket/queryA: 这个就是查询了
两个 Ajax 的参数都是一致的:
就是我们所填写的搜索参数,格式化后如下:
{
'leftTicketDTO.train_date': '2017-01-24', // 出发日
'leftTicketDTO.from_station': 'HGH', // 出发地
'leftTicketDTO.to_station': 'NXG', // 目的地
'purpose_codes': 'ADULT' // 普通(成年人)
}
所以我们可以使用这些参数来模拟搜索。
Tips:
并且发现查询结果只和 出发日期、出发地、目的地、乘客类型(普通、学生) 有关,和车次的筛选条件无关:
不填写筛选条件(返回 92 条数据):
填写筛选条件(返回 92 条数据):
所以车次的过滤,是在浏览器端完成的
思路分析——预订
找个可以预订的车次,查看 预定 按钮信息(就是控制台左上角的箭头,先点击它,再去点按钮):
可以看到使用的是 onClick 事件,执行函数是 checkG1234
(顺带多观察了几个可预定的车次,发现预订按钮点击执行的函数名都叫做 checkG1234
):
将 checkG1234
格式化:
checkG1234(
'c7JA6pPfpAnWCp5RcvN8Ks8WYk68Wv0ZxdytFe8c8vmN64VfPKCIdxXSLTCBd1Gc%2F9zXT5i7gaZy%0ATr4SWbEOeZwttRjRaGMfOHoKAnxoDJwtiEm8Ittvm7BZZu1qaJcZqFqg2EbdT%2FyissJoFkqEzenT%0AA%2F1UTQV%2BHLjKKEM6MT9SsmljBbjFH3SAhSavWoWUzjQlLsWofENBOoRg2Hu%2F%2FxSqKmWoLwQ%2F4Ku%2B%0AOZNWfWel2HJQ1Oqn5obNR5mTv8sNkJGaSCO459N4zSjH5Fnv29Nbw207GxdKQGFCVQ%3D%3D',
'00:35',
'56000K429760',
'HZH',
'NCG'
)
一共有四个参数,前三个不知道是啥,后两个是始发地和目的地。先不管,先去扒扒搜索返回的数据。
思路分析——返回数据分析
假设 K4297 在查询结果中对应的数据为 data,则将可以将预订函数参数分解:
找两条数据对比一下:
{
"queryLeftNewDTO": {
"train_no": "56000K429760",
"station_train_code": "K4297", // 车次
"start_station_telecode": "HZH",
"start_station_name": "杭州",
"end_station_telecode": "NCG",
"end_station_name": "南昌",
"from_station_telecode": "HZH",
"from_station_name": "杭州",
"to_station_telecode": "NCG",
"to_station_name": "南昌",
"start_time": "00:35",
"arrive_time": "10:12",
"day_difference": "0",
"train_class_name": "",
"lishi": "09:37",
"canWebBuy": "Y",
"lishiValue": "577",
"yp_info": "0%2BhvfBij8EeRbc3N5OhdLWdJS%2F%2FutFvI",
"control_train_day": "20300303",
"start_train_date": "20170124",
"seat_feature": "W010",
"yp_ex": "1010",
"train_seat_feature": "0",
"train_type_code": "4",
"start_province_code": "08",
"start_city_code": "0904",
"end_province_code": "11",
"end_city_code": "1104",
"seat_types": "11",
"location_code": "H1",
"from_station_no": "01",
"to_station_no": "07",
"control_day": 29,
"sale_time": "1030",
"is_support_card": "0",
"controlled_train_flag": "0",
"controlled_train_message": "正常车次,不受控",
"yz_num": "--", // 硬座
"rz_num": "--", // 软座
"yw_num": "--", // 硬卧
"rw_num": "--", // 软卧
"gr_num": "--", // 高级软卧?
"zy_num": "有", // 一等座
"ze_num": "有", // 二等座
"tz_num": "--", // 特等座
"gg_num": "--", // ?
"yb_num": "--", // ?
"wz_num": "--", // 无座
"qt_num": "--", // 其它?
"swz_num": "11" // 商务座
},
"secretStr": "'c7JA6pPfpAnWCp5RcvN8Ks8WYk68Wv0ZxdytFe8c8vmN64VfPKCIdxXSLTCBd1Gc%2F9zXT5i7gaZy%0ATr4SWbEOeZwttRjRaGMfOHoKAnxoDJwtiEm8Ittvm7BZZu1qaJcZqFqg2EbdT%2FyissJoFkqEzenT%0AA%2F1UTQV%2BHLjKKEM6MT9SsmljBbjFH3SAhSavWoWUzjQlLsWofENBOoRg2Hu%2F%2FxSqKmWoLwQ%2F4Ku%2B%0AOZNWfWel2HJQ1Oqn5obNR5mTv8sNkJGaSCO459N4zSjH5Fnv29Nbw207GxdKQGFCVQ%3D%3D",
"buttonTextInfo": "预订"
}
假设这条数据叫做 data,可以发现如下对应关系:
'data.secretStr' => 'c7JA6pPfpAnWCp5RcvN8Ks8WYk68Wv0ZxdytFe8c8vmN64VfPKCIdxXSLTCBd1Gc%2F9zXT5i7gaZy%0ATr4SWbEOeZwttRjRaGMfOHoKAnxoDJwtiEm8Ittvm7BZZu1qaJcZqFqg2EbdT%2FyissJoFkqEzenT%0AA%2F1UTQV%2BHLjKKEM6MT9SsmljBbjFH3SAhSavWoWUzjQlLsWofENBOoRg2Hu%2F%2FxSqKmWoLwQ%2F4Ku%2B%0AOZNWfWel2HJQ1Oqn5obNR5mTv8sNkJGaSCO459N4zSjH5Fnv29Nbw207GxdKQGFCVQ%3D%3D',
'data.queryLeftNewDTO.start_time' => '00:35',
'data.queryLeftNewDTO.train_no' => '56000K429760',
'data.queryLeftNewDTO.from_station_telecode' => 'HZH',
'data.queryLeftNewDTO.to_station_telecode' => 'NCG'
好了,点击预订需要的参数都找全了。
模拟搜索
这个很简单,定义一个 queryAjax 的方法,传入搜索参数 data 和回调函数 callback:
/**
* Ajax
* @param {Object} data 搜索参数
* @param {Function} callback 回调函数,用于处理返回的数据
*/
function queryAjax( data, callback ) {
var ajaxData = {
'leftTicketDTO.train_date': data.train_date,
'leftTicketDTO.from_station': data.from_station_telecode,
'leftTicketDTO.to_station': data.to_station_telecode,
'purpose_codes': data.purpose_codes
};
// log, it's no use for me
$.ajax({
type: "GET",
isTakeParam: false,
beforeSend: function( xhr ) {
xhr.setRequestHeader("If-Modified-Since", "0");
xhr.setRequestHeader("Cache-Control", "no-cache");
},
url: "/otn/leftTicket/log",
data: ajaxData,
timeout: 15000,
success: function( res ) {}
});
// query
$.ajax({
type: 'GET',
isTakeParam: false,
beforeSend: function( xhr ) {
xhr.setRequestHeader('If-Modified-Since', '0');
xhr.setRequestHeader('Cache-Control', 'no-cache');
},
url: '/otn/leftTicket/queryA',
data: ajaxData,
timeout: 10000,
success: function( res ) {
if ( res.status ) {
callback( res.data );
}
}
});
}
处理返回的数据
为了便于后期查找出我们需要的,并且可购买的车次,这里使用键值对保存可购买的车次信息:
/**
* 处理返回的数据
* @param {Array} data 返回的所有车次信息
* @return {Object} 可购买的车次 map
*/
function getAvailableTicketsMap(data) {
var i = 0,
ticket = {},
result = {};
for (i = 0; i < data.length; i++) {
ticket = {
secretStr: data[i].secretStr,
train_no: data[i].queryLeftNewDTO.train_no,
start_time: data[i].queryLeftNewDTO.start_time,
station_train_code: data[i].queryLeftNewDTO.station_train_code,
to_station_telecode: data[i].queryLeftNewDTO.to_station_telecode,
from_station_telecode: data[i].queryLeftNewDTO.from_station_telecode
};
if (ticket.secretStr !== '' && // or data[i].queryLeftNewDTO.canWebBuy === 'Y'
ticket.to_station_telecode === WISH.to_station_telecode &&
ticket.from_station_telecode === WISH.from_station_telecode &&
hasSiteType(data[i].queryLeftNewDTO) ) {
result[ticket.station_train_code] = {
train_no: ticket.train_no,
secretStr: ticket.secretStr,
start_time: ticket.start_time,
station_train_code: ticket.station_train_code,
to_station_telecode: ticket.to_station_telecode,
from_station_telecode: ticket.from_station_telecode
};
}
}
return result;
}
Tips:
分析发现,如果车次可以购买,那么 secretStr 的值不为空,并且queryLeftNewDTO.canWebBuy = 'Y'
匹配座位类型
长度为 0 就是不限制,有卖就买,返回 true;不为 0 就是遍历期望的类型数组,找到有匹配的就返回 true,否则为 false:
/**
* 是否有想要的座位
* @param {[type]} queryLeftNewDTO [description]
* @return {Boolean} true-有,false-无
*/
function hasSiteType( queryLeftNewDTO ) {
var i = 0,
wantSetTypeArr = WISH.setType;
if ( wantSetTypeArr.length === 0 ) {
return true;
}
for (i = 0; i < wantSetTypeArr.length; i++) {
if ( /有|[1-9]/.test(queryLeftNewDTO[ wantSetTypeArr[i] ]) ) {
return true;
}
}
return false;
}
匹配需要的车次
接下来就是匹配我们需要的车次了,需要传入上述所有可购买的车次:
/**
* 匹配想要购买的车次
* @param {Object} ticketsMap 可购买的所有车次
* @return {Boolean} true-匹配到,false-没匹配到
*/
function matchYourTickets( ticketsMap ) {
var i = 0,
ticket = {},
wantTrains = WISH.station_train_code;
console.log( ticketsMap );
for (i = 0; i < wantTrains.length; i++) {
ticket = ticketsMap[ wantTrains[i] ];
if ( typeof ticket !== 'undefined' ) {
clearInterval( timer ); // 清除定时器
reserveTicket( ticket ); // 预订车票
setFormData( USER ); // 填写用户信息
return true;
}
}
return false;
}
预订车票
就是调预订的那个方法,传入需要的参数而已:
/**
* 预订车票
* @param {Object} ticket 改车次信息
*/
function reserveTicket( ticket ) {
checkG1234(ticket.secretStr, ticket.start_time,ticket.train_no, ticket.from_station_telecode, ticket.to_station_telecode);
}
填写用户信息
这个就是查看登陆表单的结果了,选择器就直接写死了:
/**
* 填写表单信息
* @param {Object} user 用户信息对象
*/
function setFormData( user ) {
$( '#username' ).val( user.name );
$( '#password' ).val( user.password );
}
Tips:
点击查看元素可以看到表单的用户名、用户密码输入框的 id 名称分别为 'username'、'password'
图片验证码无解,不知道抢票软件咋弄的,有内部接口?貌似移动端有独立的接口,下次看看。
初始化
写个定时器自动查询匹配:
/**
* 初始化
*/
function init() {
// 一开始执行就查询匹配一次
queryAjax(WISH, function( data ) {
availableTicketsMap = getAvailableTicketsMap( data );
matchYourTickets( availableTicketsMap );
});
// 定时器
timer = setInterval(function() {
queryAjax(WISH, function( data ) {
availableTicketsMap = getAvailableTicketsMap( data );
matchYourTickets( availableTicketsMap );
});
}, SEARCH_RATE);
}
总结
自己开双屏,一边写代码一边看着,挂着刷还行,当然手机 App 更好吧。春运的票好难抢,今天啥都没抢到,就看明天了。抢不到就拿这个挂个一天....
Good Night ~ ~ o(* ̄▽ ̄*)ブ
—— 2016/12/26 By Live