[2018年12月29日]目前正在使用PyQT5
完成界面設(shè)計,邊學(xué)習(xí)邊寫誊爹,剛剛接觸QT
频丘。等所有都寫完搂漠,再發(fā)一篇文字桐汤。
[2019年1月15日]使用Requests
進(jìn)行網(wǎng)絡(luò)請求時,會造成UI 卡死馆截。接下來準(zhǔn)備使用QtNetwork
module 進(jìn)行網(wǎng)絡(luò)請求蜡娶,先研究下窖张。
[2019年1月23日]看了幾次QtNetwork
里的QNetworkAccessManager
宿接,使用起來比Requests
module麻煩一些梢卸,主要涉及到一些異步處理蛤高。另外完成了自定義日歷控件樣式戴陡。
寫在前面
兩周前完成了Python 12306驗證碼自動驗證、用戶登錄和查詢余票一文喜庞,后來總覺得寫得有點凌亂赋荆,于是想進(jìn)行重構(gòu),讓整個項目結(jié)構(gòu)看起來更加清晰明了酵颁。
寫完整個項目后覺得其實也很簡單,無非是使用
Session
進(jìn)行多次Get
和Post
請求嚷辅,難點在于Post請求時使用的Data
從何而來簸搞?我們先使用抓包工具(瀏覽器F12)完成一次12306平臺訂票之完整過程扁位,對需要進(jìn)行哪些網(wǎng)絡(luò)請求心里有個大概印象。使用Session
的主要原因是為了避免每次請求數(shù)據(jù)時都去考慮Cookies
,如此可能會方便很多域仇。我們將整個訂票過程中使用到的API 放在一個文件里泼掠,原因很簡單:一旦某個接口地址改變了,我們只需在此文件里進(jìn)行修改腻豌,無法在代碼里到處查找修改饲梭,省時省力。我自己之前在寫
iOS
應(yīng)用時候也是采用這樣的方式兜叨。
12306 API
class API(object):
# 登錄鏈接
login = 'https://kyfw.12306.cn/passport/web/login'
# 驗證碼驗證鏈接
captchaCheck = 'https://kyfw.12306.cn/passport/captcha/captcha-check'
# 獲取驗證碼圖片
captchaImage = 'https://kyfw.12306.cn/passport/captcha/captcha-image?login_site=E&module=login&rand=sjrand'
# 車站Code
stationCode = 'https://kyfw.12306.cn/otn/resources/js/framework/station_name.js'
# 查余票
queryTicket = 'https://kyfw.12306.cn/otn/leftTicket/query'
# 查票價
queryPrice = 'https://kyfw.12306.cn/otn/leftTicket/queryTicketPrice'
# 檢查用戶
checkUser = 'https://kyfw.12306.cn/otn/login/checkUser'
# 用戶登錄
userLogin = 'https://kyfw.12306.cn/otn/login/userLogin'
uamtk = 'https://kyfw.12306.cn/passport/web/auth/uamtk'
uamauthclient = 'https://kyfw.12306.cn/otn/uamauthclient'
initMy12306 = 'https://kyfw.12306.cn/otn/index/initMy12306'
# 確定訂單信息
submitOrderRequest = 'https://kyfw.12306.cn/otn/leftTicket/submitOrderRequest'
# initDc,獲取globalRepeatSubmitToken
initDc = 'https://kyfw.12306.cn/otn/confirmPassenger/initDc'
# 獲取曾經(jīng)用戶列表
getPassengerDTOs = 'https://kyfw.12306.cn/otn/confirmPassenger/getPassengerDTOs'
# 檢查訂單信息
checkOrderInfo = 'https://kyfw.12306.cn/otn/confirmPassenger/checkOrderInfo'
# 獲取隊列查詢
getQueueCount = 'https://kyfw.12306.cn/otn/confirmPassenger/getQueueCount'
# 確認(rèn)隊列
confirmSingleForQueue = 'https://kyfw.12306.cn/otn/confirmPassenger/confirmSingleForQueue'
常量
將項目里使用到的常量都集中在一個文件里茫死,方便管理屡久。特別需要注意的是座位類型不是固定的被环,我在寫整個項目時發(fā)現(xiàn)有幾個座位類型是變化的,比如硬座在我寫本文的時候是1
,但是之前都是A1
,其他座位類型變化情況參見具體代碼內(nèi)容版姑。
from codePlatform import CJYClient
# 12306登錄用戶名
userName = '你的12306賬號'
# 12306密碼
password = '你的12306密碼'
# 超級鷹打碼平臺
chaoJiYing = CJYClient('你的超級鷹平臺賬戶', '你的超級鷹平臺密碼','896970')
# 驗證碼圖片路徑
captchaFilePath = 'captcha.jpg'
# 車站電報碼路徑
stationCodesFilePath = 'stationsCode.txt'
# 座位類型,訂票下單時需要傳入
noSeat = 'WZ' #無座
firstClassSeat = 'M' #一等座
secondClassSeat = 'O' #二等座
advancedSoftBerth = '6' #高級軟臥 A6
hardBerth = '3' #硬臥 A3
softBerth = '4' #軟臥 A4
moveBerth = 'F' #動臥
hardSeat = '1' #硬座 A1
businessSeat = '9' #商務(wù)座 A9
Utility 工具類
通常項目中都會有很多共用方法炒嘲,我們將這些方法抽離出來放在一個工具類文件里浑劳,如此可以減少冗余代碼魔熏。
from datetime import datetime
from stationCodes import StationCodes
from color import Colored
import time
import requests
class Utility(object):
@classmethod
def getSession(self):
session = requests.session() # 創(chuàng)建session會話
session.headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36"
}
# session.verify = False # 跳過SSL驗證
return session
@classmethod
def redColor(self,str):
return Colored.red(str)
@classmethod
def greenColor(self, str):
return Colored.green(str)
# 反轉(zhuǎn)字典
@classmethod
def reversalDict(self, dict):
return {v: k for k, v in dict.items()}
# 將歷時轉(zhuǎn)化為小時和分鐘的形式
@classmethod
def getDuration(self, timeStr):
duration = timeStr.replace(':', '時') + '分'
if duration.startswith('00'):
return duration[4:]
return duration
# 獲取一個時間是周幾
@classmethod
def getWeekDay(self, date):
weekDayDict = {
0: '周一',
1: '周二',
2: '周三',
3: '周四',
4: '周五',
5: '周六',
6: '周天',
}
day = datetime.strptime(date, '%Y-%m-%d').weekday()
return weekDayDict[day]
# 轉(zhuǎn)化日期格式
@classmethod
def getDateFormat(self, date):
# date格式為2018-08-08
dateList = date.split('-')
if dateList[1].startswith('0'):
month = dateList[1].replace('0', '')
if dateList[2].startswith('0'):
day = dateList[2].replace('0', '')
return '{}月{}日'.format(month, day)
# 檢查購票日期是否合理
@classmethod
def checkDate(self, date):
localTime = time.localtime()
localDate = '%04d-%02d-%02d' % (localTime.tm_year, localTime.tm_mon, localTime.tm_mday)
# 獲得當(dāng)前時間時間戳
currentTimeStamp = int(time.time())
# 預(yù)售時長的時間戳
deltaTimeStamp = '2505600'
# 截至日期時間戳
deadTimeStamp = currentTimeStamp + int(deltaTimeStamp)
# 獲取預(yù)售票的截止日期時間
deadTime = time.localtime(deadTimeStamp)
deadDate = '%04d-%02d-%02d' % (deadTime.tm_year, deadTime.tm_mon, deadTime.tm_mday)
# print(Colored.red('請注意合理的乘車日期范圍是:{} 至 {}'.format(localDate, deadDate)))
# 判斷輸入的乘車時間是否在合理乘車時間范圍內(nèi)
# 將購票日期轉(zhuǎn)換為時間數(shù)組
trainTimeStruct = time.strptime(date, "%Y-%m-%d")
# 轉(zhuǎn)換為時間戳:
trainTimeStamp = int(time.mktime(trainTimeStruct))
# 將購票時間修改為12306可接受格式 躲雅,如用戶輸入2018-8-7則格式改為2018-08-07
trainTime = time.localtime(trainTimeStamp)
trainDate = '%04d-%02d-%02d' % (trainTime.tm_year, trainTime.tm_mon, trainTime.tm_mday)
# 比較購票日期時間戳與當(dāng)前時間戳和預(yù)售截止日期時間戳
if currentTimeStamp <= trainTimeStamp and trainTimeStamp <= deadTimeStamp:
return True, trainDate
else:
print(Colored.red('Error:您輸入的乘車日期:{}, 當(dāng)前系統(tǒng)日期:{}, 預(yù)售截止日期:{}'.format(trainDate, localDate, deadDate)))
return False, None
@classmethod
def getDate(self,dateStr):
# dateStr格式為20180801
year = time.strptime(dateStr,'%Y%m%d').tm_year
month = time.strptime(dateStr,'%Y%m%d').tm_mon
day = time.strptime(dateStr,'%Y%m%d').tm_mday
return '%04d-%02d-%02d' % (year,month,day)
# 根據(jù)車站名獲取電報碼
@classmethod
def getStationCode(self, station):
codesDict = StationCodes().getCodesDict()
if station in codesDict.keys():
return codesDict[station]
# 輸入出發(fā)地和目的地
@classmethod
def inputStation(self, str):
station = input('{}:\n'.format(str))
if not station in StationCodes().getCodesDict().keys():
print(Colored.red('Error:車站列表里無法查詢到{}'.format(station)))
station = input('{}:\n'.format(str))
return station
# 輸入乘車日期
@classmethod
def inputTrainDate(self):
trainDate = input('請輸入購票時間,格式為2018-01-01:\n')
try:
trainTimeStruct = time.strptime(trainDate, "%Y-%m-%d")
except:
print('時間格式錯誤慰于,請重新輸入')
trainDate = input('請輸入購票時間,格式為2018-01-01:\n')
timeFlag, trainDate = Utility.checkDate(trainDate)
if timeFlag == False:
trainDate = input('請輸入購票時間,格式為2018-01-01:\n')
timeFlag, trainDate = Utility.checkDate(trainDate)
return trainDate
@classmethod
def getTrainDate(self,dateStr):
# 返回格式 Wed Aug 22 2018 00: 00:00 GMT + 0800 (China Standard Time)
# 轉(zhuǎn)換成時間數(shù)組
timeArray = time.strptime(dateStr, "%Y%m%d")
# 轉(zhuǎn)換成時間戳
timestamp = time.mktime(timeArray)
# 轉(zhuǎn)換成localtime
timeLocal = time.localtime(timestamp)
# 轉(zhuǎn)換成新的時間格式
GMT_FORMAT = '%a %b %d %Y %H:%M:%S GMT+0800 (China Standard Time)'
timeStr = time.strftime(GMT_FORMAT, timeLocal)
return timeStr
特別要注意一下getTrainDate
方法里返回時間字符串格式绵脯,我使用Firefox瀏覽器抓包時發(fā)現(xiàn)格式是Wed+Aug+22+2018+00:00:00+GMT+0800+(China+Standard+Time)
,但是在項目里使用此格式時會發(fā)現(xiàn)無法請求到數(shù)據(jù)。后來使用Google瀏覽器抓包發(fā)后現(xiàn)時間字符串里沒有+
符號。
Color 類
另外為了能在 Terminal
里能使用不同顏色來顯示打印信息废境,我們定義一個Color類:
from colorama import init, Fore, Back
init(autoreset=False)
class Color(object):
# 前景色:紅色 背景色:默認(rèn)
@classmethod
def red(self, s):
return Fore.RED + s + Fore.RESET
@classmethod
# 前景色:綠色 背景色:默認(rèn)
def green(self, s):
return Fore.GREEN + s + Fore.RESET
@classmethod
# 前景色:黃色 背景色:默認(rèn)
def yellow(self, s):
return Fore.YELLOW + s + Fore.RESET
# 前景色:藍(lán)色 背景色:默認(rèn)
@classmethod
def blue(self, s):
return Fore.BLUE + s + Fore.RESET
# 前景色:洋紅色 背景色:默認(rèn)
@classmethod
def magenta(self, s):
return Fore.MAGENTA + s + Fore.RESET
# 前景色:青色 背景色:默認(rèn)
@classmethod
def cyan(self, s):
return Fore.CYAN + s + Fore.RESET
# 前景色:白色 背景色:默認(rèn)
@classmethod
def white(self, s):
return Fore.WHITE + s + Fore.RESET
# 前景色:黑色 背景色:默認(rèn)
@classmethod
def black(self, s):
return Fore.BLACK
# 前景色:白色 背景色:綠色
@classmethod
def white_green(self, s):
return Fore.WHITE + Back.GREEN + s + Fore.RESET + Back.RESET
驗證碼驗證&用戶登錄
從抓包結(jié)果來看,12306平臺首先進(jìn)行驗證碼驗證,驗證通過后才會繼續(xù)驗證用戶名和密碼堵泽。
驗證碼圖片接口是:https://kyfw.12306.cn/passport/captcha/captcha-image?login_site=E&module=login&rand=sjrand&0.05013721011282968鏈接里最后一個參數(shù)0.05013721011282968
每次請求時都不一樣迎罗。但我發(fā)現(xiàn)沒有此參數(shù)同樣能夠請求到驗證碼圖片尤辱。
驗證碼驗證時提交的參數(shù)login_site和rand是固定的,而answer是指正確圖片位置坐標(biāo)结借,但坐標(biāo)基準(zhǔn)原點是如下圖紅色箭頭處而非整個驗證碼圖片的左上角映跟。
我們可以分別采用手動和打碼平臺自動方式對驗證碼進(jìn)行驗證,手動驗證即把驗證碼圖片分割成8個小圖片荸镊,依次編號1-8躬存,每個小圖片上取固定的一個位置坐標(biāo),平臺返回驗證碼圖片后盾剩,用戶手動輸入正確驗證碼所在位置:
所謂的打碼平臺自動驗證是指用戶給打碼平臺傳入一張驗證碼圖片,平臺通過碼工去人工識別驗證碼(碼工有出錯可能)驻粟,平臺再將其結(jié)果返回給用戶蜀撑,這個過程一般也就2-3秒時間玄柏。12306驗證碼是多個坐標(biāo)拼接成的字符串粪摘,因此我們需要平臺返回多個坐標(biāo)字符串。
百度搜索打碼平臺關(guān)鍵字能夠找到很多相關(guān)平臺椎咧,其中包含打碼兔、超級鷹等脚牍。寫本文的時發(fā)現(xiàn)打碼兔平臺已經(jīng)轉(zhuǎn)型诸狭,不再提供打碼服務(wù),于是我只能去注冊超級鷹賬戶叉庐。平臺網(wǎng)站上有如何使用Python進(jìn)行打碼的相關(guān)文檔,使用時需要注意驗證碼圖片的類型,返回多個坐標(biāo)對應(yīng)的
codetype
為9004蔚万,具體請參考驗證碼類型反璃。如下代碼是超級鷹官網(wǎng)提供的斋攀,我做了一些改動淳蔼,原因是平臺返回的坐標(biāo)是以圖片的左上角為原點,這與12306坐標(biāo)基準(zhǔn)不一致存皂。
另外,我本想直接去掉
ReportError
方法的疤孕,后來發(fā)現(xiàn)超級鷹打碼平臺有出錯幾率胰柑。于是如果驗證碼驗證失敗,則向平臺提交失敗圖片的ID踩官。這樣做的目的是節(jié)省平臺積分,因為我們每提交一張驗證碼圖片給平臺進(jìn)行識別都要付出積分辩越,但倘若平臺識別錯誤,則此題積分會返回督惰。
import requests
import const
from hashlib import md5
class CJYClient:
def __init__(self, username, password, soft_id):
#平臺賬號
self.username = username
#平臺密碼
self.password = md5(password.encode('utf-8')).hexdigest()
# 軟件ID
self.soft_id = soft_id
self.base_params = {
'user' : self.username,
'pass2' : self.password,
'softid': self.soft_id,
}
self.headers = {
'Connection': 'Keep-Alive',
'User-Agent': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)',
}
def PostPic(self, img, codetype):
params = {
'codetype': codetype,
}
params.update(self.base_params)
files = {'userfile': ('ccc.jpg', img)}
result = requests.post('http://upload.chaojiying.net/Upload/Processing.php', data=params, files=files, headers=self.headers).json()
answerList = result['pic_str'].replace('|',',').split(',')
# 將平臺返回的縱坐標(biāo)減去30
for index in range(len(answerList)):
if index % 2 != 0:
answerList[index] = str(int(answerList[index])-30)
else:
answerList[index] = str(answerList[index])
answerStr = ','.join(answerList)
print('打碼平臺返回的驗證碼為:'+ answerStr)
return answerStr,result # result是打碼平臺返回的結(jié)果,answerStr是縱坐標(biāo)減去30后拼接成的字符串
def ReportError(self, im_id):
params = {
'id': im_id, # im_id:報錯驗證碼的圖片ID
}
params.update(self.base_params)
r = requests.post('http://upload.chaojiying.net/Upload/ReportError.php', data=params, headers=self.headers)
return r.json()
Login類
import const
import re
from utility import Utility
from color import Color
from APIs import API
class Login(object):
session = Utility.getSession() # 創(chuàng)建session
def __init__(self):
self.session = Login.session
# 獲取驗證碼正確答案
def getCaptchaAnswer(self):
response= self.session.get(API.captchaImage)
if response.status_code ==200:
print('驗證碼圖片請求成功')
with open(const.captchaFilePath, 'wb') as f:
f.write(response.content) # 寫入文件
else:
print(Color.red('驗證碼圖片下載失敗, 正在重試...'))
self.getCaptchaAnswer() #遞歸
try:
img = open(const.captchaFilePath, 'rb').read() #讀取文件圖片
answerStr,cjyAnswerDict = const.chaoJiYing.PostPic(img, 9004)
return answerStr,cjyAnswerDict #返回自己寫的驗證碼信息和平臺反應(yīng)的信息
except Exception as e:
print(str(e))
# 驗證碼驗證
def captchaCheck(self):
# 手動驗證
# self.getCaptchaAnswer()
# imgLocation = input("請輸入驗證碼圖片位置,以英文狀態(tài)下的分號','分割:\n")
# coordinates = {'1':'35,35', '2':'105,35', '3':'175,35', '4':'245,35',
# '5':'35,105', '6':'105,105', '7':'175,105','8':'245,105'}
# rightImgCoordinates =[]
# for i in imgLocation.split(','):
# rightImgCoordinates.append(coordinates[i])
# answer = ','.join(rightImgCoordinates)
answer,cjyAnswerDict = self.getCaptchaAnswer()
data = {
'login_site':'E', # 固定的
'rand': 'sjrand', # 固定的
'answer': answer # 驗證碼對應(yīng)的坐標(biāo)字符串
}
result = self.session.post(API.captchaCheck,data=data).json()
if result['result_code'] == '4':
print('驗證碼驗證成功')
else:
print(Color.red('Error:{}'.format(result['result_message'])))
picID = cjyAnswerDict['pic_id']
# 報錯到打碼平臺
const.chaoJiYing.ReportError(picID)
self.captchaCheck()
return
# 以下是登錄過程進(jìn)行的相關(guān)請求
def userLogin(self):
# step 1: check驗證碼
self.captchaCheck()
# step 2: login
loginData = {
'username': const.userName, # 12306用戶名
'password': const.password, # 12306密碼
'appid': 'otn' #固定
}
result = self.session.post(API.login, data=loginData).json()
# step 3:checkuser
data = {
'_json_att': ''
}
checkUser_res = self.session.post(API.checkUser, data=data)
# if checkUser_res.json()['data']['flag']:
# print("用戶在線驗證成功")
# else:
# print('檢查用戶不在線,請重新登錄')
# self.userLogin()
# return
# step 4: uamtk
data = {
'appid':'otn' # 固定
}
uamtk_res = self.session.post(API.uamtk,data= data)
newapptk = uamtk_res.json()['newapptk']
# step 5: uamauthclient
clientData = {
'tk':newapptk
}
uamauthclient_res = self.session.post(API.uamauthclient,data = clientData)
username = uamauthclient_res.json()['username']
# step 6: initMy12306
html = self.session.get(API.initMy12306).text
genderStr = re.findall(r'<div id="my12306page".*?</span>(.*?)</h3>',html,re.S)[0].replace('\n','').split('沦偎,')[0] # 獲取稱謂,如先生
print("{}{},恭喜您成功登錄12306網(wǎng)站".format(Utility.redColor(username),genderStr))
return username # 返回用戶名侈询,便于搶票時使用。當(dāng)然一個12306賬戶里可能有多個常用乘客革为,我們也可以獲取聯(lián)系人列表震檩,給其他人搶票
查詢余票
首先明確一點,即用戶在不登錄情況下也是可以查車票信息的迂猴。打開瀏覽器進(jìn)入12306余票查詢頁面查詢鏈接儡率,然后打開開發(fā)者模式,在頁面上輸入出發(fā)地為上海掷倔,目的地為成都,出發(fā)日期為2018-08-28凛虽,車票類型選擇成人凯旋,點擊查詢按鈕。我們發(fā)現(xiàn)只有如下的1個Get請求:
https://kyfw.12306.cn/otn/leftTicket/query?leftTicketDTO.train_date=2018-08-28&leftTicketDTO.from_station=SHH&leftTicketDTO.to_station=CDW&purpose_codes=ADULT
leftTicketDTO.train_date,leftTicketDTO.from_station,leftTicketDTO.to_station及purpose_codes幾個參數(shù),從參數(shù)的英文含義上不難判斷它們分別代表出發(fā)日期趣惠、出發(fā)地味悄、目的地和車票類型。但出發(fā)地怎么是SHH丢习,目的地又怎么是CDW?這些都是什么见擦?我百度了一下鲤屡,這些字符是指車站電報碼卢未。可這些數(shù)據(jù)從何而來呢滴铅?
在開發(fā)者模式打開的情況下刷新查詢頁面汉匙,發(fā)現(xiàn)多了很多請求。仔細(xì)查看每個請求都在做些什么操作绎秒?服務(wù)器又返回了什么?Oh my gosh玄呛,竟然在剛打開查詢頁面的時候就請求到了徘铝。
我們把數(shù)據(jù)請求下來并加以保存,保存的原因是這些數(shù)據(jù)一般情況下都不會改變,請求一次甲锡,下次直接使用缤沦。
import os
import re
import json
import const
from APIs import API
class StationCodes(object):
@classmethod
def getAndSaveStationCodes(self,session): # session 還是使用Login時的session
# 若文件存在劝术,則直接return
if os.path.exists(const.stationCodesFilePath):
return
res = session.get(API.stationCode)
stations = re.findall(r'([\u4e00-\u9fa5]+)\|([A-Z]+)',res.text) #\u4e00-\u95fa5是漢字的首尾
# 注意編碼格式utf-8
with open(const.stationCodesFilePath, 'w', encoding='utf-8') as f:
# ensure_ascii = False 是為了防止亂碼
f.write(json.dumps(dict(stations),ensure_ascii = False))
# 獲取電報碼字典
def getCodesDict(self):
with open(const.stationCodesFilePath, 'r', encoding='utf-8') as file:
dict = json.load(file)
return dict
獲取到車站電報碼衬吆,接下來我們就可以查詢余票了姆泻。細(xì)節(jié)部分將在代碼里進(jìn)行講解。
import const
from stationCodes import StationCodes
from utility import Utility
from color import Color
from prettytable import PrettyTable
from login import Login
from APIs import API
class LeftTicket(object):
def __init__(self):
self.session = Login.session # 還是那句話方咆,使用同一個session
def queryTickets(self):
StationCodes.getAndSaveStationCodes(self.session) # 先判斷電報碼文件是否存在瓣赂,不存在再下載保存
queryData = self.getQueryData() # 獲取trainDate,fromStationCode,toStationCode,fromStation和toStation
parameters = {
'leftTicketDTO.train_date' : queryData['trainDate'], # 日期苫纤,格式為2018-08-28
'leftTicketDTO.from_station': queryData['fromStationCode'], # 出發(fā)站電報碼
'leftTicketDTO.to_station' : queryData['toStationCode'], # 到達(dá)站電報碼
'purpose_codes' : 'ADULT' # 0X00是學(xué)生票
}
res = self.session.get(API.queryTicket,params = parameters)
trainDicts = self.getTrainInfo(res.json(), queryData)
return queryData, trainDicts # 返回查詢數(shù)據(jù)和車次信息,便于下單時使用
def getTrainInfo(self,result,queryData):
trainDict = {} # 車次信息字典
trainDicts = [] # 用于訂票
trains = [] #用于在terminal里打印
results = result['data']['result']
maps = result['data']['map']
for item in results:
trainInfo = item.split('|')
# for index, item in enumerate(trainInfo, 0):
# print('{}:\t{}'.format(index, item)
if trainInfo[11] =='Y':
trainDict['secretStr'] = trainInfo[0]
trainDict['trainNumber'] = trainInfo[2] #5l0000D35273
trainDict['trainName'] = trainInfo[3] # 車次名稱,如D352
trainDict['fromTelecode'] = trainInfo[6] #出發(fā)地電報碼
trainDict['toTelecode'] = trainInfo[7] # 出發(fā)地電報碼
trainDict['fromStation'] = maps[trainInfo[6]] # 上海
trainDict['toStation'] = maps[trainInfo[7]] # 成都
trainDict['departTime'] = Color.green(trainInfo[8]) # 出發(fā)時間
trainDict['arriveTime'] = Color.red(trainInfo[9]) # 到達(dá)時間
trainDict['totalTime'] = Utility.getDuration(trainInfo[10]) # 總用時
trainDict['leftTicket'] = trainInfo[12] # 余票
trainDict['trainDate'] = trainInfo[13] #20180822
trainDict['trainLocation'] = trainInfo[15] # H2
# 以下順序貌似也不是一直固定的颓屑,我遇到過代表硬座的幾天后代表其他座位了
trainDict[const.businessSeat] = trainInfo[32] # 商務(wù)座
trainDict[const.firstClassSeat] = trainInfo[31] #一等座
trainDict[const.secondClassSeat] = trainInfo[30] #二等座
trainDict[const.advancedSoftBerth]= trainInfo[21] #高級軟臥
trainDict[const.softBerth] = trainInfo[23] #軟臥
trainDict[const.moveBerth] = trainInfo[33]#動臥
trainDict[const.noSeat] = trainInfo[26]#無座
trainDict[const.hardBerth] = trainInfo[28]#硬臥
trainDict[const.hardSeat] = trainInfo[29]#硬座
trainDict['otherSeat'] = trainInfo[22]#其他
# 如果值為空,則將值修改為'--',有票則有字顯示為綠色器腋,無票紅色顯示
for key in trainDict.keys():
if trainDict[key] == '':
trainDict[key] = '--'
if trainDict[key] == '有':
trainDict[key] = Color.green('有')
if trainDict[key] == '無':
trainDict[key] = Color.red('無')
train = [Color.magenta(trainDict['trainName']) + Color.green('[ID]') if trainInfo[18] == '1' else trainDict['trainName'],
Color.green(trainDict['fromStation']) + '\n' + Color.red(trainDict['toStation']),
trainDict['departTime'] + '\n' + trainDict['arriveTime'],
trainDict['totalTime'], trainDict[const.businessSeat] , trainDict[const.firstClassSeat],
trainDict[const.secondClassSeat], trainDict[const.advancedSoftBerth], trainDict[const.softBerth],
trainDict[const.moveBerth], trainDict[const.hardBerth], trainDict[const.hardSeat], trainDict[const.noSeat],
trainDict['otherSeat']]
# 直接使用append方法將字典添加到列表中,如果需要更改字典中的數(shù)據(jù)措左,那么列表中的內(nèi)容也會發(fā)生改變,這是因為dict在Python里是object凉逛,不屬于primitive
# type(即int状飞、float、string自晰、None、bool)混巧。這意味著你一般操控的是一個指向object(對象)的指針咧党,而非object本身深员。下面是改善方法:使用copy()
trains.append(train)
trainDicts.append(trainDict.copy())# 注意trainDict.copy()
self.prettyPrint(trains,queryData) # 按照一定格式打印
return trainDicts
def getQueryData(self):
trainDate = Utility.inputTrainDate() # 日期
fromStation = Utility.inputStation('請輸入出發(fā)地') # 出發(fā)地
toStation = Utility.inputStation('請輸入目的地') # 目的地
fromStationCode = Utility.getStationCode(fromStation) # 出發(fā)地電報碼
toStationCode = Utility.getStationCode(toStation) # 目的地電報碼
queryData = {
'fromStation':fromStation,
'toStation':toStation,
'trainDate':trainDate,
'fromStationCode':fromStationCode,
'toStationCode':toStationCode
}
return queryData
def prettyPrint(self,trains,queryData):
header = ["車次", "車站", "時間", "歷時", "商務(wù)座","一等座", "二等座",'高級軟臥',"軟臥", "動臥", "硬臥", "硬座", "無座",'其他']
pt = PrettyTable(header)
date = queryData['trainDate']
title = '{}——>{}({} {}),共查詢到{}個可購票的車次'.format(queryData['fromStation'],queryData['toStation'],Utility.getDateFormat(date),Utility.getWeekDay(date),len(trains))
pt.title = Color.cyan(title)
pt.align["車次"] = "l" # 左對齊
for train in trains:
pt.add_row(train)
print(pt)
訂票
這個過程也有很多請求叠赐,具體在代碼里說明芭概。
import re
from utility import Utility
from urllib import parse
from APIs import API
from queryTicket import LeftTicket
from login import Login
class BookTicket(object):
def __init__(self):
self.session = Login.session
def bookTickets(self,username):
queryData, trainDicts = LeftTicket().queryTickets()
# 這個地方座位類型也是不是固定的,如硬臥有時候是3,有時是A3
seatType = input('請輸入車票類型,WZ無座,F動臥,M一等座,O二等座,1硬座,3硬臥,4軟臥,6高級軟臥,9商務(wù)座:\n')
i = 0
for trainDict in trainDicts:
if trainDict[seatType]== Utility.greenColor('有') or trainDict[seatType].isdigit():
print('為您選擇的車次為{},正在為您搶票中……'.format(Utility.redColor(trainDict['trainName'])))
self.submitOrderRequest(queryData,trainDict)
self.getPassengerDTOs(seatType,username,trainDict)
return
else:
i += 1
if i >=len(trainDicts): # 遍歷所有車次后都未能查到座位臊诊,則打印錯誤信息
print(Utility.redColor('Error:系統(tǒng)未能查詢到{}座位類型存有余票'.format(seatType)))
continue
def submitOrderRequest(self, queryData, trainDict):
data = {
'purpose_codes' : 'ADULT',
'query_from_station_name': queryData['fromStation'],
'query_to_station_name' : queryData['toStation'],
'secretStr' : parse.unquote(trainDict['secretStr']),
'tour_flag' : 'dc',
'train_date' : queryData['trainDate'],
'undefined' : ''
}
dict = self.session.post(API.submitOrderRequest, data=data).json()
if dict['status']:
print('系統(tǒng)提交訂單請求成功')
elif dict['messages'] != []:
if dict['messages'][0] == '車票信息已過期帚戳,請重新查詢最新車票信息':
print('車票信息已過期偏友,請重新查詢最新車票信息')
else:
print("系統(tǒng)提交訂單請求失敗")
def initDC(self):
# step 1: initDc
data = {
'_json_att': ''
}
res = self.session.post(API.initDc, data=data)
try:
repeatSubmitToken = re.findall(r"var globalRepeatSubmitToken = '(.*?)'", res.text)[0]
keyCheckIsChange = re.findall(r"key_check_isChange':'(.*?)'", res.text)[0]
# print('key_check_isChange:'+ key_check_isChange)
return repeatSubmitToken,keyCheckIsChange
except:
print('獲取Token參數(shù)失敗')
return
def getPassengerDTOs(self,seatType,username,trainDict):
# step 1: initDc
repeatSubmitToken, keyCheckIsChange = self.initDC()
# step2 : getPassengerDTOs
data = {
'_json_att': '',
'REPEAT_SUBMIT_TOKEN': repeatSubmitToken
}
res = self.session.post(API.getPassengerDTOs, data=data)
passengers = res.json()['data']['normal_passengers']
for passenger in passengers:
if passenger['passenger_name'] == username:
# step 3: Check order
self.checkOrderInfo(seatType, repeatSubmitToken, passenger)
# step 4:獲取隊列
self.getQueueCount(seatType, repeatSubmitToken, keyCheckIsChange, trainDict, passenger)
return
else:
print('無法購票')
def checkOrderInfo(self,seatType,repeatSubmitToken,passenger):
passengerTicketStr = '{},{},{},{},{},{},{},N'.format(seatType, passenger['passenger_flag'],
passenger['passenger_type'],
passenger['passenger_name'],
passenger['passenger_id_type_code'],
passenger['passenger_id_no'],
passenger['mobile_no'])
oldPassengerStr = '{},{},{},1_'.format(passenger['passenger_name'], passenger['passenger_id_type_code'],
passenger['passenger_id_no'])
data = {
'_json_att' : '',
'bed_level_order_num': '000000000000000000000000000000',
'cancel_flag' : '2',
'oldPassengerStr' : oldPassengerStr,
'passengerTicketStr' : passengerTicketStr,
'randCode' : '',
'REPEAT_SUBMIT_TOKEN': repeatSubmitToken,
'tour_flag' : 'dc',
'whatsSelect' : '1'
}
res = self.session.post(API.checkOrderInfo, data=data)
dict = res.json()
if dict['data']['submitStatus']:
print('系統(tǒng)校驗訂單信息成功')
if dict['data']['ifShowPassCode'] == 'Y':
print('需要再次驗證')
return True
if dict['data']['ifShowPassCode'] == 'N':
return False
else:
print('系統(tǒng)校驗訂單信息失敗')
return False
def getQueueCount(self,seatType,repeatSubmitToken,keyCheckIsChange,trainDict,passenger):
data = {
'_json_att' : '',
'fromStationTelecode' : trainDict['fromTelecode'],
'leftTicket' : trainDict['leftTicket'],
'purpose_codes' : '00',
'REPEAT_SUBMIT_TOKEN' : repeatSubmitToken,
'seatType' : seatType,
'stationTrainCode' : trainDict['trainName'],
'toStationTelecode' : trainDict['toTelecode'],
'train_date' : Utility.getTrainDate(trainDict['trainDate']),
'train_location' : trainDict['trainLocation'],
'train_no' : trainDict['trainNumber'],
}
res = self.session.post(API.getQueueCount,data= data)
if res.json()['status']:
print('系統(tǒng)獲取隊列信息成功')
self.confirmSingleForQueue(seatType,repeatSubmitToken,keyCheckIsChange,passenger,trainDict)
else:
print('系統(tǒng)獲取隊列信息失敗')
return
def confirmSingleForQueue(self,seatType,repeatSubmitToken,keyCheckIsChange,passenger,trainDict):
passengerTicketStr = '{},{},{},{},{},{},{},N'.format(seatType, passenger['passenger_flag'],
passenger['passenger_type'],
passenger['passenger_name'],
passenger['passenger_id_type_code'],
passenger['passenger_id_no'],
passenger['mobile_no'])
oldPassengerStr = '{},{},{},1_'.format(passenger['passenger_name'], passenger['passenger_id_type_code'],
passenger['passenger_id_no'])
data = {
'passengerTicketStr': passengerTicketStr,
'oldPassengerStr': oldPassengerStr,
'randCode': '',
'purpose_codes': '00',
'key_check_isChange': keyCheckIsChange,
'leftTicketStr': trainDict['leftTicket'],
'train_location': trainDict['trainLocation'],
'choose_seats': '',
'seatDetailType': '000',
'whatsSelect': '1',
'roomType': '00',
'dwAll': 'N',
'_json_att': '',
'REPEAT_SUBMIT_TOKEN': repeatSubmitToken,
}
res = Login.session.post(API.confirmSingleForQueue, data= data)
if res.json()['status']['submitStatus'] == 'true':
print('已完成訂票窿冯,請前往12306進(jìn)行支付')
else:
print('訂票失敗,請稍后重試!')
我們現(xiàn)在來訂購一張28號上海到成都的二等座車票醒串,在項目里是無法完成支付的鼻吮,必須到12306官網(wǎng)進(jìn)行支付椎木!
我們可以將訂票成功的結(jié)果以短信或者郵件的方式發(fā)送出去,提醒用戶畜伐。
短信部分我已經(jīng)寫好了,在代碼里就不展示了慎框。