Python3 實(shí)現(xiàn)查詢火車票工具

前幾天看了一個(gè)爬取12306來(lái)獲得火車票信息的教程,發(fā)現(xiàn)12306官網(wǎng)的存儲(chǔ)車票信息的 Json 數(shù)據(jù)格式已經(jīng)變了,導(dǎo)致這篇教程的代碼已經(jīng)沒(méi)法繼續(xù)使用了识窿,因此我針對(duì)新的格式重新進(jìn)行了解析箫措,最后達(dá)到了目的。在此記錄一下整個(gè)過(guò)程瘩将。


01/11/2018 更新:12306 更改了保存著余票信息的網(wǎng)址,有同學(xué)反映之前的代碼運(yùn)行會(huì)出錯(cuò),于是我修改了一下代碼蚌父,現(xiàn)在可以正常運(yùn)行了哮兰。最新的代碼在 GitHub 上,地址在文末倒數(shù)第二行苟弛。

先看一下最終效果吧

最終效果

只需要輸入查詢細(xì)節(jié)喝滞,就可以輸出你想查詢的車票信息,而且界面一目了然膏秫。

接口設(shè)計(jì)

用戶在使用這個(gè)工具的時(shí)候右遭,需要輸入1.車次類型2.始發(fā)站3.終點(diǎn)站以及4.日期$拖鳎火車有很多類型狸演,可以大致分為如下幾種:

  • -g 高鐵
  • -d 動(dòng)車
  • -t 特快
  • -k 快車
  • -z 直達(dá)

我們需要的接口就是剛剛提到的 4 種,因此接口看起來(lái)應(yīng)該是這個(gè)樣子

$ python tickets.py [-gdtkz] from to date

其中僻他,tickets.py 是這個(gè)程序的名字宵距,-gdtkz 是車次類型,from 是始發(fā)站吨拗,to 是終點(diǎn)站满哪,date 是日期,用戶在使用時(shí)需要填入這幾個(gè)信息劝篷。

需要的庫(kù)

  • requests 使用 Python 訪問(wèn) HTTP 資源
  • docopt Python3 命令行解析工具
  • prettytable 格式化信息打印工具哨鸭,見(jiàn)過(guò)過(guò) MySQL 打印數(shù)據(jù)的界面吧
  • colorama 命令行著色工具

最方便的下載方式還是pip,如果覺(jué)得pip的下載速度太慢可以參考這篇文章解決:更換 pip 源

解析參數(shù)

# coding: utf-8

