Python3 实现查询火车票工具

前几天看了一个爬取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

再点击一次查询按钮,会发现 Network 页面有所变化,点击如图所示的项目,然后进入右边显示的 Request URL

URL

你看到应该是如下图所示的一团杂乱无章的数据

杂乱无章的数据

其实这是 Json 格式的数据,里面其实保存了我们查询的车次的所有车票的信息,我们的任务就是想办法把它们提取出来并显示出来。

我们先看看刚才的 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

不难发现几个关键信息:

  • train_date=2017-11-20 这是我刚才查询的日期
  • from_station=WHN 这是始发站
  • to_station=SNN 这是终点站

其中始发站和终点站的名字是用大写字母组成的代号代替的,然而用户输入的是汉字,我们需要找到汉字和代号的对应关系。查看一下网页的源代码,搜索 station_version 关键字,找到如下位置

station_version

复制 src 中的链接,并在前面加上 12306 的一级域名,即 https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9030

打开这个链接,你会发现一个惊喜

station_version

这里面存储了全国的城市代号,接下来我们写一个脚本,把城市和代号以字典的形式存入一个 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.py

给这个字典命名为 stations,最终stations.py看起来是这样的

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 中的列表,一个车次对应列表中的一个元素,这个元素是一个特别长的字符串,但是里面却有我们需要的所有信息,包括始发站,终点站,开车时间,到达时间,总时间,以及各个座位的车票是否有剩余,下面用红框框住的是其中一个车次的数据

json

这里面除了两段很长的貌似没有意义的字符串,剩余的信息都用 | 隔开了,剩下的工作就是遍历这个列表里的所有元素,并针对每个元素进行解析。

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 实现查询火车票工具

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容