在上一篇文章中,我们实现了12306爬虫的登录功能,接下来,我们就来实现查票的功能.
其实实现查票的功能很简单,简单概括一下我们在浏览器中完成查票时的主要步骤:
1.从哪一站出发
2.终点站是哪里
3.然后选定乘车日期
既然我们已经知道是这个步骤了,那我们应该怎样通过程序的形式来实现这个步骤呢?
最主要的问题:
1.在程序中我们如何获取站点.不妨想一下,选择的站点是全都保存到一个文件中,还是分开的?
2.乘车日期是不是不能小于当前系统时间而且也不能大于铁路局规定的预售期(一般是30天左右)
好了,到目前为止,我们主要的问题是如何解决上面两个问题!
首先我们要明白一点:车票信息是通过异步加载的方式得到的
我们先看一下查票的URL:
出发日期:2018-02-22, 出发地:深圳,目的地:北京
https://kyfw.12306.cn/otn/leftTicket/queryZ?
train_date=2018-02-22&
from_station=SZQ&
to_station=BJP&
purpose_codes=ADULT
我们重点关注2个字段:
1.from_station=SZQ
2.to_station=BJP
问题来了:我们明明选择了出发地是:深圳,目的地:北京,那么在from_station中为什么是SZQ,to_station中是BJP?
from_station和to_station的值好像不是深圳和北京被加密后的值,而是和他们的汉语拼音首字母有点联系
那我们做一个大胆的猜测:12306网站那边应该是把每个站点都与一个唯一的站点代码建立起了关联!
通过以上分析,我们就有更加明确的目标去进行抓包(抓包这次使用Chrome中的工具)!
我们填好所有必要信息时,点击查询按钮,得到的结果如下:
在所有结果中我们只看到了3条信息,最主要的还是第一条,我们看看里面的结果是什么
很明显我们得到从深圳到北京的所有车次信息了!
其他两个结果都是图片,不可能是站点啊,找不到站点信息,这可怎么办?┓( ´-` )┏
那我们点击刷新按钮来看看会出现什么结果
这次好像有好多东西出来了,那我们运气会不会好一点,能找到一些站点信息呢?
哦,好像我们发现了什么东西!!!!!!
在station_name.js中我们看到了熟悉的字段:BJP,那就让我们的这里面探索探索吧!!!
那么目前为止我们的工作就只剩下代码的事情了
我们只要两个请求就好了:
1.用GET请求把station_name.js中的数据全都获取到,并保存到文件中,我们需要用到,而且最好是以字典的格式保存
2.同样用GET请求去获取查票的URL,看看有出发地到目的地有哪些车次信息.
项目结构:
完整的代码如下:
1 from login import Login
2 import os
3 import json
4 import time
5 from collections import deque, OrderedDict
6
7 class Station:
8 """ 查询车票信息 """
9
10 def __init__(self):
11 # 使用登录时候的session,这样好一些!
12 self.session = Login.session
13 self.headers = Login.headers
14
15
16 def station_name_code(self):
17 """
18 功能:获取每个站点的名字和对应的代码,并保存到本地
19 :return: 无
20 """
21 filename = 'station_name.txt'
22
23 url = 'https://kyfw.12306.cn/otn/resources/js/framework/station_name.js'
24 resp = self.session.get(url, headers=self.headers)
25 if resp.status_code == 200:
26 print('station_name_code():获取站点信息成功!')
27 with open(filename, 'w') as f:
28 for each in resp.text.split('=')[1].split('@'):
29 if each != "'":
30 f.write(each)
31 f.write('\n')
32 else:
33 print('station_name_code() error! status_code:{}, url: {}'
34 .format(resp.status_code, resp.url))
35
36 def save_station_code(self, filename):
37 """
38 功能:从站点文件中提取站点与其对应的代码,并保存到文件中
39 :return:
40 """
41
42 if not os.path.exists(filename):
43 print('save_station_code():',filename,'不存在,正在下载!')
44 self.station_name_code()
45
46 file = 'name_code.json'
47 name_code_dict = {}
48 with open(filename, 'r') as f:
49 for line in f:
50 # 对读取的行都进行split操作,然后提取站点名和其代码
51 name = line.split('|')[1] # 站点名字
52 code = line.split('|')[2] # 每个站点对应的代码
53 # 每个站点肯定都是唯一的
54 name_code_dict[name] = code
55
56 # 把name,code保存到本地文件中,方便以后使用
57 with open(file, 'w') as f:
58 # 不以ascii码编码的方式保存
59 json.dump(name_code_dict, f, ensure_ascii=False)
60
61
62 def query_ticket(self):
63 """
64 功能:查票操作
65 :return: 返回查询到的所有车次信息
66 """
67
68 data = self._query_prompt()
69 if not data:
70 print('query_ticket() error: {}'.format(data))
71 _, from_station, to_station = data.keys()
72 train_date = data.get('train_date')
73 from_station_code = data.get(from_station)
74 to_station_code = data.get(to_station)
75
76 query_param = 'leftTicketDTO.train_date={}&' \
77 'leftTicketDTO.from_station={}&' \
78 'leftTicketDTO.to_station={}&' \
79 'purpose_codes=ADULT'\
80 .format(train_date, from_station_code, to_station_code)
81
82 url = 'https://kyfw.12306.cn/otn/leftTicket/queryZ?'
83
84 full_url = url + query_param
85 resp = self.session.get(full_url, headers=self.headers)
86 if resp.status_code == 200 and resp.url == full_url:
87 print('query_ticket() 成功!然后进行车票清理工作!')
88 self._get_train_info(resp.json(), from_station, to_station)
89
90 else:
91 print('query_ticket() error! status_code:{}, url:{}\norigin_url:{}'
92 .format(resp.status_code, resp.url, full_url))
93
94 def _get_train_info(self, text, from_station, to_station):
95 """
96 功能:提取出查询到的列车信息
97 :param text: 包含所有从起点站到终点站的车次信息
98 :return: 返回所有车次信息
99 """
100 if not text:
101 print('_query_train_info() error: text为:', text)
102 # 把json文件转变成字典形式
103 result = dict(text)
104 # 判断有无车次的标志
105 if result.get('data').get('map'):
106 train_info = result.get('data').get('result')
107 train_list = deque()
108 for item in train_info:
109 split_item = item.split('|')
110 item_dict= {}
111 for index, item in enumerate(split_item,0):
112 print('{}:\t{}'.format(index, item))
113 if split_item[11] == 'Y': # 已经开始卖票了
114 item_dict['train_name'] = split_item[3] # 车次名
115 item_dict['depart_time'] = split_item[8] # 出发时间
116 item_dict['arrive_time'] = split_item[9] # 到站时间
117 item_dict['spend_time'] = split_item[10] # 经历时长
118 item_dict['wz'] = split_item[29] # 无座
119 item_dict['yz'] = split_item[28] # 硬座
120 item_dict['yw'] = split_item[26] # 硬卧
121 item_dict['rw'] = split_item[23] # 软卧
122 item_dict['td'] = split_item[32] # 特等座
123 item_dict['yd'] = split_item[31] # 一等座
124 item_dict['ed'] = split_item[30] # 二等座
125 item_dict['dw'] = split_item[33] # 动卧
126 train_list.append(item_dict)
127 # 无法买票的车次,有可能是已卖光,也有可能是还不开卖
128 elif split_item[0] == '':
129 print('_query_train_info():车次{}的票暂时不能购买!'
130 .format(split_item[3]))
131 else:
132 print('_query_train_info():车次{}还未开始卖票,起售时间为:{}'
133 .format(split_item[3], split_item[1]))
134 # 调用方法来打印列车结果
135 self._print_train(train_list, from_station, to_station)
136 else:
137 print('_get_train_info() error: 从{}站到{}站有没列车!'
138 .format(from_station, to_station))
139
140 def _print_train(self, train_info, from_station, to_station):
141 """
142 功能:打印查询到的车次信息
143 :param train_info: 提取出来的车次信息
144 :return:
145 """
146
147 if not train_info:
148 print('_print_train() error: train_info是None!')
149 return
150
151 print('从{}到{}还有余票的列车有:'.format(from_station, to_station))
152 for item in train_info:
153 if 'G' in item['train_name']: # 高铁
154 self._print_high_train_info(item)
155 elif 'D' in item['train_name']: # 动车
156 self._print_dong_train_info(item)
157 else:
158 self._print_train_info(item)
159
160 def _print_high_train_info(self, item):
161 """
162 功能:打印高铁车次信息
163 :param item: 所有高铁车次
164 :return:
165 """
166 print('车次:{:4s}\t起始时间:{:4s}\t到站时间:{:4s}\t'
167 '经历时长:{:4s}\t特等座:{:4s}\t一等座:{:4s}\t二等座:{:4s}'
168 .format(item['train_name'], item['depart_time'],item['arrive_time'],
169 item['spend_time'],item['td'], item['yd'], item['ed']))
170
171 def _print_dong_train_info(self, item):
172 """
173 功能:打印动车的车票信息
174 :param item: 所有动车车次
175 :return:
176 """
177 print('车次:{:4s}\t起始时间:{:4s}\t到站时间:{:4s}\t'
178 '经历时长:{:4s}\t一等座:{:4s}\t二等座:{:4s}\t软卧:{:4s}\t动卧:{:4s}'
179 .format(item['train_name'], item['depart_time'], item['arrive_time'],
180 item['spend_time'],item['yd'],item['ed'], item['rw'], item['dw']))
181 def _print_train_info(self,item):
182 """
183 功能:打印普通列出的车次信息
184 :param item: 所有普通车次
185 :return:
186 """
187 print('车次:{:4s}\t起始时间:{:4s}\t到站时间:{:4s}\t经历时长:{:4s}\t'
188 '软卧:{:4s}\t硬卧:{:4s}\t硬座:{:4s}\t无座:{:4s}'
189 .format(item['train_name'], item['depart_time'], item['arrive_time'],
190 item['spend_time'],item['rw'], item['yw'], item['yz'], item['wz']))
191 def _query_prompt(self):
192 """
193 功能: 与用户交互,让用户输入:出发日期,起始站和终点站并判断其正确性
194 :return: 返回正确的日期,起始站和终点站
195 """
196
197 time_flag, train_date = self._check_date()
198 if not time_flag:
199 print('_query_prompt() error:', '乘车日期不合理,请检查!!')
200 return
201 # 创建有序字典,方便取值
202 query_data = OrderedDict()
203 from_station = input('请输入起始站:')
204 to_station = input('请输入终点站:')
205
206 station_flag = True
207 filename = 'name_code.json'
208 with open(filename, 'r') as f:
209 data = dict(json.load(f))
210 stations = data.keys()
211 if from_station not in stations or to_station not in stations:
212 station_flag = False
213 print('query_prompt() error: {}或{}不在站点列表中!!'
214 .format(from_station, to_station))
215 # 获取起始站和终点站的代码
216 from_station_code = data.get(from_station)
217 to_station_code = data.get(to_station)
218 query_data['train_date'] = train_date
219 query_data[from_station] = from_station_code
220 query_data[to_station] = to_station_code
221
222 if time_flag and station_flag:
223 return query_data
224 else:
225 print('query_prompt() error! time_flag:{}, station_flag:{}'
226 .format(time_flag, station_flag))
227
228
229
230 def _check_date(self):
231 """
232 功能:检测乘车日期的正确性
233 :return: 返回时间是否为标准的形式的标志
234 """
235
236 # 获取当前时间的时间戳
237 local_time = time.localtime()
238 local_date = '{}-{}-{}'.\
239 format(local_time.tm_year, local_time.tm_mon, local_time.tm_mday)
240 curr_time_array = time.strptime(local_date, '%Y-%m-%d')
241 curr_time_stamp = time.mktime(curr_time_array)
242 # 获取当前时间
243 curr_time = time.strftime('%Y-%m-%d', time.localtime(curr_time_stamp))
244
245 # 计算出预售时长的时间戳
246 delta_time_stamp = '2505600'
247 # 算出预售票的截止日期时间戳
248 dead_time_stamp = int(curr_time_stamp) + int(delta_time_stamp)
249 dead_time = time.strftime('%Y-%m-%d', time.localtime(dead_time_stamp))
250 print('合理的乘车日期范围是:({})~({})'.format(curr_time, dead_time))
251
252 train_date = input('请输入乘坐日期(year-month-day):')
253 # 把乘车日期转换成时间戳来比较
254 # 先生成一个时间数组
255 time_array = time.strptime(train_date, '%Y-%m-%d')
256 # 把时间数组转化成时间戳
257 train_date_stamp = time.mktime(time_array)
258 # 获取标准的乘车日期
259 train_date_time = time.strftime('%Y-%m-%d', time.localtime(train_date_stamp))
260 # 做上面几步主要是把用户输入的时间格式转变成标准的格式
261 # 如用户输入:2018-2-22,那么形成的查票URL就不是正确的
262 # 只有是: 2018-02-22,组合的URL才是正确的!
263 # 通过时间戳来比较时间的正确性
264 if int(train_date_stamp) >= int(curr_time_stamp) and \
265 int(train_date_stamp) <= dead_time_stamp:
266 return True, train_date_time
267 else:
268 print('_check_date() error: 乘车日期:{}, 当前系统时间:{}, 预售时长为:{}'
269 .format(train_date_time, curr_time, dead_time))
270 return False, None
271
272
273
274 def main():
275 filename = 'station_name.txt'
276 station = Station()
277 station.station_name_code()
278 station.save_station_code(filename)
279 station.query_ticket()
280
281 if __name__ == '__main__':
282 main()
小结:在查票功能中,其实没有太多复杂的东西,不想前面登录时需要发送多个请求,在这个功能中只要发送两个请求就可以了,主要复杂的地方在于对数据的清理工作!