文章地址 :
12306搶票腳本開發(一)提綱
12306搶票腳本開發(二)解析火車站代號并分析查詢的HTTP請求
12306搶票腳本開發(三)實現一個簡單的查詢腳本
12306搶票腳本開發(四)完善上節課的代碼并面向對象
12306搶票腳本開發(五)更友好的使用方式
12306搶票腳本開發(六)更友好的時間輸入方式
12306搶票腳本開發(七)將前幾節課的成果結合起來實現一個完整的工具
簡介 :
首先我們要實現的這個腳本為了實現易用性
在選擇出發地和目的地的時候應該是讓用戶直接輸入出發地的目的地的中文名
然后系統自動去識別 , 匹配到響應的火車站代碼然后再發送 http 請求
通過分析 http 請求可以得知
12306網站在用戶選擇火車站并點擊查詢的時候 , 并沒有把火車站的中文名作為 http 請求的參數傳遞的
是這樣做的 , 事先保存了一個火車站名稱和代碼對應關系的文件(其實是一個 js 變量) , 用戶輸入火車站名 , 或者火車站名拼音的首字母之后 , js 就會解析找到真正的火車站的代號 , 當然這個代號在前臺是看不見的 , 當點擊查詢之后就會構造一個 url , url 的 get 參數中就會有城市的代碼以及這次查詢的相關信息(出發日期等等)
大家可以想一下 , 為什么在我們輸入火車站名的時候下面會模糊匹配到以已輸入的字符串開頭的火車站 , 這個就是預先下載了一個這樣的保存火車站名和首字母的映射關系的文件 , 然后 js 在檢測到輸入框中的文本發生變化的時候就進行一次檢索 , 更新匹配到的火車站 , 并顯示在前臺
可以看到這個文件其實是定義了一個 js 的變量
這個變量好像是有一定的格式
可以很容易就分析這里應該是以 '@' 這個字符來分隔每一個火車站的
然后又以 '|' 這個字符來分隔每一個火車站中的數據 , 這些數據具體是干什么的我們現在還說不準
這個要通過分析 12306 網站的 js 代碼才能得知
那么我們應該如何來分析這個變量到底是如何被解析 , 每一個字段都有什么用呢 ?
我們首先來下載 12306 網站的代碼 :
可以使用 wget 來遞歸下載整個網站
wget -r https://kyfw.12306.cn/otn/
下載完成后 , 我們需要在 js 文件或者 html 文件( html 也可能內嵌 js 代碼)中去搜索使用這個變量(station_names)的地方 , 暫時想到的方法就是搜索文件的內容 , 匹配變量名 , 不知道還有沒有更好的方法 , 比如說能不能利用 js 的引擎自動地找到引用該變量的函數什么的...
find ./ -type f -name "*.js" | xargs grep "station_names"
結果如下 :
哈 , 我們已經可以定位到一個具體的文件的具體行了 :
./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('@');
那么解析的工作肯定是在這個文件中完成的
來看看這個文件吧 :
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('|'); // 把每個火車站的信息再用 '|' 來分隔 , 也就是每個字段
if (favcityID != "" && titem[2] == favcityID) { // 這里判斷了第三個字段 , 用到了 favcityID , 先不用關注這里
favcity = titem;
array_cities.unshift(titem); // 向 array_cities 這個數組中插入一個城市 , 也就是 "@bjb|北京北|VAP|beijingbei|bjb|0" 這樣的字符串 , 加到首部
// 當fav城市位于第一頁時,避免重復顯示
if (i > 6) { // 可以發現幾乎大部分的火車站的字段都是 6 個 , 那這里也先不要太關注了
array_cities.push(titem); // 加到數組末尾
}
} else {
array_cities.push(titem); // 總是就是向這個數組中加入一個城市的所有字段組成的數組 , 需要查一下這個變量
// 只要我們能找到 js 是怎么使用這個 變量 的 , 那么我們就可以知道這 6 個字段都是什么
}
}
}
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 有一定的關系 , 不過我們這里暫時不進行登陸 , 這里應該不用特別關注
可以發現 :
array_cities[i][1] == aCityname
array_cities[i][2] == aCidyID // aCidy ? aCity ? 不知道是手誤打錯了還是....
這里第二個字段應該是城市名
第三個字段是城市ID
暫時好像還沒有找到對別的字段的引用 , 那就姑且先分析到這里
現在我們再來看看查詢余票按鈕會發送的 http 請求是什么樣的 :
可以看到 , 這里其實是發送了兩個請求 :
第一個請求的是 :
https://kyfw.12306.cn/otn/leftTicket/log?
// 根據接口名稱和返回數據可以推測出應該是記錄日志的接口
第二個請求的是 :
https://kyfw.12306.cn/otn/leftTicket/query?
// 根據接口名稱和返回數據可以推測出應該是真正查詢的接口
我們再來分析一下參數 :
https://kyfw.12306.cn/otn/leftTicket/query?
leftTicketDTO.train_date=2017-02-23&
leftTicketDTO.from_station=BJP&
leftTicketDTO.to_station=SHH&
purpose_codes=ADULT
很簡單 , 四個參數 :
leftTicketDTO.train_date : 出發日期
leftTicketDTO.from_station : 出發站的代號
leftTicketDTO.to_station : 到達站的代號
purpose_codes : ADULT 表示成人票 , 改變選項為學生票可以發現該參數的值變成了 : 0X00
再看看返回的內容 :
json的數據 , 我們可以根據變量名來大概猜一下這些鍵大概都是什么意思 :
這里為了可讀性 , 將火車的信息刪除到只剩一個
下面的注釋都是根據變量名猜的 , 不一定真正正確
但是話又說回來 , 我們查詢其實可能只關注其中的一些數據 , 并不需要把所有的鍵都搞清楚
不過還是最好搞清楚 , 有助于提高代碼分析能力~
{
"validateMessagesShowId": "_validatorMessage",
"status": true,
"httpstatus": 200,
"data": [
{
"queryLeftNewDTO": {
"train_no": "24000000G702", # 火車編號
"station_train_code": "G7", # 火車站的火車編號 ?
"start_station_telecode": "VNP", # 始發站火車站的電話代碼 ? 還是遠程代碼 ?
# 這個在 station_names 這個文件中有 , 就是第三個字段
"start_station_name": "北京南", # 始發站火車站名
"end_station_telecode": "AOH",
"end_station_name": "上海虹橋", # 終點站火車站名
"from_station_telecode": "VNP",
"from_station_name": "北京南", # 乘客上車的站的名稱
"to_station_telecode": "AOH",
"to_station_name": "上海虹橋", # 乘客下車的站的名稱
"start_time": "19:00", # 開車時間
"arrive_time": "23:56", # 到達時間
"day_difference": "0", # 是否跨天到達 ?
"train_class_name": "", # 火車的類名 ?
"lishi": "04:56", # lishi ? 歷史 ?
"canWebBuy": "Y", # 我們能不能買 ?
"lishiValue": "296", # 歷史值 ?
"yp_info": "yufrsBuLoo4eUOihUqJNFHJjp09eB27ShkcETr7CgLXSp2qD",
# 余票數據 ? 這里如果遇到中文拼音變量名 , 有一個好的云聯想輸入法會很有幫助 :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", # 首發站省份編號
"start_city_code": "0357", # 首發站城市編號
"end_province_code": "33", # 終點站省份編號
"end_city_code": "0712", # 終點站城市編號
"seat_types": "OM9", # 座位類型
"location_code": "P3",
"from_station_no": "01", # 乘客上車的站的編號(估計是在這個城市的編號 , 因為要考慮到一個城市多個站的情況) ?
"to_station_no": "04", # 乘客下車的站的編號
"control_day": 29,
"sale_time": "1230",
"is_support_card": "1", # 是否支持刷卡 ?
"controlled_train_flag": "0",
"controlled_train_message": "正常車次,不受控",
# 下面的估計就是各種作為的余票數 , 當該列車沒有這樣的座位的時候就是 "--"
# 這里具體哪個是哪個可以通過查詢不同的列車來推測 , 這樣也最快 , 也可以通過閱讀代碼
"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" # 商務座
},
# 秘密的字符串 , 暫時還不知道 , 但是這個對我們查詢余票并沒有影響 , 我們現在已經可以解析余票的數據了
# 這個字段應該是在購票的時候會用到 , 暫時先不分析
"secretStr": "ECM6UXA86obwvu3av7Tmh%2BLNXyfi8vGfR1X%2BkTrDYcvjzpdLjFYUVuYLiLR8ifoxpyot3PM528xO%0A8iTmnvT7yKjUduPszD1BtH8xNzetGrb6aGO%2FEV1HNQ1aXscPoZhFjBwle1jIFS78FxMiD1Ch9yIt%0A84qJZdXFhTYrgeD5DuBN3UAg291pgwrcbo0eMnBlJSFQNcAK%2FlkcrbohuCbeqj8Xn8qjvFssZv5L%0AcgJnsXmLe9ZqHLUoGGLUUW2GpsIi%2BVMehOiIV8XmXnd2cvcEperf4neI2tkFk0gn%2BsDpkhGvNjSK%0AaG0OW98ByPk%3D",
"buttonTextInfo": "預訂"
}
]
}
總結和預告 :
好了 , 分析到這里已經差不多了 , 下節課我們來寫代碼進行真正的查詢 , 謝謝大家的支持~
特別希望能和大家交流思路 , 希望共同進步 , 有什么問題就隨便提哦