使用Python獲取12306余票信息和票價(jià)

思路

12306車票的信息抓取來(lái)還是比較簡(jiǎn)單的造锅,其實(shí)難的如何用Python搶票袄简,12306有一個(gè)很“特殊”的驗(yàn)證碼逊桦,即使是人識(shí)別起來(lái)都有一定難度谈喳。但其實(shí)這種難度也恰是它薄弱的地方粹舵,這意味著此類驗(yàn)證碼的數(shù)量是有限的钮孵。我自己沒(méi)有抓過(guò),但我之前看過(guò)一篇文章眼滤,據(jù)說(shuō)這種碼一共是9000個(gè)左右(我記得好像作者把驗(yàn)證碼抓下來(lái)用md5加密后保存下來(lái)巴席,存到9000多個(gè)就存不下去了)。假設(shè)我們把每個(gè)驗(yàn)證碼圖片的答案都記下來(lái)诅需,理論上就可以完成自動(dòng)登陸了漾唉。聽(tīng)起來(lái)操作很費(fèi)力。

如果只是要查詢車票信息和價(jià)錢(qián)的話堰塌,就比較簡(jiǎn)單了赵刑。只有幾個(gè)需要注意的點(diǎn),這里講一下场刑。

獲得站名和其編號(hào)的對(duì)應(yīng)關(guān)系

在所有請(qǐng)求中般此,有一個(gè)js文件很重要,名字叫station_name.js?station_version=1.9053,后面的是版本號(hào)牵现。這個(gè)文件會(huì)返回一個(gè)字符串铐懊,類似于:

@bjb|北京北|VAP|beijingbei|bjb|0@bjd|北京東|BOP|beijingdong|bjd|1..........

可以看得出來(lái),站與站之間用@分隔瞎疼,然后每個(gè)站的信息字段用|分隔科乎。具體哪個(gè)對(duì)應(yīng)哪個(gè)可以看我下面的代碼。其中最重要的是第三個(gè)字段贼急,之后查詢車票信息的時(shí)候喜喂,就是用這個(gè)字段的值的。我不僅獲取并解析了這個(gè)字符串竿裂,同時(shí)也把信息寫(xiě)入了sqlite3數(shù)據(jù)庫(kù)里。

查詢剩余車票信息

查詢余票信息的是一條XHR請(qǐng)求:

https://kyfw.12306.cn/otn/leftTicket/query?leftTicketDTO.train_date=2018-05-24&leftTicketDTO.from_station=BJP&leftTicketDTO.to_station=TJP&purpose_codes=ADULT

其中train_data和purpose_codes很好理解照弥,兩個(gè)station用的就是上一節(jié)中所說(shuō)的teleCode腻异,因此要模擬這個(gè)請(qǐng)求,你需要先根據(jù)站點(diǎn)名字(如‘北京北’)從數(shù)據(jù)庫(kù)中得到其編號(hào)(如‘VAP')这揣。

這條請(qǐng)求的響應(yīng)中悔常,有三部分有用:

  • status字段,標(biāo)識(shí)了這條請(qǐng)求是否成功给赞。
  • data字段中的map字段机打,里面包好了響應(yīng)中使用的tecoCode與其對(duì)應(yīng)的站點(diǎn)名字,這樣我們就不需要再利用數(shù)據(jù)庫(kù)將編號(hào)轉(zhuǎn)會(huì)站臺(tái)名了片迅。因?yàn)椴捎玫哪:樵儾醒詍ap中數(shù)據(jù)可能不止兩個(gè),比如說(shuō)搜索’北京‘可能會(huì)返回’北京‘和’北京南‘。
  • data字段中的result字段芥挣,這個(gè)字段就包含了所有的票價(jià)信息驱闷。但是它也是經(jīng)過(guò)編碼的,同樣用|分隔空免。這里最難的地方在于這些字段實(shí)在是太多了空另,完全不知道哪個(gè)是哪個(gè)。

一個(gè)例子:

"null|預(yù)訂|240000D3110K|D311|VNP|SHH|VNP|TXP|21:11|22:19|01:08|N|0Xz%2FAQnjcOjRAf%2FlTLDixMnMJwxcpe7x|20180524|3|P2|01|02|0|0||||無(wú)||||||||||無(wú)|F040|F4|0"

前面幾個(gè)還好理解蹋砚,到了后半部分代表座位剩余數(shù)量的字段就顯得一臉懵逼了。

怎么辦坝咐?除了一個(gè)一個(gè)猜循榆,我們可以從網(wǎng)頁(yè)源代碼和js文件里找出一點(diǎn)線索。在queryLeftTicket_end_js.js?scriptVersion=1.9085文件中的2814行畅厢,可以看到:

