由于近期开始重点攻坚Python,但是并不代表我会杀入Python,纯粹只是想要捡起一些被自己玩丢了的东西,不多说了,先来一下涉及到的知识点: 
docopt, requests, colorama, prettytable 库的使用
setuptools 的使用

我的目标是要通过上面设计到工具库来完成一个可以反复使用的的一个python工具,因些我需要先设计一下工具的格式,
接口我们就按照12306官网的查询格式如下:
查询方式: 程序名 出发地 目的地 出发日
  火车类型:
      -g    高铁
      -d    动车
      -t    特快
      -k    快速
      -z    直达
  最终组合接口为:
    python pickets.py [-gdtkz] fromAds toAds DateTime

下面我们将要安装需要的库

pip3 install requests prettytable docopt colorama

接下来我解析参数,通过设计主体工具类来完成

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""火车票查询工具

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

注意:Usage为固定词汇,其他报错。

Options:
    -h,--help   显示帮助菜单
    -g          高铁
    -d          动车
    -t          特快
    -k          快速
    -z          直达

Example:
    tickets 成都 西昌 2018-05-28
    tickets -t 成都 西昌 2018-05-28
"""
from docopt import docopt
from requests import request
import prettytable
import colorama


def cli():
    """command-line interface"""

    arguments = docopt(__doc__)
    print(arguments)

if __name__ == '__main__':
    cli()

由上面的程序中,docopt会根据我们在docstring中定义的格式解析参数并返回一个数据字典

有了上面的基础,接下来我们就要开始获取数据,参数解析好以后,我们可以通过12306官网获取我们需要的数据,IE浏览器打开12306官网的查票系统,输入出发地,目的地,日期,查询,通过F12开发者模式找到请求地址(刷新一次):
955457-20161030191731578-1029360452.png
通过此请求URL,我们可以看到有我们需要的信息,或者直接点击参数查看:
可以看到参数为:出发地 查询类型(成人,学生) 日期 目的地

通过正文可以看到:返回一个json格式的数据,即一个字典,里边包括了各个站点的中文名和站点代号,我们可以利用这个接口来获取。
955457-20161030192745703-959608701.png
打开此页的源码(在页面右击选择查看源码):
955457-20161030193427968-1084386353.png
查看一下具体内容:
复制此链接前边需要加一级域名https://kyfw.12306.cn打开, 如下:
https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.8971

station_names是一个很长的字符串,里边包括了所有车站中文名,拼音,简写,代号等,这里可以看到有很多信息,我们提取我们需要的信息中文字母和代号信息,我们用正则表达式来提取:
955457-20161030194656187-97627143.png
写一个简单的脚本抓取(parse_station.py):

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import re
import requests
from pprint import pprint
# pprint: 格式化输出
url = 'https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.8971'
#获取URL
response = requests.get(url, verify=False)
#正则提取中文字母和代号
stations = re.findall(u'([\u4e00-\u9fa5]+)\|([A-Z]+)', response.text)
# indent:定义打印信息的缩进为4个空格
pprint(dict(stations), indent=4)

上面的正则表达式匹配出的结果转为字典后,字典的键是大写字母大号,这显然不是我们想要的结果,于是,我们通过一个变换将键值反过来。 我们运行这个脚本,它将以字典的形式返回所有车站和它的大写字母代号, 我们将结果重定向到 stations.py 中,
字典前边定义函数名为stations,默认使用utf-8中文显示乱码,可以切换为GBK。

python3 parse_station.py > stations.py

955457-20161102112830861-2013563554.png

万事俱备,下面我们来请求这个URL获取数据吧!这里我们使用 requests 这个库, 它提供了非常简单易用的接口,
...
import requests

def cli():
    ...
    # 添加verify=False参数不验证证书
    r = requests.get(url, verify=False)
    print(r.json())

从结果中,我们可以观察到,与车票有关的信息需要进一步提取:

def cli():
    ...
    r = requsets.get(url);
    print(r.json())

我们封装一个简单的类来解析数据:

class TrainsCollection:

header = '车次 车站 时间 历时 一等 二等 软卧 硬卧 硬座 无座'.split()

def __init__(self, available_trains, options):
    """查询到的火车班次集合

    :param available_trains: 一个列表, 包含可获得的火车班次, 每个
                             火车班次是一个字典
    :param options: 查询的选项, 如高铁, 动车, etc...
    """
    self.available_trains = available_trains
    self.options = options

def _get_duration(self, raw_train):
    duration = raw_train.get('lishi').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_no = raw_train['station_train_code']
        initial = train_no[0].lower()
        if not self.options or initial in self.options:
            train = [
                train_no,        
                '\n'.join([raw_train['from_station_name'],
                          raw_train['to_station_name']]),
                '\n'.join([raw_train['start_time'],
                           raw_train['arrive_time']]),
                self._get_duration(raw_train),
                raw_train['zy_num'],
                raw_train['ze_num'],
                raw_train['rw_num'],
                raw_train['yw_num'],
                raw_train['yz_num'],
                raw_train['wz_num'],
            ]
            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)

最后,我们将上述过程进行汇总并将结果输出到屏幕上:

...

class TrainCollection:

...
...

def cli():

"""Command-line interface"""
arguments = docopt(__doc__)
from_station = stations.get(arguments['<from>'])
to_station = stations.get(arguments['<to>'])
date = arguments['<date>']
url = ('https://kyfw.12306.cn/otn/lcxxcx/query?'
       'purpose_codes=ADULT&queryDate={}&'
       'from_station={}&to_station={}').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']['datas']
TrainsCollection(available_trains, options).pretty_print()

至此, 程序的主体已经完成了, 但是上面打印出的结果是全是黑白的,很是乏味, 我们来给它添加点颜色吧! 这里我们使用 colorama 这个命令行着色工具:

from colorama import init, Fore

init()

修改一下程序,将出发车站与出发时间显示为绿色,到达车站与到达时间显示为红色:

...
'\n'.join([Fore.GREEN + raw_train['from_station_name'] + Fore.RESET,

       Fore.RED + raw_train['to_station_name'] + Fore.RESET]),

'\n'.join([Fore.GREEN + raw_train['start_time'] + Fore.RESET,

       Fore.RED + raw_train['arrive_time'] + Fore.RESET]),

...
现在再运行程序就可以像文章开始的效果图一样了!
QQ截图20180620220842.png

完整代码

# coding: utf-8

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

Usage:
    tickets [-dgktz] <fromAds> <toAds> <DateTime>

Options:
    -h, --help 查看帮助
    -d         动车
    -g         高铁
    -k         快速
    -t         特快
    -z         直达

Examples:
    tickets 西昌 成都 2018-06-10
    tickets -dg 成都 上海 2018-06-10
"""

