12306购票流程分析(非抢票)

说在前面
购票流程的关键位置参数encryptedData未分析出来,(吐槽:还是太菜了啊)本文是个人结合网上以及抓包的分析,在此做个记录。有时间再试试encryptedData。
仅供研究学习使用,请勿用于非法用途
 

12306购票流程(20230925)

1 登录过程

1.1 获取验证模式

在预定车票时,点击预定会跳转到登录页面,在登录页输入用户名和密码后,点击登录会发送一个请求,当返回的login_check_code的值为3时,采用的是短信验证。

  • POST接口:https://kyfw.12306.cn/passport/web/checkLoginVerify
  • 表单:
参数名 说明 示例
username 用户名 xxx
appid 固定参数 otn
_json_att 固定空参数
12306购票流程分析(非抢票)_第1张图片
python示例:
import requests

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36"
}
url = 'https://kyfw.12306.cn/passport/web/checkLoginVerify'
data = {
    "username": user,
    "appid": "excater"
}
res = session.post(url, headers=headers, data=data)

1.2 获取短信验证码

之后会触发短信验证码验证的界面,需要输入身份证后4位获取短信验证码。

  • POST接口:https://kyfw.12306.cn/passport/web/getMessageCode
  • 表单:
参数名 说明 示例
appid 固定参数 otn
username 用户名 xxx
castNum 身份证后4位 3333
_json_att 固定空参数


python示例:

url = 'https://kyfw.12306.cn/passport/web/getMessageCode'
id_4 = input('请输入身份证后4位:')
data = {
    "appid": "otn",
    "username": user,
    "castNum": id_4,
    "_json_att": ""
}
res = session.post(url, headers=headers, data=data)

1.3 登录

获取短信验证码后,才到了真正的登录接口,其中的password参数是加密的,经过分析js,可以确认是sm4加密

  • POST接口:https://kyfw.12306.cn/passport/web/login
  • 表单:
参数名 说明 示例
sessionId 固定空参数
sig 固定空参数
if_check_slide_passcode_token 固定空参数
scene 固定空参数
checkMode 校验模式(默认0) 0
randCode 短信验证码 854390
username 用户名 xxx
password @+sm4加密后的密码 xxxxxxxxxx
appid 固定参数 otn
_json_att 固定空参数

python示例:

from gmssl import sm4
def _sm4_encode(data, key='tiekeyuankp12306'):
    sm4Alg = sm4.CryptSM4()  # 实例化sm4
    sm4Alg.set_key(key.encode(), sm4.SM4_ENCRYPT)  # 设置密钥
    dateStr = str(data)
    # print("明文:", dateStr)
    enRes = sm4Alg.crypt_ecb(dateStr.encode())  # 开始加密,bytes类型,ecb模式
    enHexStr = str(base64.b64encode(enRes), 'utf-8')
    # print("密文:", enHexStr)
    return enHexStr

url = "https://kyfw.12306.cn/passport/web/login"
 login_data = {
     "sessionId": "",
     "sig": "",
     "if_check_slide_passcode_token": "",
     "scene": "",
     "checkMode": "",
     "randCode": "",
     "username": user,
     "password": "@" + _sm4_encode(pwd),
     "appid": "excater"
 }
response = session.post(url, headers=headers, data=login_data)

1.4 获取tk

当我们登录成功后,会获取到uamtk,但此时还不是放松的时候,在抓包中可以看到后面还有2个post请求,经过分析后可以确认是cookies.tk的必要请求
uamtk接口请求中返回的newapptk就是cookies.tk的值

  • POST接口:https://kyfw.12306.cn/passport/web/auth/uamtk
  • 表单:
参数名 说明 示例
appid 固定参数 otn
_json_att 固定空参数

python示例:

import re

url = "https://kyfw.12306.cn/passport/web/auth/uamtk"
data = {
    "appid": "otn",
    "_json_att": ""
}
res = session.post(url, data=data)
tk = re.findall('"newapptk":"(.*?)"', res.text)[0]

1.5 设置tk

