本篇博客想写很久了,以前抢票时不知道你们有没有这种情况,比如你想买郑州到长春k926这个车次的票,但是车票买完了抢不到票,于是我就想多买几站看没有票,其实也贵不了多少。也就是说我想多买几站买这个车次郑州—>哈尔滨的票,然后到长春下车就可以了,多花一点钱买包含你的乘车区间的票就行了,这样可以增大买到票的几率。但是我在12306官网上没找到这个功能,只能一次一次地去修改出发站到达站去查询,挺麻烦的,于是就想实现一个按车次查询包含指定乘车区间的车票这样一个功能。
浏览器打开12306网站,查询郑州到长春的车票,f12在调试工具页面可以看到点击查询时请求的文件是图中这个,url地址是https://kyfw.12306.cn/otn/leftTicket/query?leftTicketDTO.train_date=2020-07-09&leftTicketDTO.from_station=ZZF&leftTicketDTO.to_station=CCT&purpose_codes=ADULT
打开可以看到这就是查询结果的数据
也就是说只要我们爬取这个url页面的余票信息就可以了
分析url,url需要的三个参数就是日期、出发站、到达站,只不过站名是用对应的code表示的,ZZF->郑州,CCT->长春
这个code和站名之间的对应关系是在加载12306页面的时候,这个js里面的:https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9151
js页面内容如下,可以看到长春对应的就是CCT,郑州对应的是ZZF,我们只要根据输入的车站名字到这个页面取到对应的code拼接成url就行了
接下来就可以爬取包含余票信息的页面了:https://kyfw.12306.cn/otn/leftTicket/query?leftTicketDTO.train_date=2020-07-09&leftTicketDTO.from_station=ZZF&leftTicketDTO.to_station=CCT&purpose_codes=ADULT
但是目前存在一个难点,可能是12306网站有相应的反爬机制,爬取不到这个页面的数据,获取的都是error.html页面的源码。看网上其它以前写的博客都是爬取这个页面的,估计是12306刚对这个做了限制。
一顿尝试之后也没能爬取到这个页面的余票信息数据,然后我就尝试直接爬取下面这个页面的源码
结果发现爬取的源码不包含余票信息,我。。。。
其实余票信息的显示是通过js动态加载的,而requests.get只是从网络获取原始的网页,不包含js执行后的源码,也就是不包含余票信息数据
这就要用到python的selenium模块了,通过selenium可以获取动态js执行后的源码(包含余票数据)
selenium最初是一个自动化测试工具,而爬虫中使用它主要是为了解决requests无法直接执行JavaScript代码的问题 selenium本质是通过驱动浏览器,完全模拟浏览器的操作,比如跳转、输入、点击、下拉等,来拿到网页渲染之后的结果,可支持多种浏览器
使用selenium模块爬取余票信息其实有两种方式,一种是直接爬取json格式的原始数据,另一种是爬取js执行后已经把余票数据渲染到页面的源码(就是审查元素内容),第一种方式经常爬取失败,我采用的第二种方式,通过正则匹配、xpath定位获取需要的信息。
只爬取了硬座、硬卧这些有需求的部分,特等座、高级软卧等这种没买过就没加进去,需要的话可以加上。
按车次查询的时候乘车区间前后扩大三站范围,范围太大觉得就没有买的必要了
完整代码如下:
环境:python3.7、lxml(支持HTML、XML解析)、requests(HTTP库)、re(正则匹配)、prettytable(第三方库,输出美观表格)、selenium(自动化测试工具)、json(json数据解析)
# -*- codeing = utf-8 -*-
# @Time : 2020/7/5 21:47
# @Author : loadding...
# @File : query_ticket.py
# @Software : PyCharm
from lxml import etree
import re
import requests
import prettytable as pt
from selenium import webdriver
import json
# 根据车站名字获取code
def get_station_code(station_name):
url = 'https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9151'
response = requests.get(url)
try:
station_code = re.findall(r'' + station_name + '\|([A-Z]+)', response.text)[0]
except:
print('输入车站不存在')
exit()
return station_code
def get_station_list(station_info):
# 构造获取停靠车站信息的url
url = 'https://kyfw.12306.cn/otn/czxx/queryByTrainNo?train_no=' + station_info[0] + '&from_station_telecode=' + \
station_info[1] + '&to_station_telecode=' + station_info[2] + '&depart_date=' + station_info[3][:4] + '-' + \
station_info[3][4:6] + '-' + station_info[3][6:8]
response = requests.get(url)
html = json.loads(response.text)
station_list = []
for i in html['data']['data']:
station_list.append(i['station_name'])
return station_list
# 标准查询
def get_standard_query(src_station_name, dst_station_name, date):
src_station_code = get_station_code(src_station_name)
dst_station_code = get_station_code(dst_station_name)
url = 'https://kyfw.12306.cn/otn/leftTicket/init?linktypeid=dc&fs=' + src_station_name + ',' + src_station_code + '&ts=' + dst_station_name + ',' + dst_station_code + '&date=' + date + '&flag=N,N,Y'
# 创建一个浏览器对象
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--headless')
driver = webdriver.Chrome(options=chrome_options)
driver.get(url)
# 获取网页源码,动态js执行后的结果与审查元素一样
page_text = driver.page_source
tree = etree.HTML(page_text)
tb = pt.PrettyTable()
tb.field_names = ['车次', '出发站/到达站', '出发时间/到达时间', '历时', '硬卧/二等卧', '软座', '硬座', '无座', '备注']
# 车次数量,因为一个车次信息包括2个tr(另一个是价格信息)
number = int(len(tree.xpath('//*[@id="queryLeftTable"]/tr')) / 2)
for i in range(number):
tbody = []
tbody.append(tree.xpath('//*[@id="queryLeftTable"]/tr[' + str(2 * i + 1) + ']/td[1]/div/div[1]/div/a')[0].text)
tbody.append(tree.xpath('//*[@id="queryLeftTable"]/tr[' + str(2 * i + 1) + ']/td[1]/div/div[2]/strong[1]')[
0].text + '/' +
tree.xpath('//*[@id="queryLeftTable"]/tr[' + str(2 * i + 1) + ']/td[1]/div/div[2]/strong[2]')[
0].text)
tbody.append(tree.xpath('//*[@id="queryLeftTable"]/tr[' + str(2 * i + 1) + ']/td[1]/div/div[3]/strong[1]')[
0].text + '/' +
tree.xpath('//*[@id="queryLeftTable"]/tr[' + str(2 * i + 1) + ']/td[1]/div/div[3]/strong[2]')[
0].text)
tbody.append(
tree.xpath('//*[@id="queryLeftTable"]/tr[' + str(2 * i + 1) + ']/td[1]/div/div[4]/strong')[0].text + '/' +
tree.xpath('//*[@id="queryLeftTable"]/tr[' + str(2 * i + 1) + ']/td[1]/div/div[4]/span')[0].text)
tbody.append(tree.xpath('//*[@id="queryLeftTable"]/tr[' + str(2 * i + 1) + ']/td[8]')[0].text)
tbody.append(tree.xpath('//*[@id="queryLeftTable"]/tr[' + str(2 * i + 1) + ']/td[9]')[0].text)
tbody.append(tree.xpath('//*[@id="queryLeftTable"]/tr[' + str(2 * i + 1) + ']/td[10]')[0].text)
tbody.append(tree.xpath('//*[@id="queryLeftTable"]/tr[' + str(2 * i + 1) + ']/td[11]')[0].text)
tbody.append(tree.xpath('//*[@id="queryLeftTable"]/tr[' + str(2 * i + 1) + ']/td[13]')[0].text)
tb.add_row(tbody)
print('------------------------------------------------- Tickets Info ------------------------------------------')
print(tb)
# 根据出发站、到达站、出发日期、车次返回一个包含余票信息的列表
def get_single_query(src_station_name, dst_station_name, date, train_num):
src_station_code = get_station_code(src_station_name)
dst_station_code = get_station_code(dst_station_name)
url = 'https://kyfw.12306.cn/otn/leftTicket/init?linktypeid=dc&fs=' + src_station_name + ',' + src_station_code + '&ts=' + dst_station_name + ',' + dst_station_code + '&date=' + date + '&flag=N,N,Y'
# 使用无界面模式,不打开浏览器
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--headless')
driver = webdriver.Chrome(options=chrome_options)
driver.get(url)
page_text = driver.page_source
tree = etree.HTML(page_text)
s = "ticket_.{5,6}" + train_num + ".{2}"
pattern = re.compile(s)
# 根据tr的id属性来获取指定车次余票信息
id = pattern.findall(page_text)[0]
tbody = []
tbody.append(tree.xpath('//*[@id="' + id + '"]/td[1]/div/div[1]/div/a')[0].text)
tbody.append(tree.xpath('//*[@id="' + id + '"]/td[1]/div/div[2]/strong[1]')[
0].text + '/' +
tree.xpath('//*[@id="' + id + '"]/td[1]/div/div[2]/strong[2]')[
0].text)
tbody.append(tree.xpath('//*[@id="' + id + '"]/td[1]/div/div[3]/strong[1]')[
0].text + '/' +
tree.xpath('//*[@id="' + id + '"]/td[1]/div/div[3]/strong[2]')[
0].text)
tbody.append(
tree.xpath('//*[@id="' + id + '"]/td[1]/div/div[4]/strong')[0].text + '/' +
tree.xpath('//*[@id="' + id + '"]/td[1]/div/div[4]/span')[0].text)
tbody.append(tree.xpath('//*[@id="' + id + '"]/td[8]')[0].text)
tbody.append(tree.xpath('//*[@id="' + id + '"]/td[9]')[0].text)
tbody.append(tree.xpath('//*[@id="' + id + '"]/td[10]')[0].text)
tbody.append(tree.xpath('//*[@id="' + id + '"]/td[11]')[0].text)
tbody.append(tree.xpath('//*[@id="' + id + '"]/td[13]')[0].text)
return tbody
# 根据车次查询
def get_train_query(src_station_name, dst_station_name, date, train_num):
src_station_code = get_station_code(src_station_name)
dst_station_code = get_station_code(dst_station_name)
url = 'https://kyfw.12306.cn/otn/leftTicket/init?linktypeid=dc&fs=' + src_station_name + ',' + src_station_code + '&ts=' + dst_station_name + ',' + dst_station_code + '&date=' + date + '&flag=N,N,Y'
# 创建一个浏览器对象
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--headless')
driver = webdriver.Chrome(options=chrome_options)
driver.get(url)
page_text = driver.page_source
tb = pt.PrettyTable()
tb.field_names = ['车次', '出发站/到达站', '出发时间/到达时间', '历时', '硬卧/二等卧', '软座', '硬座', '无座', '备注']
tb.align['车次']='1'
tb.padding_width=1#填充宽度
# 正则获取停靠站信息
s = "myStopStation.open\('[0-9]?','(.{5,6}" + train_num + ".{2})','([A-Z]*)','([A-Z]*)','([0-9]{8})'"
pattern = re.compile(s)
station_info = pattern.findall(page_text)[0]
station_list = get_station_list(station_info)
# 指定乘车区间前后扩大三站范围,范围太大也没有买票的必要了
# 出发站和到达站的索引
src_index = station_list.index(src_station_name)
dst_index = station_list.index(dst_station_name)
src_list = [] # 考虑的出发站之前的站
dst_list = []
# 对出发站的位置进行判断
if src_index > 2:
src_list.append(station_list[src_index])
src_list.append(station_list[src_index - 1])
src_list.append(station_list[src_index - 2])
src_list.append(station_list[src_index - 3])
else:
for j in range(src_index + 1):
src_list.append(station_list[j])
if dst_index < len(station_list) - 2:
dst_list.append(station_list[dst_index])
dst_list.append(station_list[dst_index + 1])
dst_list.append(station_list[dst_index + 2])
dst_list.append(station_list[dst_index + 3])
else:
for j in range(len(station_list) - src_index):
src_list.append(station_list[j])
for src in src_list:
for dst in dst_list:
result = get_single_query(src, dst, date, train_num)
tb.add_row(result)
print(
'----------------------------------------------------Tickets Info -------------------------------------------')
print(tb)
def main():
src_station_name = input('出发站:')
dst_station_name = input('到达站:')
date = input('乘车日期(yyyy-mm-dd):')
type = input('1、标准查询 2、车次查询\n请选择:')
if type == '1':
get_standard_query(src_station_name, dst_station_name, date)
elif type == '2':
train_num = input('输入车次:')
get_train_query(src_station_name, dst_station_name, date, train_num)
else:
print('你太笨了,这都能输错!!!')
exit()
if __name__ == '__main__':
main()
实现了两个功能:
1、标准查询,同12306官网查询结果
2、按车次查询,乘车区间前后扩大三站范围
标准查询:郑州-长春 2020-07-09
可以看到K926这个车次没有硬卧和硬座了,但是这个车次时间合适我就要买这个车次的话,就可以使用第二个功能了
然后就可以看到郑州-哈尔滨 K926有票,就可以购买这个,郑州-长春的硬座¥213.0,郑州-哈尔滨的硬座¥236.0,价格其实差不多少,多点钱可以买到合适的票我觉得也是ok的。
代码还可以优化,添加更加完善的功能,比如展示出票价,按车次查询结果按票价排序,可视化界面等。
后续打算使用python编写的flask轻量级框架做可视化界面提高用户体验
增加了票价信息展示,代码变动不大就不贴了,放在最后分享里,同时使用python的pyinstaller模块把程序打包成了exe文件,无需python环境直接可以执行exe文件进行车票查询(有2个exe,区别就是有一个包含了票价信息)
因为我们是python+selenium打包,打开浏览器需要谷歌驱动做支持,这个驱动文件无法打包到run.exe, 因此我们需要把chromedriver.exe复制在query_ticket.exe同级目录下,双击query_ticket.exe就可以运行程序进行车票查询,注意chromedriver.exe的版本和chrome浏览器的版本要对应,都用最新即可,我chrome浏览器是版本 83.0.4103.116(正式版本) (64 位)
执行效果如下所示
query_ticket.exe(不含票价)
标准查询:
按车次查询:
add_price_query_ticket.exe(包含票价)
标准查询:
按车次查询
源代码及打包程序项目下载地址:https://github.com/1234pinyin/project_12306