python识别火车票二维码_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]

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:

不难发现几个关键信息:

train_date=2017-11-20 这是我刚才查询的日期

from_station=WHN 这是始发站

to_station=SNN 这是终点站

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

station_version

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

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[''])

to_station = stations.get(arguments[''])

date = arguments['']

# 构建 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[''])

to_station = stations.get(arguments[''])

date = arguments['']

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

你可能感兴趣的:(python识别火车票二维码)