简介:一份基于Python编写的大麦网自动抢票脚本源码,面向有Python基础、希望提高热门演出购票成功率的开发者与自动化爱好者。核心解决手动抢票难、反应慢的问题,代码包含网络请求、票务信息解析和定时任务等模块,并支持按价位选座购买,适合作为自动化项目练习和二次开发基础。压缩包共10个文件,以py脚本、png流程图、md说明、txt依赖列表等类型为主,整体约1.37MB,目录结构简洁,包含主程序、辅助工具、配置文件及图像说明,便于快速上手。已有10801人学习下载。资源价值在于提供完整可读的Python抢票实现思路:从登录请求到监控余票、模拟购买均有代码支撑,同时附带流程图与界面截图,帮助理解抢票流程;还包含依赖文件和许可说明,方便搭建运行环境。虽然暂不支持连坐购买,但可作为学习网络请求、反爬应对和定时任务的实战素材。
1. 大麦网抢票脚本 Python源码:抢的不是手速,是请求时机和会话保持
一到热门演唱会开票日,社交平台上就会冒出各种“大麦网抢票脚本 Python源码”。我第一次接触这类脚本时并不觉得它能解决所有问题,但顺着原理拆下去,发现很多人对抢票的判断是反的:真正的瓶颈往往不在手速,而在请求时机、会话保持和验证码识别这三件事。这篇文章把一套能落地的 Python 方案拆开讲清楚。它能帮你自动轮询库存、按目标票档提交订单;它做不到逃过风控封禁,也不能保证下单一小时内一定支付成功。这个方向适合已经懂基础 Python、知道 Cookie 和 Session 是什么、愿意自己抓包确认接口的人;完全没有写过代码的朋友,建议先把 requests 环境和 Python 安装跑通,再动手改源码,而不是直接复制粘贴。
2. 先拆三层模型:票池、会话、下单链路,抓包才有方向
2.1 大麦网抢票链路是“页面、接口、状态”三层模型
抢票可以拆成五个节点:加载场次详情页、读取票池、选择票档、立即购买、提交订单并支付。用户看到的页面只是壳,真正支撑它的是两组接口:第一组是详情接口,返回库存、价格、场次信息;第二组是订单接口,负责校验、锁定库存、创建订单。
这个模型的意义在于,脚本要做的不是模拟鼠标点击,而是模拟这五个节点的网络请求序列。决定成败的关键在第 2 层的会话和第 3 层的订单字段。
我一般拿到抢票需求后不急着写代码,先打开浏览器,登录账号,按 F12 打开开发者工具的 Network 面板,过滤掉静态资源,只保留 XHR 请求,然后手动走到提交订单前一步。这一步得到的 URL、请求头和表单,比网上任何一份源码都可靠。
大麦的页面结构每个月都可能调整,依赖 CSS 选择器的脚本脆弱得很;而这套基于接口模型的脚本,只要接口路径和字段名不变,就不会受到页面改版影响。
还要注意一个很容易忽略的点:详情接口和订单接口可能挂在不同的域名下,跨域请求时 Cookie 必须同源携带。浏览器会自动处理跨域带 Cookie,但 requests 不会,所以脚本里要单独指定 Cookie 所属的域名。
2.2 抓包定位“立即购买”与“提交订单”,四个字段必须记全
开始抓包前,先把浏览器调成无痕模式,登录一次账号,再打开开发者工具。过滤 XHR 后,点击“立即购买”,找到名称里带 buy 或 order 的请求,依次记录四类信息:
| 记录项 | 说明 | 抓包位置 |
|---|---|---|
| 完整 URL | 包含 querystring,要注意活动 ID、场次 ID 参数 | Request URL |
| 请求方法 | 大多数详情是 GET,下单是 POST | Request Method |
| 非默认请求头 | 如 token、referer、x-requested-with、签名参数 | Request Headers |
| 表单或 JSON Body | 包含 skuId、buyNum、requestId 等字段 | Payload |
常见做法是先在页面上选定目标票档,再把“选票档”请求里的票档 ID 和“立即购买”里的 skuId 对应起来,写进配置文件。否则你会遇到一种诡异情况:库里票档有值,下单接口却一直提示“票档不存在”。
这里有一个最隐蔽的坑:订单接口通常带幂等参数,多数是 requestId,由客户端时间戳加随机串生成。脚本里要用 uuid 每次重新生成,不能写死,否则同一场次多次提交会被判定为重复请求。
为了避免代码和实际抓包结构脱节,我习惯把抓包结果整理成一个 JSON 文件放在脚本目录下,不在代码里写死任何 URL。这样更换场次时只需要改配置,不需要动主逻辑。
2.3 会话失效的三个表现,以及保持登录态的方法
写抢票脚本时最烦的不是并发,而是“登录状态看着正常,但订单接口突然报 401”。三个典型表现值得记下来:
- 详情接口仍正常返回库存,但订单接口返回 401 或者跳转登录页;
- 页面能打开,但购物车被清空,通常是 Token 过期被服务端注销;
- 请求头里携带的 token 已经过期,但脚本没有重新获取,于是一直拿旧 token 提交。
处理方式不是每次请求前都重新登录,而是在主循环开头做一次轻量校验。调用个人中心或订单列表接口,看状态码是 200 还是 401。
保持会话的常见做法有三条:
- 使用 requests.Session 保存 Cookie,不要在每次请求里重建 Session 对象;
- 把 Cookie 序列化到本地文件,脚本重启后能恢复会话状态;
- 在循环开始前加“检查登录”步骤,失败就告警,不进入抢票循环,避免浪费流量。
我在实际中用的是扫码登录拿 Cookie:用浏览器扫一次二维码,把 Cookie 里的关键字段存成 JSON,requests.Session加载后就能复用。这种方式比自动化输入账号密码稳定很多,至少不会触发登录验证码。
还有一点容易被忽略:请求头里的 User-Agent 必须和抓包时的浏览器保持一致。部分风控会对比 UA 与 Cookie 中的设备指纹,两处不一致会被判为脚本,后面第四章会详细说。
3. 抢票脚本骨架:库存轮询、提交订单、退票监控三段代码
3.1 技术选型:为什么先用 requests 而不是 Selenium
常见首选是 requests,配合 threading 或 asyncio。原因很简单:大麦的关键信息大多是 XHR 异步加载,用 requests 直接请求接口就能拿到完整返回,Selenium 能渲染页面但太重,容易被检测,资源占用也大。
我在生产环境里一般区分两种模式:
- 采集模式:用 requests 轮询详情接口,实时记录库存变化;
- 抢票模式:用协程并发提交订单,前置校验和提交分离。
Selenium 不是不能用,但只建议用在验证码环节。滑块验证码需要真实浏览器上下文,requests 很难自己算出轨迹参数,这时用 playwright 驱动浏览器去拖动,比用 requests 硬刚省事得多。
下面代码按 requests 方案写,不做浏览器自动化。
3.2 库存轮询与解析:Session 保持登录态,详情接口返回票池数据
import requests import time import json import threading CONFIG = { "activity_id": "123456", # 场次活动 ID,抓包得到 "item_id": "7890", # 商品 ID "sku_id": "1001", # 目标票档 ID "cookie_file": "session.json", "base_headers": { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0", "Referer": "https://piao.damai.cn/", "x-requested-with": "XMLHttpRequest" } } def load_session(): s = requests.Session() s.headers.update(CONFIG["base_headers"]) with open(CONFIG["cookie_file"], "r", encoding="utf-8") as f: cookies = json.load(f) s.cookies.update(cookies) # cookies 是一个 {"name": "value"} 字典 return s def fetch_detail(session): # 实际路径以抓包为准,这里只做示例;有的接口是 /api/detail url = "https://piao.damai.cn/api/detail" params = { "activityId": CONFIG["activity_id"], "itemId": CONFIG["item_id"], } r = session.get(url, params=params, timeout=2) text = r.text # 部分接口返回 JSONP,需要先剥掉 callback 包裹 if text.startswith("callback("): text = text[9:-2] data = json.loads(text) return data.get("result", {}) def poll_loop(session, stop_event): while not stop_event.is_set(): try: detail = fetch_detail(session) sku_list = detail.get("sku", []) target = [x for x in sku_list if str(x.get("id")) == str(CONFIG["sku_id"])] if target and int(target[0].get("status", 0)) == 1: remain = target[0].get("remainNum", 0) print("[命中] 可抢票档,剩余余量: %s" % remain) stop_event.set() except (requests.RequestException, ValueError) as e: print("[异常] %s" % e) # 随机抖动,避免每个线程同一时刻发请求 time.sleep(0.8 + random.uniform(0, 0.3))逻辑说明:
requests.Session会复用底层 TCP 连接,并自动管理 Cookie。不要每次轮询都新建 Session,否则服务端每次看到的都是新访客,限流阈值会很快触发。timeout=2是硬性设置的。响应慢时脚本不会卡死,而是抛异常走进 except 分支。实测详情接口多数在 1 秒内返回,按需调整。- 轮询间隔 0.8 秒是保守平衡点。开售瞬间想降到 0.3 也可以,但连续低间隔请求超过 10 次,很可能触发验证码或 IP 限流,得不偿失。
stop_event是threading.Event实例,线程间用它来通知“已经命中库存”,主线程收到后可以立刻进入下单流程。这样避免轮询线程还没退出,下单线程已经因为资源冲突报错。
3.3 提交订单与参数校验:请求字段一样,不代表字段值正确
import uuid def submit_order(session): # 有的场次是先调 verify 接口,拿到一个一次性 token,再提交订单 url = "https://piao.damai.cn/api/order/create" payload = { "activityId": CONFIG["activity_id"], "itemId": CONFIG["item_id"], "skuId": CONFIG["sku_id"], "buyNum": 1, "requestId": str(uuid.uuid4()), # 必须每次重新生成 } r = session.post(url, data=payload, timeout=3) try: res = r.json() except ValueError: return None, "接口返回非 JSON" if res.get("success"): order_id = res["data"]["orderId"] return order_id, None else: # 不要只看 success,要带出 errorMsg,后续日志才好定位 return None, res.get("errorMsg", "unknown")逻辑说明:
buyNum先写死为 1。要抢多张票,先在配置里设置,并查清楚当前账号的限购数量。大麦账号通常按票档限购,不是按场次,一次提交多张时要确认数量不超过限制。requestId是幂等参数,用 uuid 每次生成。如果代码里写死固定值,第二次提交会被判定为重复请求,订单不会创建。- 提交后不要立刻弹通知,先校验返回体里的
success和orderId。接口在某些时刻会返回“下单繁忙”,但订单实际已经创建,这种情况下要再查一次订单列表来确认,否则容易重复下单。 - 如果接口是“先验证库存、拿 token、再提交订单”的两段式设计,请从验证接口的返回体里取 token,不要自己伪造,否则订单接口会报签名错误。
3.4 退票监控:被忽略但实用的场景
抢票脚本的另一个用途是监控退票。售票期结束后常有回流票,这些票不会广播,只能靠高频轮询发现。
实现方式和库存轮询区别不大:
- 轮询详情接口,观察
remainNum从 0 变成正数; - 一旦大于 0,立刻拼装提交订单;
- 如果提交失败,继续下一轮,不要立即退出。
退票监控的轮询频率要比开售时低,因为长时间高频请求更容易被限流,一般 2 到 3 秒一次。这个细节是很多源码里写得最含糊的部分,却恰恰是稳定性的关键。
退出策略同样重要。退票监控不是无限循环,我一般设置一个结束时间,比如演出前 48 小时,脚本到点自动停。长期占着一台服务器跑轮询,除了电费,还可能让 IP 被标记为异常,影响下一次抢票。
4. 并发与风控:必调的 3 个参数,以及动静 IP 的取舍
4.1 重试次数、超时、请求间隔:先定默认值再按场景调整
写抢票脚本时改得最频繁的三个参数是max_retry、timeout、interval。下面是一组我常用的默认值:
| 参数 | 默认值 | 适用场景 |
|---|---|---|
| timeout (详情) | 2 秒 | 轮询库存时 |
| timeout (下单) | 3 秒 | 提交订单时 |
| max_retry | 3 次 | 单请求重试上限 |
| interval (开售) | 0.8 秒 | 开售瞬间轮询 |
| interval (退票) | 2.5 秒 | 长期监控回流票 |
timeout不能设置太短。提交订单要走校验、锁库存、创建订单的完整链路,经常超过 1 秒,设成 1 秒容易误判超时。更糟的是,服务端订单已经创建,客户端却因为超时重复提交,造成重复购票。
max_retry建议固定为 3 次。超过 3 次说明当前环境被限流或接口异常,这时停下来观察比继续盲目重试有意义。
interval是最需要反复试的参数。设成 0.2 秒确实能提升命中概率,但请求频率过高会触发连续性验证码,那几分钟基本就废了。我更建议开售瞬间用 0.3 秒坚持 5 秒,没命中就退回 0.8 秒。
多线程场景下还有一个隐藏问题:多个线程同时执行time.sleep(),醒来的时间几乎一样,会造成“请求波峰”。解决办法是加随机抖动,下面这段代码可以直接用:
import random import time def jitter_sleep(base, max_extra=0.5): # base 是基准间隔,max_extra 是随机抖动的上限 time.sleep(base + random.uniform(0, max_extra))这个抖动本身不解决问题,但它能把同一时间点出发的请求打散到一个小时间窗内,让服务端看到的请求分布更像真实用户。
4.2 多账号并发:线程之间隔离,不是简单开十个线程
多账号并发抢同一场次,听起来是提高中签率的直接办法,但实现时最容易翻车。每个账号要对应一个独立的requests.Session,Cookie、UA、设备指纹都要各自独立。如果所有请求共用同一个 UA,风控一眼就能看出是脚本。
我一般把多账号拆成多进程,而不是在一个进程里开多线程。Python 的 GIL 对 IO 密集型任务影响不大,但如果你在提交订单前有验证码识别或本地延时计算,多进程更稳,一个账号崩溃不会拖垮其他账号。
多账号的目录结构建议这样组织:
grabber/ ├── config.json # 公共配置 ├── accounts/ │ ├── account_001.json # 账号1的 cookie 和 UA │ ├── account_002.json # 账号2的 cookie 和 UA │ └── ... ├── grab.py # 主入口 └── logs/每个账号进程启动时读取自己的account_xxx.json,不要在主进程里维护一个全局 Cookie 池。这样做的好处是排查问题时能精确定位是哪个账号出了问题。
跨账号的频控策略也要分开算。两个账号同时命中库存,不要在同一秒内各自提交订单,稍微错开 0.5 秒。虽然服务端可能按账号维度频控,但同一 IP 下的并发量仍然会被观察。
4.3 IP 池与动静 IP:不要一上来就堆 IP,先看风控返回
当请求被限流时,服务端返回的现象通常是:
- 请求头正常,但接口返回“请求过于频繁”;
- 登录接口正常,详情接口偶尔正常;
- 提交订单接口返回 403。
多数人的第一反应是换 IP。这里存在成本问题:高质量住宅 IP 不便宜,演唱会场景对 IP 的消耗是瞬时的,抢票结束后这些资源就闲置了。更合理的策略是“先静后动”:先用本机 IP 跑低频轮询,能跑通就不换;确认被限流后再启用备用链路。
备用链路的实现比较简单,我一般把线路列表写进配置,每条线路记录一个失败计数,连续失败 3 次就暂时踢出池子,等待一段时间后再放回去。
import requests route_list = [ {"name": "route-a", "url": "http://route-a.example.com", "fail": 0}, {"name": "route-b", "url": "http://route-b.example.com", "fail": 0}, ] def pick_route(): # 优先选失败次数最少的线路 available = [r for r in route_list if r["fail"] < 3] if not available: return None return min(available, key=lambda x: x["fail"])["url"] def mark_fail(name): for r in route_list: if r["name"] == name: r["fail"] += 1 def mark_ok(name): for r in route_list: if r["name"] == name: r["fail"] = 0这里有一个容易踩的坑:把请求随机分发到不同线路,会让服务端看到一个账号从多个 IP 跳变,反而被判定为风险账号。正确做法是每个账号固定一条线路,只在它连续失败时才换线路,而不是每轮轮询都随机换。
提示:住宅 IP 与机房 IP 的稳定性和成本差异很大。住宅 IP 命中率通常更高,但速度不稳定;机房 IP 速度快但更容易被风控识别。预算充足优先选前者,否则先用本机跑通全流程,再考虑加入线路池。
5. 避坑清单:高频翻车场景的排查路径
5.1 现象:并发一高就提示“请求过于频繁”
原因:脚本在 0.2 秒内连续发出多个详情请求,单 IP 在短时间内被识别为爬虫特征。大麦的限流通常不是立刻 403,而是先返回“请求过于频繁”,继续重试几次后才封禁 IP。
解决:
- 把并发数压到 2 个线程以内,先观察返回码;
- 给请求加随机延时,不要用固定
sleep(0.5); - 检查是否有多个场次在同时轮询,如果是,把不同场次请求错开 10 秒。
5.2 现象:明明有票,提交订单却提示“库存不足”
原因:详情接口返回的库存是展示库存,不代表锁库存成功。热门场次的展示库存每秒都在变化,脚本看到的remainNum可能已经是 1 秒前的状态;也可能是别的账号正在锁库,你看到有票但提交时那一张已经被锁走。
解决:
- 提交订单前不要过度校验库存,直接进入提交订单流程,让接口自己做最终判断;
- 即使库存为 0,也要保留重试队列,因为退票回流的时间点不会提前显示在页面上;
- 提交后立刻校验返回结果,如果返回库存不足,可以继续循环,但重试次数限制在 2 次,避免死循环。
5.3 现象:登录正常,下单接口却报签名或指纹不一致
原因:提交订单接口不仅要带 Cookie,还要带签名参数或设备指纹参数。脚本只复制了 Cookie 与表单,遗落了签名相关的请求头。部分签名和服务端时间戳绑定,本地时间与服务器时间偏差过大也会判定失败。
解决:
- 回到浏览器开发者工具,把下单请求的完整请求头和表单逐项对比,重点看
x-sign、timestamp这类字段; - 如果签名是前端 JS 生成的,抓包拿不到恒定值,需要在页面源码里定位对应 JS,把生成逻辑用 Python 重写,这是整个方案里最容易被遮蔽的部分;
- 先用浏览器手动提交一单,确认全流程能走通后,再让脚本去仿造。签名不完整时反复跑脚本,只会让 IP 更早被标记。
5.4 现象:滑块验证码一直失败
原因:滑块验证码带轨迹检测。脚本模拟的拖动轨迹如果是匀速直线,很容易被判定为非人类操作;另一个原因是拖动顺序不对,没有“按下、移动、抬起”的完整过程,或缺少随机停顿。
解决:
- 用 playwright 或 pyautogui 驱动真实鼠标,而不是直接用 requests 提交验证 token,因为验证码需要浏览器上下文;
- 拖动轨迹用贝塞尔曲线或随机折线,不要用固定等差位移,至少保证“按下、加速、减速、抬起”的节奏;
- 验证码有 30 到 60 秒的时效,失败后不要连续重试,间歇 5 秒再触发,避免设备指纹被拉黑。
5.5 现象:订单已提交,但支付页没有跳转
原因:支付跳转依赖订单创建后的回调。如果脚本只提交了订单接口而没有继续请求支付链接,用户会觉得没抢到,但实际上已经占住库存。这种情况最坑,因为你以为失败了,其实订单已经存在。
解决:
- 提交订单成功后,从返回体里取
orderId,再按订单详情接口拿到支付地址; - 把“订单创建成功”和“支付地址已获取”两个事件都写入日志,设置高优先级通知;
- 不要尝试在脚本里模拟支付验证,支付环节人工操作更稳妥。用浏览器打开支付链接,输入密码完成支付。
6. 验证方法与日志设计:上线前先跑一轮真实演练
写完脚本不要等到开票日才首跑,正确做法是在开票前做三次演练:
第一次在非售票时段轮询详情接口,确认能拿到库存数据;第二次连续轮询 60 秒,观察有没有偶发限流;第三次模拟提交订单,把下单函数替换成打印日志,确认参数没有缺漏。
日志是抢票脚本的后悔药。我要求日志最少包含四个字段:时间戳、请求接口、返回码、命中状态。所有关键动作都要追加到本地文件,抢票失败后复盘时,这些日志比回忆可靠得多。
import logging logging.basicConfig( filename="grab_20250601.log", level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s" ) logging.info("poll detail activity=%s sku=%s", CONFIG["activity_id"], CONFIG["sku_id"]) logging.info("order created orderId=%s", order_id)这只是最基础的写法。实践中我会按场次建立独立日志文件,文件名带活动 ID,方便对比不同场次的抢票结果。多账号时日志里带上账号 ID,问题定位快很多。
上线前最后一件事,是把运行环境的时间同步好,并确认 Python 的time模块读取的是本地时间而非 UTC。曾经有一次签名总是报错,排查半天发现是服务器时区设置偏了 8 小时,签名里的时间戳一直对不上。
我在处理这类自动化任务时最大的习惯,是把它当普通工程对待:先拆需求,再抓包、建会话、写轮询、调并发、加日志,每一步都留下可回看的记录。如果某场票全部售空,不要急着把轮询频率调到极致,停下来看日志,判断是被限流还是库存确实为 0,再决定下一步动作。希望这篇从链路拆解到参数踩坑的笔记能帮你在下次开票前少走弯路,祝顺利。
本文还有配套的精品资源,点击获取