简介:一份基于Python的票星球自动抢票脚本源码包,面向有编程基础、希望了解或实现移动端抢票自动化的开发人员,解决手动刷新抢票效率低的问题。压缩包内共5个文件,含两个Python脚本(核心抢票逻辑与演示调用)、依赖清单、说明文档及在线运行配置,整体仅8KB,结构精简便于直接阅读与二次修改。脚本通过封装PXQ类管理token、show_id、session_id等关键参数,覆盖获取场次、查询余票、选择票种、填写观演人及提交订单等完整流程,并内置代理IP逻辑与开抢时间设置,可快速部署使用。已有377人学习浏览,适合需要研究票务接口交互或搭建自动化抢票工具的开发者参考。
1. 抢票脚本并没有那么玄:从登录到下单的核心链路
不少人和我聊到演出票“一秒售罄”的时候,第一反应是“是不是有黑产在用机器刷”。其实站在个人正常购票的角度,一个抢票脚本并没有那么玄,它本质上就是一个带状态保留、带重试机制、会解析响应的 HTTP 客户端。这篇文章要拆的源码,就是围绕某票务平台(下称“某票务平台”)的完整自动化购票链路:扫码登录、查场次、锁票价、提订单、轮询结果。它解决的是“手动刷新页面的速度跟不上余票变化”的问题,适合有 Python 基础、想搞清楚接口自动化流程的从业者。前提是只用于个人正常购票,不碰倒卖、不踩平台风控红线。
2. 前置准备:环境搭建、抓包思路与登录态的三层风险
2.1 环境选型:Python 3.10 + requests + 本地抓包代理
我先说结论:别用 Selenium。很多人一上来就上浏览器自动化,认为这样能绕过接口签名,实际上浏览器自动化的渲染开销大、请求特征明显,平台稍微做一下 WebDriver 检测就会弹验证码。更稳妥的做法是 pure requests,把浏览器当成一个黑匣子,只从抓包结果里提取真正需要的接口和参数。
对应这份源码,本地环境只需要三样东西:
- Python 3.10 以上,requests 库用于发 HTTP 请求,loguru 负责打印日志,python-dotenv 管理账号配置;
- 一个抓包代理,我习惯用 mitmproxy 或 Charles,目的只是为了看一次完整购票流程中浏览器到底发出哪些请求;
- 一个支持正则搜索的编辑器,用来从 response 里提取余票和场次字段。
搭建环境的命令很简单:
pip install requests loguru python-dotenv这里解释一下为什么选这三件套。requests 是同步库,配合多线程能模拟“多手指同时抢”,比 asyncio 写起来直观,排错更容易;loguru 可以把每一次接口响应耗时和状态码统一输出,方便判断脚本卡在哪个环节;dotenv 用于把账号密码、设备号这类配置隔离在代码之外,避免把敏感信息提交到仓库。
抓包代理不要一直开着,看到一次成功的下单流程,拿到接口清单就可以关掉了。否则代理证书会影响本地 Python 的 HTTPS 请求——这也是新手的第一个隐形坑:requests 在默认环境里遇到代理证书报 SSL 错误,而你的代码看起来完全没问题。
2.2 抓包定位:下单接口、参数来源与加密字段
打开抓包工具,手动走一遍“选场次 → 选票价 → 提交订单”的流程。你会看到一长串请求,但真正需要关注的只有三类:
- 登录相关:扫码后轮询的登录确认接口,返回临时 token 或可替换的 cookie;
- 场次和余票:针对每个场次的演出详情接口,通常返回景区或场馆的场次列表、票价档位、库存状态;
- 下单相关:创建订单接口,一般是 POST 请求,body 里有演出场次 ID、票价档位 ID、观演人 ID、订单备注等字段。
拿某票务平台举例,它的下单接口路径通常是/api/v1/order/create,请求 body 长这样:
{ "project_id": 12345, "session_id": 67890, "price_id": 3, "ticket_num": 1, "audience_ids": [1001], "sign": "a1b2c3d4e5f6..." }这里的sign是大多数接口防刷的核心。它不是固定字符串,而是把上述参数按一定规则拼接后做 MD5 或 HMAC 签名,再把签名结果放在请求体里。抓包时你需要确认:sign到底由哪个字段参与拼接,排序顺序是什么,有没有加盐值。
判断方法很简单:在同一个场次只改变票价档位,然后对比两次请求的sign变化;再把所有参数固定,只改其中一个字段,看sign是否变化。大约测 3 组数据就能摸清参与签名的是哪些参数。这部分源码里已经做成了独立的signer.py,后续改动只需要维护一个函数。
2.3 登录态管理:Cookie、Token 与设备指纹的常见关系
抢票脚本的第一个拦路虎就是登录态。某票务平台的登录态由三层构成:
- Cookie 层:负责维持会话,常见字段有
sessionid、token,有效期大约 2 到 4 小时; - Header 层:
User-Agent和Referer会被后端记录,一旦和登录时不一致,服务端会判定是异常设备; - 设备指纹层:平台会在登录时生成一个
device_id,这个值通常存放在本地 localStorage,并通过请求参数传给后端。脚本直接复用它即可,不需要自己伪造。
实际操作里,我更推荐“扫码登录 + cookie 持久化”的组合。第一次扫码登录后,把响应里的Set-Cookie全部保存到本地文件,后续脚本启动时直接加载,省掉每次重新登录的等待时间。下面是一段最小实现:
import requests from pathlib import Path s = requests.Session() def login_by_qrcode(): """返回登录后的 session,内部会保存 cookie 到本地""" # 第 1 步:获取一个尚未确认的二维码 key resp = s.post("https://api.example.com/auth/qrcode/generate") qr_key = resp.json()["data"]["qr_key"] print(f"请用手机 App 扫码,key={qr_key}") # 第 2 步:轮询确认结果,超时设置 60 秒 for _ in range(60): time.sleep(2) result = s.post("https://api.example.com/auth/qrcode/confirm", json={"qr_key": qr_key}) data = result.json() if data["code"] == 0: # 登录成功,cookie 已经写入 session break if data["code"] == 1: print("等待确认...") # 保存 cookie 到本地,方便下次复用 with open("cookies.txt", "w") as f: f.write(json.dumps(s.cookies.get_dict())) return s def load_session(): """从本地 cookie 文件中恢复登录态""" if Path("cookies.txt").exists(): s.cookies.update(json.loads(Path("cookies.txt").read_text())) return s这段代码有几个关键点。requests.Session()会自动帮你管理 cookie 池,但不会自动保存到文件,所以手动序列化到cookies.txt。轮询确认接口的频率不宜高于 2 秒一次,否则容易触发登录风控。加载 cookie 后建议手动检查一下会话是否有效,常见做法是请求一个只读接口,比如“我的订单列表”,如果返回 401 就说明登录态过期,需要重新扫码。
3. 核心实现:抢购流程拆成四个可复用的模块
3.1 模块一:登录与会话保持(含扫码/验证码处理)
刚才的代码已经覆盖了扫码登录的基础流程,但实际场景里还会遇到两种情况:一种是验证码弹窗,另一种是登录态中途过期。验证码的处理我建议直接人工介入,不要尝试自动识别。抢票脚本的价值在于“快速提交订单”,而不是破解验证码,后者会引入复杂度和法律风险。
源码里把登录逻辑单独放在auth.py,对外暴露两个函数:ensure_login()和refresh_token()。ensure_login()的逻辑是:先尝试加载本地 cookie,然后发一个轻量接口校验登录态;如果失败再走扫码流程。refresh_token()则是为长时间运行的脚本准备的,每隔 30 分钟用旧 token 换新 token,防止在抢票前一刻掉线。
这里有一个容易被忽略的点:某票务平台的登录态和 User-Agent 是强绑定的。你在手动抓包时用的 UA 和 requests 默认的python-requestsUA 不一样,所以初始化 Session 时要固定一个普通浏览器 UA:
from fake_useragent import UserAgent s.headers.update({ "User-Agent": UserAgent().chrome, "Referer": "https://www.example.com/", })使用fake_useragent的前提是本地能访问其内置的数据源,如果担心网络问题,也可以手动从浏览器复制一个 UA 字符串硬编码。关键是要让整个会话全程使用同一个 UA。
3.2 模块二:场次与票价余量查询
抢票的核心动作之一就是“刷新余票”。某票务平台的场次查询接口会返回一个嵌套列表,外层是场次信息,比如开始时间、结束售卖时间、场馆名称,内层是票价档位列表,每个档位有price_id、price_name、stock_status等字段。
一般stock_status有两种表达方式:布尔值true/false,或者数字0/1。源码中封装了一个query_available_sessions(project_id, session_id=None)函数,它会自动过滤掉已售罄的场次,返回一个“可下单”的字典列表。示例代码:
def query_available_sessions(session, project_id): """查询某个项目的可售场次和票价""" params = {"project_id": project_id, "_t": int(time.time())} resp = session.get("https://api.example.com/show/sessions", params=params) data = resp.json()["data"] available = [] for session_info in data["sessions"]: if session_info["sell_status"] != 1: continue # 跳过未开售或已暂停的场次 for ticket in session_info["tickets"]: if ticket["stock"] > 0: available.append({ "session_id": session_info["id"], "session_name": session_info["name"], "price_id": ticket["id"], "price": ticket["price"], "stock": ticket["stock"], }) return available这里我故意加了一个_t参数,很多平台会校验当前时间戳,防止请求被缓存。爬虫新手经常忽略这一点,导致拿到的是上一次的查询结果,以为有票,实际已经没了。stock字段如果是-1,通常表示“不限量”,但在演出场景里几乎不会出现,如果出现你可以把它当作大库存处理,但下单后仍然可能失败,原因是这类限时活动接口普遍存在“超卖”风险。
3.3 模块三:下单请求构造与订单提交
余量查询只是上半场,真正的硬仗在提交订单。下单接口对参数顺序和格式极其敏感,任何一个字段缺失都会返回“参数错误”。我把下单逻辑做成create_order(session, order_payload),它内部做三件事:
- 从 payload 中提取参与签名的字段,按字典序排序后拼接;
- 用密钥生成 sign;
- 发送 POST 请求并检查返回值。
下面是摘录的核心代码,注意我把签名逻辑独立成函数,方便做单元测试:
import hashlib import time def make_sign(params, secret): """平台签名规则假设:参数名 ASCII 排序后拼接,再拼接 secret,做 MD5""" sorted_keys = sorted(params.keys()) raw_str = "&".join(f"{k}={params[k]}" for k in sorted_keys) raw_str += secret return hashlib.md5(raw_str.encode()).hexdigest() def create_order(session, order_payload, secret): """提交订单,返回服务器响应""" order_payload["timestamp"] = int(time.time() * 1000) order_payload["sign"] = make_sign(order_payload, secret) headers = {"Content-Type": "application/json"} resp = session.post("https://api.example.com/order/create", json=order_payload, headers=headers) data = resp.json() if data.get("code") == 0: return data["data"]["order_id"] else: # 记录失败原因,便于排查 print(f"下单失败: {data.get('msg')}") return None这里隐藏着两个影响成功率的细节。一是timestamp必须是毫秒级,且与服务器时间偏差超过 10 秒会直接拒绝,所以脚本启动时最好先调用一个时间同步接口校准。二是make_sign的顺序不能动,平台方的签名算法可能还会加一个“盐值”或“时间窗口”。源码包里默认实现了 MD5 版本,如果你遇到的是 HMAC-SHA256,替换hashlib.md5为hmac.new即可,其他逻辑不用大改。
下单接口返回order_id并不代表已经锁票成功,很多平台会异步确认库存。所以提交订单后还要做一步“订单确认轮询”,一般间隔 1 到 2 秒查询订单状态,状态变为已支付才算真正抢到。如果 5 秒内没有变化,基本可以认为锁票失败,需要回到查询接口重新走一遍流程。
3.4 模块四:高频重试与并发控制
抢票脚本最核心的优势是“并发重试”。这里的并发不是指无限开线程,而是有限度的并发请求。常见的做法是同时开 3 到 5 个线程,每个线程独立执行“查询→下单”循环,某一个线程下单成功后通知其他线程停止。
源码里用一个全局事件来通知停止:
import threading import time from concurrent.futures import ThreadPoolExecutor stop_flag = threading.Event() lock = threading.Lock() success_order_id = None def rush_one(session_factory, order_payload): """单线程抢购任务,成功后设置 stop_flag""" global success_order_id while not stop_flag.is_set(): sess = session_factory() # 1. 查询余票 sessions = query_available_sessions(sess, order_payload["project_id"]) if not sessions: time.sleep(0.2) continue # 2. 选第一张可用票 target = sessions[0] payload = {**order_payload, "session_id": target["session_id"], "price_id": target["price_id"]} # 3. 下单 order_id = create_order(sess, payload, SECRET) if order_id: with lock: success_order_id = order_id stop_flag.set() print(f"抢到订单: {order_id}") return True time.sleep(0.1) def rush(worker_num=3, order_payload=None): with ThreadPoolExecutor(max_workers=worker_num) as executor: futures = [executor.submit(rush_one, session_factory, order_payload) for _ in range(worker_num)] for f in futures: f.result() return success_order_id这段代码最容易被喷的点是time.sleep(0.1)。很多人觉得抢票必须毫秒级无停顿,实际情况是平台对单 IP 的请求频率有监控,太快会触发滑块验证与封禁。我一般把单线程请求频率控制在 10 到 20 QPS,也就是说每 50 毫秒到 100 毫秒发一次请求,已经能覆盖绝大多数票务场景。如果你有多个网络出口,可以再提升并发数量,但同一出口下增加线程数带来的收益远不如设置合理的请求间隔。
4. 避坑排查:写抢票脚本最容易翻车的 5 个位置
4.1 登录态莫名失效:明明刚登录,一到抢票就 401
现象:脚本启动时ensure_login()校验通过,但轮询到第 20 次时突然返回 401,后续所有请求都报错。
原因:平台会在一定时间内检查登录设备的 IP 是否多次变动,或者检测到本地 Cookie 里的sessionid在短时间内被多个不同签发的请求端口使用。另一个可能原因是测试时用 Python shell 手动作过一次未带 UA 的请求,把会话污染了。
解决:给 Session 绑定固定的 UA 和 Referer,并且保证同一个 Session 对象只在一个线程内使用,不要在多个线程共享同一个 Session。如果必须多线程,每个线程创建独立的 Session,但共享同一个 Cookie。
4.2 请求头不全被判定为爬虫:返回 418 而不是数据
现象:同样的路径,浏览器访问正常,脚本访问返回 418(I'm a teapot)或者 443 状态码。
原因:平台对Accept、Accept-Language、Origin、Referer这几个 Header 做了交叉校验。例如某票务平台的查询接口要求Referer必须以https://www.example.com/show/开头,缺少这个值直接拒绝。
解决:从抓包请求里完整复制一份 Header 字典,放到初始化函数里全局追加。注意不要把Content-Length和Host手动写上去,requests 会自动生成。
4.3 查询显示“有票”,下单却提示“库存不足”
现象:query_available_sessions返回了stock大于 0 的票价档位,但create_order提交后返回code: 1001,库存不足。
原因:查询接口和下单接口之间存在 3 到 5 秒的缓存窗口。什么意思?前端看到的余量是 CDN 缓存的结果,而下单接口是直接读数据库库存,两个数据源不同步。另外也可能是有人在你查询后的一瞬间锁定了最后一张票。
解决:不要依赖查询结果作为下单依据,直接用上一次的price_id构造下单请求,下单失败后再重新查询。还可以在下单前多拿两个候选场次,A 场次失败立即切 B 场次,不要在一个场次上干等。
4.4 重试频率过高触发滑块验证
现象:连续请求 30 次后,接口响应里突然出现一个captcha_id字段,询问脚本需要完成滑块验证。
原因:单 IP 在短时间达到 15 QPS 以上,或者请求间的时间间隔太规律。机器人的请求时钟像节拍器一样,而人类点击大约每 0.3 秒一次且随机抖动。
解决:把固定sleep(0.1)改成随机区间random.uniform(0.08, 0.18),并且每 5 到 10 次请求暂停一次更长的时间。如果已经触发验证,最简单的处理是停掉脚本,手动在浏览器里完成一次验证,换新 cookie 再继续。
4.5 签名算法更新导致大面积报错
现象:脚本昨天还能用,今天所有请求都返回sign invalid,并且没有明显的逻辑改动。
原因:平台方更新了签名规则,可能从 MD5 换成了 HMAC-SHA256,也可能在拼接时增加了client_version字段。
解决:抓包工具翻出最新的下单请求,对比新旧两个请求的 body 差异。把sign的生成逻辑单独放在一个模块里,更新规则时只改这个模块。不要为了图方便把签名逻辑散落在各个函数里,否则每更新一次就要全局搜索替换。我习惯在sign.py里写一个版本号常量,规则一变就自增,方便回滚。
5. 验证与进阶:从能用脚本到上线稳定运行的三个门槛
5.1 自检清单
写完脚本不要急着抢票,先跑一遍离线自检。下面是能直接使用的表格,也是我每次上线前必须过的事项:
| 检查项 | 通过标准 | 失败处理 |
|---|---|---|
| 登录态复用 | 加载本地 cookie 后能请求用户资料 | 重新扫码 |
| 场次查询 | 能返回至少一场可售场次 | 检查接口参数和 UA |
| 签名正确 | 用历史数据构造请求能拿到 200 | 对照抓包 body 逐字段排查 |
| 异常恢复 | 模拟 401 时脚本能捕获并退出而不是崩溃 | 补异常处理 |
| 频率控制 | 连续运行 3 分钟无验证码 | 增加随机延时 |
5.2 进阶:参数签名逆向、行为模拟、多账号管理
第一层进阶是签名逆向。如果你遇到的是 JS 加密,可以用 PyExecJS 直接调用源码里的sign.js,这样不用自己重写算法,只要保证 Node 环境可用。但 PyExecJS 每次执行都会拉起一个新进程,性能不高。更推荐的做法是把 JS 里参与拼接的字段抠出来,用 Python 原生语法实现,性能至少提升 10 倍。
第二层进阶是行为模拟。平台的设备指纹不仅看静态参数,还会记录鼠标移动轨迹和点击间隔。有的团队会把真实用户的下单时间分布记录下来,然后让脚本按照这个分布去发单。个人使用的话,一个轻量做法是给每个请求之间加入一次“预请求”,比如先访问一次页面埋点接口,再发真正的订单请求,让路径上看起来更像人。
第三层进阶是多账号管理。一个账号抢到两张票后再换另一个账号容易触发风控,更合理的方案是每个账号独立使用一个cookies.txt,并绑定固定的 UA,线程间不交叉。这部分的复杂度不在于代码,在于账号的后勤管理。
最后一个建议是:每跑完一次抢票,务必检查订单列表,确认是否有“待支付”的订单漏掉了。脚本只负责提交订单,支付环节最好人工完成,一方面避免误操作,另一方面减少平台对异常支付的关注。
从那以后,我每次动这类脚本,都会强制走一遍“离线自检 → 高频压测 → 真实抢票”三步流程,哪怕只是改了一个参数,也不跳过。线上翻车一次的成本,远远高于本地多花十分钟。希望帮到你。
本文还有配套的精品资源,点击获取