12306-table.jpg

這已經(jīng)可以看出很多東西了冯痢,如果還有點(diǎn)迷惑,再看網(wǎng)頁(yè)的源代碼:

12306-html_table.jpg

通過(guò)審查元素框杜,你可以知道哪個(gè)id對(duì)應(yīng)哪個(gè)字段浦楣,然后再?gòu)纳蠄D上找出其對(duì)應(yīng)的字段偏移量。

查詢票價(jià)

查詢票價(jià)使用的是另外一條XHR請(qǐng)求:

https://kyfw.12306.cn/otn/leftTicket/queryTicketPrice?train_no=24000C22290F&from_station_no=01&to_station_no=03&seat_types=O9OMP&train_date=2018-05-24

這幾個(gè)參數(shù)在查詢余票的響應(yīng)中都包含了咪辱,分別是第2振劳、16、17和35個(gè)字段油狂。但是難點(diǎn)是找出seat_types中幾個(gè)值分別代表了什么座位历恐。這個(gè)我沒(méi)找到線索,但是我在網(wǎng)上找到了別人代碼里寫(xiě)的對(duì)應(yīng)關(guān)系专筷,但是現(xiàn)在找不到那份代碼了弱贼。。這里我直接給出對(duì)應(yīng)關(guān)系磷蛹。

座位類型 編號(hào) 座位類型 編號(hào)
商務(wù)座 A9 特等座 P
一等座 M 二等座 O
高級(jí)軟臥 A6 軟臥 A4
動(dòng)臥 -- 硬臥 A3
軟座 A2 硬座 A1
無(wú)座 WZ 其他 --

一條余票查詢的請(qǐng)求會(huì)對(duì)應(yīng)很多條票價(jià)查詢的請(qǐng)求吮旅,因此如果余票類型很充足,會(huì)消耗很長(zhǎng)時(shí)間(可以關(guān)閉)味咳。

代碼

除了余票數(shù)量和票價(jià)的功能代碼庇勃,我還使用了PrettyTablecolorama庫(kù),支持彩色表格打印槽驶,同時(shí)增加是否支持身份證出入站始發(fā)站责嚷、終點(diǎn)站、經(jīng)過(guò)站的標(biāo)識(shí)掂铐。代碼如下罕拂,不包括數(shù)據(jù)庫(kù)接口和配置文件揍异,其中t12306_init()只要執(zhí)行一次(省資源,執(zhí)行多次不會(huì)出錯(cuò)):

12306-result.jpg

12306.py

import requests
import ast
import json
from random import choice
from prettytable import PrettyTable
from colorama import init, Fore, Style


import Configure as Configs
import Sqlite3api as Sqlite3

init()

header = {}
header['user-agent'] = choice(Configs.FakeUserAgents)
header['Referer'] = "https://kyfw.12306.cn/otn/leftTicket/init"

def t12306_init():
    conn = Sqlite3.sqlite3_init()

    url = "https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version=1.9053"
    content = None

    ret = Sqlite3.sqlite3_execute(conn, "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='t12306'")[0][0]
    if ret == 1:
        Sqlite3.sqlite3_execute(conn, "DROP TABLE t12306")

    Sqlite3.sqlite3_execute(conn, "CREATE TABLE t12306 (stationId real, stationName text, teleCode text, pinYin text, pinYinHead text)")
    Sqlite3.sqlite3_execute(conn, "CREATE UNIQUE INDEX idx_follow_stationId on t12306(stationId)")

    try:
        response = requests.get(url, headers=header)

        if response.status_code == requests.codes.ok:
            content = response.text
    except Exception as e:
        print (e)

    data = content[:-1].split('=')[1][2:]

    for station in data.split('@'):
        fields = station.split('|')
        Sqlite3.sqlite3_execute(conn, "INSERT INTO t12306 VALUES (?,?,?,?,?)", (fields[5],fields[1],fields[2],fields[3],fields[0],))
        #print (fields[5],fields[1],fields[2],fields[3],fields[0])
            
    Sqlite3.sqlite3_close(conn)


