利用Python實現12306爬蟲--查票


在上一篇文章(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()

 

小結:在查票功能中,其實沒有太多復雜的東西,不想前面登錄時需要發送多個請求,在這個功能中只要發送兩個請求就可以了,主要復雜的地方在於對數據的清理工作!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM