☰
大麦网抢票助手全自动实现:轮询、签名与验证码的工程实践
2026/10/10 1:02:42 网站建设 项目流程

简介:面向大麦网演唱会、话剧、体育赛事等高频购票用户和票务代理,这份 Windows 环境的抢票助手通过无障碍服务模拟真实点击,支持自动登录、场次与票档选择、自动提交订单,有效减少手动抢票中的重复操作与时间差,提升热门演出出票成功率。整个资源压缩包仅 1.37MB,共 11 个文件,核心包含 Python 自动化脚本、使用说明与依赖清单,以及界面示意图和流程图,项目结构清晰,便于快速查看运行与二次修改,适合需要搭建抢票脚本的开发者参考。除核心代码外,还附有 LICENSE、.gitignore 等工程文件,适合有一定 Python 基础的读者学习无障碍自动化实现、调整抢票参数,或作为个人自动化项目的模板。目前已有 2021 人浏览学习,对于想提升购票效率或了解模拟点击方案的开发者,是一份轻量且具备实操参考价值的资源。

1. 大麦网抢票助手全自动:为什么手动点击永远慢半拍

“大麦网抢票助手全自动”说到底是一件事:把开票瞬间那十几秒里,人要做的一连串点击、等待、判断,全部交给程序去循环执行。手动点击之所以总抢不到,原因大多数时候不是网速慢,从你看见“立即购买”到手指点到按钮,再到页面提交订单,整个链路至少要走1.5秒;如果中途还弹出人机校验,这个时间直接翻倍。而脚本把这一串动作压缩成几十毫秒的接口轮询,开票前守着,接口刚放票就提交订单。

这套方案适合个人自用:帮自己抢一张演唱会票,不碰退改加价,不搞囤票倒卖。整个流程只走公开的详情页、下单页能看到的接口,不碰任何内部系统。读完你能得到一个能跑的脚本架子,以及比脚本更重要的东西——知道每个环节为什么这么做、失败了怎么查。抢票这件事,一半是技术,一半是玄学,玄学那部分改不了,技术这部分可以。

2. 技术路线先想清楚:抢票链路的四个环节与混合方案选型

2.1 抢票链路的四个环节:先知道对面在做什么

任何抢票助手要自动化,本质上都在模拟一条完整的购买链路。大麦网的买票流程,从用户视角看不复杂:打开详情页、选场次、选票档、确认观演人、提交订单。但从自动化视角拆开,它由四个有明确边界的环节组成。

第一个环节是登录态。服务器靠Cookie里的会话令牌来识别你是谁,未登录状态下所有下单接口都是拒绝访问的。第二个环节是场次票档查询,详情页会返回当前演出有哪些场次、每个场次下有哪些票档、每个票档是否还有库存。第三个环节是提交订单,这一步把演出ID、场次ID、票档ID、观演人ID、购买数量打包成一个请求发给服务端。第四个环节是人机校验,下单过程中如果被风控判定为异常,会临时弹滑块或点选验证。

这四个环节里,前两个是“高频轻量”操作,适合用程序不断轮询;后两个是“低频关键”操作,出错代价高,需要格外谨慎。搞清楚这个分层,后文的所有代码都是围着它转的。

2.2 三种自动化方案的取舍:为什么不纯用浏览器控制

做抢票自动化,常见的路线有三条:纯浏览器自动化(Playwright或Selenium)、纯接口模拟(requests)、两者混合。我实际做过前两种,最后停在混合方案上,原因是纯浏览器方案有两个硬伤。

第一个硬伤是慢。开票那一刻页面本身在加载大量资源,倒计时、弹窗、优惠券、推荐位全都挤在一起,浏览器自动化要等这些渲染完才能点击。页面越卡,脚本越慢,开票瞬间的每一百毫秒都可能决定结果。第二个硬伤是脆弱。页面改版一次,元素选择器就要重写一次,大麦网详情页的DOM结构我见过的变动频率足够让纯UI方案三天两头返工。

纯requests方案快,但门槛高。签名算法、风控参数、Cookie维护全都要自己处理,第一次跑通至少要踩十几个坑。所以我的选择是混合:登录这种“低频但必须真实环境”的操作用Playwright,轮询和下单这种“高频但只有几十行代码”的操作用requests。这样既拿到了接口方案的速度,又避开了签名和验证码里最难啃的部分。

2.3 项目目录与最小依赖:把高频和低频分开

动手写代码前,先立一个最小工程结构。我的习惯是把低频操作(登录、验证码)和高频操作(轮询、下单)拆到不同模块,这样某个环节出问题时不用牵一发动全身。

