前言:
學了挺近的python了,一直在初級徘徊不前,想着應該找點實戰性的案例來操練一下,以便熟悉各模塊的使用;在網上找到了一些有關通過爬蟲實現火車票查詢的,就拿來參考練練手了。
最終想要的實現效果就是用戶通過在命令行輸入相關的命令,然后將查詢到的車次信息打印輸出到屏幕上。命令格式:tickets [-gdtkz] <from> <to> <date> ;並且用戶可以通過輸入[-gdtkz]參數去篩選想要查找的車次類型,默認不添加參數時候輸出全部車次。此次用到的模塊有docopt、prettytable、re、urllib3、requests,其中:
docopt 模塊:是在 python 中引入了一種針對命令行參數的形式語言模塊,在代碼的最開頭使用 """ """ 文檔注釋的形式寫出符合要求的文檔,就會自動生成對應的 parse。
prettytable模塊:是 python 中的一個第三方庫,可用來生成美觀的 ASCII 格式的表格,這里主要是用來將爬取到的車次信息按照 ASCII 格式打印到屏幕。
re模塊:是python的標准庫中表示正則表達式的模塊,用來對爬取到的車次數據進行篩選匹配,得到我們最終想要的數據。
requests模塊:是用 python 語言編寫的基於 urllib 采用 Apache2 Licensed 開源協議的 HTTP 庫,主要就是用它來獲取12306網站車次信息。
urllib3模塊:詳解請參考 https://www.cnblogs.com/lincappu/p/12801817.html,這里是因為 requests 模塊在訪問 HTTPS 網站設置移除SSL認證參數 “verify=False” 后,會提示 “InsecureRequestWarning” 警告,在請求代碼前加入 “requests.packages.urllib3.disable_warnings()” 就可以過濾警告。
效果截圖:
下面就來說一下實現的步驟:
打開12306網站查詢北京到上海的火車票,並且開啟瀏覽器開發者工具界面,然后找到“Network-XHR”選項,選中左下方框中的鏈接,其中右邊“Headers”框下方中“Request URL”顯示的鏈接就是我們要找的12306火車票查詢URL。
將其復制出來分析發現,我們只需要修改train_date、from_station和to_station這三個固定參數的值就可以查詢到我們想要的列車信息了,其中train_date是列車的日期,from_station和to_station分別是首發站和終點站,但是from_station和to_station的值卻不是我們常見的中文車站名,分析對比后可以確定它是中文車站的英文編號。因此,我們需要先找到全部站點的英文編號數據。
經過查找12306頁面發現“station_name.js?station_version=1.9163”行對應的“Response”數據應該是我們需要的數據。
那么我們就把“Headers”的“Request URL”鏈接地址復制出來貼到瀏覽器上去查看一下,看看是不是我們想要的數據。“https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9163”
查看了上面的數據,的確是我們想要的數據,並且這些數據是有一定的規律的,都是通過“|”分隔,這樣我們在用正則去匹配想要的數據時候就比較容易了。好了,既然想要的數據都已經拿到了,那么我們就開始編寫代碼把我們想要的數據提取出來,下面我直接把代碼和執行結果貼出來吧。
1 #!/usr/bin/env python3 2 # -*- coding: utf-8 -*- 3 4 import re 5 import urllib3, requests # python 訪問 HTTP 資源的必備庫 6 from pprint import pprint # 打印出任何python數據結構類和方法的模塊 7 8 9 url = "https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9163" 10 requests.packages.urllib3.disable_warnings() # requests模塊在訪問HTTPS網站時,如果設置移除SSL認證參數“verify=False”,執行代碼是會提示“InsecureRequestWarning”警告,再請求頁面時加入此段代碼可以屏蔽掉警告信息 11 r = requests.get(url, verify=False) # 請求12306網站的所有城市的拼音和代號網頁,verify=False參數表示不驗證證書 12 # result = re.findall(r'([A-Z]+)\|([a-z]+)', r.text) # 通過正則表達式來匹配車站中文拼音和英文編號對應的數據 13 result = re.findall(r"([\u4e00-\u9fa5]+)\|([A-Z]+)", r.text) # 通過正則表達式來匹配車站中文名和英文編號對應的數據 14 stations = dict(result) # 將獲取的數據轉成字典 15 # print(stations["上海虹橋"]) # 驗證用 16 """ 17 請將下面輸出的結果保存到stations.py中,並在文件開頭添加一行:# coding=gbk 18 否則在調用stations.py文件時,會提示報錯。 19 """ 20 print(stations.keys()) 21 print(stations.values())
執行結果如下:
隨后將輸出的數據保存到另一個文件(stations.py)中,在文件開頭加上一句“# coding=gbk”,並在文件中定義兩函數進行中文名字和英文編碼的對應獲取,如下:
車站中文名和英文編碼已經拿到了,接下來就可以開始爬取12306網頁的車次數據了,首先我們設計一下用戶調用的接口方式。按照前面所說的我們希望用戶只要輸入出發站、終點站和出發日期就能獲得想要的列車信息,例如要查看2020年11月6日的火車票信息,只需輸入如下:
$ tickets 北京 廣州 2020-11-06
對其進行抽象可以得到接口如下:
$ tickets <from> <to> <date>
另外,我們在12306頁面查詢火車票時候可以對車次類型進行篩選,例如選擇高鐵就只顯示當天高鐵的車次信息,同時選擇高鐵和動車就顯示高鐵和動車的車次信息,那么我們就要提供一個選項來查詢特定的一種或者幾種類型的火車,所有我們應該有下面這些選項:
- -g 高鐵
- -d 動車
- -t 特快
- -k 快速
- -z 直達
將這些選項和上面的接口組合起來,最終的接口的樣子應該是這樣:
$ tickets [-gdtkz] <from> <to> <date>
下面我們直接貼出實現的代碼:
1 #!/usr/bin/env python3 2 # -*- coding: utf-8 -*- 3 4 #!/usr/bin/env python3 5 # -*- coding: utf-8 -*- 6 7 """Train tickets query via command-line. 8 9 Usage: 10 tickets [-gdtkz] <from> <to> <date> 11 12 Options: 13 -h,--help 顯示幫助信息菜單 14 -g 高鐵 15 -d 動車 16 -t 特快 17 -k 快速 18 -z 直達 19 20 Example: 21 tickets beijing shanghai 2020-11-05 22 """ 23 24 from docopt import docopt 25 # docopt 模塊是 python3 命令行參數解析工具 26 # docopt 模塊本質上是在 Python 中引入了一種針對命令行參數的形式語言,在代碼的最開頭使用 """ """文檔注釋的形式寫出符合要求的文檔,就會自動生成對應的 parse 27 # 所有出現在 Usage:(區分大小寫)和一個空行之間的文本都會被識別為一個命令組合, Usage 后的第一個字母將會被識別為這個程序的名字,所有命令組合的每一個部分(空格分隔)都會成為字典中的一個key 28 29 30 def cli(): 31 """command-line interface""" 32 arguments = docopt(__doc__) 33 print(arguments) 34 35 if __name__ == "__main__": 36 cli()
通過命令行方式運行上面代碼,得到結果如下:
$ python tickets.py 北京 廣州 2020-11-06
$ python tickets.py -g 北京 廣州 2020-11-06
接口已經實現了,接下來就是要獲取12306頁面的車次數據了,根據前面分析的只需要修改“https://kyfw.12306.cn/otn/leftTicket/query?leftTicketDTO.train_date=2020-11-06&leftTicketDTO.from_station=BJP&leftTicketDTO.to_station=SHH&purpose_codes=ADULT”鏈接中train_date、from_station和to_station參數的值就可以得到想要查詢的火車票信息。其中from_station和to_station參數的值是英文編號,需要根據用戶輸入的中文車站名去stations.py文件中找到對應的英文編號進行替換,因此需要import stations,然后通過requests模塊去抓取車次數據。實現代碼如下:
1 #!/usr/bin/env python3 2 # -*- coding: utf-8 -*- 3 4 #!/usr/bin/env python3 5 # -*- coding: utf-8 -*- 6 7 """Train tickets query via command-line. 8 9 Usage: 10 tickets [-gdtkz] <from> <to> <date> 11 12 Options: 13 -h,--help 顯示幫助信息菜單 14 -g 高鐵 15 -d 動車 16 -t 特快 17 -k 快速 18 -z 直達 19 20 Example: 21 tickets beijing shanghai 2020-11-05 22 """ 23 24 from docopt import docopt 25 # docopt 模塊是 python3 命令行參數解析工具 26 # docopt 模塊本質上是在 Python 中引入了一種針對命令行參數的形式語言,在代碼的最開頭使用 """ """文檔注釋的形式寫出符合要求的文檔,就會自動生成對應的 parse 27 # 所有出現在 Usage:(區分大小寫)和一個空行之間的文本都會被識別為一個命令組合, Usage 后的第一個字母將會被識別為這個程序的名字,所有命令組合的每一個部分(空格分隔)都會成為字典中的一個key 28 import re # 正則表達式模塊 29 import stations 30 import urllib3, requests # python 訪問 HTTP 資源的必備庫 31 32 def cli(): 33 """command-line interface""" 34 arguments = docopt(__doc__) 35 # print(arguments) 36 from_stion = stations.get_telecode(arguments["<from>"]) # 調用 get_telecode() 方法根據用戶輸入的起始車站中文名找到對應的英文編號 37 to_stion = stations.get_telecode(arguments["<to>"]) # 調用 get_telecode() 方法根據用戶輸入的終點車站中文名找到對應的英文編號 38 date = arguments["<date>"] # 獲取用戶輸入的日期 39 40 # 構建 URL 41 url = ("https://kyfw.12306.cn/otn/leftTicket/query?" 42 "leftTicketDTO.train_date={}&" 43 "leftTicketDTO.from_station={}&" 44 "leftTicketDTO.to_station={}&" 45 "purpose_codes=ADULT").format(date, from_stion, to_stion) 46 headers = { 47 # Cookie 的值自行替換一下,可以通過打開瀏覽器開發者模式復制過來 48 "Cookie": "_uab_collina=160395250285657341202147; JSESSIONID=7C56E896658518A4E5BF99889839D00C; _jc_save_wfdc_flag=dc; _jc_save_fromStation=%u5317%u4EAC%2CBJP; _jc_save_toStation=%u4E0A%u6D77%2CSHH; BIGipServerotn=1725497610.50210.0000; RAIL_EXPIRATION=1604632917257; RAIL_DEVICEID=DeBrCMshZyD9JIK2yazJV4op0oxRXXKpeio_Y27U75ZkWKFwOd6Q_i2JRVBJeN3Q9qQ7ybyTw4Vv3ImAEwdTAAh8XLXL6WGn3irR65rZyYeWtvToLkq8oVAprmAw6OPgPnqI9a9ItALNr0kFjzDkncjjGPINbqfa; BIGipServerpassport=770179338.50215.0000; route=c5c62a339e7744272a54643b3be5bf64; _jc_save_fromDate=2020-11-02; _jc_save_toDate=2020-11-01", 49 "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36" 50 } 51 requests.packages.urllib3.disable_warnings() # 屏蔽 “InsecureRequestWarning” 警告 52 r = requests.get(url, headers=headers, verify=False) # 通過 requests 模塊獲取頁面信息,verify=False 參數表示不進行證書驗證 53 raw_trains = r.json()['data']['result'] 54 print(raw_trains) 55 56 57 if __name__ == "__main__": 58 cli()
執行結果如下:
根據獲取到的數據進行分析其車次信息中車次代號、始發站、終點站、出發時間、到達時間以及座位類別等應該是有分別對應的字段,再返回12306網站去查找發現“Sources”有相關的數據信息,如下所示:
拿到這些信息之后,就開始和抓取到的車次數據以及12306頁面顯示的數據進行對比(這個過程是比較久的,需要有耐心)。我這邊抓取了很多車次的數據信息進行了對比,其中需要注意的是“商務座”和“特等座”12306頁面上雖然顯示在一起的,但是“Sources”對應的數據字段卻不是一樣的(還有我猜測二等座和二等包座的字段也可能不是一樣的,因為沒有數據去做比較,后面就忽略掉了),下面是我對比出來的結果截圖:
找到了車次信息對應的字段,就開始把數據編排成我們想要的格式吧。這里使用PrettyTable庫來進行信息對齊表格美化(這個庫要注意大小寫),因為考慮到可以根據用戶輸入的參數“-gdtkz”來篩選車次數據,所有我們要通過用戶的輸入和火車類型進行判斷,並定義一個filtrate_train()方法去篩選用戶想查看相關的車次信息,下面是此次實戰的全部代碼:
1 #!/usr/bin/env python3 2 # -*- coding: utf-8 -*- 3 4 """Train tickets query via command-line. 5 6 Usage: 7 tickets [-gdtkz] <from> <to> <date> 8 9 Options: 10 -h,--help 顯示幫助信息菜單 11 -g 高鐵 12 -d 動車 13 -t 特快 14 -k 快速 15 -z 直達 16 17 Example: 18 tickets 北京 上海 2020-10-29 19 """ 20 21 from docopt import docopt 22 # docopt 模塊是 python3 命令行參數解析工具 23 # docopt 模塊本質上是在 Python 中引入了一種針對命令行參數的形式語言,在代碼的最開頭使用 """ """文檔注釋的形式寫出符合要求的文檔,就會自動生成對應的 parse 24 # 所有出現在 Usage:(區分大小寫)和一個空行之間的文本都會被識別為一個命令組合, Usage 后的第一個字母將會被識別為這個程序的名字,所有命令組合的每一個部分(空格分隔)都會成為字典中的一個key 25 from prettytable import PrettyTable 26 import re # 正則表達式模塊 27 import stations 28 import urllib3, requests # python 訪問 HTTP 資源的必備庫 29 30 # 定義一個filtrate_train()函數,用來篩選查詢到列車車次的數據 31 def filtrate_train(pt, data_list): 32 station_train_code = data_list[3] # 車次 33 from_station_code = data_list[6] # 起始站英文代號 34 to_station_code = data_list[7] # 終點站英文代號 35 from_station_name = stations.get_name(from_station_code) # 起始站中文名稱 36 to_station_name = stations.get_name(to_station_code) # 終點站中文名稱 37 start_time = data_list[8] # 出發時間 38 arrive_time = data_list[9] # 到達時間 39 lishi = data_list[10] # 歷時 40 # 通過對比12306代碼和頁面上座位顯示結果分析出“商務座”和“特等座”對應的參數是不同的,cN[25]是特等座,cN[32]是商務座 41 business_seat = data_list[25] or data_list[32] or "--" # 商務座和特等座 42 first_class_seat = data_list[31] or "--" # 一等座 43 second_class_seat = data_list[30] or "--" # 二等座,查看12306頁面時,二等座下方有個“二等包座”,對比代碼應該是cN[27],但是沒有找到有對應數據暫時不寫上去 44 advanced_soft_sleeper = data_list[21] or "--" # 高級軟卧 45 soft_sleeper = data_list[23] or "--" # 軟卧 46 bullet_sleeper = data_list[33] or "--" # 動卧 47 hard_sleeper = data_list[28] or "--" # 硬卧 48 soft_seat = data_list[24] or "--" # 軟座,因為沒有查詢到有軟座的信息,對比了代碼參數,猜測cN[24]應該是軟座 49 hard_seat = data_list[29] or "--" # 硬座 50 not_seat = data_list[26] or "--" # 無座 51 pt.add_row([ 52 station_train_code, # 車次 53 from_station_name, # 起始站中文名稱 54 to_station_name, # 終點站中文名稱 55 start_time, # 出發時間 56 arrive_time, # 到達時間 57 lishi, # 歷時 58 business_seat, # 商務座和特等座 59 first_class_seat, # 一等座 60 second_class_seat, # 二等座 61 advanced_soft_sleeper, # 高級軟卧 62 soft_sleeper, # 軟卧 63 bullet_sleeper, # 動卧 64 hard_sleeper, # 硬卧 65 soft_seat, # 軟座 66 hard_seat, # 硬座 67 not_seat # 無座 68 ]) 69 return pt 70 71 def cli(): 72 """command-line interface""" 73 arguments = docopt(__doc__) 74 from_stion = stations.get_telecode(arguments["<from>"]) 75 to_stion = stations.get_telecode(arguments["<to>"]) 76 date = arguments["<date>"] 77 # print(from_stion, to_stion, date) 78 79 # 構建 URL 80 url = ("https://kyfw.12306.cn/otn/leftTicket/query?" 81 "leftTicketDTO.train_date={}&" 82 "leftTicketDTO.from_station={}&" 83 "leftTicketDTO.to_station={}&" 84 "purpose_codes=ADULT").format(date, from_stion, to_stion) 85 headers = { 86 # Cookie的值可以通過打開瀏覽器的開發者模式復制過來 87 "Cookie": "_uab_collina=160395250285657341202147; JSESSIONID=7C56E896658518A4E5BF99889839D00C; _jc_save_wfdc_flag=dc; _jc_save_fromStation=%u5317%u4EAC%2CBJP; _jc_save_toStation=%u4E0A%u6D77%2CSHH; BIGipServerotn=1725497610.50210.0000; RAIL_EXPIRATION=1604632917257; RAIL_DEVICEID=DeBrCMshZyD9JIK2yazJV4op0oxRXXKpeio_Y27U75ZkWKFwOd6Q_i2JRVBJeN3Q9qQ7ybyTw4Vv3ImAEwdTAAh8XLXL6WGn3irR65rZyYeWtvToLkq8oVAprmAw6OPgPnqI9a9ItALNr0kFjzDkncjjGPINbqfa; BIGipServerpassport=770179338.50215.0000; route=c5c62a339e7744272a54643b3be5bf64; _jc_save_fromDate=2020-11-02; _jc_save_toDate=2020-11-01", 88 "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36" 89 } 90 requests.packages.urllib3.disable_warnings() 91 r = requests.get(url, headers=headers, verify=False) # verify=False參數表示不進行證書驗證 92 raw_trains = r.json()['data']['result'] 93 # print(raw_trains) 94 pt = PrettyTable() 95 pt.field_names = '車次 起始站 終點站 出發時間 到達時間 歷時 商務(特等)座 一等座 二等座 高級軟卧 一等(軟)卧 動卧 二等(硬)卧 軟座 硬座 無座'.split() 96 # print(pt) 97 for raw_train in raw_trains: 98 data_list = raw_train.split("|") 99 if data_list[1] == "預訂": # 因為有停運列車,需判定該車次列車是否可以預約 100 initial = data_list[3][0].lower() # 獲取車次代號,g:高鐵,d:動車,t:特快,k:快速,z:直達 101 if not arguments["-g"] and not arguments["-d"] and not arguments["-t"] and not arguments["-k"] and not arguments["-z"]: 102 filtrate_train(pt, data_list) 103 elif arguments["-g"] and initial == "g": 104 filtrate_train(pt, data_list) 105 elif arguments["-d"] and initial == "d": 106 filtrate_train(pt, data_list) 107 elif arguments["-t"] and initial == "t": 108 filtrate_train(pt, data_list) 109 elif arguments["-k"] and initial == "k": 110 filtrate_train(pt, data_list) 111 elif arguments["-z"] and initial == "z": 112 filtrate_train(pt, data_list) 113 print(pt) 114 115 if __name__ == "__main__": 116 cli()
代碼執行結果截圖:
同時對比12306查詢到的車次信息結果截圖:
最后貼上參考鏈接:https://blog.csdn.net/qq_39380075/article/details/79841339?utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2~all~first_rank_v2~rank_v28-5-79841339.nonecase&utm_term=%E5%88%A9%E7%94%A8python%E5%AE%9E%E7%8E%B012306%E7%88%AC%E8%99%AB&spm=1000.2123.3001.4430