接下来会对接收的newapptk值进行校验,校验正常时服务端会将该cookie设置在Session中(突发奇想:不发送该请求,自己将上面的newapptk添加到cookie中是否可以正常登录)

  • POST接口:https://kyfw.12306.cn/otn/uamauthclient
  • 表单:
参数名 说明 示例
tk 获取tk时返回的newapptk 8yQK700A2D-IGiOF_aTd8OPo0VPLJKOQaPsxxxxxxxxx
_json_att 固定空参数

python示例:

url = "https://kyfw.12306.cn/otn/uamauthclient"
data = {
    "tk": tk,
    "_json_att": ""
}
response = session.post(url, headers=headers, data=data)

2 余票查询

2.1 车站编码

由于查票、购票时的站点都是采用编码形式的,需要获取车站对应的编码;返回的数据需要用正则提取一下,每条数据之间用|||分隔,数据中的各项内容用|分隔

  • GET接口:https://kyfw.12306.cn/otn/resources/js/framework/station_name.js
    |分割后的数据内容(0开始):
下标 内容
1 站点名称
2 站点编码

python示例:

url = 'https://kyfw.12306.cn/otn/resources/js/framework/station_name.js'
res = session.get(url)
text = re.search("'(.*?)'", res.text).group(1)
city_code_list = {i.split('|')[1]: i.split('|')[2] for i in text.split('|||') if i}

2.2 车票查询

经测试,余票查询是可以不登录的。载荷中的出发地与目的地是编码方式的,在此之前需要先获取站点对应的编码

  • GET接口:铁路客户服务中心
  • 载荷:
参数名 说明 示例
leftTicketDTO.train_date 出发日期 2023-10-13
leftTicketDTO.from_station 出发站点 BJP
leftTicketDTO.to_station 目的站点 SHH
purpose_codes 固定参数 ADULT

此接口返回的数据以|进行分割后,可以确认的结果目录(从0开始):

下标 内容 说明
0 secretStr 提交请求时会用到
1 状态 用于判断是否可预定
2 train_no 提交预定请求时会用到
3 车次
6 出发站名
7 到达站名
8 出发时间
9 到达时间
10 历时
12 leftTicket 提交预定请求与确认配置信息时用到
15 train_location 提交预定请求与确认配置信息时用到
23 软卧数
26 无座数
28 硬卧数
29 硬座数
30 二等座
31 一等座
32 商务座

python示例:

def queryZ(gotime, st_city, ds_city):  # secretStr
    if (datetime.datetime.strptime(gotime, '%Y-%m-%d')-datetime.datetime.now()).days > 15:
        logger.error('超出可预定时间,无法查询')
        quit(401)

    code_city = {v: k for k, v in city_code.items()}
    url = "https://kyfw.12306.cn/otn/leftTicket/queryZ"
    params = {
        "leftTicketDTO.train_date": gotime,
        "leftTicketDTO.from_station": city_code[st_city],
        "leftTicketDTO.to_station": city_code[ds_city],
        "purpose_codes": "ADULT"
    }
    response = session.get(url, headers=headers, cookies=cookies, params=params)
    try:
        result = [i for i in response.json()['data']['result']]
        parse_format = {
            0: "secretStr",
            1: "状态",
            2: "train_no",
            3: "车次",
            6: "出发站名",
            7: "到达站名",
            8: "出发时间",
            9: "到达时间",
            10: "历时",
            12: "leftTicket",
            15: "train_location",
            23: "软卧",
            26: "无座",
            28: "硬卧",
            29: "硬座",
            30: "二等座",
            31: "一等座",
            32: "商务座"
        }
        data_list = [{v: i.split('|')[k] for k, v in parse_format.items()} for i in result]
        for data in data_list:
            data['出发站名'] = code_city[data['出发站名']]
            data['到达站名'] = code_city[data['到达站名']]
        # logger.info(f'数据提取后:{data_list}')
        return data_list
    except requests.exceptions.JSONDecodeError:
        logger.error('[queryZ]requests.exceptions.JSONDecodeError: 请求出现错误,返回数据非json格式')

3 预定

所有预定请求都需要携带登录后的cookies

3.1 提交请求

在选定了车次后,点击预定按钮会提交请求

  • POST接口:https://kyfw.12306.cn/otn/leftTicket/submitOrderRequest
  • 表单:
