前幾天看了一個爬取12306來獲得火車票信息的教程,發現12306官網的存儲車票信息的 Json 數據格式已經變了,導致這篇教程的代碼已經沒法繼續使用了,因此我針對新的格式重新進行了解析,最后達到了目的。在此記錄一下整個過程。
01/11/2018 更新:12306 更改了保存著余票信息的網址,有同學反映之前的代碼運行會出錯,于是我修改了一下代碼,現在可以正常運行了。最新的代碼在 GitHub 上,地址在文末倒數第二行。
先看一下最終效果吧
只需要輸入查詢細節,就可以輸出你想查詢的車票信息,而且界面一目了然。
接口設計
用戶在使用這個工具的時候,需要輸入1.車次類型2.始發站3.終點站以及4.日期。火車有很多類型,可以大致分為如下幾種:
- -g 高鐵
- -d 動車
- -t 特快
- -k 快車
- -z 直達
我們需要的接口就是剛剛提到的 4 種,因此接口看起來應該是這個樣子
$ python tickets.py [-gdtkz] from to date
其中,tickets.py
是這個程序的名字,-gdtkz
是車次類型,from
是始發站,to
是終點站,date
是日期,用戶在使用時需要填入這幾個信息。
需要的庫
-
requests
使用 Python 訪問 HTTP 資源 -
docopt
Python3 命令行解析工具 -
prettytable
格式化信息打印工具,見過過 MySQL 打印數據的界面吧 -
colorama
命令行著色工具
最方便的下載方式還是pip
,如果覺得pip
的下載速度太慢可以參考這篇文章解決:更換 pip 源
解析參數
# coding: utf-8
"""命令行火車票查看器
Usage:
tickets [-gdtkz] <from> <to> <date>
Options:
-h,--help 顯示幫助菜單
-g 高鐵
-d 動車
-t 特快
-k 快速
-z 直達
Example:
tickets 武漢 上海 2017-11-20
tickets -dg 北京 南京 2017-11-20
"""
from docopt import docopt
def cli():
"""command-line interface"""
arguments = docopt(__doc__)
print(arguments)
if __name__ == '__main__':
cli()
上面的程序中,docopt
會根據我們在程序開頭定義的格式自動解析出參數并返回一個字典,也就是arguments
,然后打印出這個字典的內容。
運行一下這個程序,比如查詢一下11月20號從武漢到十堰的動車和快車,可以得到解析的結果如下所示,這和我們的接口是對應的
獲取數據
整個過程的關鍵是從 12306 獲取數據和解析數據。
打開 12306 官網,點擊“余票查詢”,進入如下網頁
隨便查詢一下車票,比如我查一下 11 月 20 號從武漢到十堰的票,如圖
然后進入開發者模式下的 Network 頁面,如圖所示(我的瀏覽器是 Chrome,不同瀏覽器的進入方法可能不一樣,不清楚的可以百度)
再點擊一次查詢按鈕,會發現 Network 頁面有所變化,點擊如圖所示的項目,然后進入右邊顯示的 Request URL
你看到應該是如下圖所示的一團雜亂無章的數據
其實這是 Json 格式的數據,里面其實保存了我們查詢的車次的所有車票的信息,我們的任務就是想辦法把它們提取出來并顯示出來。
我們先看看剛才的 URL:
不難發現幾個關鍵信息:
-
train_date=2017-11-20
這是我剛才查詢的日期 -
from_station=WHN
這是始發站 -
to_station=SNN
這是終點站
其中始發站和終點站的名字是用大寫字母組成的代號代替的,然而用戶輸入的是漢字,我們需要找到漢字和代號的對應關系。查看一下網頁的源代碼,搜索 station_version 關鍵字,找到如下位置
復制 src 中的鏈接,并在前面加上 12306 的一級域名,即 https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9030
打開這個鏈接,你會發現一個驚喜
這里面存儲了全國的城市代號,接下來我們寫一個腳本,把城市和代號以字典的形式存入一個 Python 文件
新建 parse_station.py
文件,并寫入以下代碼
import re
import requests
from pprint import pprint
url = 'https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.8971'
response = requests.get(url, verify=False)
stations = re.findall(u'([\u4e00-\u9fa5]+)\|([A-Z]+)', response.text)
pprint(dict(stations), indent=4)
這里用到了正則表達式,通過正則表達式把所有漢字和后面緊跟著的字母解析出來。
運行這個腳本,它將以字典的形式返回所有車站和代號, 并將結果保存到到 stations.py
文件中
$ python3 parse_station.py > stations.py
打開stations.py
文件,看起來是這樣的(因為這個字典沒有名字,所以 Pycharm 發出了 warning,所以界面看起來黃黃的...)
給這個字典命名為 stations,最終stations.py
看起來是這樣的
現在,用戶輸入車站的中文名,我們就可以直接從這個字典中獲取它的字母代碼了:
...
from stations import stations
def cli():
"""command-line interface"""
arguments = docopt(__doc__)
from_station = stations.get(arguments['<from>'])
to_station = stations.get(arguments['<to>'])
date = arguments['<date>']
# 構建 URL
url = 'https://kyfw.12306.cn/otn/leftTicket/queryZ?leftTicketDTO.train_date={}&leftTicketDTO.from_station=' \
'{}&leftTicketDTO.to_station={}&purpose_codes=ADULT'.format(date, from_station, to_station)
回想一下我們的最終目的是從 Json 數據中解析出車票的信息,我們先向存儲 Json 數據的 URL 發送請求:
...
import requests
def cli():
...
# 添加verify=False參數不驗證證書
r = requests.get(url, verify=False)
print(r.json())
這里打印出了 Json 數據,的確是雜亂無章的,下一步就進行解析。
解析數據
仔細觀察和對比 Json 數據和 12306 網站上顯示的車票信息,可以發現所有的車票信息都存儲在 r.json()["data"]["result"]
下,并且存儲的形式是 Python 中的列表,一個車次對應列表中的一個元素,這個元素是一個特別長的字符串,但是里面卻有我們需要的所有信息,包括始發站,終點站,開車時間,到達時間,總時間,以及各個座位的車票是否有剩余,下面用紅框框住的是其中一個車次的數據
這里面除了兩段很長的貌似沒有意義的字符串,剩余的信息都用 |
隔開了,剩下的工作就是遍歷這個列表里的所有元素,并針對每個元素進行解析。
class TrainsCollection:
header = '車次 車站 時間 歷時 商務特等座 一等座 二等座 高級軟臥 軟臥 硬臥 硬座 無座'.split()
def __init__(self, available_trains, station_map, options):
"""查詢到的火車班次集合
:param available_trains: 一個列表, 包含著所有車次的信息
:param station_map: 一個字典,包含不同代號對應的站點
:param options: 查詢的選項, 如高鐵, 動車, etc...
"""
self.available_trains = available_trains
self.station_map = station_map
self.options = options
def geturation(self, duration):
duration = duration.replace(':', '小時') + '分'
if duration.startswith('00'):
return duration[4:]
if duration.startswith('0'):
return duration[1:]
return duration
@property
def trains(self):
for raw_train in self.available_trains:
# 利用正則表達式得到列車的類型
train_type = re.findall('[\u4e00-\u9fa5]+\|\w+\|(\w)', raw_train)[0].lower()
if train_type in self.options and '售' not in raw_train and '停運' not in raw_train:
station = re.findall('(\w+)\|(\w+)\|\d+:', raw_train)[0] # 元組,保存始發站和終點站的代號
s_station = station[0] # 始發站的代號
e_station = station[1] # 終點站的代號
train = [
# 車次
re.findall('[\u4e00-\u9fa5]+\|\w+\|(\w+)', raw_train)[0],
# 始發站和終點站
'\n'.join([Fore.MAGENTA+self.station_map[s_station]+Fore.RESET,
Fore.BLUE+self.station_map[e_station]+Fore.RESET]),
# 發車時間和到站時間
'\n'.join([Fore.MAGENTA+re.findall('\|(\d+:\d+)', raw_train)[0]+Fore.RESET,
Fore.BLUE+re.findall('\|(\d+:\d+)', raw_train)[1]+Fore.RESET]),
self.geturation(re.findall('\|(\d+:\d+)', raw_train)[-1]), # 行駛總時間
re.findall('(\d){8}\|(\w*\|){18}(\w*)', raw_train)[0][-1], # 商務特等座
re.findall('(\d){8}\|(\w*\|){17}(\w*)', raw_train)[0][-1], # 一等座
re.findall('(\d){8}\|(\w*\|){16}(\w*)', raw_train)[0][-1], # 二等座
re.findall('(\d){8}\|(\w*\|){7}(\w*)', raw_train)[0][-1], # 高級軟臥
re.findall('(\d){8}\|(\w*\|){9}(\w*)', raw_train)[0][-1], # 軟臥
re.findall('(\d){8}\|(\w*\|){14}(\w*)', raw_train)[0][-1], # 硬臥
re.findall('(\d){8}\|(\w*\|){15}(\w*)', raw_train)[0][-1], # 硬座
re.findall('(\d){8}\|(\w*\|){12}(\w*)', raw_train)[0][-1] # 無座
]
yield train
def pretty_print(self):
pt = PrettyTable()
pt._set_field_names(self.header)
for train in self.trains:
pt.add_row(train)
print(pt)
我們封裝一個類專門用來解析數據,這個類對傳來的列表進行遍歷,并用正則表達式解析每一個元素,然后把這些信息存儲在列表train
中,最后再通過prettytable
庫將所有信息有序的打印出來。
在原教程中,車票的信息是存儲在 12306 網站中的字典里的,因此解析十分方便,然而后來 12306 將車票信息的存儲格式改為了列表,使得信息的提取變難了,但是只要將正則表達式正確運用,依然可以解析出我們想要的信息,只不過比字典要麻煩一些而已。
顯示結果
最后,我們將上述過程進行匯總并將結果輸出到屏幕上:
def cli():
"""command-line interface"""
arguments = docopt(__doc__)
from_station = stations.get(arguments['<from>'])
to_station = stations.get(arguments['<to>'])
date = arguments['<date>']
# 構建 URL
url = 'https://kyfw.12306.cn/otn/leftTicket/query?leftTicketDTO.train_date={}&leftTicketDTO.from_station=' \
'{}&leftTicketDTO.to_station={}&purpose_codes=ADULT'.format(date, from_station, to_station)
options = ''.join([
key for key, value in arguments.items() if value is True
])
r = requests.get(url, verify=False)
available_trains = r.json()['data']['result']
station_map = r.json()['data']['map']
TrainsCollection(available_trains, station_map, options).pretty_print()
其中,我們通過colorama
庫為站點和時間信息添加了顏色,使結果看起來更加舒服。
全部代碼
由于stations.py
中的字典很長,所以就不在這里將所有代碼貼出來了,感興趣的可以到 Github 上下載查看:Python3 實現火車票查詢工具
原文地址:Python3 實現查詢火車票工具