"""命令行火車票查看器

Usage:
    tickets [-gdtkz] <from> <to> <date>

Options:
    -h,--help   顯示幫助菜單
    -g          高鐵
    -d          動(dòng)車
    -t          特快
    -k          快速
    -z          直達(dá)

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會(huì)根據(jù)我們?cè)诔绦蜷_(kāi)頭定義的格式自動(dòng)解析出參數(shù)并返回一個(gè)字典像鸡,也就是arguments,然后打印出這個(gè)字典的內(nèi)容哈恰。

運(yùn)行一下這個(gè)程序只估,比如查詢一下11月20號(hào)從武漢到十堰的動(dòng)車和快車,可以得到解析的結(jié)果如下所示着绷,這和我們的接口是對(duì)應(yīng)的

演示

獲取數(shù)據(jù)

整個(gè)過(guò)程的關(guān)鍵是從 12306 獲取數(shù)據(jù)和解析數(shù)據(jù)蛔钙。

打開(kāi) 12306 官網(wǎng),點(diǎn)擊“余票查詢”荠医,進(jìn)入如下網(wǎng)頁(yè)

余票查詢

隨便查詢一下車票吁脱,比如我查一下 11 月 20 號(hào)從武漢到十堰的票,如圖

隨便查詢

然后進(jìn)入開(kāi)發(fā)者模式下的 Network 頁(yè)面彬向,如圖所示(我的瀏覽器是 Chrome兼贡,不同瀏覽器的進(jìn)入方法可能不一樣,不清楚的可以百度)

開(kāi)發(fā)者模式-Network

再點(diǎn)擊一次查詢按鈕娃胆,會(huì)發(fā)現(xiàn) Network 頁(yè)面有所變化遍希,點(diǎn)擊如圖所示的項(xiàng)目,然后進(jìn)入右邊顯示的 Request URL

URL

你看到應(yīng)該是如下圖所示的一團(tuán)雜亂無(wú)章的數(shù)據(jù)

雜亂無(wú)章的數(shù)據(jù)

其實(shí)這是 Json 格式的數(shù)據(jù)缕棵,里面其實(shí)保存了我們查詢的車次的所有車票的信息孵班,我們的任務(wù)就是想辦法把它們提取出來(lái)并顯示出來(lái)。

我們先看看剛才的 URL:

https://kyfw.12306.cn/otn/leftTicket/query?leftTicketDTO.train_date=2017-11-20&leftTicketDTO.from_station=WHN&leftTicketDTO.to_station=SNN&purpose_codes=ADULT

不難發(fā)現(xiàn)幾個(gè)關(guān)鍵信息:

  • train_date=2017-11-20 這是我剛才查詢的日期
  • from_station=WHN 這是始發(fā)站
  • to_station=SNN 這是終點(diǎn)站

其中始發(fā)站和終點(diǎn)站的名字是用大寫字母組成的代號(hào)代替的招驴,然而用戶輸入的是漢字篙程,我們需要找到漢字和代號(hào)的對(duì)應(yīng)關(guān)系。查看一下網(wǎng)頁(yè)的源代碼别厘,搜索 station_version 關(guān)鍵字虱饿,找到如下位置

station_version

復(fù)制 src 中的鏈接,并在前面加上 12306 的一級(jí)域名触趴,即 https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9030

打開(kāi)這個(gè)鏈接氮发,你會(huì)發(fā)現(xiàn)一個(gè)驚喜

station_version

這里面存儲(chǔ)了全國(guó)的城市代號(hào),接下來(lái)我們寫一個(gè)腳本冗懦,把城市和代號(hào)以字典的形式存入一個(gè) 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)

這里用到了正則表達(dá)式,通過(guò)正則表達(dá)式把所有漢字和后面緊跟著的字母解析出來(lái)披蕉。

運(yùn)行這個(gè)腳本颈畸,它將以字典的形式返回所有車站和代號(hào), 并將結(jié)果保存到到 stations.py 文件中

$ python3 parse_station.py > stations.py

打開(kāi)stations.py文件,看起來(lái)是這樣的(因?yàn)檫@個(gè)字典沒(méi)有名字没讲,所以 Pycharm 發(fā)出了 warning眯娱,所以界面看起來(lái)黃黃的...)

stations.py

給這個(gè)字典命名為 stations,最終stations.py看起來(lái)是這樣的

stations.py

現(xiàn)在爬凑,用戶輸入車站的中文名徙缴,我們就可以直接從這個(gè)字典中獲取它的字母代碼了:

...
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>']
    # 構(gòu)建 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 數(shù)據(jù)中解析出車票的信息,我們先向存儲(chǔ) Json 數(shù)據(jù)的 URL 發(fā)送請(qǐng)求:

...
import requests

def cli():
    ...
    # 添加verify=False參數(shù)不驗(yàn)證證書(shū)
    r = requests.get(url, verify=False)
    print(r.json())

這里打印出了 Json 數(shù)據(jù)嘁信,的確是雜亂無(wú)章的于样,下一步就進(jìn)行解析。

解析數(shù)據(jù)

仔細(xì)觀察和對(duì)比 Json 數(shù)據(jù)和 12306 網(wǎng)站上顯示的車票信息潘靖,可以發(fā)現(xiàn)所有的車票信息都存儲(chǔ)在 r.json()["data"]["result"] 下百宇,并且存儲(chǔ)的形式是 Python 中的列表,一個(gè)車次對(duì)應(yīng)列表中的一個(gè)元素秘豹,這個(gè)元素是一個(gè)特別長(zhǎng)的字符串携御,但是里面卻有我們需要的所有信息,包括始發(fā)站既绕,終點(diǎn)站啄刹,開(kāi)車時(shí)間,到達(dá)時(shí)間凄贩,總時(shí)間誓军,以及各個(gè)座位的車票是否有剩余,下面用紅框框住的是其中一個(gè)車次的數(shù)據(jù)

json

這里面除了兩段很長(zhǎng)的貌似沒(méi)有意義的字符串疲扎,剩余的信息都用 | 隔開(kāi)了昵时,剩下的工作就是遍歷這個(gè)列表里的所有元素捷雕,并針對(duì)每個(gè)元素進(jìn)行解析。

class TrainsCollection:

    header = '車次 車站 時(shí)間 歷時(shí) 商務(wù)特等座 一等座 二等座 高級(jí)軟臥 軟臥 硬臥 硬座 無(wú)座'.split()

    def __init__(self, available_trains, station_map, options):
        """查詢到的火車班次集合

        :param available_trains: 一個(gè)列表, 包含著所有車次的信息
        :param station_map: 一個(gè)字典壹甥,包含不同代號(hào)對(duì)應(yīng)的站點(diǎn)
        :param options: 查詢的選項(xiàng), 如高鐵, 動(dòng)車, etc...
        """
        self.available_trains = available_trains
        self.station_map = station_map
        self.options = options

    def geturation(self, duration):
        duration = duration.replace(':', '小時(shí)') + '分'
        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:
            # 利用正則表達(dá)式得到列車的類型
            train_type = re.findall('[\u4e00-\u9fa5]+\|\w+\|(\w)', raw_train)[0].lower()
            if train_type in self.options and '售' not in raw_train and '停運(yùn)' not in raw_train:
                station = re.findall('(\w+)\|(\w+)\|\d+:', raw_train)[0]    # 元組救巷,保存始發(fā)站和終點(diǎn)站的代號(hào)
                s_station = station[0]   # 始發(fā)站的代號(hào)
                e_station = station[1]   # 終點(diǎn)站的代號(hào)
                train = [
                    # 車次
                    re.findall('[\u4e00-\u9fa5]+\|\w+\|(\w+)', raw_train)[0],
                    # 始發(fā)站和終點(diǎn)站
                    '\n'.join([Fore.MAGENTA+self.station_map[s_station]+Fore.RESET,
                               Fore.BLUE+self.station_map[e_station]+Fore.RESET]),
                    # 發(fā)車時(shí)間和到站時(shí)間
                    '\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]),  # 行駛總時(shí)間
                    re.findall('(\d){8}\|(\w*\|){18}(\w*)', raw_train)[0][-1],  # 商務(wù)特等座
                    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],   # 高級(jí)軟臥
                    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]   # 無(wú)座
                ]
                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)

我們封裝一個(gè)類專門用來(lái)解析數(shù)據(jù),這個(gè)類對(duì)傳來(lái)的列表進(jìn)行遍歷句柠,并用正則表達(dá)式解析每一個(gè)元素浦译,然后把這些信息存儲(chǔ)在列表train中,最后再通過(guò)prettytable庫(kù)將所有信息有序的打印出來(lái)溯职。

在原教程中精盅,車票的信息是存儲(chǔ)在 12306 網(wǎng)站中的字典里的,因此解析十分方便谜酒,然而后來(lái) 12306 將車票信息的存儲(chǔ)格式改為了列表叹俏,使得信息的提取變難了,但是只要將正則表達(dá)式正確運(yùn)用僻族,依然可以解析出我們想要的信息她肯,只不過(guò)比字典要麻煩一些而已。

顯示結(jié)果

最后鹰贵,我們將上述過(guò)程進(jìn)行匯總并將結(jié)果輸出到屏幕上:

def cli():
    """command-line interface"""
    arguments = docopt(__doc__)
    from_station = stations.get(arguments['<from>'])
    to_station = stations.get(arguments['<to>'])
    date = arguments['<date>']
    # 構(gòu)建 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()

其中晴氨,我們通過(guò)colorama庫(kù)為站點(diǎn)和時(shí)間信息添加了顏色,使結(jié)果看起來(lái)更加舒服碉输。

全部代碼

由于stations.py中的字典很長(zhǎng)籽前,所以就不在這里將所有代碼貼出來(lái)了,感興趣的可以到 Github 上下載查看:Python3 實(shí)現(xiàn)火車票查詢工具


原文地址:Python3 實(shí)現(xiàn)查詢火車票工具

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末敷钾,一起剝皮案震驚了整個(gè)濱河市枝哄,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌阻荒,老刑警劉巖挠锥,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異侨赡,居然都是意外死亡蓖租,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門羊壹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蓖宦,“玉大人,你說(shuō)我怎么就攤上這事油猫〕砻” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵情妖,是天一觀的道長(zhǎng)睬关。 經(jīng)常有香客問(wèn)我诱担,道長(zhǎng),這世上最難降的妖魔是什么电爹? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任蔫仙,我火速辦了婚禮,結(jié)果婚禮上藐不,老公的妹妹穿的比我還像新娘匀哄。我一直安慰自己秦效,他們只是感情好雏蛮,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著阱州,像睡著了一般挑秉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上苔货,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天犀概,我揣著相機(jī)與錄音,去河邊找鬼夜惭。 笑死姻灶,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的诈茧。 我是一名探鬼主播产喉,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼敢会!你這毒婦竟也來(lái)了曾沈?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤鸥昏,失蹤者是張志新(化名)和其女友劉穎塞俱,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體吏垮,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡障涯,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了膳汪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片像樊。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖旅敷,靈堂內(nèi)的尸體忽然破棺而出生棍,到底是詐尸還是另有隱情,我是刑警寧澤媳谁,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布涂滴,位于F島的核電站友酱,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏柔纵。R本人自食惡果不足惜缔杉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望搁料。 院中可真熱鬧或详,春花似錦、人聲如沸郭计。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)昭伸。三九已至梧乘,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間庐杨,已是汗流浹背选调。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留灵份,地道東北人仁堪。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像填渠,于是被迫代替她去往敵國(guó)和親弦聂。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容