def check_left_ticket(train_date, from_station, to_station, purpose_codes, need_price=False):
    url = "https://kyfw.12306.cn/otn/leftTicket/query"

    payload = {
        'leftTicketDTO.train_date': train_date,
        'leftTicketDTO.from_station': from_station,
        'leftTicketDTO.to_station': to_station,
        'purpose_codes': purpose_codes
    }

    content = ''
    try:
        response = requests.get(url, headers=header, params=payload)
        if response.status_code == requests.codes.ok:
            response.encoding = 'utf-8'
            content = response.text
    except Exception as e:
        print (e)
    
    data = json.loads(content)
    #print (data)
    if data.get('status') == False:
        print ("獲取數(shù)據(jù)失敗聂受。")
        return

    # 站點(diǎn)編號(hào)->站點(diǎn)名字 的Map
    name_map = data.get('data').get('map')

    table = PrettyTable()
    table.field_names = ["車次", "出發(fā)/到達(dá)", "出發(fā)/到達(dá)時(shí)間", "歷時(shí)", "可否網(wǎng)購(gòu)", "商務(wù)座", "特等座", "一等座", "二等座", "高級(jí)軟臥", "軟臥", "動(dòng)臥", "硬臥", "軟座","硬座", "無(wú)座", "其他"]
    table.align["車次"] = "l"
    table.align["出發(fā)/到達(dá)"] = "l"

    for ticket_list in data.get('data').get('result'):
        field = ticket_list.split('|')
        flag_id = "[身]" if field[18]=='1' else "" # 是否支持身份證
        flag_from = "[始]" if field[4] == field[6] else "[過(guò)]" # 是否始發(fā)站
        flag_to = "[終]" if field[5] == field[7] else "[過(guò)]"# 是否終點(diǎn)站

        price = {}
        if need_price == True:
            ret = query_ticket_price(field[2],field[16],field[17],field[35],train_date)
            price = ret if ret else {}

        table.add_row([
            field[3] + (Fore.YELLOW + flag_id + Fore.RESET ) ,
            '\n'.join([Fore.LIGHTGREEN_EX + flag_from + name_map.get(field[6]) + Fore.RESET,
                        Fore.LIGHTRED_EX + flag_to + name_map.get(field[7]) + Fore.RESET]),
            '\n'.join([Fore.LIGHTGREEN_EX + field[8] + Fore.RESET,
                        Fore.LIGHTRED_EX + field[9] + Fore.RESET]),
            field[10],
            "是" if field[10] else "否",
            # 商務(wù)座
            "{0:s}{1:s}".format(field[32] if field[32] else "--", ("\n" + Style.BRIGHT + price.get('A9')) + Style.RESET_ALL if price.get('A9') else ""),
            # 特等座
            "{0:s}{1:s}".format(field[25] if field[25] else "--", ("\n" + Style.BRIGHT + price.get('P')) + Style.RESET_ALL if price.get('P') else ""),
            # 一等座
            "{0:s}{1:s}".format(field[31] if field[31] else "--", ("\n" + Style.BRIGHT + price.get('M')) + Style.RESET_ALL if price.get('M') else ""),
            # 二等座 
            "{0:s}{1:s}".format(field[30] if field[30] else "--", ("\n" + Style.BRIGHT + price.get('O')) + Style.RESET_ALL if price.get('O') else ""),
            # 高級(jí)軟臥
            "{0:s}{1:s}".format(field[21] if field[21] else "--", ("\n" + Style.BRIGHT + price.get('A6')) + Style.RESET_ALL if price.get('A6') else ""),
            # 軟臥
            "{0:s}{1:s}".format(field[23] if field[23] else "--", ('\n' + Style.BRIGHT + price.get('A4')) + Style.RESET_ALL if price.get('A4') else ""),
            # 動(dòng)臥
            field[33] if field[33] else "--",
            # 硬臥
            "{0:s}{1:s}".format(field[28] if field[28] else "--", ("\n" + Style.BRIGHT + price.get('A3')) + Style.RESET_ALL if price.get('A3') else ""),
            # 軟座
            "{0:s}{1:s}".format(field[24] if field[24] else "--", ("\n" + Style.BRIGHT + price.get('A2')) + Style.RESET_ALL if price.get('A2') else ""),
            # 硬座
            "{0:s}{1:s}".format(field[29] if field[29] else "--", ("\n" + Style.BRIGHT + price.get('A1')) + Style.RESET_ALL if price.get('A1') else ""),
            # 無(wú)座
            "{0:s}{1:s}".format(field[26] if field[26] else "--", ("\n" + Style.BRIGHT + price.get('WZ')) + Style.RESET_ALL if price.get('WZ') else ""),
            # 其他
            field[22] if field[22] else "--"
            ])
        
    print (table)

def query_ticket_price(train_no, from_station_no, to_station_no, seat_types, train_date):
    url = "https://kyfw.12306.cn/otn/leftTicket/queryTicketPrice"

    #print (train_no, from_station_no, to_station_no, seat_types, train_date)
    payload = {
        'train_no': train_no,
        'from_station_no': from_station_no,
        'to_station_no': to_station_no,
        'seat_types': seat_types,
        'train_date': train_date,
    }

    try:
        response = requests.get(url, headers=header, params=payload)
        if response.status_code == requests.codes.ok:
            #response.encoding = 'utf-8'
            content = response.text
    except Exception as e:
        print (e)

    data = json.loads(content)

    if data.get('status') == False:
        print ("獲取數(shù)據(jù)失敗蒿秦。")
        return None

    return data.get('data')


