流程分析(非抢票)"/>
12306购票流程分析(非抢票)
说在前面
购票流程的关键位置参数encryptedData未分析出来,(吐槽:还是太菜了啊)本文是个人结合网上以及抓包的分析,在此做个记录。有时间再试试encryptedData。
仅供研究学习使用,请勿用于非法用途
12306购票流程(20230925)
1 登录过程
1.1 获取验证模式
在预定车票时,点击预定会跳转到登录页面,在登录页输入用户名和密码后,点击登录会发送一个请求,当返回的login_check_code
的值为3时,采用的是短信验证。
- POST接口:
- 表单:
参数名 | 说明 | 示例 |
---|---|---|
username | 用户名 | xxx |
appid | 固定参数 | otn |
_json_att | 固定空参数 |
python示例:
import requestsheaders = {"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 = ''
data = {"username": user,"appid": "excater"
}
res = session.post(url, headers=headers, data=data)
1.2 获取短信验证码
之后会触发短信验证码验证的界面,需要输入身份证后4位获取短信验证码。
- POST接口:
- 表单:
参数名 | 说明 | 示例 |
---|---|---|
appid | 固定参数 | otn |
username | 用户名 | xxx |
castNum | 身份证后4位 | 3333 |
_json_att | 固定空参数 |
python示例:
url = ''
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接口:
- 表单:
参数名 | 说明 | 示例 |
---|---|---|
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() # 实例化sm4sm4Alg.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 enHexStrurl = ""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接口:
- 表单:
参数名 | 说明 | 示例 |
---|---|---|
appid | 固定参数 | otn |
_json_att | 固定空参数 |
python示例:
import reurl = ""
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接口:
- 表单:
参数名 | 说明 | 示例 |
---|---|---|
tk | 获取tk时返回的newapptk | 8yQK700A2D-IGiOF_aTd8OPo0VPLJKOQaPsxxxxxxxxx |
_json_att | 固定空参数 |
python示例:
url = ""
data = {"tk": tk,"_json_att": ""
}
response = session.post(url, headers=headers, data=data)
2 余票查询
2.1 车站编码
由于查票、购票时的站点都是采用编码形式的,需要获取车站对应的编码;返回的数据需要用正则提取一下,每条数据之间用|||
分隔,数据中的各项内容用|
分隔
- GET接口:.js
以|
分割后的数据内容(0开始):
下标 | 内容 |
---|---|
1 | 站点名称 |
2 | 站点编码 |
python示例:
url = '.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): # secretStrif (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 = ""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_listexcept requests.exceptions.JSONDecodeError:logger.error('[queryZ]requests.exceptions.JSONDecodeError: 请求出现错误,返回数据非json格式')
3 预定
所有预定请求都需要携带登录后的cookies
3.1 提交请求
在选定了车次后,点击预定按钮会提交请求
- POST接口:
- 表单:
参数名 | 说明 | 示例 |
---|---|---|
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 = ""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接口:
- 表单:
参数名 | 说明 | 示例 |
---|---|---|
_json_att | 固定空参数 |
需要用正则在返回数据中提取两个参数,用于后续的请求 | 参数名 | 说明 | 正则示例 |
---|---|---|---|
REPEAT_SUBMIT_TOKEN | 用于多个请求 | var globalRepeatSubmitToken = '(.*?)' | |
key_check_isChange | 用于确认配置信息 | 'key_check_isChange':'(.*?)' |
python示例:
def initDc(): # REPEAT_SUBMIT_TOKEN key_check_isChangeurl = ""data = {"_json_att": ""}text = session.post(url, headers=headers, data=data).textinfo_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接口:
- 表单:
参数名 | 说明 | 示例 |
---|---|---|
_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 oldPassengerStrurl = ""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接口:
- 表单:
参数名 | 说明 | 示例 |
---|---|---|
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 = ""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接口:
- 表单:
参数名 | 说明 | 示例 |
---|---|---|
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 = ""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 = ""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接口:
- 载荷:
参数名 | 说明 | 示例 |
---|---|---|
random | 毫秒时间戳 | 1695223890986 |
tourFlag | 固定参数 | dc |
_json_att | 固定空参数 | |
REPEAT_SUBMIT_TOKEN | 3.2请求确认预定信息获取 | a14fe59b302c8bf7b7901aee95811111 |
python示例:
def queryOrderWaitTime(info_dict): # orderSequence_nourl = ""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 orderIdlogger.info('[queryOrderWaitTime]未获得orderId,正在进行新一次请求')
4.2 排队(已排到)
在等待时间结束后,通过排队等待的接口,获取data.orderId,用于后续的操作
5 出票
等待结束就可以出票了
- POST接口:
- 表单:
参数名 | 说明 | 示例 |
---|---|---|
orderSequence_no | 4.2排队获取 | EC26727456 |
_json_att | 固定空参数 | |
REPEAT_SUBMIT_TOKEN | 3.2请求确认预定信息获取 | a14fe59b302c8bf7b7901aee95811111 |
python示例:
def resultOrderForDcQueue(orderId, info_dict):url = ""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购票流程分析(非抢票)
发布评论