参数名 说明 示例
secretStr 2.2车票查询时获取的参数 uL7wOFr0GvhxkFOHvM64
train_date 出发时间 2023-10-02
back_train_date 购票时间 2023-09-18
tour_flag 固定参数 dc
purpose_codes 固定参数 ADULT
query_from_station_name 出发站点 长沙
query_to_station_name 目的站点 上海
undefined 固定空参数

python示例:

def submitOrderRequest(secretStr, gotime, st_city, ds_city):
    url = "https://kyfw.12306.cn/otn/leftTicket/submitOrderRequest"
    data = {
        "secretStr": parse.unquote(secretStr),
        "train_date": gotime,
        "back_train_date": ctime,
        "tour_flag": "dc",
        "purpose_codes": "ADULT",
        "query_from_station_name": st_city,
        "query_to_station_name": ds_city,
        "undefined": ""
    }
    res = session.post(url, headers=headers, data=data)
    if res.json()['messages']:
        logger.warning(res.json()['messages'])
        return res.json()['messages']
    else:
        logger.info('[submitOrderRequest]提交请求成功')

3.2 请求确认预定信息

提交请求后会触发请求的确认,该接口返回的是HTML数据

  • POST接口:https://kyfw.12306.cn/otn/confirmPassenger/initDc
  • 表单:
参数名 说明 示例
_json_att 固定空参数
需要用正则在返回数据中提取两个参数,用于后续的请求 参数名 说明 正则示例
REPEAT_SUBMIT_TOKEN 用于多个请求 var globalRepeatSubmitToken = '(.*?)'
key_check_isChange 用于确认配置信息 'key_check_isChange':'(.*?)'

python示例:

def initDc():  # REPEAT_SUBMIT_TOKEN  key_check_isChange
    url = "https://kyfw.12306.cn/otn/confirmPassenger/initDc"
    data = {
        "_json_att": ""
    }
    text = session.post(url, headers=headers, data=data).text
    info_dict = {
        "REPEAT_SUBMIT_TOKEN": re.findall("var globalRepeatSubmitToken = '(.*?)'", text)[0],
        "key_check_isChange": re.findall("'key_check_isChange':'(.*?)'", text)[0]

    }
    return info_dict

3.3 获取乘客信息

在进入选座界面后,需要选择乘客

  • POST接口:https://kyfw.12306.cn/otn/confirmPassenger/getPassengerDTOs
  • 表单:
参数名 说明 示例
_json_att 固定空参数
REPEAT_SUBMIT_TOKEN 3.2请求确认预定信息获取 a14fe59b302c8bf7b7901aee95811111

返回的乘客列表在data.normal_passengers中,其中订单需要用到的项有:

参数名 说明 示例
passenger_name 姓名 张三
passenger_id_type_code 身份认证代码 1
passenger_id_no 身份证 333***333
mobile_no 手机号 159***3333
allEncStr 确认订单信息与确认配置信息用到 639...
passenger_type 是否成人 1

python示例:

def getPassengerDTOs(info_dict):  # passengerTicketStr oldPassengerStr
    url = "https://kyfw.12306.cn/otn/confirmPassenger/getPassengerDTOs"
    data = {
        "_json_att": "",
        "REPEAT_SUBMIT_TOKEN": info_dict["REPEAT_SUBMIT_TOKEN"]
    }
    res = session.post(url, headers=headers, data=data)
    return res.json()['data']['normal_passengers']

3.4 确认订单信息

在选择乘客后点击确认,会进行订单校验

  • POST接口:https://kyfw.12306.cn/otn/confirmPassenger/checkOrderInfo
  • 表单:
参数名 说明 示例
cancel_flag 固定参数 2
bed_level_order_num 固定参数 000000000000000000000000000000
passengerTicketStr 乘客信息 O,0,1,xxx,333333,159333,N,43253...
oldPassengerStr 乘客信息 xxx,1,333***333,1_
tour_flag 固定参数 dc
whatsSelect 固定参数 1
sessionId 固定空参数
sig 固定空参数
scene 固定参数 nc_login
_json_att 固定空参数
REPEAT_SUBMIT_TOKEN 3.2请求确认预定信息获取 a14fe59b302c8bf7b7901aee95811111

