说明文字:

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_stationsif __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文件
    配置链接:https://blog.csdn.net/wang_hugh/article/details/88775868

用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显示与控制窗体代码。

项目说明:根据上面设计的主窗体效果图,可以看出来,该项目主要分为三大模块:车票查询、卧铺售票分析、车票起售时间。第二个模块最复杂,所以先说简单的两个模块

****小小提示:以下内容把我自己手写总结的图和代码配合起来看,比较容易理解 * ***

模块一:车票查询:

下面这个图是我整理的车票查询的步骤:

关键代码如下:

  1. 在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_()
  1. 在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=snewSeat.append(s)  # 保存新的座位信息data.append(newSeat)print(newSeat)return data    # 返回整理好的车次信息

代码中 print(response.url) 如果输出的是:https://www.12306.cn/mormhweb/logFiles/error.html 也就是上面说的错误页面,就说明12306爬虫失败,你的ip应该被锁住了。这种情况很正常,不必担心。

  1. 在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)
  1. 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 Trueexcept: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)

运行结果如下图:(运行不出来的话,也别担心,我也只是偶尔一次运行成功了。这个图片是网上找到的,就是让大家看看结果是什么样子,有便于更好的理解代码,心态放好哈哈哈哈哈)

模块二:车票起售时间查询

这个模块比较简单些,我整理的步骤如下:

  1. 在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的时候设置换行 行数+1if x==0:i+=1self.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)# 设置高度为动态高度根据行数确定高度 每行300self.scrollAreaWidgetContents_2.setMinimumHeight((i+1)*100)# 设置网格布局控件动态高度self.gridLayoutWidget.setGeometry(QtCore.QRect(0,0,950,((i+1)*100)))
  1. 在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造成损失。如果运行不出结果,就说明代码有问题了)

模块三:卧铺售票分析(比较难理解)

先给出我整理的复杂的步骤,看这密密麻麻的字就知道内容很多

主要分为两个部分:部分一是卧铺售票分析区域。第二个部分卧铺车票数量折线图。下面分别来说这两个部分:

模块三部分一:卧铺售票的查询与分析

  1. 在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。

  1. 然后在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])      # 如果存在就将车次信息中是否有卧铺的信息添加至整合后的车次信息中breakif is_today_ture==False:       # 如果今天没有某一趟车信息就标记为--info.append('--')is_three_true=Falsefor i in three_car_list:if info[0] in i:is_three_true=Trueinfo.append(i[6])breakif is_three_true==False:info.append('--')is_five_true=Falsefor i in five_car_list:if info[0] in i:is_five_true=Trueinfo.append(i[6])breakif 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', '--', '有', '有']]
  1. 然后在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+=2if 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分,属于卧铺紧张。

  1. 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])      # 如果存在就将车次信息中是否有卧铺的信息添加至整合后的车次信息中breakif is_today_ture==False:       # 如果今天没有某一趟车信息就标记为--info.append('--')is_three_true=Falsefor i in three_car_list:if info[0] in i:is_three_true=Trueinfo.append(i[6])breakif is_three_true==False:info.append('--')is_five_true=Falsefor i in five_car_list:if info[0] in i:is_five_true=Trueinfo.append(i[6])breakif 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+=2if 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列表主要用某车次是否还有卧铺车票以及分析车票的紧张程度

  1. 上面的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不让你爬取它哦,主要还是理解代码)

模块三部分二:卧铺车票折线图绘制

  1. 在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)   # 将车票数量添加至临时列表中breakif is_today_true==False:    # 如果今天没有某一趟列车,说明该车次无票为0number_list.append(0)is_three_true = Falsefor three_today in three_list:if train_number[0] in three_today:is_three_true = Truenumber = self.statistical_quantity(three_today[6:9])number_list.append(number)breakif is_three_true == False:number_list.append(0)is_five_true = Falsefor five_today in five_list:if train_number[0] in five_today:is_five_true = Truenumber = self.statistical_quantity(five_today[6:9])number_list.append(number)breakif 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)                    # 将折线图添加至底部水平布局当中
  1. 在show_windos.py文件中创建statistical_quantity()方法,用于统计车票数量。如果是有,增加20个车票;如果是无,增加0个车票;如果是数字,就增加对应的数字。
#统计车票数量
def statistical_quantity(selfself,msg):number=0                      # 车票初始值for i in msg:if i=='有':               # 如果是有,增加20个车票number+=20if i=='无' or i=='':      # 如果是无或者空,就增加0个车票number+=0if i.isdigit():           # 如果是数字,就直接增加对应的数字number+=int(i)return number
  1. 创建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('卧铺车票数量走势图')

运行结果如下图:(同样运行不出来折线图是正常的)

哎呀呀呀!我终于写完了。
感觉写的好长,自己完成的一个小项目,记录一下吧。
有什么问题或者需要源代码的,可以评论。我看到的就会回复!!!

