Python12306訂票

用戶登錄
查詢車票

[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)看起來更加清晰明了酵颁。

項目結(jié)構(gòu)

寫完整個項目后覺得其實也很簡單,無非是使用Session進(jìn)行多次GetPost請求嚷辅,難點在于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_siterand是固定的,而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)的codetype9004蔚万,具體請參考驗證碼類型反璃。
如下代碼是超級鷹官網(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

查詢余票參數(shù)

leftTicketDTO.train_date,leftTicketDTO.from_station,leftTicketDTO.to_stationpurpose_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)
查詢結(jié)果

訂票

這個過程也有很多請求叠赐,具體在代碼里說明芭概。

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)行支付椎木!


訂單信息.png

我們可以將訂票成功的結(jié)果以短信或者郵件的方式發(fā)送出去,提醒用戶畜伐。
短信部分我已經(jīng)寫好了,在代碼里就不展示了慎框。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市馅精,隨后出現(xiàn)的幾起案子洲敢,更是在濱河造成了極大的恐慌,老刑警劉巖哮塞,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異家凯,居然都是意外死亡绊诲,警方通過查閱死者的電腦和手機(jī)掂之,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來胰蝠,“玉大人茸塞,你說我怎么就攤上這事钾虐⌒Ю溃” “怎么了丐枉?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵籍嘹,是天一觀的道長。 經(jīng)常有香客問我颂碘,道長头岔,這世上最難降的妖魔是什么峡竣? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮稚伍,結(jié)果婚禮上个曙,老公的妹妹穿的比我還像新娘。我一直安慰自己猴贰,他們只是感情好米绕,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著碱鳞,像睡著了一般。 火紅的嫁衣襯著肌膚如雪崩泡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天磷蜀,我揣著相機(jī)與錄音褐隆,去河邊找鬼。 笑死歇攻,一個胖子當(dāng)著我的面吹牛缴守,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播村砂,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼评腺!你這毒婦竟也來了歇僧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎柄错,沒想到半個月后给猾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡池颈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片矿微。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖娜庇,靈堂內(nèi)的尸體忽然破棺而出名秀,到底是詐尸還是另有隱情,我是刑警寧澤汁掠,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布考阱,位于F島的核電站秽之,受9級特大地震影響考榨,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜愤诱,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望科吭。 院中可真熱鬧,春花似錦牺弄、人聲如沸势告。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽遗遵。三九已至雄坪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間登澜,已是汗流浹背脑蠕。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人掀虎。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像二打,于是被迫代替她去往敵國和親址儒。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容