文章地址 :
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": "预订"
}
]
}
总结和预告 :
好了 , 分析到这里已经差不多了 , 下节课我们来写代码进行真正的查询 , 谢谢大家的支持~
特别希望能和大家交流思路 , 希望共同进步 , 有什么问题就随便提哦