import requests
from docopt import docopt
from prettytable import PrettyTable
from colorama import init, Fore

from stations import stations


init()

class TrainsCollection:

    header = '车次 车站 时间 历时 一等 二等 软卧 硬卧 硬座 无座'.split()

    def __init__(self, available_trains, options):
        """查询到的火车班次集合

        :param available_trains: 一个列表, 包含可获得的火车班次, 每个
                                 火车班次是一个字典
        :param options: 查询的选项, 如高铁, 动车, etc...
        """
        self.available_trains = available_trains
        self.options = options

    def _get_duration(self, raw_train):
        duration = raw_train.get('lishi').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_no = raw_train['station_train_code']
            initial = train_no[0].lower()
            if not self.options or initial in self.options:
                train = [
                    train_no,        
                    '\n'.join([Fore.GREEN + raw_train['from_station_name'] + Fore.RESET,
                               Fore.RED + raw_train['to_station_name'] + Fore.RESET]),
                    '\n'.join([Fore.GREEN + raw_train['start_time'] + Fore.RESET,
                               Fore.RED + raw_train['arrive_time'] + Fore.RESET]),
                    self._get_duration(raw_train),
                    raw_train['zy_num'],
                    raw_train['ze_num'],
                    raw_train['rw_num'],
                    raw_train['yw_num'],
                    raw_train['yz_num'],
                    raw_train['wz_num'],
                ]
                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)


def cli():
    """Command-line interface"""
    arguments = docopt(__doc__)
    from_station = stations.get(arguments['<from>'])
    to_station = stations.get(arguments['<to>'])
    date = arguments['<date>']
    url = ('https://kyfw.12306.cn/otn/lcxxcx/query?'
           'purpose_codes=ADULT&queryDate={}&'
           'from_station={}&to_station={}').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']['datas']
    TrainsCollection(available_trains, options).pretty_print()


if __name__ == '__main__':
    cli()
最后修改:2018 年 06 月 20 日
如果觉得我的文章对你有用,请随意赞赏