python示例:

def checkOrderInfo(info_dict, pass_info):
    url = "https://kyfw.12306.cn/otn/confirmPassenger/checkOrderInfo"
    name = pass_info['passenger_name']
    id_type = pass_info['passenger_id_type_code']
    id_no = pass_info['passenger_id_no']
    mob_no = pass_info['mobile_no']
    enc_str = pass_info['allEncStr']
    pass_type = pass_info['passenger_type']

    data = {
        "cancel_flag": "2",
        "bed_level_order_num": "000000000000000000000000000000",
        "passengerTicketStr": f"O,0,1,{name},{id_type},{id_no},{mob_no},N,{enc_str}",
        "oldPassengerStr": f"{name},{id_type},{id_no},{pass_type}_",
        "tour_flag": "dc",
        "whatsSelect": "1",
        "sessionId": "",
        "sig": "",
        "scene": "nc_login",
        "_json_att": "",
        "REPEAT_SUBMIT_TOKEN": info_dict["REPEAT_SUBMIT_TOKEN"]
    }
    response = session.post(url, headers=headers, data=data)

3.5 提交预定请求

订单校验完成会提交预定请求

  • POST接口:https://kyfw.12306.cn/otn/confirmPassenger/getQueueCount
  • 表单:
参数名 说明 示例
train_date 出发日0时的中国标准时间 Mon Oct 02 2023 00:00:00 GMT+0800 (中国标准时间)
train_no 2.2车票查询获取 6c000G13420K
stationTrainCode 2.2车票查询获取 G1342
seatType 固定参数 O
fromStationTelecode 2.2车票查询获取(出发) CWQ
toStationTelecode 2.2车票查询获取(目的) AOH
leftTicket 2.2车票查询获取 foZ%2BJb5xBT%2By%2BAqnZJZxzd3B2QGfJ7pWZhXMm7vJO7ncjrkD
purpose_codes 固定参数 00
train_location 2.2车票查询获取 QX
_json_att 固定空参数
REPEAT_SUBMIT_TOKEN 3.2请求确认预定信息获取 a14fe59b302c8bf7b7901aee95811111

python示例:

def getQueueCount(gotime, ticket, info_dict):
    GMT_FORMAT = '%a %b %d %Y %H:%M:%S GMT+0800 (中国标准时间)'
    url = "https://kyfw.12306.cn/otn/confirmPassenger/getQueueCount"
    data = {
        "train_date": datetime.datetime.strptime(gotime, "%Y-%m-%d").strftime(GMT_FORMAT),
        "train_no": ticket["train_no"],
        "stationTrainCode": ticket["车次"],
        "seatType": "O",  # 固定值
        "fromStationTelecode": city_code[ticket["出发站名"]],
        "toStationTelecode": city_code[ticket["到达站名"]],
        "leftTicket": ticket["leftTicket"],
        "purpose_codes": "00",  # 固定值
        "train_location": ticket["train_location"],
        "_json_att": "",
        "REPEAT_SUBMIT_TOKEN": info_dict["REPEAT_SUBMIT_TOKEN"]
    }
    response = session.post(url, headers=headers, data=data)

3.6 确认配置信息

页面手动点击的最后一个,确认配置信息

  • POST接口:铁路客户服务中心
  • 表单:
参数名 说明 示例
passengerTicketStr 乘客信息 O,0,1,xxx,333333,159333,N,43253...
oldPassengerStr 乘客信息 xxx,1,333***333,1_
purpose_codes 固定参数 00
key_check_isChange 3.2请求确认预定信息获取 B0426EDD36A030B0FCB98DDDF816BCF3305170B8121EE9E749C11111
leftTicketStr 2.2车票查询获取 foZ%2BJb5xBT%2By%2BAqnZJZxzd3B2QGfJ7pWZhXMm7vJO7ncjrkD
train_location 2.2车票查询获取 QX
choose_seats 固定空参数
seatDetailType 固定参数 000
is_jy 固定参数 N
is_cj 固定参数 Y
encryptedData js加密参数(貌似 json_ua ) vYcLvqvAAUYBTH...
whatsSelect 固定参数 1
roomType 固定参数 00
dwAll 固定参数 N
_json_att 固定空参数
REPEAT_SUBMIT_TOKEN 3.2请求确认预定信息获取 a14fe59b302c8bf7b7901aee95811111

