news 2026/10/18 15:02:38

Python+requests实现12306抢票脚本:从登录到下单的完整链路拆解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python+requests实现12306抢票脚本:从登录到下单的完整链路拆解

简介:这是一份面向Python开发者与自动化技术爱好者的12306抢票脚本源码包,聚焦高并发场景下的车票查询与下单流程自动化,适合具备一定网络编程基础、希望研究请求处理与验证码识别实现思路的读者参考。压缩包共134个文件,约62.84MB,以61个py源码与48个pyc编译文件为主体,另含h5机器学习模型、png与jpeg界面截图、txt与log日志、yml与sh部署配置,以及Dockerfile等容器构建文件,整体结构覆盖脚本运行、模型加载与环境部署多个环节。目前已有446人学习下载。通过阅读源码,读者可了解网络请求封装、登录验证、票务轮询、下单支付等模块的组织方式,并借助模型文件与容器配置理解验证码识别与独立部署的实现路径,同时留意相关使用条款与合规风险。

1. 从一张候补订单说起:这个抢票脚本到底能干什么

去年春运前,我帮家里人盯一张从南方回中部城市的硬卧。放票时间一到,页面刷出来就是候补,连选座的机会都没有。手动点了三天,最后是靠一个跑在本地的小脚本抢到的——它做的事其实不复杂:在放票瞬间把查询、提交订单、确认这几个请求按正确顺序打出去,比人手快那么几百毫秒。这份qiangpiao12306-12306抢票脚本就是干这个的,它把 12306 的余票查询、下单、订单确认这条链路用代码串了起来,适合想学 HTTP 接口调用、想自己掌控抢票节奏、或者单纯想拿一个真实场景练手的开发者。它不是外挂,不破解验证码,走的是正常接口调用,能不能抢到取决于网络、放票量和运气,但至少把「手速」这个变量交给了程序。

2. 技术选型与目录结构:为什么用 Python 而不是浏览器插件

2.1 选 Python + requests 的取舍

抢票这件事,核心矛盾是「在正确的时间发出正确的请求」。浏览器插件受限于页面渲染和浏览器调度,脚本则可以直接控制请求时机。这个项目选 Python,主要看中三点:一是requests库对 HTTP 会话、Cookie 保持、超时重试的支持足够成熟,12306 的接口需要维持登录态,用Session对象管理 Cookie 是最省事的做法;二是 Python 的time、datetime模块处理放票时间点很直接,配合threading或schedule做定时触发;三是调试成本低,改一行参数就能重跑,不用重新加载扩展。

常见做法是用requests.Session()贯穿整个流程,登录、查票、下单共用同一个会话,避免 Cookie 丢失导致的「登录成功但下单提示未登录」。我一般还会在 Session 上挂一个HTTPAdapter,设置连接池和重试策略,防止偶发的网络抖动直接让脚本崩掉。

2.2 目录结构长什么样

项目正文没有给出完整目录,但按这类脚本的常规组织方式,结构大致如下:

qiangpiao12306/ ├── main.py # 入口,负责调度和主循环 ├── config.py # 出发站、到达站、日期、车次偏好等配置 ├── login.py # 登录模块,处理扫码或账号登录 ├── query.py # 余票查询,解析返回的车次列表 ├── order.py # 提交订单、确认订单 ├── utils.py # 时间处理、日志、重试装饰器 └── requirements.txt # 依赖清单

config.py是改造时最常动的地方,把车次、席别、乘客信息集中在这里,主逻辑不用改。utils.py里通常放一个retry装饰器,对查询和下单接口做有限次重试,避免一次超时就放弃。

2.3 依赖与运行环境

依赖清单一般只有几个:

pip install requests pip install schedule

requests负责所有 HTTP 交互,schedule负责定时任务。如果登录用扫码方式,可能还需要Pillow来展示二维码图片。运行环境 Python 3.7 以上即可,不需要额外的浏览器驱动,这也是它比 Selenium 方案轻量的地方。

提示:不同版本的 12306 接口参数会有调整,跑之前先确认query.py里的 URL 和参数名是否还对得上,接口一变,脚本就会返回空列表或报错。

3. 核心链路拆解:从登录到下单的四个关键请求

3.1 登录态维持:Session 与 Cookie

12306 的接口几乎都要求登录态,所以第一步是把登录后的 Cookie 拿住。用requests.Session()的好处是,登录请求返回的Set-Cookie会自动存进 Session,后续请求自动带上。

import requests # 创建全局会话,所有请求共用 session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...", "Referer": "https://kyfw.12306.cn/otn/leftTicket/init", }) def login_by_cookie(cookie_str): """把浏览器里复制出来的 Cookie 塞进 Session,跳过登录流程""" for item in cookie_str.split("; "): key, value = item.split("=", 1) session.cookies.set(key, value)

