Python12306訂票
寫在前面
兩週前完成了 Python 12306驗證碼自動驗證、使用者登入和查詢餘票 一文,後來總覺得寫得有點凌亂,於是想進行重構,讓整個專案結構看起來更加清晰明瞭。

專案結構
寫完整個專案後覺得其實也很簡單,無非是使用 Session
進行多次 Get
和 Post
請求,難點在於Post請求時使用的 Data
從何而來?我們先使用抓包工具(瀏覽器F12)完成一次12306平臺訂票之完整過程,對需要進行哪些網路請求心裡有個大概印象。使用 Session
的主要原因是為了避免每次請求資料時都去考慮 Cookies
,如此可能會方便很多。
iOS
應用時候也是採用這樣的方式。
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' # 獲取曾經使用者列表 getPassengerDTOs = 'https://kyfw.12306.cn/otn/confirmPassenger/getPassengerDTOs' # 檢查訂單資訊 checkOrderInfo = 'https://kyfw.12306.cn/otn/confirmPassenger/checkOrderInfo' # 獲取佇列查詢 getQueueCount = 'https://kyfw.12306.cn/otn/confirmPassenger/getQueueCount' # 確認佇列 confirmSingleForQueue = 'https://kyfw.12306.cn/otn/confirmPassenger/confirmSingleForQueue'
常量
將專案裡使用到的常量都集中在一個檔案裡,方便管理。特別需要注意的是座位型別不是固定的,我在寫整個專案時發現有幾個座位型別是變化的,比如硬座在我寫本文的時候是 1
,但是之前都是 A1
,其他座位型別變化情況參見具體程式碼內容。
fromcodePlatform importCJYClient # 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'#商務座 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()# 建立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): returnColored.red(str) @classmethod def greenColor(self, str): return Colored.green(str) # 反轉字典 @classmethod def reversalDict(self, dict): return {v: k for k, v in dict.items()} # 將歷時轉化為小時和分鐘的形式 @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] # 轉化日期格式 @classmethod def getDateFormat(self, date): # date格式為2018-08-08 dateList = date.split('-') if dateList[1].startswith('0'): month = dateList[1].replace('0', '') else: month = dateList[1] if dateList[2].startswith('0'): day = dateList[1].replace('0', '') else: day = dateList[2] 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) # 獲得當前時間時間戳 currentTimeStamp = int(time.time()) # 預售時長的時間戳 deltaTimeStamp = '2505600' # 截至日期時間戳 deadTimeStamp = currentTimeStamp + int(deltaTimeStamp) # 獲取預售票的截止日期時間 deadTime = time.localtime(deadTimeStamp) deadDate = '%04d-%02d-%02d' % (deadTime.tm_year, deadTime.tm_mon, deadTime.tm_mday) # print(Colored.red('請注意合理的乘車日期範圍是:{} 至 {}'.format(localDate, deadDate))) # 判斷輸入的乘車時間是否在合理乘車時間範圍內 # 將購票日期轉換為時間陣列 trainTimeStruct = time.strptime(date, "%Y-%m-%d") # 轉換為時間戳: 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) # 比較購票日期時間戳與當前時間戳和預售截止日期時間戳 if currentTimeStamp <= trainTimeStamp and trainTimeStamp <= deadTimeStamp: return True, trainDate else: print(Colored.red('Error:您輸入的乘車日期:{}, 當前系統日期:{}, 預售截止日期:{}'.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) # 根據車站名獲取電報碼 @classmethod def getStationCode(self, station): codesDict = StationCodes().getCodesDict() if station in codesDict.keys(): return codesDict[station] # 輸入出發地和目的地 @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) # 轉換成時間陣列 timeArray = time.strptime(dateStr, "%Y%m%d") # 轉換成時間戳 timestamp = time.mktime(timeArray) # 轉換成localtime timeLocal = time.localtime(timestamp) # 轉換成新的時間格式 GMT_FORMAT = '%a %b %d %Y %H:%M:%S GMT+0800 (China Standard Time)' timeStr = time.strftime(GMT_FORMAT, timeLocal) return timeStr
特別要注意一下 getTrainDate
方法裡返回時間字串格式,我使用Firefox瀏覽器抓包時發現格式是 Wed+Aug+22+2018+00:00:00+GMT+0800+(China+Standard+Time)
,但是在專案裡使用此格式時會發現無法請求到資料。後來使用Google瀏覽器抓包發後現時間字串裡沒有 +
符號。
Color 類
另外為了能在 Terminal
裡能使用不同顏色來顯示列印資訊,我們定義一個Color類:
from colorama import init, Fore, Back init(autoreset=False) class Color(object): #前景色:紅色背景色:預設 @classmethod def red(self, s): return Fore.RED + s + Fore.RESET @classmethod #前景色:綠色背景色:預設 def green(self, s): return Fore.GREEN + s + Fore.RESET @classmethod #前景色:黃色背景色:預設 def yellow(self, s): return Fore.YELLOW + s + Fore.RESET #前景色:藍色背景色:預設 @classmethod def blue(self, s): return Fore.BLUE + s + Fore.RESET #前景色:洋紅色背景色:預設 @classmethod def magenta(self, s): return Fore.MAGENTA + s + Fore.RESET #前景色:青色背景色:預設 @classmethod def cyan(self, s): return Fore.CYAN + s + Fore.RESET #前景色:白色背景色:預設 @classmethod def white(self, s): return Fore.WHITE + s + Fore.RESET #前景色:黑色背景色:預設 @classmethod def black(self, s): return Fore.BLACK #前景色:白色背景色:綠色 @classmethod def white_green(self, s): return Fore.WHITE + Back.GREEN + s + Fore.RESET + Back.RESET
驗證碼驗證&使用者登入
從抓包結果來看,12306平臺首先進行驗證碼驗證,驗證通過後才會繼續驗證使用者名稱和密碼。
驗證碼圖片介面是: ofollow,noindex">https://kyfw.12306.cn/passport/captcha/captcha-image?login_site=E&module=login&rand=sjrand&0.05013721011282968 連結裡最後一個引數 0.05013721011282968
每次請求時都不一樣。但我發現沒有此引數同樣能夠請求到驗證碼圖片。
驗證碼驗證時提交的引數 login_site 和 rand 是固定的,而 answer 是指正確圖片位置座標,但座標基準原點是如下圖紅色箭頭處而非整個驗證碼圖片的左上角。

