多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

大麦网抢票助手全自动实现:轮询、签名与验证码的工程实践

大麦网抢票助手全自动实现:轮询、签名与验证码的工程实践 简介面向大麦网演唱会、话剧、体育赛事等高频购票用户和票务代理这份 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 chromiumddddocr是本地跑验证码缺口识别的库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(headlessFalse) 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, timeout60000) 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, encodingutf-8) as f: json.dump(cookies, f, ensure_asciiFalse, indent2) browser.close() def load_session(): 把本地Cookie注入到requests.Session import requests session requests.Session() with open(COOKIE_FILE, r, encodingutf-8) as f: cookies json.load(f) for c in cookies: session.cookies.set(c[name], c[value], domainc.get(domain, )) return session逻辑说明移动端Web的Cookie字段比PC端少很多维护成本低判断登录成功的标志是会话Cookie里出现特定字段这里以你抓包看到的登录态字段名为准。headlessFalse是必须的因为扫码过程需要真实浏览器窗口扫码完成后脚本会自动保存整个过程大约三十秒。参数说明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}, timeout5, ) return resp.json()[ret][data][performModel][performList]逻辑说明这个接口一次返回该演出所有场次和每个场次下的票价、库存状态。它只读不写频率可以高一些但也别低于2秒一次否则大量请求堆积在风控侧没有意义。返回结构里的performList是核心嵌套的priceList里每个票档有soldOut字段这是判断有没有票的关键。提交订单是全程最高危的请求参数必须对齐def submit_order(session, sku, buyers, num2): 提交订单创建一笔待支付订单 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/, jsondata, timeout8, ) 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, num2) 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(detFalse, ocrFalse) 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_targetTrue) return result[target][0]逻辑说明slide_match接收背景图和滑块图返回缺口中心点的x坐标偏移。拿到偏移后拖动轨迹不要直接跳过去——系统会检测移动轨迹的人类特征。常见做法是先加速后减速总时长控制在0.5到1.2秒之间中间加一点抖动。但轨迹模拟只是降低被风控判别的概率没有百分百稳定。参数说明simple_targetTrue适用于缺口边缘较清晰的场景。页面换版后识别准确率会明显下降如果连续三次识别出来的偏移量都不稳定说明识别已经失效。我自己的经验是本地识别准确率低于七成时不如把脚本暂停等一次人工拖动——至少不会因为连续失败被账号风控盯上。风控就是个黑匣子你看不到它的判定规则只能通过失败次数反向推断。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 观演人提交总报“身份信息校验失败”现象观演人的名字和身份证号在页面能正常选择程序提交订单却提示身份信息不匹配。原因下单接口要求传观演人IDbuyerId不是名字加身份证号。手工拼的字符串过不了服务端校验。解决先用“常用观演人”接口拉取当前账号的观演人列表过滤出有效的买家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.comWindows用户手动同步系统时间就行。之后按顺序做三件事先跑一次登录态刷新和自检确认返回SUCCESS再跑一次查询接口确认能拿到场次列表最后把日志打开确认轮询正常输出。整个预热过程控制在3分钟内之后进入开票前1分钟的高频轮询模式。6.3 日志复盘抢不到也要知道为什么抢票失败的复盘比抢票成功更值钱。日志里固定记录四类信息时间、事件、耗时、失败码。推荐用单行文本格式方便事后grep2025-06-01 18:59:58.123 | POLL | perform2025-06-01-19:30 | sku580 | soldoutfalse | cost0.21s 2025-06-01 19:00:01.045 | ORDER | sku580 | buyers2 | retSUCCESS | orderId...抢不到时看日志能回答三个问题轮询在哪个区间开始发现余票、下单请求发出后多久才返回、失败码是哪一类。这三点定位完优化方向就很明确。我自己踩过最重的一次坑就是把签名参数写成静态结果开票前几分钟才发现所有请求都返回FAIL_SYS_ILLEGAL_ACCESS那次彻底翻车。后来我形成一个习惯每次改完参数开票前至少跑一次模拟模式日志里能看到完整的请求链路才敢上真实场次。这个习惯救了我好几次希望帮到你。本文还有配套的精品资源点击获取
返回列表