*** 說明***:
這不是黑科技押逼,并不是自動購票步藕,請根據自己的需求使用,建議使用搶票 APP 靠譜一些挑格,360 搶票也比這個好咙冗,至少它能智能識圖!F雾消!
只是自動查詢你想要的車次,并自動點擊預訂挫望,自動填寫用戶名和密碼立润,但是圖片驗證碼還需要自己點擊(這個無解)。
本文先說使用方法媳板,再講解桑腮。
使用方法
首先,打開 12306 的 車票預定頁面
-
摁 F12 鍵打開瀏覽器控制臺(Chrome 瀏覽為例)蛉幸,選擇 console到旦,如下圖所示:
console.png 將配置好的代碼粘貼到圖中區(qū)域,按 Enter 鍵回車就行巨缘√硗可以在 Network 里面看到已經在刷了:
-
建議運行時關閉控制臺 或者 將控制臺放到右側之類,不然小屏幕下登陸框會看不全若锁。點擊控制臺右側的三個豎點選擇:
setRight.png
可能的結果
第一種情況是需要重新登錄一次搁骑,這種很常見。用戶名密碼都自動幫你填好了,然后自己再填這個坑爹的驗證碼吧(ˉ▽ˉ仲器;)...煤率,示意圖如下:
另一種情況是直接跳到購買頁面,你需要自己勾選乘客和點擊提交訂單即可:
無論是哪種結果乏冀,都需要重新運行代碼蝶糯。在控制臺按向上箭頭,再按回車鍵就行(當然重新粘貼也行)辆沦。
<br />
配置代碼
先粘代碼:
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: [ // 想買的車次,排前面的優(yōu)先
'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: 'xxx@163.com', // 用戶名稱
password: 'xxx' // 用戶密碼
},
SEARCH_RATE = 5000, // 刷新頻率妒茬,5000 毫秒
timer = null,
matchTicket = {},
availableTicketsMap = {};
/**
* Ajax
* @param {Object} data 搜索參數(shù)
* @param {Function} callback 回調函數(shù),用于處理返回的數(shù)據
*/
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);
}
}
});
}
/**
* 處理返回的數(shù)據
* @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() {
// 一開始執(zhí)行就查詢匹配一次
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 部分。
<br />
城市 code 查詢
至于城市 code 怎么查詢铭腕,請在官方代碼中查找银择,代碼很長很長...鏈接地址
使用瀏覽器的 Ctrl + F 查找你所想查找的城市,比如 “杭州東”(注意:杭州和杭州東的 code 不一樣)累舷,緊接著杭州東后面的字母就是對應的 code :
建議使用可搜索的城市區(qū)間欢摄,在官網測試過的;注意大小寫是區(qū)分的
<br />
思路分析——搜索功能
好了笋粟,接下來是思路分析。
先填寫搜索條件析蝴,點擊搜索害捕,查看控制臺 Network 里面的 XHR 記錄,也就是發(fā)送的 Ajax 了:
可以發(fā)現(xiàn)闷畸,每次點擊搜索尝盼,都會發(fā)送兩個 Ajax 請求:
- /otn/leftTicket/log:這個看名字像是記錄搜索日志,不是很清楚
- /otn/leftTicket/queryA: 這個就是查詢了
兩個 Ajax 的參數(shù)都是一致的:
就是我們所填寫的搜索參數(shù)佑菩,格式化后如下:
{
'leftTicketDTO.train_date': '2017-01-24', // 出發(fā)日
'leftTicketDTO.from_station': 'HGH', // 出發(fā)地
'leftTicketDTO.to_station': 'NXG', // 目的地
'purpose_codes': 'ADULT' // 普通(成年人)
}
所以我們可以使用這些參數(shù)來模擬搜索盾沫。
Tips:
并且發(fā)現(xiàn)查詢結果只和 出發(fā)日期、出發(fā)地殿漠、目的地赴精、乘客類型(普通、學生) 有關绞幌,和車次的篩選條件無關:
不填寫篩選條件(返回 92 條數(shù)據):
noFilter.png
填寫篩選條件(返回 92 條數(shù)據):
hasFilter.png
所以車次的過濾蕾哟,是在瀏覽器端完成的
<br />
思路分析——預訂
找個可以預訂的車次,查看 預定 按鈕信息(就是控制臺左上角的箭頭,先點擊它谭确,再去點按鈕):
可以看到使用的是 onClick 事件帘营,執(zhí)行函數(shù)是 checkG1234
(順帶多觀察了幾個可預定的車次,發(fā)現(xiàn)預訂按鈕點擊執(zhí)行的函數(shù)名都叫做 checkG1234
):
將 checkG1234
格式化:
checkG1234(
'c7JA6pPfpAnWCp5RcvN8Ks8WYk68Wv0ZxdytFe8c8vmN64VfPKCIdxXSLTCBd1Gc%2F9zXT5i7gaZy%0ATr4SWbEOeZwttRjRaGMfOHoKAnxoDJwtiEm8Ittvm7BZZu1qaJcZqFqg2EbdT%2FyissJoFkqEzenT%0AA%2F1UTQV%2BHLjKKEM6MT9SsmljBbjFH3SAhSavWoWUzjQlLsWofENBOoRg2Hu%2F%2FxSqKmWoLwQ%2F4Ku%2B%0AOZNWfWel2HJQ1Oqn5obNR5mTv8sNkJGaSCO459N4zSjH5Fnv29Nbw207GxdKQGFCVQ%3D%3D',
'00:35',
'56000K429760',
'HZH',
'NCG'
)
一共有四個參數(shù)逐哈,前三個不知道是啥芬迄,后兩個是始發(fā)地和目的地。先不管昂秃,先去扒扒搜索返回的數(shù)據禀梳。
<br />
思路分析——返回數(shù)據分析
假設 K4297 在查詢結果中對應的數(shù)據為 data,則將可以將預訂函數(shù)參數(shù)分解:
找兩條數(shù)據對比一下:
{
"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": "預訂"
}
假設這條數(shù)據叫做 data,可以發(fā)現(xiàn)如下對應關系:
'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'
好了唯咬,點擊預訂需要的參數(shù)都找全了纱注。
<br />
模擬搜索
這個很簡單,定義一個 queryAjax 的方法胆胰,傳入搜索參數(shù) data 和回調函數(shù) callback:
/**
* Ajax
* @param {Object} data 搜索參數(shù)
* @param {Function} callback 回調函數(shù)狞贱,用于處理返回的數(shù)據
*/
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 );
}
}
});
}
<br />
處理返回的數(shù)據
為了便于后期查找出我們需要的,并且可購買的車次瞎嬉,這里使用鍵值對保存可購買的車次信息:
/**
* 處理返回的數(shù)據
* @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:
分析發(fā)現(xiàn)碳想,如果車次可以購買,那么 secretStr 的值不為空啡浊,并且queryLeftNewDTO.canWebBuy = 'Y'
<br />
匹配座位類型
長度為 0 就是不限制钳吟,有賣就買,返回 true;不為 0 就是遍歷期望的類型數(shù)組舆乔,找到有匹配的就返回 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;
}
<br />
匹配需要的車次
接下來就是匹配我們需要的車次了籍铁,需要傳入上述所有可購買的車次:
/**
* 匹配想要購買的車次
* @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;
}
<br />
預訂車票
就是調預訂的那個方法,傳入需要的參數(shù)而已:
/**
* 預訂車票
* @param {Object} ticket 改車次信息
*/
function reserveTicket( ticket ) {
checkG1234(ticket.secretStr, ticket.start_time,ticket.train_no, ticket.from_station_telecode, ticket.to_station_telecode);
}
<br />
填寫用戶信息
這個就是查看登陸表單的結果了拒名,選擇器就直接寫死了:
/**
* 填寫表單信息
* @param {Object} user 用戶信息對象
*/
function setFormData( user ) {
$( '#username' ).val( user.name );
$( '#password' ).val( user.password );
}
Tips:
點擊查看元素可以看到表單的用戶名吩愧、用戶密碼輸入框的 id 名稱分別為 'username'、'password'
form.png
圖片驗證碼無解增显,不知道搶票軟件咋弄的雁佳,有內部接口脐帝?貌似移動端有獨立的接口,下次看看糖权。
<br />
初始化
寫個定時器自動查詢匹配:
/**
* 初始化
*/
function init() {
// 一開始執(zhí)行就查詢匹配一次
queryAjax(WISH, function( data ) {
availableTicketsMap = getAvailableTicketsMap( data );
matchYourTickets( availableTicketsMap );
});
// 定時器
timer = setInterval(function() {
queryAjax(WISH, function( data ) {
availableTicketsMap = getAvailableTicketsMap( data );
matchYourTickets( availableTicketsMap );
});
}, SEARCH_RATE);
}
<br />
總結
自己開雙屏堵腹,一邊寫代碼一邊看著,掛著刷還行星澳,當然手機 App 更好吧疚顷。春運的票好難搶,今天啥都沒搶到禁偎,就看明天了腿堤。搶不到就拿這個掛個一天....
Good Night ~ ~ o(* ̄▽ ̄*)ブ
—— 2016/12/26 By Live