简介:这是一份面向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 schedulerequests负责所有 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 Nonerandom参数是时间戳,防止缓存。轮询间隔 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跑五分钟,确认没有触发风控再上真实下单。抢票脚本拼的不是谁快,是谁能稳定地把请求发出去。希望这个拆解能帮你少走点弯路,把这份脚本跑起来、改成自己顺手的样子。
本文还有配套的精品资源,点击获取