1.安装docopt、urllib、requests
2.实现程序基础框架
# -*- coding:utf-8 -*-
"""
Train tickets query program.
Usage:
crawl12306 [-dgktz] <from> <to> <date>
Options:
-h --help Show this screen
-d 动车
-g 高铁
-k 快速
-t 特快
-z 直达
"""
from docopt import docopt
def crawler():
arguments = docopt(__doc__,version = "v1.0")
print(arguments) #可以测试出是否能从终端获取参数,返回字典
from_station = arguments.get('<from>') #从字典查值
to_station = arguments.get('<to>')
date = arguments.get('<date>')
url = '' #这个时候应该会发现url不知道怎么写
if __name__ == '__main__':
crawler()
3.测试一下当前效果,尤其是docopt的效果
4.找出request中url的构造规则
容易发现,12306余票查询的页面使用了Ajax,可以找到其header中的request url
可以发现,这个url的基础是'https://kyfw.12306.cn/otn/leftTicket/query?',后面的一系列都是参数,包括时间、始发站、终点站、成人等。而车站的名称都是字母码表示的。要想构造完整的url,必须知道站名和字母码的关系表。
5.找出站名-字母码关系表
找到12306出发站与目标站的地名对应文件,鼠标右键->copy link address,可以看到目前使用的文件外链是:https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9061,点进去可看到json数据
6.实现站名到字母码的转换函数
import re
import requests
def parse_stations():
url = 'https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9061'
response = requests.get(url,verify=False)
# print(response.text) #测试用
stations = re.findall(u'([\u4e00-\u9fa5]+)\|([A-Z]+)', response.text) #\u4e00-\u9fa5是Unicode汉字编码范围
# print(type(stations)) #测试用
# print(stations) #测试用
stationsDict = dict(stations)
# print(stationsDict) #测试用
# print(type(dict(stations))) #测试用
return stationsDict
可以测试一下,测试的时候取消“#”注释:
7.完善②中的url构造
from docopt import docopt
from urllib.parse import urlencode
def generate_url(staionDict):
arguments = docopt(__doc__,version = "v1.0") #可以测试出是否能从终端获取参数,返回字典
print(arguments)
from_station = stationsDict.get(arguments.get('<from>'), None)
to_station = stationsDict.get(arguments.get('<to>'), None)
date = arguments.get('<date>')
standard_date = date[0:4]+'-'+date[4:6]+'-'+date[6:]
print(standard_date)
url = 'https://kyfw.12306.cn/otn/leftTicket/query?'+\
urlencode({'leftTicketDTO.train_date': standard_date,
'leftTicketDTO.from_station': from_station,
'leftTicketDTO.to_station': to_station,
'purpose_codes': 'ADULT'})
print(url)
return url
8.完善一下逻辑,测试一下:
# -*- coding:utf-8 -*-
"""
This is a train tickets query program.
#必须有空行,不然参数不识别的好吗?这个bug真心难找……
Usage:
crawl12306 [-dgktz] <from> <to> <date>
Options:
-h --help Show this screen
-d 动车
-g 高铁
-k 快速
-t 特快
-z 直达
Examples:
crawl12306 北京 上海 20180808
crawl12306 -dg 成都 广州 20180808
"""
from docopt import docopt
from urllib.parse import urlencode
def generate_url(staionDict):
arguments = docopt(__doc__,version = "v1.0") #可以测试出是否能从终端获取参数,返回字典
print(arguments)
from_station = stationsDict.get(arguments.get('<from>'), None)
to_station = stationsDict.get(arguments.get('<to>'), None)
date = arguments.get('<date>')
standard_date = date[0:4]+'-'+date[4:6]+'-'+date[6:]
print(standard_date)
url = 'https://kyfw.12306.cn/otn/leftTicket/query?'+\
urlencode({'leftTicketDTO.train_date': standard_date,
'leftTicketDTO.from_station': from_station,
'leftTicketDTO.to_station': to_station,
'purpose_codes': 'ADULT'})
print(url)
return url
def crawler(url):
pass
import re
import requests
def parse_stations():
url = 'https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9061'
response = requests.get(url,verify=False)
# print(response.text)
stations = re.findall(u'([\u4e00-\u9fa5]+)\|([A-Z]+)', response.text) #\u4e00-\u9fa5是Unicode汉字编码范围
# print(type(stations))
# print(stations)
stationsDict = dict(stations)
# print(stationsDict)
# print(type(dict(stations)))
return stationsDict
if __name__ == '__main__':
stationsDict = parse_stations()
url = generate_url(stationsDict)
crawler(url)
在这里,特地打印出了构造的url,对比④中的request url,可以认为是合理的。
9.完成车次信息爬取
有了正确的request url,可以看一下返回的数据的构造
可以修改crawler函数如下:
def crawler(url):
response = requests.get(url,verify= False)
trians_info = response.json()['data']['result'] #转化为json,再通过索引查找所需信息部分
print(trians_info)
可以发现打印出好长一串字符(如果日期不是设置在今天且没有车次了的话:)),这些字符串应该是可以用json解析的,不然12306的界面显示肯定会混乱不堪,对吧?初步怀疑是两个json中的一个,先打开第一个。注意图中标记的{}可以prettyprint此json,不然多难看不是?
美观多了,对吧?
鼠标放在json文本窗口中,ctrl+f打开搜索功能,搜索t-list,这样就能减少阅读工作量,至于为什么要搜t-list,也算是一种基于经验的判断:
搜索发现疑似目标
复制出一段来,以备修改:
var cq = ct[cr].split("|");
cw.secretHBStr = cq[36];
cw.secretStr = cq[0];
cw.buttonTextInfo = cq[1];
var cu = [];
cu.train_no = cq[2];
cu.station_train_code = cq[3];
cu.start_station_telecode = cq[4];
cu.end_station_telecode = cq[5];
cu.from_station_telecode = cq[6];
cu.to_station_telecode = cq[7];
cu.start_time = cq[8];
cu.arrive_time = cq[9];
cu.lishi = cq[10];
cu.canWebBuy = cq[11];
cu.yp_info = cq[12];
cu.start_train_date = cq[13];
cu.train_seat_feature = cq[14];
cu.location_code = cq[15];
cu.from_station_no = cq[16];
cu.to_station_no = cq[17];
cu.is_support_card = cq[18];
cu.controlled_train_flag = cq[19];
cu.gg_num = cq[20] ? cq[20] : "--";
cu.gr_num = cq[21] ? cq[21] : "--";
cu.qt_num = cq[22] ? cq[22] : "--";
cu.rw_num = cq[23] ? cq[23] : "--";
cu.rz_num = cq[24] ? cq[24] : "--";
cu.tz_num = cq[25] ? cq[25] : "--";
cu.wz_num = cq[26] ? cq[26] : "--";
cu.yb_num = cq[27] ? cq[27] : "--";
cu.yw_num = cq[28] ? cq[28] : "--";
cu.yz_num = cq[29] ? cq[29] : "--";
cu.ze_num = cq[30] ? cq[30] : "--";
cu.zy_num = cq[31] ? cq[31] : "--";
cu.swz_num = cq[32] ? cq[32] : "--";
cu.srrb_num = cq[33] ? cq[33] : "--";
cu.yp_ex = cq[34];
cu.seat_types = cq[35];
cu.exchange_train_flag = cq[36];
通过文本替换,修改为符合python语法的格式,也可猜到部分标记的意思:
data_list = i.split("|")
train_no = data_list[2]
station_train_code = data_list[3]
start_station_telecode = data_list[4] #始发站代码
end_station_telecode = data_list[5] #终点站代码
from_station_telecode = data_list[6] #出发站代码
to_station_telecode = data_list[7] #到达站代码
start_time = data_list[8] #出发时间
arrive_time = data_list[9] #到达时间
lishi = data_list[10] #历时
canWebBuy = data_list[11]
yp_info = data_list[12]
start_train_date = data_list[13]
train_seat_feature = data_list[14]
location_code = data_list[15]
from_station_no = data_list[16]
to_station_no = data_list[17]
is_support_card = data_list[18]
controlled_train_flag = data_list[19]
gg_num = data_list[20] or "--"
gr_num = data_list[21] or "--" #高级软卧
qt_num = data_list[22] or "--" #其他
rw_num = data_list[23] or "--" #软卧
rz_num = data_list[24] or "--" #软座
tz_num = data_list[25] or "--"
wz_num = data_list[26] or "--" #无座
yb_num = data_list[27] or "--"
yw_num = data_list[28] or "--" #硬卧
yz_num = data_list[29] or "--" #硬座
ze_num = data_list[30] or "--" #二等座
zy_num = data_list[31] or "--" #一等座
swz_num = data_list[32] or "--" #商务座/特等座
srrb_num = data_list[33] or "--" #动卧
yp_ex = data_list[34]
seat_types = data_list[35]
exchange_train_flag = data_list[36]
但是,这里面有不少信息是我们的程序功能用不上的,可以筛选一下(不知道对应的是啥可以打印出来看看)。同时,由于需要用车站字母码反查车站名称,可以构造一个新的dict。
new_dict = {v: k for k, v in stationsDict.items()}
至此完整的crawler函数如下:
def crawler(url):
response = requests.get(url)
# print(response.text) #测试用
trians_info = response.json()['data']['result'] #先将网页的response转化为json,再通过索引查找所需的车次信息部分
# print(trians_info) #测试用
for i in trians_info:
data_list = i.split("|")
train_no = data_list[2] #列车编号,如2400000G710Q
station_train_code = data_list[3] #列车车次号,如G71
from_station_telecode = data_list[6] #出发站代码
to_station_telecode = data_list[7] #到达站代码
start_time = data_list[8] #出发时间
arrive_time = data_list[9] #到达时间
lishi = data_list[10] #历时
gr_num = data_list[21] or "--" #高级软卧
rw_num = data_list[23] or "--" #软卧
rz_num = data_list[24] or "--" #软座
wz_num = data_list[26] or "--" #无座
yw_num = data_list[28] or "--" #硬卧
yz_num = data_list[29] or "--" #硬座
ze_num = data_list[30] or "--" #二等座
zy_num = data_list[31] or "--" #一等座
swz_num = data_list[32] or "--" #商务座
data = [ #上述信息有些放着好看,我们需要实现的功能用不上
station_train_code,
new_dict.get(from_station_telecode), #通过出发车站字母码反查出发车站名称,查不到默认返回None
new_dict.get(to_station_telecode),
start_time,
arrive_time,
lishi,
zy_num,
ze_num,
rw_num,
yw_num,
yz_num
]
yield data
11.安装prettytable
12.完成信息打印函数
from prettytable import PrettyTable
def prettyPrint(datagenerator):
pt = PrettyTable()
pt._set_field_names('车次 出发站 到达站 出发时间 到达时间 历时 一等座 二等座 软卧 硬卧 硬座'.split())
for i in datagenerator: #注意到i是list类型
pt.add_row(i)
print(pt)
再完善一下程序逻辑:
if __name__ == '__main__':
stationsDict = parse_stations()
url = generate_url(stationsDict)
new_dict = {v: k for k, v in stationsDict.items()}
datagenerator = crawler(url)
prettyPrint(datagenerator)
测试一下。小功告成!
14.解决一个问题
目前仍存在的问题是选项options还没有发挥作用。解决办法如下:
在generate_url()函数定义中,加入:
options = [k for k,v in arguments.items() if v == True] #获取options,构造成一个list
return url,options。
传入crawler()函数,函数定义中,在station_train_code = data_list[3]后加入判断:
if not options or '-'+station_train_code[0].lower() in options:
#如果没有选项,默认全部查找,或者将车次如G71的G取出来,转化为g,构造成-g查看是否包含在选项中,是则执行车次信息获取
再完善一下程序逻辑:
if __name__ == '__main__':
stationsDict = parse_stations()
url,options = generate_url(stationsDict)
new_dict = {v: k for k, v in stationsDict.items()}
datagenerator = crawler(url,options)
prettyPrint(datagenerator)
执行一下:
15.打印界面美化
可使用prettyprint的功能使界面分行显示,效果如下,还可以安装使用colorama模块进行着色(此功能懒得实现了):
总结:
本程序实现了基于python3的12306火车票余票查询工具,主体思想参考的是实验楼公开课程,只是模块化更加明显,且使用了美妙的yield。
只是,车站-字母码可以生成一次便保存省得每次都重新生成,更进一步地,嫌找json解析数据太过麻烦不如使用scrapy对接spash/selenium实现所见即所得。
还可以使用python内建的input()函数替代docopy实现更适合新手的简化版,或者可以从面向类编程的角度实现进阶版。