文章地址 :
12306搶票腳本開(kāi)發(fā)(一)提綱
12306搶票腳本開(kāi)發(fā)(二)解析火車站代號(hào)并分析查詢的HTTP請(qǐng)求
12306搶票腳本開(kāi)發(fā)(三)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的查詢腳本
12306搶票腳本開(kāi)發(fā)(四)完善上節(jié)課的代碼并面向?qū)ο?/a>
12306搶票腳本開(kāi)發(fā)(五)更友好的使用方式
12306搶票腳本開(kāi)發(fā)(六)更友好的時(shí)間輸入方式
12306搶票腳本開(kāi)發(fā)(七)將前幾節(jié)課的成果結(jié)合起來(lái)實(shí)現(xiàn)一個(gè)完整的工具
簡(jiǎn)介 :
首先我們要實(shí)現(xiàn)的這個(gè)腳本為了實(shí)現(xiàn)易用性
在選擇出發(fā)地和目的地的時(shí)候應(yīng)該是讓用戶直接輸入出發(fā)地的目的地的中文名
然后系統(tǒng)自動(dòng)去識(shí)別 , 匹配到響應(yīng)的火車站代碼然后再發(fā)送 http 請(qǐng)求
通過(guò)分析 http 請(qǐng)求可以得知
12306網(wǎng)站在用戶選擇火車站并點(diǎn)擊查詢的時(shí)候 , 并沒(méi)有把火車站的中文名作為 http 請(qǐng)求的參數(shù)傳遞的
是這樣做的 , 事先保存了一個(gè)火車站名稱和代碼對(duì)應(yīng)關(guān)系的文件(其實(shí)是一個(gè) js 變量) , 用戶輸入火車站名 , 或者火車站名拼音的首字母之后 , js 就會(huì)解析找到真正的火車站的代號(hào) , 當(dāng)然這個(gè)代號(hào)在前臺(tái)是看不見(jiàn)的 , 當(dāng)點(diǎn)擊查詢之后就會(huì)構(gòu)造一個(gè) url , url 的 get 參數(shù)中就會(huì)有城市的代碼以及這次查詢的相關(guān)信息(出發(fā)日期等等)
大家可以想一下 , 為什么在我們輸入火車站名的時(shí)候下面會(huì)模糊匹配到以已輸入的字符串開(kāi)頭的火車站 , 這個(gè)就是預(yù)先下載了一個(gè)這樣的保存火車站名和首字母的映射關(guān)系的文件 , 然后 js 在檢測(cè)到輸入框中的文本發(fā)生變化的時(shí)候就進(jìn)行一次檢索 , 更新匹配到的火車站 , 并顯示在前臺(tái)
可以看到這個(gè)文件其實(shí)是定義了一個(gè) js 的變量
這個(gè)變量好像是有一定的格式
可以很容易就分析這里應(yīng)該是以 '@' 這個(gè)字符來(lái)分隔每一個(gè)火車站的
然后又以 '|' 這個(gè)字符來(lái)分隔每一個(gè)火車站中的數(shù)據(jù) , 這些數(shù)據(jù)具體是干什么的我們現(xiàn)在還說(shuō)不準(zhǔn)
這個(gè)要通過(guò)分析 12306 網(wǎng)站的 js 代碼才能得知
那么我們應(yīng)該如何來(lái)分析這個(gè)變量到底是如何被解析 , 每一個(gè)字段都有什么用呢 ?
我們首先來(lái)下載 12306 網(wǎng)站的代碼 :
可以使用 wget 來(lái)遞歸下載整個(gè)網(wǎng)站
wget -r https://kyfw.12306.cn/otn/
下載完成后 , 我們需要在 js 文件或者 html 文件( html 也可能內(nèi)嵌 js 代碼)中去搜索使用這個(gè)變量(station_names)的地方 , 暫時(shí)想到的方法就是搜索文件的內(nèi)容 , 匹配變量名 , 不知道還有沒(méi)有更好的方法 , 比如說(shuō)能不能利用 js 的引擎自動(dòng)地找到引用該變量的函數(shù)什么的...
find ./ -type f -name "*.js" | xargs grep "station_names"
結(jié)果如下 :
哈 , 我們已經(jīng)可以定位到一個(gè)具體的文件的具體行了 :
./otn/resources/js/framework/city_name.js
821: if (typeof (station_names) != "undefined") {
822: if (station_names.indexOf(join) == -1) {
1186: if (typeof (station_names) != "undefined") {
1188: var cities = station_names.split('@');
那么解析的工作肯定是在這個(gè)文件中完成的
來(lái)看看這個(gè)文件吧 :
if (typeof (station_names) != "undefined") {
// 分拆城市信息
var cities = station_names.split('@');
for ( var i = 0; i < cities.length; i++) { // 遍歷所有的火車站
var titem = cities[i];
var raha = titem.toString().charAt(0);
for(var k in city_name_character){
if (raha == city_name_character[k]) {
liarray_cities_array[k].push(titem.split('|'));
}
}
if (titem.length > 0) { // @bjb|北京北|VAP|beijingbei|bjb|0
titem = titem.split('|'); // 把每個(gè)火車站的信息再用 '|' 來(lái)分隔 , 也就是每個(gè)字段
if (favcityID != "" && titem[2] == favcityID) { // 這里判斷了第三個(gè)字段 , 用到了 favcityID , 先不用關(guān)注這里
favcity = titem;
array_cities.unshift(titem); // 向 array_cities 這個(gè)數(shù)組中插入一個(gè)城市 , 也就是 "@bjb|北京北|VAP|beijingbei|bjb|0" 這樣的字符串 , 加到首部
// 當(dāng)fav城市位于第一頁(yè)時(shí)堆缘,避免重復(fù)顯示
if (i > 6) { // 可以發(fā)現(xiàn)幾乎大部分的火車站的字段都是 6 個(gè) , 那這里也先不要太關(guān)注了
array_cities.push(titem); // 加到數(shù)組末尾
}
} else {
array_cities.push(titem); // 總是就是向這個(gè)數(shù)組中加入一個(gè)城市的所有字段組成的數(shù)組 , 需要查一下這個(gè)變量
// 只要我們能找到 js 是怎么使用這個(gè) 變量 的 , 那么我們就可以知道這 6 個(gè)字段都是什么
}
}
}
liarray_cities1 = liarray_cities_array[0].concat(liarray_cities_array[1]).concat(liarray_cities_array[2]).concat(liarray_cities_array[3]).concat(liarray_cities_array[4]);
liarray_cities2 = liarray_cities_array[5].concat(liarray_cities_array[6]).concat(liarray_cities_array[7]).concat(liarray_cities_array[8]).concat(liarray_cities_array[9]);
liarray_cities3 = liarray_cities_array[10].concat(liarray_cities_array[11]).concat(liarray_cities_array[12]).concat(liarray_cities_array[13]).concat(liarray_cities_array[14]);
liarray_cities4 = liarray_cities_array[15].concat(liarray_cities_array[16]).concat(liarray_cities_array[17]).concat(liarray_cities_array[18]).concat(liarray_cities_array[19]);
liarray_cities5 = liarray_cities_array[20].concat(liarray_cities_array[21]).concat(liarray_cities_array[22]).concat(liarray_cities_array[23]).concat(liarray_cities_array[24]).concat(liarray_cities_array[25]);
list_stations[0] = [liarray_cities_array[0],liarray_cities_array[1],liarray_cities_array[2],liarray_cities_array[3],liarray_cities_array[4]];
list_stations[1] = [liarray_cities_array[5],liarray_cities_array[6],liarray_cities_array[7],liarray_cities_array[8],liarray_cities_array[9]];
list_stations[2] = [liarray_cities_array[10],liarray_cities_array[11],liarray_cities_array[12],liarray_cities_array[13],liarray_cities_array[14]];
list_stations[3] = [liarray_cities_array[15],liarray_cities_array[16],liarray_cities_array[17],liarray_cities_array[18],liarray_cities_array[19]];
list_stations[4] = [liarray_cities_array[20]/*,liarray_cities_array[21]*/,liarray_cities_array[22],liarray_cities_array[23],liarray_cities_array[24],liarray_cities_array[25]];
for ( var i = 0; i < array_cities.length; i++) {
array_cities[i].push(i);
}
}
這里的 favcityID 似乎和 cookie 有一定的關(guān)系 , 不過(guò)我們這里暫時(shí)不進(jìn)行登陸 , 這里應(yīng)該不用特別關(guān)注
可以發(fā)現(xiàn) :
array_cities[i][1] == aCityname
array_cities[i][2] == aCidyID // aCidy ? aCity ? 不知道是手誤打錯(cuò)了還是....
這里第二個(gè)字段應(yīng)該是城市名
第三個(gè)字段是城市ID
暫時(shí)好像還沒(méi)有找到對(duì)別的字段的引用 , 那就姑且先分析到這里
現(xiàn)在我們?cè)賮?lái)看看查詢余票按鈕會(huì)發(fā)送的 http 請(qǐng)求是什么樣的 :
可以看到 , 這里其實(shí)是發(fā)送了兩個(gè)請(qǐng)求 :
第一個(gè)請(qǐng)求的是 :
https://kyfw.12306.cn/otn/leftTicket/log?
// 根據(jù)接口名稱和返回?cái)?shù)據(jù)可以推測(cè)出應(yīng)該是記錄日志的接口
第二個(gè)請(qǐng)求的是 :
https://kyfw.12306.cn/otn/leftTicket/query?
// 根據(jù)接口名稱和返回?cái)?shù)據(jù)可以推測(cè)出應(yīng)該是真正查詢的接口
我們?cè)賮?lái)分析一下參數(shù) :
https://kyfw.12306.cn/otn/leftTicket/query?
leftTicketDTO.train_date=2017-02-23&
leftTicketDTO.from_station=BJP&
leftTicketDTO.to_station=SHH&
purpose_codes=ADULT
很簡(jiǎn)單 , 四個(gè)參數(shù) :
leftTicketDTO.train_date : 出發(fā)日期
leftTicketDTO.from_station : 出發(fā)站的代號(hào)
leftTicketDTO.to_station : 到達(dá)站的代號(hào)
purpose_codes : ADULT 表示成人票 , 改變選項(xiàng)為學(xué)生票可以發(fā)現(xiàn)該參數(shù)的值變成了 : 0X00
再看看返回的內(nèi)容 :
json的數(shù)據(jù) , 我們可以根據(jù)變量名來(lái)大概猜一下這些鍵大概都是什么意思 :
這里為了可讀性 , 將火車的信息刪除到只剩一個(gè)
下面的注釋都是根據(jù)變量名猜的 , 不一定真正正確
但是話又說(shuō)回來(lái) , 我們查詢其實(shí)可能只關(guān)注其中的一些數(shù)據(jù) , 并不需要把所有的鍵都搞清楚
不過(guò)還是最好搞清楚 , 有助于提高代碼分析能力~
{
"validateMessagesShowId": "_validatorMessage",
"status": true,
"httpstatus": 200,
"data": [
{
"queryLeftNewDTO": {
"train_no": "24000000G702", # 火車編號(hào)
"station_train_code": "G7", # 火車站的火車編號(hào) ?
"start_station_telecode": "VNP", # 始發(fā)站火車站的電話代碼 ? 還是遠(yuǎn)程代碼 ?
# 這個(gè)在 station_names 這個(gè)文件中有 , 就是第三個(gè)字段
"start_station_name": "北京南", # 始發(fā)站火車站名
"end_station_telecode": "AOH",
"end_station_name": "上海虹橋", # 終點(diǎn)站火車站名
"from_station_telecode": "VNP",
"from_station_name": "北京南", # 乘客上車的站的名稱
"to_station_telecode": "AOH",
"to_station_name": "上海虹橋", # 乘客下車的站的名稱
"start_time": "19:00", # 開(kāi)車時(shí)間
"arrive_time": "23:56", # 到達(dá)時(shí)間
"day_difference": "0", # 是否跨天到達(dá) ?
"train_class_name": "", # 火車的類名 ?
"lishi": "04:56", # lishi ? 歷史 ?
"canWebBuy": "Y", # 我們能不能買 ?
"lishiValue": "296", # 歷史值 ?
"yp_info": "yufrsBuLoo4eUOihUqJNFHJjp09eB27ShkcETr7CgLXSp2qD",
# 余票數(shù)據(jù) ? 這里如果遇到中文拼音變量名 , 有一個(gè)好的云聯(lián)想輸入法會(huì)很有幫助 :D
"control_train_day": "20301231",
"start_train_date": "20170223",
"seat_feature": "O3M393",
"yp_ex": "O0M090",
"train_seat_feature": "3",
"train_type_code": "2",
"start_province_code": "31", # 首發(fā)站省份編號(hào)
"start_city_code": "0357", # 首發(fā)站城市編號(hào)
"end_province_code": "33", # 終點(diǎn)站省份編號(hào)
"end_city_code": "0712", # 終點(diǎn)站城市編號(hào)
"seat_types": "OM9", # 座位類型
"location_code": "P3",
"from_station_no": "01", # 乘客上車的站的編號(hào)(估計(jì)是在這個(gè)城市的編號(hào) , 因?yàn)橐紤]到一個(gè)城市多個(gè)站的情況) ?
"to_station_no": "04", # 乘客下車的站的編號(hào)
"control_day": 29,
"sale_time": "1230",
"is_support_card": "1", # 是否支持刷卡 ?
"controlled_train_flag": "0",
"controlled_train_message": "正常車次垦写,不受控",
# 下面的估計(jì)就是各種作為的余票數(shù) , 當(dāng)該列車沒(méi)有這樣的座位的時(shí)候就是 "--"
# 這里具體哪個(gè)是哪個(gè)可以通過(guò)查詢不同的列車來(lái)推測(cè) , 這樣也最快 , 也可以通過(guò)閱讀代碼
"gg_num": "--",
"gr_num": "--",
"qt_num": "--",
"rw_num": "--",
"rz_num": "--",
"tz_num": "--",
"wz_num": "--",
"yb_num": "--",
"yw_num": "--",
"yz_num": "--",
"ze_num": "有", # 二等座
"zy_num": "10", # 一等座
"swz_num": "8" # 商務(wù)座
},
# 秘密的字符串 , 暫時(shí)還不知道 , 但是這個(gè)對(duì)我們查詢余票并沒(méi)有影響 , 我們現(xiàn)在已經(jīng)可以解析余票的數(shù)據(jù)了
# 這個(gè)字段應(yīng)該是在購(gòu)票的時(shí)候會(huì)用到 , 暫時(shí)先不分析
"secretStr": "ECM6UXA86obwvu3av7Tmh%2BLNXyfi8vGfR1X%2BkTrDYcvjzpdLjFYUVuYLiLR8ifoxpyot3PM528xO%0A8iTmnvT7yKjUduPszD1BtH8xNzetGrb6aGO%2FEV1HNQ1aXscPoZhFjBwle1jIFS78FxMiD1Ch9yIt%0A84qJZdXFhTYrgeD5DuBN3UAg291pgwrcbo0eMnBlJSFQNcAK%2FlkcrbohuCbeqj8Xn8qjvFssZv5L%0AcgJnsXmLe9ZqHLUoGGLUUW2GpsIi%2BVMehOiIV8XmXnd2cvcEperf4neI2tkFk0gn%2BsDpkhGvNjSK%0AaG0OW98ByPk%3D",
"buttonTextInfo": "預(yù)訂"
}
]
}
總結(jié)和預(yù)告 :
好了 , 分析到這里已經(jīng)差不多了 , 下節(jié)課我們來(lái)寫代碼進(jìn)行真正的查詢 , 謝謝大家的支持~
特別希望能和大家交流思路 , 希望共同進(jìn)步 , 有什么問(wèn)題就隨便提哦