python示例:

def confirmSingleForQueue(ticket, info_dict, pass_info):
    url = "https://kyfw.12306.cn/otn/confirmPassenger/confirmSingleForQueue"
    name = pass_info['passenger_name']
    id_type = pass_info['passenger_id_type_code']
    id_no = pass_info['passenger_id_no']
    mob_no = pass_info['mobile_no']
    enc_str = pass_info['allEncStr']
    pass_type = pass_info['passenger_type']
    data = {
        "passengerTicketStr": f"O,0,1,{name},{id_type},{id_no},{mob_no},N,{enc_str}",
        "oldPassengerStr": f"{name},{id_type},{id_no},{pass_type}_",
        "purpose_codes": "00",
        "key_check_isChange": info_dict["key_check_isChange"],
        "leftTicketStr": ticket["leftTicket"],
        "train_location": ticket["train_location"],
        "choose_seats": "",
        "seatDetailType": "000",
        "is_jy": "N",
        "is_cj": "Y",
        "encryptedData": "xxxx",
        "whatsSelect": "1",
        "roomType": "00",
        "dwAll": "N",
        "_json_att": "",
        "REPEAT_SUBMIT_TOKEN": info_dict["REPEAT_SUBMIT_TOKEN"]
    }
    response = session.post(url, headers=headers, data=data)

4 排队等待

4.1 排队(未排到)

确认配置信息后,会进行排队等待,需要通过返回的data.waitTime确定需要等待的时间(当waitTime为-1时等待结束)

  • GET接口:https://kyfw.12306.cn/otn/confirmPassenger/queryOrderWaitTime
  • 载荷:
参数名 说明 示例
random 毫秒时间戳 1695223890986
tourFlag 固定参数 dc
_json_att 固定空参数
REPEAT_SUBMIT_TOKEN 3.2请求确认预定信息获取 a14fe59b302c8bf7b7901aee95811111

python示例:

def queryOrderWaitTime(info_dict):  # orderSequence_no
    url = "https://kyfw.12306.cn/otn/confirmPassenger/queryOrderWaitTime"
    while True:
        logger.info('[queryOrderWaitTime]排队等待中...')
        params = {
            "random": f"{int(time.time()*1000)}",
            "tourFlag": "dc",
            "_json_att": "",
            "REPEAT_SUBMIT_TOKEN": info_dict["REPEAT_SUBMIT_TOKEN"]
        }
        res = session.get(url, headers=headers, params=params)
        orderId = res.json()['data']['orderId']
        if orderId:
            return orderId
        logger.info('[queryOrderWaitTime]未获得orderId,正在进行新一次请求')

4.2 排队(已排到)

在等待时间结束后,通过排队等待的接口,获取data.orderId,用于后续的操作

5 出票

等待结束就可以出票了

  • POST接口:https://kyfw.12306.cn/otn/confirmPassenger/resultOrderForDcQueue
  • 表单:
参数名 说明 示例
orderSequence_no 4.2排队获取 EC26727456
_json_att 固定空参数
REPEAT_SUBMIT_TOKEN 3.2请求确认预定信息获取 a14fe59b302c8bf7b7901aee95811111

python示例:

def resultOrderForDcQueue(orderId, info_dict):
    url = "https://kyfw.12306.cn/otn/confirmPassenger/resultOrderForDcQueue"
    data = {
        "orderSequence_no": orderId,
        "_json_att": "",
        "REPEAT_SUBMIT_TOKEN": info_dict["REPEAT_SUBMIT_TOKEN"]
    }
    response = session.post(url, headers=headers, data=data)
    if response.json()['data']['submitStatus']:
        logger.info('[resultOrderForDcQueue]已成功预定,您可以登录后台支付了')

12306购票流程分析(非抢票)_第2张图片

你可能感兴趣的:(Python,python,开发语言)