想法是用python编写一个车票查询工具,可以很好的锻炼一下自己python的网络编程能力。
1、第一步设计这个API的接口:
根据12306的设计我的API接口也要有出发站,目的站,车次类型,日期四部分。
车次类型:
-g 高铁
-d 动车
-t 特快
-k 快速
-z 直达
需要用到的库:
requests,使用 Python 访问 HTTP 资源的必备库。
docopt,Python3 命令行参数解析工具。
prettytable, 格式化信息打印工具,能让你像 MySQL 那样打印数据。
colorama,命令行着色工具
因为我这机子上python版本众多,但默认python版本为2.7,python3的pip3、settools没有安装。
下面安装:
sudo apt-get install python3-setuptools
sudo easy_install3 pip requests docopt prettytable colorama
安装这三个库:
sudo pip3 install
新建tickets.py 文件,代码如下:
# coding: utf-8
""" 命令行火车票查看器
Usage:
tickets [-gdtzk] <from> <to> <date>
Options:
-h,--help 显示帮助菜单
-g 高铁
-d 动车
-t 特快
-k 快速
-z 直达
Example:
tickets -gd 沈阳 北京 2017-03-01
"""
#command-line-interface
from docopt import docopt
def cli():
arguments=docopt(__doc__)
print(arguments)
if __name__ == "__main__":
cli()
“# coding: utf-8 ”是为了避免输出中文乱码的一种编码方式,确立输出文件为在python3.x之后没有这个问题。
接下来是python命令行解析工具docopt的固定格式,这个工具简洁方便。
“if name == “main“: cli()” 这就代码的作用是当直接运行tickets.py 时会打印命令行参数提示信息。
每个模块都有内置方法name,当直接被调用时为main,被导入使用时为他的文件名。所以这里执行cli().
效果:
2.网站应答分析
打开12306,随便输入时间和地点,最好浏览器为火狐装载firebug插件。点击查询:
消息头信息:注意这个URl:https://kyfw.12306.cn/otn/leftTicket/后加了四个参数,始发站,目的站,时间,和是否半价票。
在抓取的请求和应答中,找到如下图这样的报文,他的参数有始发站,目的站,时间,这样的才是我们要在python中想要获取的数据。但是站点名都是缩写,想想你登12306查火车票, 你输入的是汉字吧, 而我们发现了请求的URL中是车站代码, 这样我们就知道汉字到代码的转换是在前端(JS)完成的, 那么前端源码中肯定有转换对应表.
这个响应信息就是我们想要的:
我们要通过查看源代码找找这个缩写和站点名是怎样对应的。
看看:/otn/resources/js/framework/station_name.js?station_version=1.8997,源是从这里来的,我们也要从这里获取站名对应表,这就是前面说的转换对应表。
新建一个teststation.py用来获取站名代码如下:
# coding: utf-8
import re
import requests
from pprint import pprint
url = 'https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.8997'
response = requests.get(url,verify=False)
station = re.findall(u'([\u4e00-\u9fa5]+)\|([A-Z]+)',response.text)
pprint(dict(station),indent=4)
re模块是一个用来检索的一个模块,导入他我们可以把与之匹配的信息通过pprint方法打印出来。requests模块是http请求必用的模块。
之后导入到stations.py 文件中:
python3.5 teststations.py > stations.py
这里边执行我用了python3.5,因为默认的python 为2.7版本,对输出中文不支持。
效果:
对输出的字典文件加个stations名。
现在,用户输入车站的中文名,我们就可以直接从这个字典中获取它的字母代码了。
3、数据的获取
# coding: utf-8
"""命令行火车票查看器
Usage:
tickets [-gdtkz] <from> <to> <date>
Options:
-h,--help 显示帮助菜单
-g 高铁
-d 动车
-t 特快
-k 快速
-z 直达
Example:
tickets 北京 上海 2016-10-10
tickets -dg 成都 南京 2016-10-10
"""
#command- line -interface
from docopt import docopt
from stations import stations
import requests
def cli():
arguments = docopt(__doc__)
from_station = stations.get(arguments['<from>'])
to_station = stations.get(arguments['<to>'])
date = arguments['<date>']
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)
r = requests.get(url,verify=False)
print (r.json())
if __name__ == "__main__":
cli()
这段代码大意就是把tickets.py 的参数值赋给三个变量,然后赋给由第二遍得到的URl中,再由get()函数接受数据,打印其JSON格式。
效果是这样的:
分析一下我们可以看到其实得到的数据是四个字典,第三个字典中有嵌套了字典列表:
4、 解析数据
代码如下:
这是根据现在12306网站写的接口,它的接口经常变动。第三步得到的数据形式也总在变化。
# coding: utf-8
"""命令行火车票查看器
Usage:
tickets [-gdtkz] <from> <to> <date>
Options:
-h,--help 显示帮助菜单
-g 高铁
-d 动车
-t 特快
-k 快速
-z 直达
Example:
tickets 北京 上海 2016-10-10
tickets -dg 成都 南京 2016-10-10
"""
from docopt import docopt
from stations import stations
import requests
from prettytable import PrettyTable
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):
train_no = self.available_trains['station_train_code']
initial = train_no[0].lower()
if not self.options or initial in self.options:
train = [
train_no,
'\n'.join([self.available_trains['from_station_name'],
self.available_trains['to_station_name']]),
'\n'.join([self.available_trains['start_time'],
self.available_trains['arrive_time']]),
self._get_duration(self.available_trains),
self.available_trains['zy_num'],
self.available_trains['ze_num'],
self.available_trains['rw_num'],
self.available_trains['yw_num'],
self.available_trains['yz_num'],
self.available_trains['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)
#command- line -interface
def cli():
arguments = docopt(__doc__)
from_station = stations.get(arguments['<from>'])
to_station = stations.get(arguments['<to>'])
date = arguments['<date>']
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)
rows = r.json()['data']
for row in rows:
available_trains = row['queryLeftNewDTO']
#print (available_trains)
TrainsCollection(available_trains, options).pretty_print()
if __name__ == "__main__":
cli()
“ @property”
Python中有一个被称为属性函数(property)的小概念,它可以做一些有用的事情。能做以下几点:
将类方法转换为只读属性
重新实现一个属性的setter和getter方法
使用属性函数的最简单的方法之一是将它作为一个方法的装饰器来使用。这可以让你将一个类方法转变成一个类属性。当我需要做某些值的合并时,我发现这很有用。其他想要获取它作为方法使用的人,发现在写转换函数时它很有用。
rows = r.json()
这条代码的后的导出文件:
{
'messages': [],
'validateMessagesShowId': '_validatorMessage',
'data': [
{'queryLeftNewDTO':
{'controlled_train_flag': '1',
'yw_num': '--',
'end_station_name': '杭州',
'from_station_name': '沈阳北',
'yz_num': '--',
'gr_num': '--',
'rz_num': '--',
'start_station_name': '哈尔滨',
'train_no': '010000Z17804',
'location_code': 'B2',
'swz_num': '--',
'from_station_telecode': 'SBT',
'lishiValue': '5999',
'wz_num': '--',
'start_province_code': '06',
'tz_num': '--',...}]}
之后使用json函数来匹配字典中的[‘data’]关键字,我们看一下匹配后的导出文件:
rows = r.json()['data']
[
{'queryLeftNewDTO':
{'controlled_train_flag': '1',
'yw_num': '--',
'end_station_name': '杭州',
'from_station_name': '沈阳北',
'yz_num': '--',
'gr_num': '--',
'rz_num': '--',
'start_station_name': '哈尔滨',
'train_no': '010000Z17804',
'location_code': 'B2',
'swz_num': '--',
'from_station_telecode': 'SBT',
'lishiValue': '5999',
'wz_num': '--',
'start_province_code': '06',
'tz_num': '--',
'station_train_code': 'Z178',
'to_station_telecode': 'ZJH',
'rw_num': '--',
'controlled_train_message': '列车运行图调整,暂停发售',
'from_station_no': '05',
'yp_info': '',
'arrive_time': '24:00',...]
这是一个列表,我们用for来检索:
for row in rows:
available_trains = row['queryLeftNewDTO']
这是我们要的信息了
{'day_difference': '0',
'end_station_telecode': 'AOH',
'lishiValue': '527',
'controlled_train_flag': '0',
'start_city_code': '0034',
'yb_num': '--',
'lishi': '08:47',
'ze_num': '有',
'is_support_card': '1',
'tz_num': '无',
'start_station_name': '哈尔滨西',
'station_train_code': 'G1202',
'yz_num': '--',
'end_station_name': '上海虹桥',
'rz_num': '--',...}
效果图: