说在前面
购票流程的关键位置参数encryptedData未分析出来,(吐槽:还是太菜了啊)本文是个人结合网上以及抓包的分析,在此做个记录。有时间再试试encryptedData。
仅供研究学习使用,请勿用于非法用途
在预定车票时,点击预定会跳转到登录页面,在登录页输入用户名和密码后,点击登录会发送一个请求,当返回的login_check_code
的值为3时,采用的是短信验证。
参数名 | 说明 | 示例 |
---|---|---|
username | 用户名 | xxx |
appid | 固定参数 | otn |
_json_att | 固定空参数 |
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)
之后会触发短信验证码验证的界面,需要输入身份证后4位获取短信验证码。
参数名 | 说明 | 示例 |
---|---|---|
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)
获取短信验证码后,才到了真正的登录接口,其中的password
参数是加密的,经过分析js,可以确认是sm4加密
参数名 | 说明 | 示例 |
---|---|---|
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)
当我们登录成功后,会获取到uamtk
,但此时还不是放松的时候,在抓包中可以看到后面还有2个post请求,经过分析后可以确认是cookies.tk
的必要请求
在uamtk
接口请求中返回的newapptk
就是cookies.tk
的值
参数名 | 说明 | 示例 |
---|---|---|
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]
接下来会对接收的newapptk
值进行校验,校验正常时服务端会将该cookie设置在Session中(突发奇想:不发送该请求,自己将上面的newapptk
添加到cookie中是否可以正常登录)
参数名 | 说明 | 示例 |
---|---|---|
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)
由于查票、购票时的站点都是采用编码形式的,需要获取车站对应的编码;返回的数据需要用正则提取一下,每条数据之间用|||
分隔,数据中的各项内容用|
分隔
|
分割后的数据内容(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}
经测试,余票查询是可以不登录的。载荷中的出发地与目的地是编码方式的,在此之前需要先获取站点对应的编码
参数名 | 说明 | 示例 |
---|---|---|
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格式')
所有预定请求都需要携带登录后的cookies
在选定了车次后,点击预定按钮会提交请求
参数名 | 说明 | 示例 |
---|---|---|
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]提交请求成功')
提交请求后会触发请求的确认,该接口返回的是HTML数据
参数名 | 说明 | 示例 |
---|---|---|
_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
在进入选座界面后,需要选择乘客
参数名 | 说明 | 示例 |
---|---|---|
_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']
在选择乘客后点击确认,会进行订单校验
参数名 | 说明 | 示例 |
---|---|---|
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)
订单校验完成会提交预定请求
参数名 | 说明 | 示例 |
---|---|---|
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)
页面手动点击的最后一个,确认配置信息
参数名 | 说明 | 示例 |
---|---|---|
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)
确认配置信息后,会进行排队等待,需要通过返回的data.waitTime确定需要等待的时间(当waitTime为-1时等待结束)
参数名 | 说明 | 示例 |
---|---|---|
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,正在进行新一次请求')
在等待时间结束后,通过排队等待的接口,获取data.orderId,用于后续的操作
等待结束就可以出票了
参数名 | 说明 | 示例 |
---|---|---|
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]已成功预定,您可以登录后台支付了')