在上一篇文章(http://www.cnblogs.com/fangtaoa/p/8321449.html)中,我們實現了12306爬蟲的登錄功能,接下來,我們就來實現查票的功能.
其實實現查票的功能很簡單,簡單概括一下我們在瀏覽器中完成查票時的主要步驟:
1.從哪一站出發
2.終點站是哪里
3.然后選定乘車日期
既然我們已經知道是這個步驟了,那我們應該怎樣通過程序的形式來實現這個步驟呢?
最主要的問題:
1.在程序中我們如何獲取站點.不妨想一下,選擇的站點是全都保存到一個文件中,還是分開的?
2.乘車日期是不是不能小於當前系統時間而且也不能大於鐵路局規定的預售期(一般是30天左右)
好了,到目前為止,我們主要的問題是如何解決上面兩個問題!
首先我們要明白一點:車票信息是通過異步加載的方式得到的
我們先看一下查票的URL:
出發日期:2018-02-22, 出發地:深圳,目的地:北京
https://kyfw.12306.cn/otn/leftTicket/queryZ?
leftTicketDTO.train_date=2018-02-22&
leftTicketDTO.from_station=SZQ&
leftTicketDTO.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()
小結:在查票功能中,其實沒有太多復雜的東西,不想前面登錄時需要發送多個請求,在這個功能中只要發送兩個請求就可以了,主要復雜的地方在於對數據的清理工作!