说明文字:
1.本项目只是一个练习,熟悉python爬虫技术,没有任何用途
2.最后运行的结果有时候会成功,有时候会显示错误界面,如下图所示。因为12306怎么可能允许你一直爬它呢
开发工具准备:
- 开发工具:PyCharm
- 内置模块:sys,time,datetime,os,json,re
- 第三方模块:PyQt5,pyqt5-tools,requests,matplotlib
准备工作:
- 下载数据文件:stations.text(车站名称文件)和time.text(起售时间文件)
创建get_station.py文件:
import json
import re #通过正则表达式匹配处理相应的字符串
import os #判断某个路径下的某个文件
import requests #处理网络请求
def get_selling_time():
url='https://www.12306.cn/index/script/core/common/qss_v10082.js'
response=requests.get(url,verify=True) #请求并进行验证
print(response.text)
print(type(response.text))
json_str=re.findall('{[^}]+}',response.text) #匹配括号内所有内容
print(json_str)
time_js=json.loads(json_str[0]) #解析JSON数据
print(time_js)
write(str(time_js),'time.text') #调用写入方法
def get_station():
#发送请求获取所有车站名称,通过输入的站名称转化查询地址的参数
url='https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9151'
response=requests.get(url,verify=True) #请求并进行验证
#print(response.text)
print(type(response.text))
#[\u4e00-\u9fa5]+表示匹配给定字符中任意一个汉字
stations=re.findall('([\u4e00-\u9fa5]+)\|([A-Z]+)',response.text) #获取需要的车站名称
#print(stations)
#print(type(stations))
stations=dict(stations) #转换为字典
#print(stations)
stations=str(stations) #转换为字符串类型否则无法写入文件
#print(stations)
write(stations, 'stations.text') #调用写入方法
#写入文件
def write(stations,file_name):
file=open(file_name,'w',encoding='utf_8_sig') #以写模式打开文件
file.write(stations) #写入数据
file.close()
#读文件
def read(file_name):
file=open(file_name,'r',encoding='utf_8_sig') #以读模式打开文件
data=file.readline() #读取文件
file.close()
return data
#判断文件是否存在
def is_stations(file_name):
is_stations=os.path.exists(file_name)
return is_stations
if __name__=='__main__':
if is_stations('stations.text') is False:
get_station() # 下载所有车站文件
if is_stations('time.text') is False:
get_selling_time() # 下载起售时间文件
- 在PyCharm中设置PyQt5工具:
1.Qt Designer:主要进行主窗体的UI设计,最后保存的是ui文件
2.Pyuic工具:将.ui文件转成.py文件
3.Pyrcc工具:将.qrc文件转成.py文件
配置链接:
用Qt Designer设计的主窗体效果图:
项目结构:
- img_resources文件夹主要是图片资源文件,主窗体UI设计用到的两个.png图片和一个.qrc图片资源文件
- ui文件夹保存的是Qt Designer设计的窗体ui文件
- window.py是window.ui用Pyuic工具转换的
- img_rc.py是img.qrc用Pyrcc工具转换的
- get_stations.py是下载车站名称与起售时间代码,运行之后会出现stations.text(车站名称文件)和time.text(起售时间文件)
- 最重要的就是:query_request.py查询网络请求代码 以及 show_window.py显示与控制窗体代码。
项目说明:根据上面设计的主窗体效果图,可以看出来,该项目主要分为三大模块:车票查询、卧铺售票分析、车票起售时间。第二个模块最复杂,所以先说简单的两个模块
****小小提示:以下内容把我自己手写总结的图和代码配合起来看,比较容易理解 *
模块一:车票查询:
下面这个图是我整理的车票查询的步骤:
关键代码如下:
- 在show_window.py文件中创建on_click()方法,在该方法中:首先获取输入的内容,然后进行参数审核,接着发送查询请求调用query()方法,最后将查询结果显示在窗体表格中(调用displayTable()方法)。
#主窗体的查询按钮
def on_click(self):
get_from=self.textEdit.toPlainText() # 获取出发地
get_to=self.textEdit_2.toPlainText() # 获取到达地
get_date=self.textEdit_3.toPlainText() # 获取出发时间
# 判断车站文件是否存在
if is_stations('stations.text') is True:
stations=eval(read('stations.text')) # 读取所有车站并转换为字典类型
#判断所有参数是否为空
if get_from!="" and get_to!="" and get_date!="":
#判断输入的车站名称是否都存在,以及时间格式是否正确
if get_from in stations and get_to in stations and self.is_valid_date(get_date):
#计算时间差
time_difference=self.time_difference(self.get_time(),get_date).days
# 12306官方要求智能查询30天以内的车票
if 0 <= time_difference <= 29:
#在所有车站文件中找到对应的参数、出发地
from_station=stations[get_from]
#print(from_station)
to_station=stations[get_to]
#print(to_station)
#发送查询请求,并获取返回信息
data=query(get_date,get_from,from_station,get_to,to_station)
print('正确1')
self.checkBox_default()
if len(data) !=0:
#将车票信息显示到表格中
print('正确2')
self.displayTable(len(data),16,data)
else:
messageDialog('警告','没有返回的网络数据!')
else:
messageDialog('警告', '超出查询日期的范围!')
else:
messageDialog('警告', '输入的站名不存在,或日期格式不正确!')
else:
messageDialog('警告', '请填写车站名称!')
else:
messageDialog('警告', '未下载车站查询文件!')
#消息提示框,参数title为提示框标题文字,message为提示信息
def messageDialog(title,message):
msg_box=QMessageBox(QMessageBox.Warning,title,message)
msg_box.exec_()
- 在query_request.py文件中创建query()方法,该方法需要三个参数:出发日期、出发地、目的地。在该方法中,查询请求地址是通过format()方法对地址进行格式化。由于获取到的JSON信息比较乱,所以在获取指定数据时通过split()进行分割,通过与浏览器余票查询页面中的数据逐个对比找出数据所对应的位置。
5-7 目的地 3 车次 6 出发地 8 出发时间 9 到达时间 10 历时 26 无坐 29 硬座
24 软座 28 硬卧 33 动卧 23 软卧 21 高级软卧 30 二等座 31 一等座 32 商务座特等座
data=[] # 保存整理好的车次信息
type_data=[] # 保存分类后的车次信息(如高铁,动车等)
def query(date,get_from,from_station,get_to,to_station):
data.clear() # 清空数据
type_data.clear()
# 查询请求地址
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)
response=requests.get(url,headers=header,verify=False) # 发送查询请求
if response.text.startswith(u'\ufeff'):
response.text = response.text.encode('utf8')[3:].decode('utf8')
response.encoding = 'utf-8'
print(response.url)
result = json.loads(response.text)
result=result['data']['result']
# 判断车站文件是否存在
if is_stations('stations.text') :
stations=eval(read('stations.text')) # 读取所有车站并转换为字典类型
if len(stations)!=0: # 判断返回数据是否为空
for i in result:
tmp_list=i.split('|') # 分割数据并添加到列表中
print(tmp_list)
#因为查询结果中出发站和到达站为站名的缩写字母,所以需要在车站库中找到对应的车站名称
from_station=list(stations.keys())[list(stations.values()).index(tmp_list[6])]
to_station=list(stations.keys())[list(stations.values()).index(tmp_list[7])]
#创建座位数组,由于返回的作为数据中含有空值,所以将空改成“--”这样好识别
seat=[tmp_list[3],from_station,to_station,tmp_list[8],tmp_list[9],tmp_list[10],
tmp_list[32],tmp_list[31],tmp_list[30],tmp_list[21],tmp_list[23],
tmp_list[33],tmp_list[28],tmp_list[24],tmp_list[29],tmp_list[26]]
print(seat)
newSeat=[]
#循环将座位信息中的空值改成“--”
for s in seat:
if s=="":
s="--"
else:
s=s
newSeat.append(s) # 保存新的座位信息
data.append(newSeat)
print(newSeat)
return data # 返回整理好的车次信息
代码中 print(response.url) 如果输出的是:https://www.12306.cn/mormhweb/logFiles/error.html 也就是上面说的错误页面,就说明12306爬虫失败,你的ip应该被锁住了。这种情况很正常,不必担心。
- 在show_window.py文件中创建displayTable()方法,用于将车票信息显示到主窗体的表格中。
#显示车次信息的表格
#train参数为共有多少趟列车,该参数为表格的行
#info参数为每趟列车的具体信息,例如有座、无座、卧铺等,该参数作为表格的列
def displayTable(self,train,info,data):
self.model.clear()
for row in range(train):
for column in range(info):
#添加表格内容
item=QStandardItem(data[row][column])
#向表格存储模式中添加表格具体信息
self.model.setItem(row,column,item)
#设置表格存储数据的模式
self.tableView.setModel(self.model)
- on_click()里面调用的一些基本方法如下(很好理解):self.is_valid_date()判断输入的日期是否合法;self.time_difference()计算两个日期相差的天数;self.get_time()获取当前日期;self.checkBox_default()车次类型的复选框取消勾选。
#判断是否是一个有效的日期字符串
def is_valid_date(self, str):
try:
time.strptime(str, "%Y-%m-%d")
return True
except:
return False
# 获取系统当前时间并转换请求数据所需要的格式
def get_time(self):
# 获得当前时间时间戳
now = int(time.time())
# 转换为其它日期格式,如:"%Y-%m-%d %H:%M:%S"
timeStruct = time.localtime(now)
strTime = time.strftime("%Y-%m-%d", timeStruct)
return strTime
# 计算购票时间差,因为只能提前购买29天的车票
def time_difference(self, in_time, new_time):
# 将字符串日期转换为struct_time时间对象
in_time = time.strptime(in_time, "%Y-%m-%d")
new_time = time.strptime(new_time, "%Y-%m-%d")
# 将struct_time时间对象转换为datetime对象
in_time = datetime.datetime(in_time[0], in_time[1], in_time[2])
new_time = datetime.datetime(new_time[0], new_time[1], new_time[2])
# 返回两个变量相差的值,就是相差天数
return new_time - in_time
# 将所有车次分类复选框取消勾选
def checkBox_default(self):
self.checkBox_G.setChecked(False)
self.checkBox_D.setChecked(False)
self.checkBox_Z.setChecked(False)
self.checkBox_T.setChecked(False)
self.checkBox_K.setChecked(False)
运行结果如下图:(运行不出来的话,也别担心,我也只是偶尔一次运行成功了。这个图片是网上找到的,就是让大家看看结果是什么样子,有便于更好的理解代码,心态放好哈哈哈哈哈)
模块二:车票起售时间查询
这个模块比较简单些,我整理的步骤如下:
- 在show_window.py中创建query_time_click()方法,查询并显示车票起售时间。首先调用query_time()方法查询起售车站对应的站名与起售时间,然后将网格布局清空,接着创建控件并设置属性,最后加载控件并且显示
#车票起售时间查询按钮的事件处理
def query_time_click(self):
station=self.lineEdit.text() # 获取需要查询的起售车站
#print(station)
stations_time=eval(read('time.text')) # 读取所有车站与起售时间并转换为字典类型
stations=eval(read('stations.text')) # 读取所有车站并转换为字典类型
if station in stations_time:
# 查询起售车站对应的站名与起售时间
name_lit,time_list=query_time(stations.get(station))
# 每次点循环删除管理器的控件
if self.gridLayout.count()!=0:
while self.gridLayout.count():
item=self.gridLayout.takeAt(0) # 获取第一个控件
widget=item.widget() # 删除控件
widget.deleteLater()
i=-1 # 行数标记
for n in range(len(name_lit)):
x=n % 4 # x 确定每行显示的个数 0,1,2,3 每行4个
# 当x为0的时候设置换行 行数+1
if x==0:
i+=1
self.widget=QtWidgets.QWidget() # 创建布局
self.widget.setObjectName("widget"+str(n)) # 给布局命名
# 设置布局样式
self.widget.setStyleSheet('QWidget#' + "widget" + str(n) + "{border:2px solid rgb(175, 175, 175);background-color: rgb(255, 255, 255);}")
# 创建个Qlabel控件用于显示图片 设置控件在QWidget中
self.label=QtWidgets.QLabel(self.widget)
self.label.setAlignment(QtCore.Qt.AlignCenter)
# 设置大小
self.label.setGeometry(QtCore.QRect(10,10,210,65))
font=QtGui.QFont() # 创建字体对象
font.setPointSize(11) # 设置字体大小
font.setBold(True) # 开启粗体属性
font.setWeight(75) # 设置文字粗细
self.label.setFont(font) # 设置字体
# 设置显示站名与起售时间
self.label.setText(name_lit[n]+' '+time_list[n])
# 把动态创建的widegt布局添加到gridLayout中 i,x分别代表:行数以及每行的个数
self.gridLayout.addWidget(self.widget,i,x)
# 设置高度为动态高度根据行数确定高度 每行300
self.scrollAreaWidgetContents_2.setMinimumHeight((i+1)*100)
# 设置网格布局控件动态高度
self.gridLayoutWidget.setGeometry(QtCore.QRect(0,0,950,((i+1)*100)))
- 在query_request.py中首先创建两个用于保存车站名称与起售时间的列表,然后创建query_time()方法,用于发送查询车票起售时间的网络请求,这里是post请求,而且会用到表单参数{“station_telecode”:station}。将返回的信息添加至对应的列表当中,最后将两个列表信息返回。
station_name_list=[] #保存起售车站名称列表
station_time_list=[] #保存起售车站对应时间列表
#查询车票起售时间
def query_time(station):
station_name_list.clear()
station_time_list.clear()
#读取所有车站并转换为字典类型
stations=eval(read('time.text'))
url='https://www.12306.cn/index/otn/index12306/queryScSname'
#表单参数,station参数为需要搜索车站的英文缩写
form_data={"station_telecode":station}
response=requests.post(url,data=form_data,verify=True) # 请求并进行验证
response.encoding='utf-8' # 对请求所返回的数据进行编码
json_data=json.loads(response.text) # 解析json数据
data=json_data.get('data') # 获取json中可用数据,也就是查询车站所对应的站名
for i in data: # 遍历查询车站所对应的所有站名
if i in stations: # 在站名时间文件中,判断是否存在该站名
station_name_list.append(i) # 有该站名就将站名添加至列表中
for name in station_name_list: # 遍历筛选后的站名
time=stations.get(name) # 通过站名获取对应的时间
station_time_list.append(time) # 将时间保存至列表
return station_name_list,station_time_list
运行结果如图:(这个是可以运行出来的,因为它和模块一车票查询用到的url是不一样的,不会对12306造成损失。如果运行不出结果,就说明代码有问题了)
模块三:卧铺售票分析(比较难理解)
先给出我整理的复杂的步骤,看这密密麻麻的字就知道内容很多
主要分为两个部分:部分一是卧铺售票分析区域。第二个部分卧铺车票数量折线图。下面分别来说这两个部分:
模块三部分一:卧铺售票的查询与分析
- 在show_window.py文件中创建query_ticketing_analysis_click()方法,作为卧铺售票分析查询按钮的事件处理方法。该方法先获取输入的出发地和目的地。再调用query_ticketing_analysis()方法分别查询今天、三天内、五天内的卧铺信息。(query_ticketing_analysis()方法在后面的代码5)
#卧铺售票分析查询按钮的事件处理方法
def query_ticketing_analysis_click(self):
self.info_table=[] # 保存窗体表格的车次信息
today_car_list.clear() # 清空今天列车信息,已处理是否有票
three_car_list.clear() # 清空三天列车信息,已处理是否有票
five_car_list.clear() # 清空五天列车信息,已处理是否有票
today_list.clear() # 清空今天列车信息,未处理是否有票
three_list.clear() # 清空三天列车信息,未处理是否有票
five_list.clear() # 清空五天列车信息,未处理是否有票
get_from=self.textEdit_analysis_from.toPlainText() # 获取出发地
get_to=self.textEdit_analysis_to.toPlainText() # 获取到达地
stations=eval(read('stations.text')) # 读取所有车站并转换为字典类型
if get_from!="" and get_to!="":
if get_from in stations and get_to in stations:
from_station=stations[get_from] # 在所有车站文件中找到对应的参数,出发地
to_station=stations[get_to] # 目的地
today=datetime.datetime.now() # 获取当天日期
three_set=datetime.timedelta(days=+2) # 三天内偏移天数
five_set=datetime.timedelta(days=+4) # 五天内偏移天数
three_day=(today+three_set).strftime('%Y-%m-%d') # 三天格式化后的日期
five_day=(today+five_set).strftime('%Y-%m-%d') # 五天格式化后的日期
today=today.strftime('%Y-%m-%d') # 今天格式化后的日期
#发送查询今天卧铺票信息的网络请求,并获取返回的信息
query_ticketing_analysis(today,from_station,to_station,1)
# 发送查询三天内卧铺票信息的网络请求,并获取返回的信息
query_ticketing_analysis(three_day,from_station,to_station,3)
# 发送查询五天内卧铺票信息的网络请求,并获取返回的信息
query_ticketing_analysis(five_day,from_station,to_station,5)
上面这个代码比较好理解。假如这里的today为2020-08-19,那么three_day就是2020-08-21,five_day就是2020-08-23。
- 然后在query_ticketing_analysis_click()方法中,将所有车次信息进行整合及筛选。这里用到了集合set。因为集合最好的一个用途就是去掉重复元素,集合中每个元素都是唯一的。主要是两个for循环
info_set=set() # 创建筛选车次集合,将相同车次进行整合,查看共有几趟列车
for i in today_car_list+three_car_list+five_car_list:
#因为在集合中必须是字符串才能整合,所以将车次信息转换为字符串类型,方便车次整合
info_set.add(str(i[0:6]))
for info in info_set: # 遍历车次信息
info=eval(info) # 将车次信息再次转换成列表
is_today_ture=False # 判断今天是否存在某趟列车的标记
for i in today_car_list: # 遍历今天的车次信息,该车次信息是没有筛选的信息
if info[0] in i: # 判断整合后的车次,在今天的车次信息中是否存在
is_today_ture=True # 存在就进行标记
info.append(i[6]) # 如果存在就将车次信息中是否有卧铺的信息添加至整合后的车次信息中
break
if is_today_ture==False: # 如果今天没有某一趟车信息就标记为--
info.append('--')
is_three_true=False
for i in three_car_list:
if info[0] in i:
is_three_true=True
info.append(i[6])
break
if is_three_true==False:
info.append('--')
is_five_true=False
for i in five_car_list:
if info[0] in i:
is_five_true=True
info.append(i[6])
break
if is_five_true==False:
info.append('--')
self.info_table.append(info) # 将最后的结果添加至窗体表格的列表中
这里可能比较难理解一些!有的人可能会问为什么要整合筛选车次信息呢?怎么整合的呢?举个例子吧,比较好理解一些。
假如上面1的代码调用query_ticketing_analysis()方法之后today_car_list,three_car_list,five_car_list的结果分别是:
today_car_list=[['k105','北京西','深圳','23:18','04:20','29:02','无']]
three_car_list=[['k105','北京西','深圳','23:18','04:20','29:02','有'],
['z313','北京','深圳','08:30','12:50','28:20','有'],]
five_car_list=[['k105','北京西','深圳','23:18','04:20','29:02','有'],
['z313','北京','深圳','08:30','12:50','28:20','有'],]
上面三个列表关于车次k105的一些基本信息(车次、出发地、目的地、出发时间、到达时间、历时)重复了三次,有些累赘;而车次z313重复了两次。这些重复信息只出现一次就可以了,所以要用set集合对他们进行整合筛选。
for i in today_car_list+three_car_list+five_car_list:
#因为在集合中必须是字符串才能整合,所以将车次信息转换为字符串类型,方便车次整合
info_set.add(str(i[0:6]))
如果是同一个车次,那么today_car_list,three_car_list,five_car_list三个列表中前6个元素(i[0]到i[5])的内容都是相同的。这是第一个for循环,每次将i[0]到i[5]的内容(也就是车次、出发地、目的地、出发时间、到达时间、历时)转成字符串,再添加到集合里,可以过滤掉多余的重复信息。
也就是说today_car_list,three_car_list,five_car_list三个列表的最后一个 ‘有’或’无’ 不在info_set里面。这时候info_set里的内容就是下图所示,每个车次的基本信息只出现一次,而没有重复。
{"['k105', '北京西', '深圳', '23:18', '04:20', '29:02']",
"['z313', '北京', '深圳', '08:30', '12:50', '28:20']"}
代码2的第2个for循环是将today_car_list,three_car_list,five_car_list三个列表中的最后一个元素 ‘有’或’无’ 添加到info里面。如果列表中没有该车次,就将’- -‘添加到info里面。就如today_car_list没有z313车次,所以在info里面z313车次的“今天”卧铺票信息就是’- -’。执行第2个for循环后,info里的内容就是下图所示
[['k105', '北京西', '深圳', '23:18', '04:20', '29:02', '无', '有', '有'],
['z313', '北京', '深圳', '08:30', '12:50', '28:20', '--', '有', '有']]
- 然后在query_ticketing_analysis_click()方法中,将已经筛选好的车次信息与对应卧铺是否有票信息显示在主窗体的表格中,然后对车次与卧铺信息进行积分计算。
self.tableWidget.setRowCount(len(self.info_table)) # 设置表格行数
self.tableWidget.setColumnCount(9)
#设置表格内容文字大小
font=QtGui.QFont()
font.setPointSize(12)
self.tableWidget.setFont(font)
#根据窗体大小拉伸表格
self.tableWidget.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
#遍历最终的信息
for row in range(len(self.info_table)):
fraction=0 # 分数,根据该分数判断列车的紧张程度
for column in range(9):
if column==6: # 如果某趟列车当天无票
if self.info_table[row][column]=='无' or self.info_table[row][column]=='--':
fraction+=3 # 计三分
if column==7:
if self.info_table[row][column]=='无' or self.info_table[row][column]=='--':
fraction+=2
if column==8:
if self.info_table[row][column]=='无' or self.info_table[row][column]=='--':
fraction+=1
#分数大于或等于5分的车次为红色,说明该车次卧铺非常紧张
if fraction>=5:
#定位是哪趟车次符合该条件,遍历该车次信息
for i in range(len(self.info_table[row])):
#表格中的信息
item=QtWidgets.QTableWidgetItem(self.info_table[row][i])
item.setBackground(QColor(255,0,0)); # 设置该车次背景颜色
self.tableWidget.setItem(row,i,item) # 设置表格显示的内容
#橙色,说明改车次卧铺紧张
if 1 <= fraction <= 4:
#定位是哪趟车次符合该条件,遍历该车次信息
for i in range(len(self.info_table[row])):
#表格中的信息
item=QtWidgets.QTableWidgetItem(self.info_table[row][i])
item.setBackground(QColor(255,170,0)); # 设置该车次背景颜色
self.tableWidget.setItem(row,i,item) # 设置表格显示的内容
#说明该车次卧铺不紧张
if fraction==0:
#定位是哪趟车次符合该条件,遍历该车次信息
for i in range(len(self.info_table[row])):
#表格中的信息
item=QtWidgets.QTableWidgetItem(self.info_table[row][i])
item.setBackground(QColor(85,170,0)); # 设置该车次背景颜色
self.tableWidget.setItem(row,i,item) # 设置表格显示的内容
根据该代码就可以得出k105得分是3分,属于卧铺紧张。z313也是3分,属于卧铺紧张。
- query_ticketing_analysis_click()方法的整体代码如下
#卧铺售票分析查询按钮的事件处理方法
def query_ticketing_analysis_click(self):
self.info_table=[] # 保存窗体表格的车次信息
today_car_list.clear() # 清空今天列车信息,已处理是否有票
three_car_list.clear() # 清空三天列车信息,已处理是否有票
five_car_list.clear() # 清空五天列车信息,已处理是否有票
today_list.clear() # 清空今天列车信息,未处理是否有票
three_list.clear() # 清空三天列车信息,未处理是否有票
five_list.clear() # 清空五天列车信息,未处理是否有票
get_from=self.textEdit_analysis_from.toPlainText() # 获取出发地
get_to=self.textEdit_analysis_to.toPlainText() # 获取到达地
stations=eval(read('stations.text')) # 读取所有车站并转换为字典类型
if get_from!="" and get_to!="":
if get_from in stations and get_to in stations:
from_station=stations[get_from] # 在所有车站文件中找到对应的参数,出发地
to_station=stations[get_to] # 目的地
today=datetime.datetime.now() # 获取当天日期
three_set=datetime.timedelta(days=+2) # 三天内偏移天数
five_set=datetime.timedelta(days=+4) # 五天内偏移天数
three_day=(today+three_set).strftime('%Y-%m-%d') # 三天格式化后的日期
five_day=(today+five_set).strftime('%Y-%m-%d') # 五天格式化后的日期
today=today.strftime('%Y-%m-%d') # 今天格式化后的日期
#发送查询今天卧铺票信息的网络请求,并获取返回的信息
query_ticketing_analysis(today,from_station,to_station,1)
# 发送查询三天内卧铺票信息的网络请求,并获取返回的信息
query_ticketing_analysis(three_day,from_station,to_station,3)
# 发送查询五天内卧铺票信息的网络请求,并获取返回的信息
query_ticketing_analysis(five_day,from_station,to_station,5)
info_set=set() # 创建筛选车次集合,将相同车次进行整合,查看共有几趟列车
for i in today_car_list+three_car_list+five_car_list:
#因为在集合中必须是字符串才能整合,所以将车次信息转换为字符串类型,方便车次整合
info_set.add(str(i[0:6]))
for info in info_set: # 遍历车次信息
info=eval(info) # 将车次信息再次转换成列表
is_today_ture=False # 判断今天是否存在某趟列车的标记
for i in today_car_list: # 遍历今天的车次信息,该车次信息是没有筛选的信息
if info[0] in i: # 判断整合后的车次,在今天的车次信息中是否存在
is_today_ture=True # 存在就进行标记
info.append(i[6]) # 如果存在就将车次信息中是否有卧铺的信息添加至整合后的车次信息中
break
if is_today_ture==False: # 如果今天没有某一趟车信息就标记为--
info.append('--')
is_three_true=False
for i in three_car_list:
if info[0] in i:
is_three_true=True
info.append(i[6])
break
if is_three_true==False:
info.append('--')
is_five_true=False
for i in five_car_list:
if info[0] in i:
is_five_true=True
info.append(i[6])
break
if is_five_true==False:
info.append('--')
self.info_table.append(info) # 将最后的结果添加至窗体表格的列表中
self.tableWidget.setRowCount(len(self.info_table)) # 设置表格行数
self.tableWidget.setColumnCount(9)
#设置表格内容文字大小
font=QtGui.QFont()
font.setPointSize(12)
self.tableWidget.setFont(font)
#根据窗体大小拉伸表格
self.tableWidget.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
#遍历最终的信息
for row in range(len(self.info_table)):
fraction=0 # 分数,根据该分数判断列车的紧张程度
for column in range(9):
if column==6: # 如果某趟列车当天无票
if self.info_table[row][column]=='无' or self.info_table[row][column]=='--':
fraction+=3 # 计三分
if column==7:
if self.info_table[row][column]=='无' or self.info_table[row][column]=='--':
fraction+=2
if column==8:
if self.info_table[row][column]=='无' or self.info_table[row][column]=='--':
fraction+=1
#分数大于或等于5分的车次为红色,说明该车次卧铺非常紧张
if fraction>=5:
#定位是哪趟车次符合该条件,遍历该车次信息
for i in range(len(self.info_table[row])):
#表格中的信息
item=QtWidgets.QTableWidgetItem(self.info_table[row][i])
item.setBackground(QColor(255,0,0)); # 设置该车次背景颜色
self.tableWidget.setItem(row,i,item) # 设置表格显示的内容
#橙色,说明改车次卧铺紧张
if 1 <= fraction <= 4:
#定位是哪趟车次符合该条件,遍历该车次信息
for i in range(len(self.info_table[row])):
#表格中的信息
item=QtWidgets.QTableWidgetItem(self.info_table[row][i])
item.setBackground(QColor(255,170,0)); # 设置该车次背景颜色
self.tableWidget.setItem(row,i,item) # 设置表格显示的内容
#说明该车次卧铺不紧张
if fraction==0:
#定位是哪趟车次符合该条件,遍历该车次信息
for i in range(len(self.info_table[row])):
#表格中的信息
item=QtWidgets.QTableWidgetItem(self.info_table[row][i])
item.setBackground(QColor(85,170,0)); # 设置该车次背景颜色
self.tableWidget.setItem(row,i,item) # 设置表格显示的内容
5.在query_request.py中 创建query_ticketing_analysis()方法,在该方法中首先发送查询请求,然后分别对今天、三天内、五天内的车票信息进行处理与分类。
today_car_list=[] # 保存今天列车信息,已经处理是否有票
three_car_list=[] # 保存三天列车信息,已经处理是否有票
five_car_list=[] # 保存五天列车信息,已经处理是否有票
today_list=[] # 保存今天列车信息,未处理是否有票
three_list=[] # 保存三天列车信息,未处理是否有票
five_list=[] # 保存五天列车信息,未处理是否有票
#查询卧铺售票分析数据
def query_ticketing_analysis(date,from_station,to_station,which_day):
#查询请求地址
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)
#发送查询请求
response=requests.get(url)
# 将json数据转换为字典类型,通过键值对取数据
result=response.json()
result=result['data']['result']
# 判断车站文件是否存在
if is_stations('stations.text') :
stations=eval(read('stations.text')) # 读取所有车站并转换为字典类型
if len(stations)!=0: # 判断返回数据是否为空
for i in result:
# 分割数据并添加到列表中
tmp_list=i.split('|')
#print(tmp_list)
#因为查询结果中出发站和到达站为站名的缩写字母,所以需要在车站库中找到对应的车站名称
from_station=list(stations.keys())[list(stations.values()).index(tmp_list[6])]
to_station=list(stations.keys())[list(stations.values()).index(tmp_list[7])]
#创建座位数组,其中包含高级软卧、软卧、硬卧
seat=[tmp_list[3],from_station,to_station,tmp_list[8],tmp_list[9],tmp_list[10],
tmp_list[21],tmp_list[23],tmp_list[28]]
#print(seat)
#判断今天的车次信息
if which_day==1:
#将高铁、动、C开头的车次排除
if seat[0].startswith('G') is False and seat[0].startswith('D') is False and seat[0].startswith('C') is False:
#将高级软卧、软卧、硬卧未处理信息添加至列表中
today_list.append(seat)
#判断某车次是否有票
new_seat=is_ticket(tmp_list,from_station,to_station)
#将判断后的车次信息添加至对应的列表中
today_car_list.append(new_seat)
#判断三天的车次信息
if which_day==3:
if seat[0].startswith('G') is False and seat[0].startswith('D') is False and seat[0].startswith('C') is False:
three_list.append(seat)
new_seat=is_ticket(tmp_list,from_station,to_station)
three_car_list.append(new_seat)
#判断五天的车次信息
if which_day==5:
if seat[0].startswith('G') is False and seat[0].startswith('D') is False and seat[0].startswith('C') is False:
five_list.append(seat)
new_seat=is_ticket(tmp_list,from_station,to_station)
five_car_list.append(new_seat)
此代码运行完之后,得到的6个列表的数据类似于下图:
today_list、three_list、five_list包含的信息主要的是:车次、出发地、目的地、出发时间、到达时间、历时、高级软卧票数、软卧票数、硬卧票数
today_car_list、three_car_list、five_car_list包含的信息主要的是:车次、出发地、目的地、出发时间、到达时间、历时、是否有卧铺票
today_list=[['k105','北京西','深圳','23:18','04:20','29:02','--','无','无']]
today_car_list=[['k105','北京西','深圳','23:18','04:20','29:02','无']]
three_list=[['k105','北京西','深圳','23:18','04:20','29:02','--','有','有'],
['z313','北京','深圳','08:30','12:50','28:20','--','无','有'],]
three_car_list=[['k105','北京西','深圳','23:18','04:20','29:02','有'],
['z313','北京','深圳','08:30','12:50','28:20','有'],]
five_list=[['k105','北京西','深圳','23:18','04:20','29:02','有','有','有'],
['z313','北京','深圳','08:30','12:50','28:20','无','10','有'],]
five_car_list=[['k105','北京西','深圳','23:18','04:20','29:02','有'],
['z313','北京','深圳','08:30','12:50','28:20','有'],]
today_list、three_list、five_list三个列表主要用于后面的计算并显示卧铺数量的折线图
today_car_list、three_car_list、five_car_li列表主要用某车次是否还有卧铺车票以及分析车票的紧张程度
- 上面的query_ticketing_analysis()方法调用了is_ticket()方法,用于判断某车次是否还有卧铺车票。如果高级软卧、软卧、硬卧其中有一个有票的话(对应的是’有’或者是数字),就说明该类车次有卧铺车票。否则就是没票
#判断高级软卧、软卧、硬卧是否有票
def is_ticket(tmp_list,from_station,to_station):
# 判断高级软卧、软卧、硬卧任何一个有票的话,就说明该趟车有卧铺票
if tmp_list[21]=='有' or tmp_list[23]=='有' or tmp_list[28]=='有':
tmp_tem='有'
else:
# 判断高级软卧、软卧、硬卧对应的如果是数字说明也有票,其他为无票
if tmp_list[21].isdigit() or tmp_list[23].isdigit() or tmp_list[28].isdigit():
tmp_tem='有'
else:
tmp_tem='无'
#创建新的座位列表,显示某趟车是否有卧铺票
new_seat=[tmp_list[3],from_station,to_station,tmp_list[8],tmp_list[9],tmp_list[10],tmp_tem]
return new_seat
运行结果如下图:(如果运行不出来结果是正常的,还是因为12306不让你爬取它哦,主要还是理解代码)
模块三部分二:卧铺车票折线图绘制
- 在show_window.py文件中创建show_brokenn_line()方法,用于显示卧铺车票数量的折线图。主要会用到today_list、three_list、five_list这三个列表的信息。
首先对每个车次中今天、三天内、五天内的卧铺数量分别进行统计(调用statistical_quantity()方法),然后实现当车次信息量大时,添加滚动条扩大折线图高度,然后创建自定义的画布对象,在调用broken_line()方法实现折线图的绘制,最后将折线图显示在主窗体的水平布局中。
# 显示卧铺车票数量折线图
def show_broken_line(self):
train_number_list=[] # 保存车次
tickets_number_list=[] # 保存今天,三天内,五天内所有车次的卧铺数量
# 遍历车次信息
for train_number in self.info_table:
number_list=[] # 临时保存车票数量
if self.horizontalLayout.count()!=0:
# 每次点循环删除管理器的组件
while self.horizontalLayout.count():
item=self.horizontalLayout.takeAt(0) # 获取第一个组件
widget=item.widget() # 删除组件
widget.deleteLater()
is_today_true=False # 判断今天是否存在某趟列车的标记
for today in today_list:
# 判断今天的车次信息中是否有该车次
if train_number[0] in today:
is_today_true=True # 存在就进行标记
number=self.statistical_quantity(today[6:9]) # 调用统计车票数量的方法
number_list.append(number) # 将车票数量添加至临时列表中
break
if is_today_true==False: # 如果今天没有某一趟列车,说明该车次无票为0
number_list.append(0)
is_three_true = False
for three_today in three_list:
if train_number[0] in three_today:
is_three_true = True
number = self.statistical_quantity(three_today[6:9])
number_list.append(number)
break
if is_three_true == False:
number_list.append(0)
is_five_true = False
for five_today in five_list:
if train_number[0] in five_today:
is_five_true = True
number = self.statistical_quantity(five_today[6:9])
number_list.append(number)
break
if is_five_true == False:
number_list.append(0)
tickets_number_list.append(number_list)
train_number_list.append(train_number[0])
#车次信息大时,添加滚动条扩大折线图高度
if len(train_number_list)>=9:
self.scrollAreaWidgetContents.setMinimumHeight(len(train_number_list)*30)
self.horizontalLayoutWidget.setGeometry(QtCore.QRect(0,0,951,(len(train_number_list)*30)))
#创建画布对象
line=PlotCanvas()
line.broken_line(tickets_number_list,train_number_list) # 调用折线图方法
self.horizontalLayout.addWidget(line) # 将折线图添加至底部水平布局当中
- 在show_windos.py文件中创建statistical_quantity()方法,用于统计车票数量。如果是有,增加20个车票;如果是无,增加0个车票;如果是数字,就增加对应的数字。
#统计车票数量
def statistical_quantity(selfself,msg):
number=0 # 车票初始值
for i in msg:
if i=='有': # 如果是有,增加20个车票
number+=20
if i=='无' or i=='': # 如果是无或者空,就增加0个车票
number+=0
if i.isdigit(): # 如果是数字,就直接增加对应的数字
number+=int(i)
return number
- 创建chart.py文件,在该文件中创建PlotCanvas类,通过__init()__()方法进行初始化,创建图形以及初始化画布。然后创建broken_line()方法,用于显示车票走势的折线图。(感觉基本上都是固定的写法)
import matplotlib # 导入图表模块
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas # 图形画布
import matplotlib.pyplot as plt # 导入绘图模块
class PlotCanvas(FigureCanvas):
def __init__(self,parent=None,width=0,height=0,dpi=100):
#避免中文乱码
matplotlib.rcParams['font.sans-serif']=['SimHei']
matplotlib.rcParams['axes.unicode_minus']=False
#创建图形
fig=plt.figure(figsize=(width,height),dpi=dpi)
#初始化图形画布
FigureCanvas.__init__(self,fig)
self.setParent(parent) # 设置父类
def broken_line(self,number,train_list):
"""
linewidth:折线的宽度
marker:折点的形状
markerfacecolor:折点实心颜色
markersize:折点大小
"""
day_x=['今天','三天内','五天内'] # x轴折线点
for index,n in enumerate(number):
#绘制折线
plt.plot(day_x,n,linewidth=1,marker='o',markerfacecolor='blue',markersize=8,label=train_list[index])
plt.legend(bbox_to_anchor=(-0.03,1)) # 让图例生效。并设置图例显示位置
plt.title('卧铺车票数量走势图')
运行结果如下图:(同样运行不出来折线图是正常的)
哎呀呀呀!我终于写完了。
感觉写的好长,自己完成的一个小项目,记录一下吧。
有什么问题或者需要源代码的,可以评论。我看到的就会回复!!!