【Python大神教你 】实现12306余票监控
前言
由于经常会遇到没票等现象,所以需要使用软件进行抢票,但由于一些软件有优先级问题,对于不舍得花钱的我来讲,并不是很好的体验,所以想自己尝试写一个类似的功能。
环境/插件
- Python3.6
- urllib(用于请求数据)
- smtplib(发送邮件)
文件目录
GrabTicket文件夹
-
city.py(12306城市)
-
GrabTicket.py(入口文件)
-
GrabTicketOperation.py(主文件)
-
GrabTicketSmtp.py(发送邮件文件)
分析
首先我们打开12306余票查询窗口
上图红色框的地方,就是表示列车有无车票的地方,我们需要根据这里边的数据来判断。
这里边有一些需要注意的就是,里边表示有票的有字符串“有”和数字“2”,所以我们需要对这两种情况进行判断。
接下来我们使用浏览器的开发者工具,来检查看看是否有接口可以使用:
这里我们可以看出,12306的列车查询是使用接口调用的,我们再来看看接口返回的数据:
我们可以清晰的看到,我们需要的列车数据是在data里边的result里边的数组,所以后面,我们只需要获取这里边的数据来判断就可以了。
分析数据
首先,我们得先分析数据,得出我们需要的数据字段,所以我们先写一段程序用来分析:
【python学习qq裙:10667510 送入门学习资料,萌新程序媛大本营】
# 复制接口数据result里边的一条数据出来分析
results = ["null|23:00-06:00系统维护时间|6i000D312606|D3126|IOQ|NJH|IOQ|AOH|07:00|18:43|11:43|IS_TIME_NOT_BUY|qYF9CwzWBb4rPwv7Upcl6nOKai0yleG2FqmgmU4EFKXjmLhu|20180721|3|Q6|01|28|1|0|||||||有||||有|无|||O0M0O0|OMO|0"]
# 初始化数组键值
c = 0
# 对结果集进行循环
for i in results:
# 将数据拆分成新的数组,并进行循环
for n in i.split('|'):
# 输出数组中每一个的数据n,以及下标值c
print('[%s] %s' %( c,n ))
# 下标值+1
c += 1
# 重置下标值c
c = 0
# 多个数据换行
print('\n\t')
运行代码,我们来看看效果图:
测试多几次之后,我们可以得出我们需要的数据所在的位置,接下来我们修改下程序进行输出:
# 复制接口数据result里边的一条数据出来分析 results = ["null|23:00-06:00系统维护时间|6i000D312606|D3126|IOQ|NJH|IOQ|AOH|07:00|18:43|11:43|IS_TIME_NOT_BUY|qYF9CwzWBb4rPwv7Upcl6nOKai0yleG2FqmgmU4EFKXjmLhu|20180721|3|Q6|01|28|1|0|||||||有||||有|无|||O0M0O0|OMO|0"] j = 1 # 初始化数组下标值 c = 0 # 初始化列车数组的下标值 index = 0 # 初始化列车数组 trains = [] # 对结果集进行循环 for i in results: # 为列车数组新增一个空数组元素 trains.append([]) # 将数据拆分成新的数组,并进行循环 for n in i.split('|'): # 输出数组中每一个的数据n,以及下标值c print('[%s] %s' %( c,n )) # 将每一个数据依次放入到列车数组中 trains[index].append(n) # 下标值+1 c += 1 # 重置下标值c c = 0 # 多个数据换行 print('\n\t') # 列车数组下标值+1 index += 1 # 对处理好的列车数组进行循环遍历 for train in trains: # 打印我们所需要的数据 print('火车:%s' %(train[3])) print('出发地:%s' %(train[6])) print('目的地:%s' %(train[7])) print('发车时间:%s' %(train[8])) print('到达时间:%s' %(train[9])) print('历时时间:%s' %(train[10])) print('商务座/特等座:%s' %(train[32])) print('一等座:%s' %(train[31])) print('二等座:%s' %(train[30])) print('高级软卧:%s' %(train[21])) print('软卧:%s' %(train[23])) print('硬卧:%s' %(train[28])) print('硬座:%s' %(train[29])) print('无座:%s' %(train[26])) print('\n\t')
运行,我们来看看结果:
这里边就是我们的结果集了,我们去12306页面对照一下:
看,我们的数据能对的上,证明我们已经分析对了数据,接下来,我们就可以实现我们的爬虫代码了:
from urllib import request import ssl import json # 通过爬虫爬取数据 def getTrains(): # 请求地址 url = 'https://kyfw.12306.cn/otn/leftTicket/query?leftTicketDTO.train_date=2018-07-21&leftTicketDTO.from_station=SZQ&leftTicketDTO.to_station=SHH&purpose_codes=ADULT' # 请求头 headers = { 'User-Agent': r'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36' } # 设置请求 req = request.Request(url, headers=headers) # 发送请求 html = request.urlopen(req).read().decode('utf-8') # 格式化数据 dict = json.loads(html) # 获取想要的数据 result= dict['data']['result'] return result # 复制接口数据result里边的一条数据出来分析 results = getTrains() # 初始化数组下标值 c = 0 # 初始化列车数组的下标值 index = 0 # 初始化列车数组 trains = [] # 对结果集进行循环 for i in results: # 为列车数组新增一个空数组元素 trains.append([]) # 将数据拆分成新的数组,并进行循环 for n in i.split('|'): # 将每一个数据依次放入到列车数组中 trains[index].append(n) # 下标值+1 c += 1 # 重置下标值c c = 0 # 列车数组下标值+1 index += 1 # 对处理好的列车数组进行循环遍历 for train in trains: # 打印我们所需要的数据 print('火车:%s' %(train[3])) print('出发地:%s' %(train[6])) print('目的地:%s' %(train[7])) print('发车时间:%s' %(train[8])) print('到达时间:%s' %(train[9])) print('历时时间:%s' %(train[10])) print('商务座/特等座:%s' %(train[32])) print('一等座:%s' %(train[31])) print('二等座:%s' %(train[30])) print('高级软卧:%s' %(train[21])) print('软卧:%s' %(train[23])) print('硬卧:%s' %(train[28])) print('硬座:%s' %(train[29])) print('无座:%s' %(train[26])) print('\n\t')
我们运行一下,看看结果:
这样,我们就能得到我们每一辆列车的数据了。
分析URL地址
第一步,我们已经分析出了我们的数据,现在我们开始写爬虫,再写之前,我们还需要分析一下12306列车接口URL的规律,这样才方便我们组合URL,查询不同城市、时间点的列车数据:
# 12306接口地址
https://kyfw.12306.cn/otn/leftTicket/query?leftTicketDTO.train_date=2018-07-21&leftTicketDTO.from_station=SZQ&leftTicketDTO.to_station=SHH&purpose_codes=ADULT
这里我们主要注意以下几个参数的用途:
- leftTicketDTO.train_date:出发时间
- leftTicketDTO.from_station:出发地
- leftTicketDTO.to_station:目的地
获取城市json文件
我们同时还注意到,我们的城市给转换成了大写的英文字母,这是12306自己的转换机制,所以我们需要试着来找一下12306是否有保存城市的json文件:
这样,我们就找到了12306的城市json文件了,我们可以将它保存下来,现在我们就准备就绪了。
根据城市名,获取城市代号
用户在输入了城市之后,我们需要获取到用户的城市代号,从上例的代码中,我们可以看到我们引入了一个city.py的文件,我就是将城市处理放在这个文件里边进行的:
# -*- coding: UTF-8 -*- # @Time : 2018/04/04 14:58 # @Author : 小罗 # @File : city.py # @Software : PyCharm # @Python Version : 3.6 # @About : 12306城市文件 # 获取城市列表 def getCitys(city): # 城市数据(城市数据过多,这里只显示一部分,请自行去12306处获取完整的城市数据) favorite_names = '@bjb|北京北|VAP|beijingbei|bjb|aaa@qq.com|北京东|BOP|beijingdong|bjd|aaa@qq.com|北京|BJP|beijing|bj|aaa@qq.com|北京南|VNP|beijingnan|bjn|aaa@qq.com|北京西|BXP|beijingxi|bjx|aaa@qq.com|广州南|IZQ|guangzhounan|gzn|aaa@qq.com|重庆北|CUW|chongqingbei|cqb|aaa@qq.com|重庆|CQW|chongqing|cq|aaa@qq.com|重庆南|CRW|chongqingnan|cqn|aaa@qq.com|重庆西|CXW|chongqingxi|cqx|aaa@qq.com|广州东|GGQ|guangzhoudong|gzd|10'; # 遍历所有城市 for i in favorite_names.split( '@'): if i: tmp = i.split( '|') if city == tmp[1]: return tmp[2] return False
这样,我们就可以通过调用city.getCitys()方法,传入我们输入的城市名称,就可以准确的获取我们的城市代号了。
实现用户输入
首先,我们需要让用户输入自己需要查询的出发地、目的地、出发时间,并对这些数据进行判断是否合格:
import GrabTicketOperation import ssl import city import time import re from datetime import datetime # 关闭ssh证书验证 ssl._create_default_https_context = ssl._create_unverified_context # 输入城市 def cityStation(ntype = 1): # 换行 print('\n\t') # 初始化提示语 passtext = '' # 判断是出发地还是目的地 if (ntype == 1): passtext = '出发地' else: passtext = '目的地' # 开始无限循环,保证用户输对为止 while(1): # 获取用户输入的数据 city_station = input('请输入%s:' %( passtext )) # 检查输入的城市 city_stations = city.getCitys(city_station) # 判断输入是否正确 if (city_stations == False): # 不正确,提示,并且重新输入 print('找不到 %s 这个城市' %(city_station)) else: # 输入正确,跳出循环 break # 返回正确的城市编号 return city_stations # 验证时间 def timeStation(): # 换行 print('\n\t') # 开始无限循环,保证用户输对为止 while(1): # 获取用户输入的数据 setOutTime = input('请输入出发时间(例:2018-04-04):') # 判断时间格式是否正确 if (checkTimeFormat(setOutTime) == False): print('请输入正确的时间格式,如:2018-04-04') else: # 将用户输入的日期转化为时间戳 timeArray = time.strptime(setOutTime, "%Y-%m-%d") # 转换为时间戳: timeStamp = int(time.mktime(timeArray)) # 获取当前时间的时间戳 nowtime = time.time() # 获取当天0点的时间戳 nowtimeStamp = int(nowtime - nowtime % 86400 - 28800) # 判断时间大小 if (timeStamp < nowtimeStamp): print('出发日期不能小于当前时间') else: break # 返回正确的城市编号 return setOutTime def checkTimeFormat(setOutTime): # 判断日期格式 date_text = re.search(r"(\d{4}-\d{2}-\d{2})",setOutTime) # 判断时间格式是否正确 try: if date_text == None: return False date_text = date_text.group(0) if date_text != datetime.strptime(date_text, "%Y-%m-%d").strftime('%Y-%m-%d'): return False else: return True except ValueError: return False # 出发地 from_station = cityStation(1) # 目的地 to_station = cityStation(2) # 出发时间 setOutTime = timeStation() # 实例化类(后面需要编写这个类) grabTicket = GrabTicketOperation.GrabTicket(from_station, to_station, setOutTime) # 输入数据 grabTicket.callQueryTrains()
我们运行这个程序(需要将GrabTicketOperation.py,这个类的引入和使用给注释掉,后期会增加这个类),来看看结果:
编写操作类
接下来就是编写我们的操作类了,这也是主要的文件:
# -*- coding: UTF-8 -*- # @Time : 2018/04/04 14:58 # @Author : 小罗 # @File : GrabTicketOperation.py # @Software : PyCharm # @Python Version : 3.6 # @About : 12306抢票操作类 from splinter.browser import Browser import urllib from urllib import request import ssl import city import json from GrabTicketSmtp import GrabTicketSmtp class GrabTicket: # 出发地 from_station = '' # 目的地 to_station = '' # 出发时间 setOutTime = '' # 123056列车请求路径 durl = 'https://kyfw.12306.cn/otn/leftTicket/query?' # 构造函数 # @string from_station 出发地 # @string to_station 目的地 # @string time 时间,如:2018-04-04 def __init__(self, from_station, to_station, setOutTime): # 出发城市 self.from_station = from_station # 目的地城市 self.to_station = to_station # 出发时间 self.setOutTime = setOutTime # 拼接URL地址 def getSplicingUrl(self): url = self.durl + 'leftTicketDTO.train_date=' + urllib.parse.quote(self.setOutTime) + '&leftTicketDTO.from_station=' + urllib.parse.quote(self.from_station) + '&leftTicketDTO.to_station=' + urllib.parse.quote(self.to_station) + '&purpose_codes=ADULT' return url # 抓取数据 def curlTrainsInfo(self): # 获取链接 url = self.getSplicingUrl() # 请求头 headers = { # 'User-Agent': r'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36' 'User-Agent': r'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36' } # 设置请求 req = request.Request(url, headers=headers) # 发送请求 html = request.urlopen(req).read().decode('utf-8') # 格式化数据 dicts = json.loads(html) # 获取想要的数据 result = dicts['data']['result'] return result # 处理数据 def handleResultDatas(self, datas): j = 1 c = 0 index = 0 trains = [] for i in datas: trains.append([]) for n in i.split('|'): #print('[%s] %s' %( c,n )) trains[index].append(n) c += 1 c = 0 #print('\n\t') j += 1 index += 1 return trains # 判断是否大于0 def isCheckValueInt(self, value): value = int(value) if value > 0: # 内容 self.intTrain = True return True return False # 将数据转化为整形 def StringTurnInt(self, train): if train[32].isdigit(): if self.isCheckValueInt(train[32]): return True if train[31].isdigit(): if self.isCheckValueInt(train[31]): return True if train[30].isdigit(): if self.isCheckValueInt(train[30]): return True if train[21].isdigit(): if self.isCheckValueInt(train[21]): return True if train[23].isdigit(): if self.isCheckValueInt(train[23]): return True if train[28].isdigit(): if self.isCheckValueInt(train[28]): return True if train[29].isdigit(): if self.isCheckValueInt(train[29]): return True if train[26].isdigit(): if self.isCheckValueInt(train[26]): return True # 输出数据 def outputResults(self, trains): content = '' # 初始化序号 num = 1 for train in trains: self.isIntTrain = False self.StringTurnInt(train) if train[32] == '有' or train[31] == '有' or train[30] == '有' or train[21] == '有' or train[23] == '有' or train[28] == '有' or train[29] == '有' or train[26] == '有' or self.isIntTrain == True: self.traincontents = [] # 内容前缀 traincontent_prefixs = [ '<tr>', '<td>' + str(num) + '</td>', '<td>' + train[3] + '</td>', '<td>' + train[6] + '</td>', '<td>' + train[7] + '</td>', '<td>' + train[8] + '</td>', '<td>' + train[9] + '</td>', '<td>' + train[10] + '</td>' ] traincontent_prefix = ''.join(traincontent_prefixs) # 内容后缀 traincontent_suffix = '</tr>' num = num + 1 # 商务座 self.getIsStandbyTicket(train[32]) # 一等座 self.getIsStandbyTicket(train[31]) # 二等座 self.getIsStandbyTicket(train[30]) # 高级软卧: self.getIsStandbyTicket(train[21]) # 软卧: self.getIsStandbyTicket(train[23]) # 硬卧: self.getIsStandbyTicket(train[28]) # 硬座: self.getIsStandbyTicket(train[29]) # 无座: self.getIsStandbyTicket(train[26]) traincontent = ''.join(self.traincontents) content = content + traincontent_prefix + traincontent + traincontent_suffix if content == '': return False return content # 获取是否有无余票 def getIsStandbyTicket(self, value): if value.isdigit(): # 内容 self.traincontents.append('<td style="color: #26a306;font-weight: 400;">' + value + '</td>') elif value == '有': # 内容 self.traincontents.append('<td style="color: #26a306;font-weight: 400;">有</td>') else: self.traincontents.append('<td>无</td>') # 获取邮件内容 - 标题 def getEmailContentTitle(self): emailTitle = '<tr><th colspan="30">12306余票监控</th></tr>' return emailTitle # 获取邮件内容 - 列表标题 def getEmailContentListTitle(self): emaillistTitles = [ '<tr>', '<td>序号</td>', '<td>列车</td>', '<td>出发地</td>', '<td>目的地</td>', '<td>发车时间</td>', '<td>到达时间</td>', '<td>历时时间</td>', '<td>商务座/特等座</td>', '<td>一等座</td>', '<td>二等座</td>', '<td>高级软卧</td>', '<td>软卧</td>', '<td>硬卧</td>', '<td>硬座</td>', '<td>无座</td>', '</tr>' ] return ''.join(emaillistTitles) # 发送邮件 def sentEmail(self, trains): # 获取标题 emailTitle = self.getEmailContentTitle() # 获取列表标题 emaillistTitle = self.getEmailContentListTitle() # 获取列表内容 emailListContent = self.outputResults(trains) if emailListContent == False: return False # 拼接数据 emailContents = [ '<table>', '<thead>', emailTitle, emaillistTitle, '</thead>', '<tbody>', emailListContent, '</tbody>', '</table>' ] emailContent = ''.join(emailContents) # 实例化邮件类 grabTicket = GrabTicketSmtp('aaa@qq.com', emailContent) # 输入数据 grabTicket.sendEmail() return True # 调用函数 def callQueryTrains(self): # 抓取数据 result = self.curlTrainsInfo() # 处理数据 trains = self.handleResultDatas(result) # 输出数据 self.sentEmail(trains)
编写邮件发送类
【python学习qq裙:10667510 送入门学习资料,萌新程序媛大本营】
# -*- coding: UTF-8 -*-
# @Time : 2018/04/04 14:58
# @Author : 小罗
# @File : GrabTicketSmtp.py
# @Software : PyCharm
# @Python Version : 3.6
# @About : 发送邮件类
import smtplib
from email.header import Header
from email.mime.text import MIMEText
class GrabTicketSmtp:
# SMTP服务器
mail_host = "smtp.163.com"
# 用户名
mail_user = "aaa@qq.com"
# 授权密码,非登录密码
mail_pass = ""
# 发件人邮箱(最好写全, 不然会失败)
sender = "aaa@qq.com"
# 邮箱标题
title = '好消息!列车有余票呀!'
# 构造函数
# @string receivers 收件人
# @string content 邮箱内容
def __init__(self, receivers, content):
# 邮件内容
self.content = content
# 收件人
self.receivers = [receivers]
# 获取邮箱标题
def getTitle(self):
return self.title
# 发送邮件
def sendEmail(self):
print(self.mail_host)
print(self.mail_user)
print(self.mail_pass)
print(self.sender)
print(self.title)
print(self.receivers)
# 内容, 格式, 编码
message = MIMEText(self.content, 'html', 'utf-8')
message['From'] = "{}".format(self.sender)
message['To'] = ",".join(self.receivers)
message['Subject'] = self.getTitle()
try:
# 启用SSL发信, 端口一般是465
smtpObj = smtplib.SMTP_SSL(self.mail_host, 465)
# 登录验证
smtpObj.login(self.mail_user, self.mail_pass)
# 发送
smtpObj.sendmail(self.sender, self.receivers, message.as_string())
# 发送成功
print("发送成功")
except smtplib.SMTPException as e:
# 发送失败
print(e)
if __name__ == '__main__':
sendEmail()
【python学习qq裙:10667510 送入门学习资料,萌新程序媛大本营】
完善调用类
import GrabTicketOperation
import ssl
import city
import time
import re
from datetime import datetime
# 关闭ssh证书验证
ssl._create_default_https_context = ssl._create_unverified_context
# 输入城市
def cityStation(ntype = 1):
# 初始化提示语
passtext = ''
# 判断是出发地还是目的地
if (ntype == 1):
passtext = '出发地'
else:
passtext = '目的地'
# 开始无限循环,保证用户输对为止
while(1):
# 获取用户输入的数据
city_station = input('请输入%s:' %( passtext ))
# 检查输入的城市
city_stations = city.getCitys(city_station)
# 判断输入是否正确
if (city_stations == False):
# 不正确,提示,并且重新输入
print('找不到 %s 这个城市' %(city_station))
else:
# 输入正确,跳出循环
break
# 返回正确的城市编号
return city_stations
# 验证时间
def timeStation():
# 开始无限循环,保证用户输对为止
while(1):
# 获取用户输入的数据
setOutTime = input('请输入出发时间(例:2018-04-04):')
# 判断时间格式是否正确
if (checkTimeFormat(setOutTime) == False):
print('请输入正确的时间格式,如:2018-04-04')
else:
# 将用户输入的日期转化为时间戳
timeArray = time.strptime(setOutTime, "%Y-%m-%d")
# 转换为时间戳:
timeStamp = int(time.mktime(timeArray))
# 获取当前时间的时间戳
nowtime = time.time()
# 获取当天0点的时间戳
nowtimeStamp = int(nowtime - nowtime % 86400 - 28800)
# 判断时间大小
if (timeStamp < nowtimeStamp):
print('出发日期不能小于当前时间')
else:
break
# 返回正确的城市编号
return setOutTime
def checkTimeFormat(setOutTime):
# 判断日期格式
date_text = re.search(r"(\d{4}-\d{2}-\d{2})",setOutTime)
# 判断时间格式是否正确
try:
if date_text == None:
return False
date_text = date_text.group(0)
if date_text != datetime.strptime(date_text, "%Y-%m-%d").strftime('%Y-%m-%d'):
return False
else:
return True
except ValueError:
return False
# 出发地 from_station = cityStation(1) # 目的地 to_station = cityStation(2) # 出发时间 setOutTime = timeStation() # 实例化类 grabTicket = GrabTicketOperation.GrabTicket(from_station, to_station, setOutTime) # 输入数据 grabTicket.callQueryTrains()
执行程序
接下来我们执行调用类文件,来看看我们的结果:
这里边显示我们的邮件已经发送成功,接下来我们来看看我们的邮件:
我们再来对照一下12306的数据,看是否一致:
这样,我们就能简单的实现12306的余票监控了。
下一篇: 一致性模型 博客分类: 分布式数据库