驗證碼圖片
我們可以分別採用手動和打碼平臺自動方式對驗證碼進行驗證,手動驗證即把驗證碼圖片分割成8個小圖片,依次編號1-8,每個小圖片上取固定的一個位置座標,平臺返回驗證碼圖片後,使用者手動輸入正確驗證碼所在位置:

驗證碼圖片分割
所謂的打碼平臺自動驗證是指使用者給打碼平臺傳入一張驗證碼圖片,平臺通過碼工去人工識別驗證碼 (碼工有出錯可能) ,平臺再將其結果返回給使用者,這個過程一般也就2-3秒時間。12306驗證碼是多個座標拼接成的字串,因此我們需要平臺返回多個座標字串。
百度搜尋 打碼平臺 關鍵字能夠找到很多相關平臺,其中包含 打碼兔 、 超級鷹 等。寫本文的時發現打碼兔平臺已經轉型,不再提供打碼服務,於是我只能去註冊超級鷹賬戶。平臺網站上有如何使用 Python 進行打碼的 相關文件 ,使用時需要注意驗證碼圖片的型別,返回多個座標對應的 codetype
為 9004 ,具體請參考 驗證碼型別 。
如下程式碼是超級鷹官網提供的,我做了一些改動,原因是平臺返回的座標是以圖片的左上角為原點,這與12306座標基準不一致。
另外,我本想直接去掉ReportError
方法的,後來發現超級鷹打碼平臺有出錯機率。於是如果驗證碼驗證失敗,則向平臺提交失敗圖片的ID。這樣做的目的是節省平臺積分,因為我們每提交一張驗證碼圖片給平臺進行識別都要付出積分,但倘若平臺識別錯誤,則此題積分會返回。
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(',') # 將平臺返回的縱座標減去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是打碼平臺返回的結果,answerStr是縱座標減去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 importUtility from color import Color from APIs import API class Login(object): session = Utility.getSession()# 建立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#返回自己寫的驗證碼資訊和平臺反應的資訊 except Exception as e: print(str(e)) # 驗證碼驗證 def captchaCheck(self): # 手動驗證 # self.getCaptchaAnswer() # imgLocation = input("請輸入驗證碼圖片位置,以英文狀態下的分號','分割:\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# 驗證碼對應的座標字串 } 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 # 以下是登入過程進行的相關請求 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網站".format(Utility.redColor(username),genderStr)) return username# 返回使用者名稱,便於搶票時使用。當然一個12306賬戶裡可能有多個常用乘客,我們也可以獲取聯絡人列表,給其他人搶票
查詢餘票
首先明確一點,即使用者在不登入情況下也是可以查車票資訊的。開啟瀏覽器進入12306餘票查詢頁面 查詢連結 ,然後開啟開發者模式,在頁面上輸入出發地為 上海 ,目的地為 成都 ,出發日期為 2018-08-28 ,車票型別選擇 成人 ,點選 查詢 按鈕。我們發現只有如下的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 幾個引數,從引數的英文含義上不難判斷它們分別代表出發日期、出發地、目的地和車票型別。但出發地怎麼是 SHH ,目的地又怎麼是 CDW ?這些都是什麼?我百度了一下,這些字元是指車站電報碼。可這些資料從何而來呢?
在開發者模式開啟的情況下重新整理查詢頁面,發現多了很多請求。仔細檢視每個請求都在做些什麼操作?伺服器又返回了什麼?Oh my gosh,竟然在剛開啟查詢頁面的時候就請求到了。