这段代码走的是「手动复制 Cookie」的路线,适合不想在脚本里处理登录验证码的场景。session.headers里的Referer很关键,12306 会校验来源,缺了它查询接口可能直接返回 403。login_by_cookie把浏览器 F12 里拿到的 Cookie 字符串拆开逐条写入,省去登录模块。参数说明:cookie_str是从已登录页面复制的完整 Cookie,注意不要漏掉RAIL_EXPIRATION和RAIL_DEVICEID这两个,它们和登录态绑定。

3.2 余票查询:解析车次列表

查询接口返回的是 JSON,里面data.result是一个字符串数组,每个元素用|分隔,字段位置固定但不好记。常见做法是按索引取值,封装成字典。

def query_tickets(date, from_station, to_station): """查询指定日期和区间的余票,返回车次字典列表""" url = "https://kyfw.12306.cn/otn/leftTicket/query" params = { "leftTicketDTO.train_date": date, # 格式 2025-01-20 "leftTicketDTO.from_station": from_station, # 车站代码,如 BJP "leftTicketDTO.to_station": to_station, "purpose_codes": "ADULT", } resp = session.get(url, params=params, timeout=5) result = resp.json()["data"]["result"] tickets = [] for item in result: fields = item.split("|") tickets.append({ "train_no": fields[3], # 车次编号 "train_code": fields[2], # 如 G1234 "from": fields[6], "to": fields[7], "start_time": fields[8], "arrive_time": fields[9], "secret": fields[0], # 下单时要用到的凭证 "seat": fields[30], # 二等座余票 }) return tickets

逻辑说明:params里的车站要用代码而不是中文名,BJP代表北京,WHN代表武汉,代码表在 12306 页面源码里能查到。fields[0]那个secret字符串是下单接口的入场券,每次查询都会变,必须和车次一起传下去。fields[30]是二等座余票,不同席别索引不同,硬座、一等座要另取。查询失败时先看resp.status_code,403 多半是 Referer 或 Cookie 问题,返回空result则可能是日期格式或车站代码写错。

3.3 提交订单:把 secret 和乘客信息拼起来

查到票之后,下单接口需要车次、secret、乘客信息、席别等参数。这一步最容易翻车,因为参数名和格式要求严格。

def submit_order(train, passenger_name, seat_type="O"): """提交订单,seat_type 默认 O 代表二等座""" url = "https://kyfw.12306.cn/otn/confirmPassenger/confirmSingleForQueue" data = { "secretStr": train["secret"], "train_date": "2025-01-20", "query_from_station_name": train["from"], "query_to_station_name": train["to"], "passengerTicketStr": f"{seat_type},0,1,{passenger_name},1,1234567890", "oldPassengerStr": f"{passenger_name},1,1234567890,1_", } resp = session.post(url, data=data, timeout=5) return resp.json()

passengerTicketStr的格式是「席别,票种,证件类型,姓名,乘客类型,证件号」,字段顺序不能乱。oldPassengerStr是常旅客信息,末尾的下划线是固定格式。seat_type用O表示二等座,M表示一等座,1表示硬座,具体代码在 12306 的席别枚举里。返回结果里status为true且data.submitStatus为true才算提交成功,否则要看data.errMsg里的具体原因,常见的是「乘客信息有误」或「余票不足」。

3.4 确认订单与轮询

提交成功后进入排队,需要轮询确认结果。这一步用循环加time.sleep控制频率,太密会被限流。

import time def confirm_order(): """轮询确认订单结果,直到成功或超时""" url = "https://kyfw.12306.cn/otn/confirmPassenger/queryOrderWaitTime" for _ in range(30): resp = session.get(url, params={"random": int(time.time() * 1000)}, timeout=5) data = resp.json()["data"] if data.get("orderId"): return data["orderId"] # 拿到订单号即成功 time.sleep(1) return None

random参数是时间戳,防止缓存。轮询间隔 1 秒比较稳妥,30 次大约 30 秒,超时就可以认为这轮没抢到,回到查询步骤重来。拿到orderId后,整个链路就走通了。

4. 避坑与常见问题:那些让我重跑十几次的坑

4.1 查询返回空列表

现象:脚本跑起来不报错,但result是空的,一列车都查不到。原因通常是车站代码写错,或者日期格式不对。12306 要求日期是YYYY-MM-DD,车站是电报码不是拼音。解决:去 12306 页面查一次,从 Network 面板里把真实的from_station和to_station抄下来,别凭记忆写。

4.2 下单提示「未登录」

现象:查询正常,一下单就返回未登录。原因是查询和下单用了不同的 Session,或者 Cookie 过期。解决:确保全局只有一个session对象,登录后先调一次查询接口验证登录态,再进入下单流程。Cookie 里的RAIL_EXPIRATION是过期时间戳,过期了就得重新登录。

4.3 请求被限流或返回 403

现象:连续查询几次后接口开始返回 403 或空数据。原因是请求频率太高,触发了风控。解决:在查询循环里加time.sleep(0.5)到1秒,不要用while True无间隔刷。Referer 和 User-Agent 也要固定,频繁变化反而可疑。

4.4 提交订单成功但确认失败

现象:submitStatus是 true,但轮询一直拿不到orderId。原因是排队人数太多,或者乘客信息里的证件号格式不对。解决:检查passengerTicketStr里的证件号是否和 12306 账户里的一致,轮询次数可以适当增加,但不要超过 60 次,否则可能被判定为异常。

4.5 放票时间点没对上

现象:脚本在放票前就开始跑,但一直查不到票,等查到的时候已经没了。原因是本地时间和服务器时间有偏差。解决:在脚本启动时先请求一次 12306 的服务器时间接口,算出本地偏移量,用偏移后的时间做定时触发,别直接信本机时钟。

5. 二次开发:把脚本改成你自己的抢票助手

5.1 从「能跑」到「好用」的三个改造点

第一,把配置抽成 JSON 文件。config.py里硬编码车次和乘客信息,改一次要动代码。改成config.json,主逻辑读文件,换车次不用碰 Python。第二,加日志和通知。抢到票之后只打印一行orderId容易错过,接一个本地的声音提醒或者写进日志文件,事后能查。第三,把单次抢票改成多车次轮询。一次只盯一趟车,失败就没了;改成按优先级列表循环查询,命中任意一趟就下单,成功率会高不少。

5.2 一个可复用的重试装饰器

网络请求失败是常态,与其在每个函数里写 try-except,不如抽一个装饰器:

import functools import time def retry(times=3, delay=1): """失败重试装饰器,times 为重试次数,delay 为间隔秒数""" def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for i in range(times): try: return func(*args, **kwargs) except Exception as e: if i == times - 1: raise time.sleep(delay) return wrapper return decorator

把它挂在query_tickets和submit_order上,偶发的超时和连接错误就不会直接中断整个流程。times建议设 3,delay设 1 秒,再大就影响抢票时机了。

5.3 验证改造是否有效的办法

改完之后别直接上真实车次试,先用一个已经没票的车次跑一遍,看查询、解析、日志输出是否正常;再用一个有余票的短途车次走完整链路,确认下单和确认都能拿到orderId。最后把放票时间往前调一分钟,模拟真实抢票节奏,观察脚本在时间点前后的请求频率是否合理。我一般会在main.py里加一个--dry-run参数,只查询不下单,用来验证配置和网络。

5.4 我踩过的一个教训

有次我把轮询间隔设成了 0.2 秒,觉得越快越好,结果跑了不到十次就被限流,后面半小时所有请求都返回 403,连正常查询都做不了。从那以后我每次改频率参数,都强制先用--dry-run跑五分钟,确认没有触发风控再上真实下单。抢票脚本拼的不是谁快,是谁能稳定地把请求发出去。希望这个拆解能帮你少走点弯路,把这份脚本跑起来、改成自己顺手的样子。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 15:00:31

Aspose.Slides 25.5 与 JDK 16 兼容性实战指南

简介:本资源是面向Java开发者的企业级PPT文档处理解决方案,适用于需在Spring Boot、Servlet等后台架构中实现PowerPoint自动化生成、格式转换与深度编辑的中高级开发场景。资源提供无水印、无页数限制、动画功能完整的Aspose.Slides 25.5破解版核心库&am…

作者头像 李华
网站建设 2026/10/18 15:00:02

企业API聚合平台怎么选:2026年国际与国内主流平台全评测排名

2026年的企业AI架构有个绕不开的现实:既要GPT-5.5的通用能力,也要Claude Opus 4.6的深度推理,还想接Gemini 3.1 Pro、DeepSeek V4、Qwen 3.6、Llama 4这些新势力,同时网络要通、结算要统一、合规要过关。API聚合平台因此成了企业采…

作者头像 李华
网站建设 2026/10/18 14:59:24

CPU流水线、乱序执行与缓存优化:程序员视角的性能实战

1. 从“流水线”到“乱序执行”:CPU内部到底在忙什么很多程序员对CPU的理解停留在“取指、译码、执行”这三步,觉得CPU就是一个听话的工人,给一条指令就干一件事。但如果你真去翻一翻现代处理器的技术手册,会发现这个“工人”其实…

作者头像 李华
网站建设 2026/10/18 14:58:57

一套制品库接入 28 种格式:Maven、Docker、npm、PyPI、Helm 实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/18 14:58:48

建筑施工安全技术资料如何变成项目台账?链路拆解与归档实操

简介:建筑施工安全技术资料是现场安全管理的底账,而非应付检查的废纸。其核心原理是把方案、交底、检查、验收、整改五个环节串成管理链路,再按塔吊、基坑、临电等对象建立检索维度,形成“链路对象”的矩阵台账。通过统一编号、逐…

作者头像 李华
网站建设 2026/10/18 14:58:32

MapReduce核心原理与实战调优:从设计思想到Shuffle优化

1. 为什么今天还要聊MapReduce第一次接触MapReduce是在一个日志分析项目里,当时手里有几十台机器的访问日志,单机跑脚本要跑一整夜,第二天早上还没出结果。后来把任务拆成Map和Reduce两个阶段丢到集群上,同样的数据量,…

作者头像 李华