python爬取12306(火车票分析助手)相关推荐

  1. Python 爬取12306火车票

    获取火车站 stations.py #import certifi #import urllib3 import re import requests from pprint import pprin ...

  2. python爬火车票是不是违法_python利用selenium+requests+beautifulsoup爬取12306火车票信息...

    在高速发展的时代.乘车出远门是必不可少的,有些查询信息是要收费的.这里打造免费获取火车票信息 想要爬取12306火车票信息,访问12306官方网站,输入出发地,目的地  ,时间  之后点击确定,这是我 ...

  3. 2021最新 python爬取12306列车信息自动抢票并自动识别验证码(三)购票篇

    项目前言 tiebanggg又来更新了,项目--[12306-tiebanggg-master]注:本项目仅供学习研究,如若侵犯到贵公司权益请联系我第一时间进行删除:切忌用于一切非法途径,否则后果自行 ...

  4. 2021最新python爬取12306列车信息自动抢票并自动识别验证码

    项目描述 项目前言 tiebanggg又来更新了,项目--[12306-tiebanggg-master]注:本项目仅供学习研究,如若侵犯到贵公司权益请联系我第一时间进行删除:切忌用于一切非法途径,否 ...

  5. python爬取12306列车信息自动抢票并自动识别验证码(一)列车数据获取篇

    项目前言 自学python差不多有一年半载了,这两天利用在甲方公司搬砖空闲之余写了个小项目--[12306-tiebanggg-master].注:本项目仅供学习研究,如若侵犯到贵公司权益请联系我第一 ...

  6. python爬取12306列车信息自动抢票并自动识别验证码(二)selenium登录验证篇

    项目前言 自学python差不多有一年半载了,这两天利用在甲方公司搬砖空闲之余写了个小项目--[12306-tiebanggg-master]注:本项目仅供学习研究,如若侵犯到贵公司权益请联系我第一时 ...

  7. python 爬取12306数据

    学了好久的 java  换个语言试试 就选择了 简单易学的python ,学了一段时间看到别人都在爬取12306网站的数据,我也尝试尝试,发现 12306网站的数据更新太快.返回的json数据变得越来 ...

  8. Python爬取并简单分析2024年普通高校招生专业(专业类)选考科目要求

    推荐教材: <Python网络程序设计(微课版)>,ISBN:978-7-302-58312-7,董付国,清华大学出版社,2021年8月出版,京东.当当.淘宝均有销售 配套资源: 教学大纲 ...

  9. 使用python爬取12306上面所有车次数据

    在爬取12306之前需要做的工作就是: 1,分析请求过程 2,分析是否需要处理cookie 3,编写代码 4,测试爬取网站是否有访问次数限制 5,部署到正式服务器上 这里重点写 如何分析请求过程: 这 ...

  10. Python爬取12306车票信息

    Python3爬取12306车票信息 第一次写爬虫,咱从入门级--12306车票爬取 开始 我们要爬取的信息是https://www.12306.cn/index/上的车票信息 当我们选择出发地和目的 ...

最新文章

  1. Bootstrap按钮
  2. asp.net 路径(转)
  3. Lua 脚本获取 EVAL EVALSHA 命令的参数
  4. bim 模型web页面展示_BIM+装配式建筑工程师2020年必须拿下的技能证书
  5. postgresql update使用别名_PostgreSQL逻辑复制之pglogical
  6. [No0000111]java9环境变量配置bat
  7. Win8下怎样安装Win7 or Win7下怎样安装win8?
  8. java 二进制文件 数据库_java从数据库中读取二进制文件并....
  9. react-native多图选择、图片裁剪(支持ad/ios图片个数控制)
  10. python路径在哪里设置_找Python安装目录,设置环境路径以及在命令行运行python脚本实例...
  11. php无限分类下载,php无限分类
  12. 【李宏毅2020 ML/DL】P77 Generative Adversarial Network | Theory behind GAN
  13. 如何判断各个IE浏览器版本
  14. java: org.luaj.vm2.LuaError:XXX module not found lua脚本初始化出错(转)
  15. opencv中rect的用法
  16. 微信小程序商店:极乐小程序商店推荐之601~700
  17. Excel文件转换为XML以及Linux文件编码格式转换
  18. springboot配置druid内置监控页面
  19. 44444444444444
  20. 帆软报表入门操作(一)

热门文章

  1. 嵌入式的日常工作内容有哪些?
  2. 使用server酱进行服务器端口异常报警
  3. 集合删除元素技巧 removeIf
  4. 华为设备命令总结(转)
  5. 新一代AI顾泽苍:自组织概率模型SDL主导的机器学习(公号回复“顾泽苍AI”下载PDF资料,欢迎转发、赞赏支持科普)
  6. C++性能优化(一)——应用程序性能优化简介
  7. r星你的计算机网络正在自动发送查询,当你在《GTA4》中的网吧,输入了知名网站会怎样?网费真贵!...
  8. Frida官方文档-Gadget
  9. php旅行社系统开题报告,旅行社管理系统毕业及设计开题报告范文.doc
  10. Python核心编程朱红庆_朱红庆作品_朱红庆简介_朱红庆作品大全-当当网