電報碼介面
我們把資料請求下來並加以儲存,儲存的原因是這些資料一般情況下都不會改變,請求一次,下次直接使用。
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-\u95fa5]+)\|([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
獲取到車站電報碼,接下來我們就可以查詢餘票了。細節部分將在程式碼裡進行講解。
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'],# 出發站電報碼 'leftTicketDTO.to_station': queryData['toStationCode'],# 到達站電報碼 'purpose_codes': 'ADULT'# 0X00是學生票 } res = self.session.get(API.queryTicket,params = parameters) trainDicts = self.getTrainInfo(res.json(), queryData) return queryData, trainDicts# 返回查詢資料和車次資訊,便於下單時使用 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] #出發地電報碼 trainDict['toTelecode']= trainInfo[7] # 出發地電報碼 trainDict['fromStation']= maps[trainInfo[6]]# 上海 trainDict['toStation']= maps[trainInfo[7]]# 成都 trainDict['departTime']= Color.green(trainInfo[8])# 出發時間 trainDict['arriveTime']= Color.red(trainInfo[9])# 到達時間 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]# 商務座 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方法將字典新增到列表中,如果需要更改字典中的資料,那麼列表中的內容也會發生改變,這是因為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('請輸入出發地')# 出發地 toStation = Utility.inputStation('請輸入目的地')# 目的地 fromStationCode = Utility.getStationCode(fromStation) # 出發地電報碼 toStationCode = Utility.getStationCode(toStation)# 目的地電報碼 queryData = { 'fromStation':fromStation, 'toStation':toStation, 'trainDate':trainDate, 'fromStationCode':fromStationCode, 'toStationCode':toStationCode } return queryData def prettyPrint(self,trains,queryData): header = ["車次", "車站", "時間", "歷時", "商務座","一等座", "二等座",'高階軟臥',"軟臥", "動臥", "硬臥", "硬座", "無座",'其他'] 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商務座:\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:系統未能查詢到{}座位型別存有餘票'.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('系統提交訂單請求成功') elif dict['messages'] != []: if dict['messages'][0] == '車票資訊已過期,請重新查詢最新車票資訊': print('車票資訊已過期,請重新查詢最新車票資訊') else: print("系統提交訂單請求失敗") 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引數失敗') 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('系統校驗訂單資訊成功') if dict['data']['ifShowPassCode'] == 'Y': print('需要再次驗證') return True if dict['data']['ifShowPassCode'] == 'N': return False else: print('系統校驗訂單資訊失敗') 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('系統獲取佇列資訊成功') self.confirmSingleForQueue(seatType,repeatSubmitToken,keyCheckIsChange,passenger,trainDict) else: print('系統獲取佇列資訊失敗') 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進行支付') else: print('訂票失敗,請稍後重試!')
我們現在來訂購一張28號上海到成都的二等座車票,在專案裡是無法完成支付的,必須到12306官網進行支付!

訂單資訊.png
我們可以將訂票成功的結果以簡訊或者郵件的方式傳送出去,提醒使用者。
簡訊部分我已經寫好了,在程式碼裡就不展示了。