*** 說(shuō)明***:
這不是黑科技,并不是自動(dòng)購(gòu)票,請(qǐng)根據(jù)自己的需求使用,建議使用搶票 APP 靠譜一些,360 搶票也比這個(gè)好,至少它能智能識(shí)圖!!!
只是自動(dòng)查詢(xún)你想要的車(chē)次,并自動(dòng)點(diǎn)擊預(yù)訂,自動(dòng)填寫(xiě)用戶(hù)名和密碼,但是圖片驗(yàn)證碼還需要自己點(diǎn)擊(這個(gè)無(wú)解)。
本文先說(shuō)使用方法,再講解。
使用方法
首先,打開(kāi) 12306 的 車(chē)票預(yù)定頁(yè)面
-
摁 F12 鍵打開(kāi)瀏覽器控制臺(tái)(Chrome 瀏覽為例),選擇 console,如下圖所示:
console.png 將配置好的代碼粘貼到圖中區(qū)域,按 Enter 鍵回車(chē)就行。可以在 Network 里面看到已經(jīng)在刷了:
-
建議運(yùn)行時(shí)關(guān)閉控制臺(tái) 或者 將控制臺(tái)放到右側(cè)之類(lèi),不然小屏幕下登陸框會(huì)看不全。點(diǎn)擊控制臺(tái)右側(cè)的三個(gè)豎點(diǎn)選擇:
setRight.png
可能的結(jié)果
第一種情況是需要重新登錄一次,這種很常見(jiàn)。用戶(hù)名密碼都自動(dòng)幫你填好了,然后自己再填這個(gè)坑爹的驗(yàn)證碼吧(ˉ▽ˉ;)...,示意圖如下:
另一種情況是直接跳到購(gòu)買(mǎi)頁(yè)面,你需要自己勾選乘客和點(diǎn)擊提交訂單即可:
無(wú)論是哪種結(jié)果,都需要重新運(yùn)行代碼。在控制臺(tái)按向上箭頭,再按回車(chē)鍵就行(當(dāng)然重新粘貼也行)。
<br />
配置代碼
先粘代碼:
var WISH = {
train_date: '2017-01-24', // 乘車(chē)日期
from_station_telecode: 'HGH', // HGH - 杭州東
to_station_telecode: 'NXG', // NXG - 南昌西
purpose_codes: 'ADULT', // ADULT - 成年人,0X00 - 學(xué)生
station_train_code: [ // 想買(mǎi)的車(chē)次,排前面的優(yōu)先
'G2365',
'G1417',
'G1341'
],
setType: [ // 座位類(lèi)型,不填 - 不限制; yz_num - 硬座; rz_num - 軟座; yw_num - 硬臥; rw_num - 軟臥; gr_num - 高級(jí)軟臥; zy_num - 一等座; ze_num - 二等座; tz_num - 特等座; wz_num - 無(wú)座; qt_num - 其它; swz_num - 商務(wù)座
"zy_num", // 一等座
"ze_num", // 二等座
"wz_num" // 無(wú)座
]
},
USER = {
name: 'xxx@163.com', // 用戶(hù)名稱(chēng)
password: 'xxx' // 用戶(hù)密碼
},
SEARCH_RATE = 5000, // 刷新頻率,5000 毫秒
timer = null,
matchTicket = {},
availableTicketsMap = {};
/**
* Ajax
* @param {Object} data 搜索參數(shù)
* @param {Function} callback 回調(diào)函數(shù),用于處理返回的數(shù)據(jù)
*/
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ù)據(jù)
* @param {Array} data 返回的所有車(chē)次信息
* @return {Object} 可購(gòu)買(mǎi)的車(chē)次 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-無(wú)
*/
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;
}
/**
* 匹配想要購(gòu)買(mǎi)的車(chē)次
* @param {Object} ticketsMap 可購(gòu)買(mǎi)的所有車(chē)次
* @return {Boolean} true-匹配到,false-沒(méi)匹配到
*/
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); // 清除定時(shí)器
reserveTicket(ticket); // 預(yù)訂車(chē)票
setFormData(USER); // 填寫(xiě)用戶(hù)信息
return true;
}
}
return false;
}
/**
* 預(yù)訂車(chē)票
* @param {Object} ticket 改車(chē)次信息
*/
function reserveTicket(ticket) {
checkG1234(ticket.secretStr, ticket.start_time, ticket.train_no, ticket.from_station_telecode, ticket.to_station_telecode);
}
/**
* 填寫(xiě)表單信息
* @param {Object} user 用戶(hù)信息對(duì)象
*/
function setFormData(user) {
$('#username').val(user.name);
$('#password').val(user.password);
}
/**
* 初始化
*/
function init() {
// 一開(kāi)始執(zhí)行就查詢(xún)匹配一次
queryAjax(WISH, function(data) {
availableTicketsMap = getAvailableTicketsMap(data);
matchYourTickets(availableTicketsMap);
});
// 定時(shí)器
timer = setInterval(function() {
queryAjax(WISH, function(data) {
availableTicketsMap = getAvailableTicketsMap(data);
matchYourTickets(availableTicketsMap);
});
}, SEARCH_RATE);
}
init();
// 用于下一個(gè)頁(yè)面
// function buy() {
// $( '#normalPassenger_0' ).click();
// $( '#submitOrder_id' ).click();
// }
// buy();
需要配置的信息就是代碼開(kāi)頭的 WISH 部分。
<br />
城市 code 查詢(xún)
至于城市 code 怎么查詢(xún),請(qǐng)?jiān)诠俜酱a中查找,代碼很長(zhǎng)很長(zhǎng)...鏈接地址
使用瀏覽器的 Ctrl + F 查找你所想查找的城市,比如 “杭州東”(注意:杭州和杭州東的 code 不一樣),緊接著杭州東后面的字母就是對(duì)應(yīng)的 code :
建議使用可搜索的城市區(qū)間,在官網(wǎng)測(cè)試過(guò)的;注意大小寫(xiě)是區(qū)分的
<br />
思路分析——搜索功能
好了,接下來(lái)是思路分析。
先填寫(xiě)搜索條件,點(diǎn)擊搜索,查看控制臺(tái) Network 里面的 XHR 記錄,也就是發(fā)送的 Ajax 了:
可以發(fā)現(xiàn),每次點(diǎn)擊搜索,都會(huì)發(fā)送兩個(gè) Ajax 請(qǐng)求:
- /otn/leftTicket/log:這個(gè)看名字像是記錄搜索日志,不是很清楚
- /otn/leftTicket/queryA: 這個(gè)就是查詢(xún)了
兩個(gè) Ajax 的參數(shù)都是一致的:
就是我們所填寫(xiě)的搜索參數(shù),格式化后如下:
{
'leftTicketDTO.train_date': '2017-01-24', // 出發(fā)日
'leftTicketDTO.from_station': 'HGH', // 出發(fā)地
'leftTicketDTO.to_station': 'NXG', // 目的地
'purpose_codes': 'ADULT' // 普通(成年人)
}
所以我們可以使用這些參數(shù)來(lái)模擬搜索。
Tips:
并且發(fā)現(xiàn)查詢(xún)結(jié)果只和 出發(fā)日期、出發(fā)地、目的地、乘客類(lèi)型(普通、學(xué)生) 有關(guān),和車(chē)次的篩選條件無(wú)關(guān):
不填寫(xiě)篩選條件(返回 92 條數(shù)據(jù)):
noFilter.png
填寫(xiě)篩選條件(返回 92 條數(shù)據(jù)):
hasFilter.png
所以車(chē)次的過(guò)濾,是在瀏覽器端完成的
<br />
思路分析——預(yù)訂
找個(gè)可以預(yù)訂的車(chē)次,查看 預(yù)定 按鈕信息(就是控制臺(tái)左上角的箭頭,先點(diǎn)擊它,再去點(diǎn)按鈕):
可以看到使用的是 onClick 事件,執(zhí)行函數(shù)是 checkG1234
(順帶多觀察了幾個(gè)可預(yù)定的車(chē)次,發(fā)現(xiàn)預(yù)訂按鈕點(diǎ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'
)
一共有四個(gè)參數(shù),前三個(gè)不知道是啥,后兩個(gè)是始發(fā)地和目的地。先不管,先去扒扒搜索返回的數(shù)據(jù)。
<br />
思路分析——返回?cái)?shù)據(jù)分析
假設(shè) K4297 在查詢(xún)結(jié)果中對(duì)應(yīng)的數(shù)據(jù)為 data,則將可以將預(yù)訂函數(shù)參數(shù)分解:
找兩條數(shù)據(jù)對(duì)比一下:
{
"queryLeftNewDTO": {
"train_no": "56000K429760",
"station_train_code": "K4297", // 車(chē)次
"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": "正常車(chē)次,不受控",
"yz_num": "--", // 硬座
"rz_num": "--", // 軟座
"yw_num": "--", // 硬臥
"rw_num": "--", // 軟臥
"gr_num": "--", // 高級(jí)軟臥?
"zy_num": "有", // 一等座
"ze_num": "有", // 二等座
"tz_num": "--", // 特等座
"gg_num": "--", // ?
"yb_num": "--", // ?
"wz_num": "--", // 無(wú)座
"qt_num": "--", // 其它?
"swz_num": "11" // 商務(wù)座
},
"secretStr": "'c7JA6pPfpAnWCp5RcvN8Ks8WYk68Wv0ZxdytFe8c8vmN64VfPKCIdxXSLTCBd1Gc%2F9zXT5i7gaZy%0ATr4SWbEOeZwttRjRaGMfOHoKAnxoDJwtiEm8Ittvm7BZZu1qaJcZqFqg2EbdT%2FyissJoFkqEzenT%0AA%2F1UTQV%2BHLjKKEM6MT9SsmljBbjFH3SAhSavWoWUzjQlLsWofENBOoRg2Hu%2F%2FxSqKmWoLwQ%2F4Ku%2B%0AOZNWfWel2HJQ1Oqn5obNR5mTv8sNkJGaSCO459N4zSjH5Fnv29Nbw207GxdKQGFCVQ%3D%3D",
"buttonTextInfo": "預(yù)訂"
}
假設(shè)這條數(shù)據(jù)叫做 data,可以發(fā)現(xiàn)如下對(duì)應(yīng)關(guā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'
好了,點(diǎn)擊預(yù)訂需要的參數(shù)都找全了。
<br />
模擬搜索
這個(gè)很簡(jiǎn)單,定義一個(gè) queryAjax 的方法,傳入搜索參數(shù) data 和回調(diào)函數(shù) callback:
/**
* Ajax
* @param {Object} data 搜索參數(shù)
* @param {Function} callback 回調(diào)函數(shù),用于處理返回的數(shù)據(jù)
*/
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ù)據(jù)
為了便于后期查找出我們需要的,并且可購(gòu)買(mǎi)的車(chē)次,這里使用鍵值對(duì)保存可購(gòu)買(mǎi)的車(chē)次信息:
/**
* 處理返回的數(shù)據(jù)
* @param {Array} data 返回的所有車(chē)次信息
* @return {Object} 可購(gòu)買(mǎi)的車(chē)次 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),如果車(chē)次可以購(gòu)買(mǎi),那么 secretStr 的值不為空,并且queryLeftNewDTO.canWebBuy = 'Y'
<br />
匹配座位類(lèi)型
長(zhǎng)度為 0 就是不限制,有賣(mài)就買(mǎi),返回 true;不為 0 就是遍歷期望的類(lèi)型數(shù)組,找到有匹配的就返回 true,否則為 false:
/**
* 是否有想要的座位
* @param {[type]} queryLeftNewDTO [description]
* @return {Boolean} true-有,false-無(wú)
*/
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 />
匹配需要的車(chē)次
接下來(lái)就是匹配我們需要的車(chē)次了,需要傳入上述所有可購(gòu)買(mǎi)的車(chē)次:
/**
* 匹配想要購(gòu)買(mǎi)的車(chē)次
* @param {Object} ticketsMap 可購(gòu)買(mǎi)的所有車(chē)次
* @return {Boolean} true-匹配到,false-沒(méi)匹配到
*/
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 ); // 清除定時(shí)器
reserveTicket( ticket ); // 預(yù)訂車(chē)票
setFormData( USER ); // 填寫(xiě)用戶(hù)信息
return true;
}
}
return false;
}
<br />
預(yù)訂車(chē)票
就是調(diào)預(yù)訂的那個(gè)方法,傳入需要的參數(shù)而已:
/**
* 預(yù)訂車(chē)票
* @param {Object} ticket 改車(chē)次信息
*/
function reserveTicket( ticket ) {
checkG1234(ticket.secretStr, ticket.start_time,ticket.train_no, ticket.from_station_telecode, ticket.to_station_telecode);
}
<br />
填寫(xiě)用戶(hù)信息
這個(gè)就是查看登陸表單的結(jié)果了,選擇器就直接寫(xiě)死了:
/**
* 填寫(xiě)表單信息
* @param {Object} user 用戶(hù)信息對(duì)象
*/
function setFormData( user ) {
$( '#username' ).val( user.name );
$( '#password' ).val( user.password );
}
Tips:
點(diǎn)擊查看元素可以看到表單的用戶(hù)名、用戶(hù)密碼輸入框的 id 名稱(chēng)分別為 'username'、'password'
form.png
圖片驗(yàn)證碼無(wú)解,不知道搶票軟件咋弄的,有內(nèi)部接口?貌似移動(dòng)端有獨(dú)立的接口,下次看看。
<br />
初始化
寫(xiě)個(gè)定時(shí)器自動(dòng)查詢(xún)匹配:
/**
* 初始化
*/
function init() {
// 一開(kāi)始執(zhí)行就查詢(xún)匹配一次
queryAjax(WISH, function( data ) {
availableTicketsMap = getAvailableTicketsMap( data );
matchYourTickets( availableTicketsMap );
});
// 定時(shí)器
timer = setInterval(function() {
queryAjax(WISH, function( data ) {
availableTicketsMap = getAvailableTicketsMap( data );
matchYourTickets( availableTicketsMap );
});
}, SEARCH_RATE);
}
<br />
總結(jié)
自己開(kāi)雙屏,一邊寫(xiě)代碼一邊看著,掛著刷還行,當(dāng)然手機(jī) App 更好吧。春運(yùn)的票好難搶?zhuān)裉焐抖紱](méi)搶到,就看明天了。搶不到就拿這個(gè)掛個(gè)一天....
Good Night ~ ~ o(* ̄▽?zhuān)?)ブ
—— 2016/12/26 By Live