前言
由於經常會遇到沒票等現象,所以需要使用軟件進行搶票,但由於一些軟件有優先級問題,對於不舍得花錢的我來講,並不是很好的體驗,所以想自己嘗試寫一個類似的功能。
環境/插件
- Python3.6
- urllib(用於請求數據)
- smtplib(發送郵件)
文件目錄
GrabTicket文件夾
-
city.py(12306城市)
-
GrabTicket.py(入口文件)
-
GrabTicketOperation.py(主文件)
-
GrabTicketSmtp.py(發送郵件文件)
分析
首先我們打開12306余票查詢窗口

上圖紅色框的地方,就是表示列車有無車票的地方,我們需要根據這里邊的數據來判斷。
這里邊有一些需要注意的就是,里邊表示有票的有字符串“有”和數字“2”,所以我們需要對這兩種情況進行判斷。
接下來我們使用瀏覽器的開發者工具,來檢查看看是否有接口可以使用:

這里我們可以看出,12306的列車查詢是使用接口調用的,我們再來看看接口返回的數據:

我們可以清晰的看到,我們需要的列車數據是在data里邊的result里邊的數組,所以后面,我們只需要獲取這里邊的數據來判斷就可以了。
分析數據
首先,我們得先分析數據,得出我們需要的數據字段,所以我們先寫一段程序用來分析:
# 復制接口數據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|0@bjd|北京東|BOP|beijingdong|bjd|1@bji|北京|BJP|beijing|bj|2@bjn|北京南|VNP|beijingnan|bjn|3@bjx|北京西|BXP|beijingxi|bjx|4@gzn|廣州南|IZQ|guangzhounan|gzn|5@cqb|重慶北|CUW|chongqingbei|cqb|6@cqi|重慶|CQW|chongqing|cq|7@cqn|重慶南|CRW|chongqingnan|cqn|8@cqx|重慶西|CXW|chongqingxi|cqx|9@gzd|廣州東|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('a710292863@qq.com', emailContent)
# 輸入數據
grabTicket.sendEmail()
return True
# 調用函數
def callQueryTrains(self):
# 抓取數據
result = self.curlTrainsInfo()
# 處理數據
trains = self.handleResultDatas(result)
# 輸出數據
self.sentEmail(trains)
編寫郵件發送類
# -*- 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 = "kafeiwudeshaonian@163.com"
# 授權密碼,非登錄密碼
mail_pass = ""
# 發件人郵箱(最好寫全, 不然會失敗)
sender = "kafeiwudeshaonian@163.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()
完善調用類
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的余票監控了。
結語
- 此程序有許多改進的地方,大家可以根據自己的情況去完善;
- 后期還會有更多的文章供大家參考、討論,讓小編跟大家一起學習、進步;
- 此文章如需轉載,請注明出處:https://www.cnblogs.com/kafeixiaoluo/p/9329500.html。