damai_bot/ ├─ requirements.txt ├─ core/ │ ├─ session.py # 登录态获取与刷新(低频) │ ├─ poller.py # 场次票档轮询(高频) │ ├─ order.py # 下单与失败重试(低频关键) │ └─ logger.py # 结构化日志 ├─ data/ │ └─ cookies.json # 本地会话文件 └─ main.py # 入口与主循环

依赖文件里最小集合是这样:

pip install requests playwright ddddocr pillow playwright install chromium

ddddocr是本地跑验证码缺口识别的库,Pillow用来处理验证码图片,requests和playwright分别承担接口请求和浏览器控制。目录划分的核心逻辑是:session.py和poller.py尽量独立,不互相调用,这样后面调试轮询逻辑时不需要每次都经过登录。

3. 抢票主流程落地:抓包、轮询、下单一把梭

3.1 先抓包再写代码:把一次手动下单完整录下来

写轮询代码之前,第一件事永远是用浏览器开发者工具把一次完整的手动下单过程录下来。不要凭记忆猜接口路径,猜出来的参数十有八九对不上。操作步骤是这样:用无痕窗口打开大麦网网页版并登录,按F12打开Network面板,勾选Preserve log,然后进入目标演出详情页,手动点一次“立即购买”,走到订单确认页。

在Network面板的过滤框输入mtop.trade,就能看到下单相关的几条XHR请求。右键那条创建订单的请求,选择“Copy as cURL”,把内容存到本地文件。这一步得到的cURL包里,最有用的是三样东西:Cookie、请求路径、业务参数名。它们构成了后面所有代码的参数基准。

有一点必须说清楚:cURL里的sign签名参数是当时时间戳算出来的,十几分钟后就会失效。不要天真地把这段cURL转成Python就上线,它只能作为参数命名的参照,签名问题后面单独处理。抓包这一步的价值是告诉你接口长什么样,不是给你一份能直接用的代码。

3.2 登录态持久化:扫码一次,Cookie复活一周

登录态是抢票脚本的生命线。我见过不少人写好轮询逻辑后,一直卡在“未登录”上,问题就出在登录态没有持久化。常见做法是:用Playwright启动一个真实浏览器窗口,人工扫码登录一次,然后把Cookie序列化到本地文件,之后requests全部复用它。

import json import time from playwright.sync_api import sync_playwright COOKIE_FILE = "data/cookies.json" def refresh_login(): """用Playwright打开登录页,人工扫码,把Cookie存到本地""" with sync_playwright() as p: browser = p.chromium.launch(headless=False) context = browser.new_context( viewport={"width": 390, "height": 844}, user_agent=( "Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) " "AppleWebKit/605.1.15 (KHTML, like Gecko) " "Version/17.2 Mobile/15E148 Safari/604.1" ) ) page = context.new_page() page.goto("https://m.damai.cn/damai/home/index.html", timeout=60000) page.click("text=登录/注册") print("请在浏览器窗口完成扫码登录,成功后脚本会自动保存会话。") for _ in range(60): time.sleep(1) cookies = context.cookies() if any(c.get("name") == "unb" for c in cookies): break with open(COOKIE_FILE, "w", encoding="utf-8") as f: json.dump(cookies, f, ensure_ascii=False, indent=2) browser.close() def load_session(): """把本地Cookie注入到requests.Session""" import requests session = requests.Session() with open(COOKIE_FILE, "r", encoding="utf-8") as f: cookies = json.load(f) for c in cookies: session.cookies.set(c["name"], c["value"], domain=c.get("domain", "")) return session

逻辑说明:移动端Web的Cookie字段比PC端少很多,维护成本低;判断登录成功的标志是会话Cookie里出现特定字段,这里以你抓包看到的登录态字段名为准。headless=False是必须的,因为扫码过程需要真实浏览器窗口;扫码完成后脚本会自动保存,整个过程大约三十秒。

参数说明:viewport设成390x844是iPhone的常规尺寸,UA用iPhone Safari,目的是让服务端看到一个最大众化的环境,而不是让脚本看起来“特殊”。轮询等待的60秒是扫码超时,如果超过这个时间还没登录成功,脚本会带着不完整的Cookie退出,这种情况要人工确认。

登录之后加一个自检函数,避免“以为登录了,实际上会话已经过期”。用requests访问用户信息接口,看返回值是否以SUCCESS结尾,这是后续所有操作的前提。

3.3 轮询与下单:高频查票、低频提交的串行循环

登录态稳定后,核心就是两个函数:查场次票档、提交订单。先看查询:

import time import requests POLL_INTERVAL = 2.0 # 轮询间隔,单位秒 ITEM_ID = "你的演出itemId" # 详情页URL中的id参数 def query_perform_list(session): """查询场次列表,返回所有场次和票档,字段名以抓包结果为准""" resp = session.get( "https://mtop.damai.cn/h5/mtop.damai.wireless.item.detail/1.0/", params={"itemId": ITEM_ID}, timeout=5, ) return resp.json()["ret"]["data"]["performModel"]["performList"]

逻辑说明:这个接口一次返回该演出所有场次和每个场次下的票价、库存状态。它只读不写,频率可以高一些,但也别低于2秒一次,否则大量请求堆积在风控侧没有意义。返回结构里的performList是核心,嵌套的priceList里每个票档有soldOut字段,这是判断有没有票的关键。

提交订单是全程最高危的请求,参数必须对齐:

def submit_order(session, sku, buyers, num=2): """提交订单:创建一笔待支付订单""" data = { "itemId": sku["itemId"], "performId": sku["performId"], "skuId": sku["skuId"], "buyerId": ",".join(buyers), "quantity": num, } resp = session.post( "https://mtop.damai.cn/h5/mtop.trade.order.create/4.0/", json=data, timeout=8, ) ret = resp.json()["ret"][0] if ret.endswith("SUCCESS"): return True, resp.json()["data"]["orderId"] return False, ret

逻辑说明:创建订单需要四类参数——演出ID、场次ID、票档ID、观演人ID。前三个能从3.3的查询结果里直接拿到,观演人ID必须从“常用观演人”接口拉,不能自己拼身份证号传,这是后面避坑章节要展开的。quantity是购买数量,默认2,表示一场票两张。返回值里以SUCCESS结尾才算成功,其他都是业务失败,需要把错误码记录下来。

参数说明:timeout设8秒,因为下单接口在开票高峰时响应很慢,但超时长短于10秒,目的是快速失败、快速重试。buyerId用逗号拼接支持多观演人,大麦网一个订单最多绑定的观演人数量你可以在下单页确认,常见是2到4人。

3.4 主循环串联:全自动抢票的完整线程模型

前面三个函数合起来,就是一条完整的自动抢票链路。主循环的关键决策点只有一个:控制“查询”和“下单”之间的节奏。

BUYER_IDS = ["观演人ID1", "观演人ID2"] # 从常用联系人接口获取 def main_loop(session, target_perform, target_price): while True: try: perform_list = query_perform_list(session) for perf in perform_list: if perf["performName"] != target_perform: continue for sku in perf["priceList"]: if sku["price"] == target_price and not sku["soldOut"]: ok, msg = submit_order(session, sku, BUYER_IDS, num=2) if ok: print(f"[下单成功] 订单号:{msg}") return print(f"[下单失败] {msg},继续轮询") time.sleep(POLL_INTERVAL) except Exception as exc: print(f"[异常] {exc},等待后重试") time.sleep(3)

逻辑说明:主循环先遍历场次,匹配目标演出时间;再遍历票档,匹配目标价位;两项都命中且未售罄,就立即提交订单。下单成功直接return退出,下单失败则继续下一轮轮询。这里有个细节:下单失败不要立刻用相同参数重试,连续两次相同参数的快速请求很容易触发“重复提交”风控。

参数说明:target_perform用场次名称精确匹配,比如“2025-06-01 19:30”;target_price用价位数字匹配,比如580的票就写580。这里会遇到一个坑,票价字符串和实际价格数字可能不一致——有的票档叫“看台580”,有的直接叫“580”,避坑章节会专门说。

4. 全自动的三个硬骨头:签名、验证码与请求频率

4.1 签名参数处理:从cURL到动态sign

大麦网的mtop接口体系里,每个请求都要带sign签名参数。这个签名由页面里的mtop.js生成,把业务参数、时间戳、会话令牌按固定算法算出来,改任何一个业务参数,签名都要重新算。我刚开始做的时候试过把cURL里的sign直接写死,结果十分钟后全部请求返回FAIL_SYS_ILLEGAL_ACCESS,那次彻底翻车。

正确做法是从页面里把签名算法翻译成Python。不同端的mtop.js实现有差异,但整体套路一致:

import hashlib import hmac from urllib.parse import urlencode APP_KEY = "12574478" # 以抓包结果为准 def gen_mtop_sign(token: str, timestamp: str, params: dict) -> str: """生成mtop请求的sign,算法逻辑参考页面mtop.js的对应函数""" token = token.replace("_", "=").replace("~", "/") sign_source = urlencode(sorted(params.items())) + "&" + timestamp + "&" + token sign = hmac.new( token.encode("utf-8"), sign_source.encode("utf-8"), hashlib.sha256, ).hexdigest() return sign