if __name__ == "__main__":
    #t12306_init()
    conn = Sqlite3.sqlite3_init()

    date = input("請(qǐng)輸入乘車時(shí)間(YYYY-MM-DD): ")
    from_station = input("請(qǐng)輸入出發(fā)車站: ")
    to_station = input("請(qǐng)輸入到達(dá)車站: ")
    purpose_codes = input("請(qǐng)輸入類型(1-成人票): ")
    from_s = to_s = ticket_type = None
    
    ret = Sqlite3.sqlite3_execute(conn, "SELECT teleCode FROM t12306 WHERE stationName ='{0:s}'".format(from_station))#[0][0]
    from_s = ret[0][0]

    ret = Sqlite3.sqlite3_execute(conn, "SELECT teleCode FROM t12306 WHERE stationName ='{0:s}'".format(to_station))#[0][0]
    to_s = ret[0][0]

    Sqlite3.sqlite3_close(conn)

    if purpose_codes == 1:
        ticket_type = 'ADULT'

    check_left_ticket(date, from_s, to_s, ticket_type, True)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蛋济,隨后出現(xiàn)的幾起案子棍鳖,更是在濱河造成了極大的恐慌,老刑警劉巖碗旅,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件渡处,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡祟辟,警方通過(guò)查閱死者的電腦和手機(jī)医瘫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)旧困,“玉大人醇份,你說(shuō)我怎么就攤上這事『鹁撸” “怎么了僚纷?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)拗盒。 經(jīng)常有香客問(wèn)我怖竭,道長(zhǎng),這世上最難降的妖魔是什么陡蝇? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任痊臭,我火速辦了婚禮,結(jié)果婚禮上登夫,老公的妹妹穿的比我還像新娘广匙。我一直安慰自己,他們只是感情好恼策,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布艇潭。 她就那樣靜靜地躺著,像睡著了一般戏蔑。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鲁纠,一...
    開(kāi)封第一講書(shū)人閱讀 51,763評(píng)論 1 307
  • 那天总棵,我揣著相機(jī)與錄音,去河邊找鬼改含。 笑死情龄,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播骤视,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼鞍爱,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了专酗?” 一聲冷哼從身側(cè)響起睹逃,我...
    開(kāi)封第一講書(shū)人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎祷肯,沒(méi)想到半個(gè)月后沉填,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡佑笋,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年翼闹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蒋纬。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡猎荠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蜀备,到底是詐尸還是另有隱情关摇,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布琼掠,位于F島的核電站拒垃,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏瓷蛙。R本人自食惡果不足惜悼瓮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望艰猬。 院中可真熱鬧横堡,春花似錦、人聲如沸冠桃。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)食听。三九已至胸蛛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間樱报,已是汗流浹背葬项。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留迹蛤,地道東北人民珍。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓襟士,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親嚷量。 傳聞我的和親對(duì)象是個(gè)殘疾皇子陋桂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

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

  • 關(guān)于Mongodb的全面總結(jié) MongoDB的內(nèi)部構(gòu)造《MongoDB The Definitive Guide》...
    中v中閱讀 31,938評(píng)論 2 89
  • 點(diǎn)擊查看原文 Web SDK 開(kāi)發(fā)手冊(cè) SDK 概述 網(wǎng)易云信 SDK 為 Web 應(yīng)用提供一個(gè)完善的 IM 系統(tǒng)...
    layjoy閱讀 13,772評(píng)論 0 15
  • 在朋友圈看到一篇文章《孩子,你不必做父母的拯救者》蝶溶,想起當(dāng)初自己接觸自我成長(zhǎng)的緣分嗜历。 曾經(jīng),我一直很想為爸媽做些什...
    漫步的小馬駒閱讀 496評(píng)論 11 8
  • 周杰倫有一首歌《聽(tīng)媽媽的話》身坐,歌詞里寫(xiě)道‘為什么別人在那看漫畫(huà)秸脱,我卻在學(xué)畫(huà)畫(huà),對(duì)著鋼琴說(shuō)話部蛇。別人在玩游戲摊唇,我...
    綠芒果君閱讀 541評(píng)論 0 0
  • 文/行動(dòng)的小怪獸 盛夏的一個(gè)午后,我和德德盤(pán)腿坐在空調(diào)房的涼席上涯鲁,看著阿牛好幾年前拍的電影《初戀紅豆冰》巷查,笑得不能...
    行動(dòng)的小怪獸閱讀 609評(píng)論 2 4