用Python写一个命令行火车票查看器

作者: protream | 来源:发表于2016-06-20 22:48 被阅读14013次
    tickets

    当你想查询一下火车票信息的时候,你还在上12306官网吗?或是打开你手机里的APP?下面让我们来用Python写一个命令行版的火车票查看器, 只要在命令行敲一行命令就能获得你想要的火车票信息!如果你刚掌握了Python基础,这将是个不错的小练习。

    接口设计

    一个应用写出来最终是要给人使用的,哪怕只是给你自己使用。所以,首先应该想想你希望怎么使用它?让我们先给这个小应用起个名字吧,既然及查询票务信息,那就叫它tickets好了。我们希望用户只要输入出发站,到达站以及日期就让就能获得想要的信息,所以tickets应该这样被使用:

    
    $ tickets from to date
    
    

    另外,火车有各种类型,高铁、动车、特快、快速和直达,我们希望可以提供选项只查询特定的一种或几种的火车,所以,我们应该有下面这些选项:

    • -g 高铁
    • -d 动车
    • -t 特快
    • -k 快速
    • -z 直达

    这几个选项应该能被组合使用,所以,最终我们的接口应该是这个样子的:

    $ tickets [-gdtkz] from to date
    

    接口已经确定好了,剩下的就是实现它了。

    开发环境

    写Python程序的一个良好实践是使用virtualenv这个工具建一个虚拟的环境。我们的程序使用Python3开发,下面在你的工作目录下建一个文件夹tickets,进去创建一个虚拟环境:

    $ virtualenv -p /usr/bin/python3 venv
    

    通过下面的命令激活它:

    $ . venv/bin/activate
    

    解析参数

    Python有很多写命令行应用的工具,如argparse, docopt, options...这里,我们选用docopt这个简单易用的工具,我们先安装它:

    $ pip3 install docopt
    
    

    docopt可以按我们在文档字符串中定义的格式来解析参数,在tickets.py中:

    # coding: utf-8
    
    """Train tickets query via command-line.
    
    Usage:
        tickets [-gdtkz] <from> <to> <date>
    
    Options: 
        -h,--help        显示帮助菜单
        -g               高铁
        -d               动车
        -t               特快
        -k               快速
        -z               直达
    
    Example:
        tickets 南京 北京 2016-07-01
        tickets -dg 南京 北京 2016-07-01
    """
    from docopt import docopt
    
    def cli():
        """command-line interface""" 
        arguments = docopt(__doc__)
        print(arguments)
    
    if __name__ == '__main__': 
        cli()
    

    下面我们运行一下这个程序:

    
    $ python3 tickets.py 上海 北京 2016-07-01
    
    

    我们得到下面的参数解析结果:

    {'-d': False, '-g': False, '-k': False, '-t': False, '-z': False, '<date>': '2016-07-01', '<from>': '上海', '<to>': '北京'}
    

    获取数据

    参数已经解析好了,下面就是如何获取数据了,这也是最主要的部分。首先我们打开12306,进入余票查询页面,如果你使用chrome,那么按F12打开开发者工具,选中Network一栏,在查询框钟我们输入上海北京,日期2016-07-01, 点击查询,我们在调试工具发现,查询系统实际上请求了这个URL

    https://kyfw.12306.cn/otn/lcxxcx/query?purpose_codes=ADULT&queryDate=2016-07-01&from_station=SHH&to_station=BJP
    

    并且返回的是JSON格式的数据!接下来问题就简单了,我们只需要构建请求URL然后解析返回的JSON数据就可以了。但是我们发现,URL里面from_stationto_station并不是汉字,而是一个代号,而用户输入的是汉字,我们要如何获取代号呢?我们打开网页源码看看有没有什么发现。

    啊哈!果然,我们在网页里面找到了这个链接:点我, 这里面貌似是包含了所有车站的中文名,拼音,简写和代号等信息, 我们在项目目录下将它保存为stations.html。但是这些信息挤在一起,而我们只想要中文名和大写字母的代号信息,怎么办呢?

    BINGO!正则表达式,我们写个小脚本来匹配提取出想要的信息吧, 在parse.py中:

    # coding: utf-8
    import re
    from pprint import pprint
    
    with open('stations.html', 'r') as f:
        text = f.read()
        stations = re.findall(u'([\u4e00-\u9fa5]+)\|([A-Z]+)', text)
        pprint(dict(stations), indent=4)
    

    我们运行这个脚本,它将以字典的形式返回所有车站和它的大写字母代号, 我们将结果重定向到stations.py中,

    $ python3 parse.py > stations.py
    

    我们为这个字典加名字,stations, 最终,stations.py文件是这样的:

    
    stations = {    
        '一间堡': 'YJT',
        '一面坡': 'YPB',
         ... 
        '龙镇': 'LZA',
        '龙骨甸': 'LGM'
    }
    

    现在,用户输入车站的中文名,我们就可以直接从这个字典中获取它的字母代码了:

    ...
    from stations import stations
    
    def cli():
        arguments = docopt(__doc__)
        from_staion = stations.get(arguments['<from>'])
        to_station = stations.get(arguments['<to>'])
        date = arguments['<date>']
        # 构建URL
        url = 'https://kyfw.12306.cn/otn/lcxxcx/query?purpose_codes=ADULT&queryDate={}&from_station={}&to_station={}'.format(
            date, from_staion, to_station
    )
    

    万事俱备,下面我们来请求这个URL获取数据吧!这里我们使用requests这个库, 先安装它:

    $ pip3 install requests
    

    它提供了非常简单易用的接口,

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

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

    def cli():
        ...
        r = requsets.get(url);
        rows = r.json()['data']['datas']
    

    显示结果

    数据已经获取到了,剩下的就是提取我们要的信息并将它显示出来。prettytable这个库可以让我们它像MySQL数据库那样格式化显示数据。

    $ pip3 install prettytable
    

    这样使用它:

    ...
    from prettytable import PrettyTable
    
    def cli():
        ...
        headers = '车次 车站 时间 历时 商务 一等 二等 软卧 硬卧 软座 硬座 无座'.split()
        pt = PrettyTable()
        pt._set_field_names(headers)
        for row in rows:
        # 从row中根据headers过滤信息, 然后调用pt.add_row()添加到表中
            ...
        print(pt)
    

    练习

    下面一些问题留给你做练习:

    • 从每一行row中按照headers过滤信息,将结果添加到prettytable中
    1. 像开始的图片中那样将出发站和到达站,出发时间和到达时间显示为一列

    2. 添加颜色(提示:使用colorama, termcorlor或ANSI转义字符)

    3. 添加异常处理,如果用户输入的车站或日期有错误怎么办?如果网络异常怎么办?...

    4. 添加更多的日期格式支持: 比如用户输入20160701也可以查

    5. 添加参数支持,用户可以指定火车类型

    如果你还有问题,可以去github查看源码, 或者来我的博客下面提问:

    相关文章

      网友评论

      • 猩程变:请问下为什么在mac上用pycharm导入代码,提示找不到Browser这个模块???
      • 编程小白张辽儿:请问博主我是把源码都放在一个文件夹下然后运行tickets.py 吗 感觉好像不行
      • cswb5511:博主 请问 我为什么一直报错解析不了url呢?我自己在网站找的也不对,去你的github上找的代码也不对

        Traceback (most recent call last):
        File "ticket.py", line 128, in <module>
        Cli().run()
        File "ticket.py", line 123, in run
        trains = r.json()['data']['result']
        File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/requests/models.py", line 892, in json
        return complexjson.loads(self.text, **kwargs)
        File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/json/__init__.py", line 354, in loads
        return _default_decoder.decode(s)
        File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/json/decoder.py", line 339, in decode
        obj, end = self.raw_decode(s, idx=_w(s, 0).end())
        File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/json/decoder.py", line 357, in raw_decode
        raise JSONDecodeError("Expecting value", s, err.value) from None
        json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
      • 28df86880fae:先马后读
      • 韩大熊宝要姓张:我学习你的文章自己也写了一部分, 有人要我贴一下你的ticket.py 我拿一下你的链接~、
      • hiochou:第2**8个赞,学习了!
      • 02d37c5b0a3a:楼主您好,我有几个小问题想请教您.
        其实我发现,这个代码是用Python3写的.但是,我作为一个python2的用户应该怎么把这个程序移植到python2上去呢.
        02d37c5b0a3a:不谈print,貌似用到python3特性的是这个正则匹配过程.
        stations = re.findall(u'([\u4e00-\u9fa5]+)\|([A-Z]+)', text)
        这个u''python没有
      • 63459855e334:谢谢分享
      • iridium:不错
      • 4149877099f7:很完整:+1::+1::+1:
      • 4588e4274830:这种还是可视化感觉方便。
      • 查尔德77:chrome 做这个,比wireshark好用多了
      • 查尔德77:谢谢你的文章
      • 雨笋情缘:过俩天我也玩玩
      • ffcb75909bd2:请问下,我按照你的步骤做下去,在这一步:
        r = requests.get(url, verify=False)
        print(r.json())
        InsecureRequestWarning: Unverified HTTPS request is being made. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.org/en/latest/security.html
        InsecureRequestWarning)
        这样的错误,就是强烈建议要验证证书,如果去掉verify=False,会出现一大堆错误。。。
        protream:@Dentao 如果相同的参数你到12306官网能查,程序不能查,说明你URL构建有误,打印出来和官网的请求URL对比下
        ffcb75909bd2:谢谢博主的解答,用warning包可以忽略这些警告,但是我的print(r.json()) 返回的是-1 是不是我的requestes请求没成功的原因
        protream:@Dentao
        对这个程序而言,只是做个简单的查询,没必要添加证书,你可以让程序忽略这些警告:
        import urllib3
        urllib3.disable_warnings()
      • 7296791ae90d:这个好像更全面一些
      • 羽恒:您好 我有根据您的这篇文章练习 有问题向您请教 方便加我QQ1461165480
      • AlexDM:👍👏
      • 程小喵喵:爱这种语言
      • 30531d53d56d:刚开始学习Python的小白
      • FicowShen:好文!
      • ae7efb622fdc:谢谢!学习了
      • BoscoRCtech:良心干货啊
      • KeyLiu7:mark,学习了
      • 小刘琪抱了一下你的小腰:程序员真的好棒
      • RicoX2016:厉害,学习了
      • 靳小都:正打算入门py,膜拜一下
      • superoidlau:厉害:+1:🏻
      • aaf8bd8a2e04:厉害哦。先mark了。
      • ee41efc4a190:mark学习了简单易懂,很棒
      • tmpbook:博客用什么搭建的呢
        protream:@tmpbook 你是说我的博客?用的Hugo,生成静态网站,托管在github上。
        tmpbook:@protream 我是说前端啦
        protream:@tmpbook Python+Flask你可以试试
      • 韩大熊宝要姓张:挺好的.学习了
      • 垚子:mark

      本文标题:用Python写一个命令行火车票查看器

      本文链接:https://www.haomeiwen.com/subject/ouawdttx.html