逻辑说明:这段代码是示意结构,不要直接照抄。正确的做法是打开你抓包时对应环境里的mtop.js,搜索“sign”关键词,找到生成函数后逐行翻译成Python。验证翻译是否正确的标准很简单:相同的参数和时间戳,你算出的sign和页面生成的一致。

参数说明:token来自Cookie或请求头里的会话令牌,timestamp是毫秒级时间戳。任何业务参数变化都会导致sign变化,所以签名函数要和query_perform_list、submit_order放到同一个模块,确保每次请求前动态生成。如果返回FAIL_SYS_ILLEGAL_ACCESS,第一优先级查sign,第二才查Cookie。

4.2 滑块与点选验证:识别速度与出错的兜底

下单过程中如果被风控判定异常,会弹滑块或点选验证。全自动方案里,最常见的处理是用ddddocr在本地识别滑块缺口位置,然后把识别到的距离返回给拖动逻辑。

import ddddocr def solve_slide_distance(bg_file, fg_file): """识别滑块缺口位置,返回x轴偏移像素""" det = ddddocr.DdddOcr(det=False, ocr=False) with open(bg_file, "rb") as bg_f, open(fg_file, "rb") as fg_f: result = det.slide_match(fg_f.read(), bg_f.read(), simple_target=True) return result["target"][0]

逻辑说明:slide_match接收背景图和滑块图,返回缺口中心点的x坐标偏移。拿到偏移后,拖动轨迹不要直接跳过去——系统会检测移动轨迹的人类特征。常见做法是先加速后减速,总时长控制在0.5到1.2秒之间,中间加一点抖动。但轨迹模拟只是降低被风控判别的概率,没有百分百稳定。

参数说明:simple_target=True适用于缺口边缘较清晰的场景。页面换版后识别准确率会明显下降,如果连续三次识别出来的偏移量都不稳定,说明识别已经失效。我自己的经验是:本地识别准确率低于七成时,不如把脚本暂停,等一次人工拖动——至少不会因为连续失败被账号风控盯上。风控就是个黑匣子,你看不到它的判定规则,只能通过失败次数反向推断。

4.3 请求频率与退避:轮询不是越快越好

很多人写抢票脚本有个误区,觉得查询间隔越短越好。实际上请求频率过高,风控系统会直接限流或让会话掉线。我的参数表是这样定的,按不同阶段区分:

阶段间隔理由
开票前常规轮询2~3秒减少无效请求,保持会话健康
开票前1分钟0.8~1.2秒追求余票第一时间出现
下单失败后递增退避防止被判定为异常跳变

退避策略用固定数组实现,连续失败次数越多,等待越长:

BACKOFF = [1, 3, 10, 30, 60] def wait_before_retry(fail_count): """连续失败时按指数级递增等待""" idx = min(fail_count, len(BACKOFF) - 1) time.sleep(BACKOFF[idx])

逻辑说明:这个退避机制解决两个问题:短期内高频失败会触发风控,固定等待可以让请求模式更接近人类行为;同时给服务端一点时间恢复。连续失败次数建议存到内存变量里,每次下单失败就累加,下单成功就清零。

参数说明:BACKOFF的五个值不是随便定的,1秒处理瞬时错误,3秒处理网络抖动,10秒以上处理风控拦截。超过60秒还在失败,说明大概率不是频率问题,而是Cookie失效或签名错了,这时候应该告警而不是硬扛。

5. 避坑实录:抢票助手最容易翻车的五个位置

5.1 接口显示有票,下单却返回库存不足

现象:轮询接口返回某票档可售,但提交订单后返回“库存不足”或“手慢了”。

原因:展示接口和下单接口读的库存缓存不同步。详情页的库存是聚合缓存,更新有延迟;下单接口读的才是实时库存。开票瞬间大量用户同时刷新,缓存延迟可能持续十几秒。

解决:不要把查询结果的isSoldOut当作下单依据,它只是“尝试下单”的触发条件。下单返回库存不足时,把它当成一次正常的轮询结果,继续循环,不要在那里卡住报错。

5.2 观演人提交总报“身份信息校验失败”

现象:观演人的名字和身份证号在页面能正常选择,程序提交订单却提示身份信息不匹配。

原因:下单接口要求传观演人ID(buyerId),不是名字加身份证号。手工拼的字符串过不了服务端校验。

解决:先用“常用观演人”接口拉取当前账号的观演人列表,过滤出有效的买家ID,再塞进下单参数。这个接口是只读的,可以放在登录自检之后跑一次,结果缓存到本地。

