前几天看了一个爬取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 页面有所变化,点击如图所示的项目,然后进入右边显示的 Request URL
你看到应该是如下图所示的一团杂乱无章的数据
其实这是 Json 格式的数据,里面其实保存了我们查询的车次的所有车票的信息,我们的任务就是想办法把它们提取出来并显示出来。
我们先看看刚才的 URL:
不难发现几个关键信息:
-
train_date=2017-11-20
这是我刚才查询的日期 -
from_station=WHN
这是始发站 -
to_station=SNN
这是终点站
其中始发站和终点站的名字是用大写字母组成的代号代替的,然而用户输入的是汉字,我们需要找到汉字和代号的对应关系。查看一下网页的源代码,搜索 station_version 关键字,找到如下位置
复制 src 中的链接,并在前面加上 12306 的一级域名,即 https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9030
打开这个链接,你会发现一个惊喜
这里面存储了全国的城市代号,接下来我们写一个脚本,把城市和代号以字典的形式存入一个 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,最终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 中的列表,一个车次对应列表中的一个元素,这个元素是一个特别长的字符串,但是里面却有我们需要的所有信息,包括始发站,终点站,开车时间,到达时间,总时间,以及各个座位的车票是否有剩余,下面用红框框住的是其中一个车次的数据
这里面除了两段很长的貌似没有意义的字符串,剩余的信息都用 |
隔开了,剩下的工作就是遍历这个列表里的所有元素,并针对每个元素进行解析。
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 实现查询火车票工具