1. 程式人生 > >用python 通過12306api抓取列車資訊

用python 通過12306api抓取列車資訊

PS:本文為學習參考例項。程式碼與上述大體相同。

首先了解這些查詢介面是怎麼來的

chrome是個好東西,特別是它的控制檯能看到很多細節。
12306網站通過chrome可以看到查詢票的api
chrome控制檯
其中有log? 和 queryA?兩種開頭的介面,網上介紹log是判斷服務是否正常,用queryA進行查詢

#在python控制檯測試
> import requests
> url = 'https://kyfw.12306.cn/otn/leftTicket/log?leftTicketDTO.train_date=2017-01-11&leftTicketDTO.from_station=BJP&leftTicketDTO.to_station=XAY&purpose_codes=ADULT'
# 這裡傳入了三個變數, train_date from_station to_station 站名為縮寫標準,獲得方法見後面介紹 r = requests.get(url, verify = False) #chrome可以看到它用的是get方法, verify為忽略https 解析結果r.text為字典形式的字串,通過r.json()得到解析後結果, ticks = r.json() 分析結構, ticks['data']儲存所有車次的資訊。G653車次的查詢結果表示為 g653 = ticks['data'][1]['queryLeftNewDTO']

從結果中提取需要的欄位展示

12306查詢結果列表的表頭包含如下欄位:
車次 出發站/到達站 出發時間/達到時間 歷時 商務座 特等座 一等座 二等座 高階軟臥 軟臥 硬臥 軟座 硬座 無座 其他 備註
分析g653對應的key分別為:
station_train_code from_station_name/to_station_name start_time/arrive_time lishi/day_difference swz_num tz_num zy_num ze_num gr_num rw_num yw_num rz_num yz_num wz_num qt_num
車次型別分(G,D,Z,T,K)首位不是字母的是普快。

如下程式碼根據以上通過介面查詢的結果,並根據指定列車型別過濾

#程式碼與參考中大體相同
#coding=utf-8
from prettytable import PrettyTable

class TrainCollection(object):
    """
    解析列車資訊
    """
    header = '序號 車次 出發站/到達站 出發時間/達到時間 歷時 商務座  特等座 一等座 二等座 高階軟臥 軟臥 硬臥 軟座 硬座 無座'.split()

    def __init__(self, rows, traintypes):
        self.rows = rows
        self.traintypes = traintypes   

    def _get_duration(self, row):
        """
        獲取列車執行時間
        這裡的row = row['queryLeftNewDTO']
        """
        duration = row.get('lishi').replace(':', '小時') + '分'
        if duration.startswith('00'):
            return duration[4:]
        elif duration.startswith('0'):
            return duration[1:]
        return duration   

    @property    #將方法trains裝飾為屬性,之後以屬性方式訪問
    def trains(self):
        """
        解析原有采集資料,提取需要的欄位保留
        """
        result = []   
        flag = 0
        for row in self.rows:
            row = row['queryLeftNewDTO']
            if row['station_train_code'][0] in self.traintypes or row['station_train_code'].isdigit() and 'P' in self.traintypes:
            # 如果全是數字為普快型別,自定義為P
                flag += 1
                train = [
                        flag, row['station_train_code'],
                        '/'.join([row['from_station_name'], row['to_station_name']]), 
                        '/'.join([row['start_time'], row['arrive_time']]),
                        self._get_duration(row),
                        row['swz_num'], row['tz_num'],row['zy_num'],
                        row['ze_num'],row['gr_num'],row['rw_num'],
                        row['yw_num'],row['rz_num'], row['yz_num'],row['wz_num']
                        ]
                result.append(train)
        return result           

    def print_pretty(self):
        """
            通過prettytable模組將列表欄位美觀輸出
        """
        pt = PrettyTable()
        pt._set_field_names(self.header)
        for train in self.trains:
            pt.add_row(train)
        print(pt)

命令列引數方式查詢

參考中用來解析命令列引數使用了docopt[介紹]模組。
程式期望的執行方法是輸入形如
python tickets.py -gdt beijing shanghai 2016-08-25來查詢展示。

  • 首先,需要將引數中的-gdt 轉換為內部使用的不帶-大寫形式的字元列表。

可以接受的列車型別包含types = [‘-d’, ‘-g’, ‘-t’, ‘-z’, ‘-k’, ‘-p’]
train_type = []
for i in types:
if i in args.keys():
train_type.append(i[-1].upper())

  • 其次,引數需要將接收的地名拼音轉換為內部的唯一地名程式碼

站名程式碼基本不變,參考中介紹了通過一個介面預先匯出所有站名程式碼供程式直接使用。

#coding=utf-8

import requests
import re
from pprint import pprint

def get_stations():
    url = 'https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.8968'
#瀏覽器中直接輸入上面連結不用帶version版本,也可以看到
    r = requests.get(url, verify = False)
    patter = re.compile('([A-Z]+)\|([a-z]+)')
    items = dict(re.findall(patter, r.text))
    # 將結果中的大寫字母程式碼,車站拼音 反轉為 車站拼音->程式碼的字典結構
    stations = dict(zip(items.values(), items.keys()))
    pprint(stations, indent=4)
if __name__ == '__main__':
    get_stations()

執行指令碼並定向到stations.py中,修改其中的字典頭部賦值為stations = {…}
這樣在之後指令碼匯入該模組時可以使用其中的字典變數stations.

最終程式碼:

#coding=utf-8
"""
12306 tickets 查詢

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

Options:
    -h --help   Show this screen.
    -g          高鐵
    -d          動車
    -t          特快
    -k          快速
    -z          直達
    -p          普快

Example:
    tickets.py  -gdt beijing shanghai 2016-11-12
"""

import requests
from docopt import docopt 

from TrainCollection import TrainCollection
from stations import stations

class TicketsSearch(object):
    def __init__(self):
        """
        通過類的初始化獲取指令碼命令列引數
        """
        self.args = docopt(__doc__)    #通過解析指令碼__doc__文字得到引數結構

    def _get_traintype(self):
        """
        獲取列車型別
        """
        types = ['-d', '-g', '-t', '-z', '-k', '-p'] 
        train_type = []
        for i in types:
            if i in self.args.keys():
                train_type.append(i[-1].upper())
        if train_type:
            return train_type
        else:
            return ['G', 'D', 'T', 'Z', 'K', 'P']

    def cli(self):
        """根據變數執行查詢"""
        from_station = stations.get(self.args['<from>'])
        to_station = stations.get(self.args['<to>'])
        leave_time = self.args['<date>']

        url = 'https://kyfw.12306.cn/otn/leftTicket/queryA?leftTicketDTO.train_date={0}&leftTicketDTO.from_station={1}&leftTicketDTO.to_station={2}&purpose_codes=ADULT'.format(leave_time, from_station, to_station)

        #查詢
        r = requests.get(url, verify=False)
        if 'data' not in r.json().keys():
            print("查詢日期錯誤,或不再預售期")
            exit
        traindatas = r.json()['data']

        #篩選
        traintypes = self._get_traintype()
        trains = TrainCollection(traindatas, traintypes)

        #列印
        trains.print_pretty()

if __name__ == '__main__':
    cli = TicketsSearch()
    cli.cli()

該查詢需要完善的地方:
1. 時間引數的多種格式,這裡僅支援2017-01-11格式,應該增加其他型別。同時對於日期不再範圍內,需要人為控制,避免報錯
2. 站名拼音引數的搜尋似乎並非嚴格,而是有一定模糊,比如beijing shanghai 會包含北京南。看來不用人為進行模糊匹配