5.3 订单创建成功,却在App里找不到待支付订单

现象:程序返回orderId,日志显示下单成功,但App里没有待支付订单。

原因:订单创建后被风控标记为异常关闭,通常发生在会话状态异常或设备指纹跳变时。也有一种情况是下单接口返回的orderId是预占单,支付页面没跳转就自动释放了。

解决:下单成功后不要直接庆祝,立刻用同一会话查询订单状态。如果状态不是“待支付”,把错误码记到日志,并准备下一轮重试。支付环节我始终主张交给人工处理,脚本只负责把订单创建出来,支付自动化涉及资金风险,没有意义。

5.4 开票当天登录态失效,所有请求返回会话过期

现象:前一周测试一切正常,开票当天所有请求全部返回会话过期。

原因:Cookie里的会话令牌有时效,加上“滑动过期”机制——长时间没有新请求也会掉线。前一天测试完就关脚本,第二天开票时Cookie已经凉了。

解决:开票前两小时加一个保活任务,用Playwright加载一次首页和订单页,刷新Cookie后再保存一次。这个动作放在定时任务里,开票当天早上跑一遍,能少一次翻车。

5.5 多实例并发抢同一个账号,结果互相顶掉

现象:开了三个脚本实例抢同一场次,每个实例都提示“已有订单进行中”或全部下单失败。

原因:同一账号同时发起多个下单请求,服务端有幂等锁,后到的请求会把先到的顶掉。多实例并发在同一个账号上不但没有加速效果,反而互相干扰。

解决:一个账号只跑一个实例,不同场次的抢购也不要在同一账号下并行。要并发就分散到不同账号,每个账号各自串行执行。多账号并发时注意出口网络环境要保持彼此独立,我一般不会让多个账号从同一出口网络同时高频请求。

6. 上线前最后一公里:从能跑到能抢到

6.1 用mock接口做一次全链路演练

抢票脚本不能在真实开票时做第一次测试。常见做法是先用一个mock函数替代真实下单接口,跑一遍完整循环,统计每一步的耗时。

import time import random import statistics def fake_check_and_order(): """模拟一次查询+下单的耗时,用于演练,不连真实接口""" time.sleep(random.uniform(0.15, 0.35)) return random.random() > 0.1 if __name__ == "__main__": cost = [] for _ in range(50): t0 = time.perf_counter() fake_check_and_order() cost.append(time.perf_counter() - t0) print("平均耗时:%.3fs 最大耗时:%.3fs" % (statistics.mean(cost), max(cost)))

逻辑说明:演练的价值是暴露串行链路上哪一步最慢。如果平均耗时超过0.5秒,在真实开票场景里就会落后于其他脚本。真抢票时把这个函数替换成真实的query_perform_list和submit_order即可,主循环结构完全不用动。

参数说明:random.uniform(0.15, 0.35)模拟网络抖动,50次样本能看出耗时分布。重点看最大耗时,不是平均耗时——最大耗时代表最坏情况下的体验。

6.2 对时与预热:开票前十分钟应该做什么

脚本写得再好,系统时间慢五秒就全废了。开票当天第一件事是校准本地时间,Linux和macOS直接跑:

sudo ntpdate -u ntp.aliyun.com

Windows用户手动同步系统时间就行。之后按顺序做三件事:先跑一次登录态刷新和自检,确认返回SUCCESS;再跑一次查询接口,确认能拿到场次列表;最后把日志打开,确认轮询正常输出。整个预热过程控制在3分钟内,之后进入开票前1分钟的高频轮询模式。

6.3 日志复盘:抢不到也要知道为什么

抢票失败的复盘比抢票成功更值钱。日志里固定记录四类信息:时间、事件、耗时、失败码。推荐用单行文本格式,方便事后grep:

2025-06-01 18:59:58.123 | POLL | perform=2025-06-01-19:30 | sku=580 | soldout=false | cost=0.21s 2025-06-01 19:00:01.045 | ORDER | sku=580 | buyers=2 | ret=SUCCESS | orderId=...

抢不到时,看日志能回答三个问题:轮询在哪个区间开始发现余票、下单请求发出后多久才返回、失败码是哪一类。这三点定位完,优化方向就很明确。我自己踩过最重的一次坑就是把签名参数写成静态,结果开票前几分钟才发现所有请求都返回FAIL_SYS_ILLEGAL_ACCESS,那次彻底翻车。后来我形成一个习惯:每次改完参数,开票前至少跑一次模拟模式,日志里能看到完整的请求链路才敢上真实场次。这个习惯救了我好几次,希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询