使用Python获取12306余票信息和票价

思路

12306车票的信息抓取来还是比较简单的,其实难的如何用Python抢票,12306有一个很“特殊”的验证码,即使是人识别起来都有一定难度。但其实这种难度也恰是它薄弱的地方,这意味着此类验证码的数量是有限的。我自己没有抓过,但我之前看过一篇文章,据说这种码一共是9000个左右(我记得好像作者把验证码抓下来用md5加密后保存下来,存到9000多个就存不下去了)。假设我们把每个验证码图片的答案都记下来,理论上就可以完成自动登陆了。听起来操作很费力。

如果只是要查询车票信息和价钱的话,就比较简单了。只有几个需要注意的点,这里讲一下。

获得站名和其编号的对应关系

在所有请求中,有一个js文件很重要,名字叫station_name.js?station_version=1.9053,后面的是版本号。这个文件会返回一个字符串,类似于:

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

可以看得出来,站与站之间用@分隔,然后每个站的信息字段用|分隔。具体哪个对应哪个可以看我下面的代码。其中最重要的是第三个字段,之后查询车票信息的时候,就是用这个字段的值的。我不仅获取并解析了这个字符串,同时也把信息写入了sqlite3数据库里。

查询剩余车票信息

查询余票信息的是一条XHR请求:

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很好理解,两个station用的就是上一节中所说的teleCode,因此要模拟这个请求,你需要先根据站点名字(如‘北京北’)从数据库中得到其编号(如‘VAP')。

这条请求的响应中,有三部分有用:

  • status字段,标识了这条请求是否成功。
  • data字段中的map字段,里面包好了响应中使用的tecoCode与其对应的站点名字,这样我们就不需要再利用数据库将编号转会站台名了。因为采用的模糊查询,所以map中数据可能不止两个,比如说搜索’北京‘可能会返回’北京‘和’北京南‘。
  • data字段中的result字段,这个字段就包含了所有的票价信息。但是它也是经过编码的,同样用|分隔。这里最难的地方在于这些字段实在是太多了,完全不知道哪个是哪个。

一个例子:

"null|预订|240000D3110K|D311|VNP|SHH|VNP|TXP|21:11|22:19|01:08|N|0Xz%2FAQnjcOjRAf%2FlTLDixMnMJwxcpe7x|20180524|3|P2|01|02|0|0||||无||||||||||无|F040|F4|0"

前面几个还好理解,到了后半部分代表座位剩余数量的字段就显得一脸懵逼了。

怎么办?除了一个一个猜,我们可以从网页源代码和js文件里找出一点线索。在queryLeftTicket_end_js.js?scriptVersion=1.9085文件中的2814行,可以看到:

12306-table.jpg

这已经可以看出很多东西了,如果还有点迷惑,再看网页的源代码:

12306-html_table.jpg

通过审查元素,你可以知道哪个id对应哪个字段,然后再从上图上找出其对应的字段偏移量。

查询票价

查询票价使用的是另外一条XHR请求:

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

这几个参数在查询余票的响应中都包含了,分别是第2、16、17和35个字段。但是难点是找出seat_types中几个值分别代表了什么座位。这个我没找到线索,但是我在网上找到了别人代码里写的对应关系,但是现在找不到那份代码了。。这里我直接给出对应关系。

座位类型 编号 座位类型 编号
商务座 A9 特等座 P
一等座 M 二等座 O
高级软卧 A6 软卧 A4
动卧 -- 硬卧 A3
软座 A2 硬座 A1
无座 WZ 其他 --

一条余票查询的请求会对应很多条票价查询的请求,因此如果余票类型很充足,会消耗很长时间(可以关闭)。

代码

除了余票数量和票价的功能代码,我还使用了PrettyTablecolorama库,支持彩色表格打印,同时增加是否支持身份证出入站始发站、终点站、经过站的标识。代码如下,不包括数据库接口和配置文件,其中t12306_init()只要执行一次(省资源,执行多次不会出错):

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 ("获取数据失败。")
        return

    # 站点编号->站点名字 的Map
    name_map = data.get('data').get('map')

    table = PrettyTable()
    table.field_names = ["车次", "出发/到达", "出发/到达时间", "历时", "可否网购", "商务座", "特等座", "一等座", "二等座", "高级软卧", "软卧", "动卧", "硬卧", "软座","硬座", "无座", "其他"]
    table.align["车次"] = "l"
    table.align["出发/到达"] = "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 "[过]" # 是否始发站
        flag_to = "[终]" if field[5] == field[7] else "[过]"# 是否终点站

        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 "否",
            # 商务座
            "{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 ""),
            # 高级软卧
            "{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 ""),
            # 动卧
            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 ""),
            # 无座
            "{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 ("获取数据失败。")
        return None

    return data.get('data')


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

    date = input("请输入乘车时间(YYYY-MM-DD): ")
    from_station = input("请输入出发车站: ")
    to_station = input("请输入到达车站: ")
    purpose_codes = input("请输入类型(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)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,837评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,551评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,417评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,448评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,524评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,554评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,569评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,316评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,766评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,077评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,240评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,912评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,560评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,176评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,425评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,114评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,114评论 2 352

推荐阅读更多精彩内容

  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,917评论 2 89
  • 点击查看原文 Web SDK 开发手册 SDK 概述 网易云信 SDK 为 Web 应用提供一个完善的 IM 系统...
    layjoy阅读 13,744评论 0 15
  • 在朋友圈看到一篇文章《孩子,你不必做父母的拯救者》,想起当初自己接触自我成长的缘分。 曾经,我一直很想为爸妈做些什...
    漫步的小马驹阅读 494评论 11 8
  • 周杰伦有一首歌《听妈妈的话》,歌词里写道‘为什么别人在那看漫画,我却在学画画,对着钢琴说话。别人在玩游戏,我...
    绿芒果君阅读 536评论 0 0
  • 文/行动的小怪兽 盛夏的一个午后,我和德德盘腿坐在空调房的凉席上,看着阿牛好几年前拍的电影《初恋红豆冰》,笑得不能...
    行动的小怪兽阅读 603评论 2 4