
简介压缩包内提供一份名为“大麦抢票.user.js”的JavaScript用户脚本及配套说明文档面向希望提升大麦网热门演出抢票成功率的技术用户。脚本通过自动填写购票信息、模拟快速点击、自动刷新余票等方式优化购票流程同时附带仓库管理系统相关的文字说明涵盖物品入库、出库、盘点、库存查询等核心业务逻辑帮助理解自动化操作在不同场景中的应用。资源共包含 2 个文件分别为 txt 说明文件和 js 脚本文件整体大小仅 9KB结构精简。已有 81 人学习下载适合对票务自动化脚本或仓库管理后台开发感兴趣的初级至中级学习者参考。阅读说明文件可弄清脚本与仓库管理内容的联系脚本代码则可用于理解浏览器自动化操作与页面交互实现是一份小巧但兼具实用性和学习价值的样例。1. 大麦抢票.user.js 在解决什么问题——用户脚本的本质大麦抢票.user.js 这个文件名容易让人以为是个现成的抢票工具其实它只是一段以 .user.js 结尾的用户脚本由浏览器里的脚本管理器在匹配页面上自动执行。用户脚本比前端框架更早出现从 2005 年前后 Greasemonkey 流行到 Tampermonkey 成为事实标准它解决的一直是把高频、重复的页面操作变成可维护、随时开关的 JavaScript。票务页面的抢票流程恰好符合这个特征——操作路径固定、按钮状态靠 DOM 更新驱动、对点击时机敏感。下面从零搭一个 user.js 骨架再把监听、选票、提交链路拆开最后落在参数调优和可观测性上。适合能写业务代码、但没系统写过用户脚本的工程师。2. 搭好 Tampermonkey用最小 user.js 跑通注入链路用户脚本和 node 脚本最大的差别在于宿主。.user.js 文件本身不直接运行它依赖一个扩展程序把脚本注入到指定页面。对前端工程师来说第一步不是写业务逻辑而是把注入链路跑通——确认脚本真的在目标页面上执行了再开始写监听逻辑。这个环节出问题后面所有代码都是空转。2.1 为什么是 Tampermonkey元数据块怎么理解用户脚本管理器负责脚本生命周期什么时候注入、注入哪些 URL、脚本的存储与更新以及提供跨域存储、通知等 GM API。Tampermonkey 是当前兼容性最好的实现Chrome 与 Edge 都能从官方扩展商店安装如果公司浏览器限制安装扩展开源实现 Violentmonkey 可以做备选它在 GM API 完整度和更新频率上略弱但基础注入和存储足够用。安装后工具栏会出现 Tampermonkey 图标右键打开管理面板可以查看脚本列表、存储内容和更新记录。脚本头部有一段从// UserScript到// /UserScript的元数据块它不是普通注释而是管理器的安装协议字段直接决定脚本如何被注入。下表是几个最关键的字段元数据字段作用典型值name脚本名显示在管理面板中damai-ticketmatch匹配的 URL 模式决定注入范围https://detail.damai.cn/item.htm*run-at注入时机document-endgrant声明可用的 GM APInone或GM_notificationnoframes是否在 iframe 中执行不加该项则 iframe 也会注入match的语义是整段 URL 前缀匹配星号放在最后可以覆盖 query string 的所有变化。run-at document-end表示 DOM 解析完成后、页面脚本执行期间注入对静态 DOM 操作足够用需要更早介入时才用document-start。noframes值得单独留意大麦的部分业务页面用 iframe 嵌套订单区域如果你不希望在子框架里重复执行脚本保留这一项可以省掉很多脚本执行了两次的困惑。2.2 最小骨架先证明脚本能注入页面写一个只输出日志、不做任何业务操作的最小脚本// UserScript // name damai-ticket-skeleton // namespace local.dev.scripts // version 0.1.0 // description 大麦详情页注入测试不执行任何订单操作 // match https://detail.damai.cn/item.htm* // run-at document-end // grant none // noframes // /UserScript (function () { use strict; console.log([damai] injected at new Date().toISOString()); console.log([damai] page title: document.title); })();这段代码只输出两条日志用来验证注入链路。match写成https://detail.damai.cn/item.htm*星号覆盖?id123和?id123spma这类 query 变化。grant none表示不声明任何 GM API此时脚本运行在页面上下文可以直接访问window和document。noframes保证脚本只在顶层页面执行不会因为页面里的 iframe 被反复注入。提示grant none与沙箱模式是两套上下文。一旦在元数据块声明了任意 GM APITampermonkey 会把脚本放进沙箱想访问页面全局变量就必须显式使用unsafeWindow。很多脚本装上后没反应排查到最后都是因为这个切换导致代码抛异常。2.3 调试入口console 之外还有 unsafeWindow脚本注入后打开 DevTools 的 Console 面板能看到[damai] injected这条日志。看不到时优先检查三件事扩展是否启用、match是否匹配当前 URL、脚本是否在管理面板里被关闭。Checks 完成后确认页面全局变量是下一步。在 Console 里执行类似Object.keys(window).filter(k k.includes(damai) || k.includes(detail))的语句可以看到页面自己挂了哪些全局对象。这个方法比直接猜类名可靠因为页面改版时类名经常变全局变量名的变化反而少。unsafeWindow在沙箱模式下使用。比如某次页面把演出信息挂在了window.__data而脚本声明了grant GM_notification那么代码里要写unsafeWindow.__data直接写window.__data拿到的会是沙箱环境里的空对象。调试时如果发现某个全局量在脚本里读不到先去确认是否已经触发沙箱模式再决定是不是需要通过unsafeWindow绕一层。3. 抢票脚本的核心链路DOM 监听、票档选择与自动提交骨架跑通之后剩下的工作都在前端工程范畴内定位节点、监听状态变化、模拟点击、处理反馈。这条链路的每一环都有对应的调试方法和失败模式下面按顺序拆。3.1 先分析页面结构不要上来就写代码大麦详情页的常见抢票路径是在详情页选择场次和票档进入订单确认页勾选观演人提交订单。具体页面形态差异不小有的票档按看台/内场分组平铺有的演出需要先在座位图上手工选座。第一步要做的不是写代码而是花十分钟打开 DevTools逐层展开 DOM确认这几个关键节点分别是什么标签、禁用状态用什么属性表达。通常票档按钮的文案里包含价格和缺货或立即购买字样观演人勾选框的文本就是姓名本身提交按钮位于订单确认页底部。一个稳健的定位策略是用文本匹配加叶子节点过滤而不是硬编码第几个按钮——页面改版时类名经常变文案的变化相对少而且语义上不容易混淆。还要注意移动端 H5 和 PC 端的页面结构可能不同match需要对应更新脚本主体逻辑尽量做成与平台无关的纯 DOM 操作。3.2 用 MutationObserver 等可点击的那一刻开票前票档按钮一般是 disabled 或显示缺货登记开票瞬间服务端下发新状态前端渲染成可点击按钮。抢票脚本要做的是在这个 DOM 变化发生后的几十毫秒内完成点击。用 setInterval 轮询也可以但固定间隔的轮询会带来额外延迟而 MutationObserver 由 DOM 变化直接驱动检查配合小防抖窗口更贴近时机。// 抢票核心监听 DOM 变化一旦目标按钮可点击立即执行回调 function waitForClickable(buttonMatcher, onClick, debounceMs 100) { let timer null; const check () { const btn buttonMatcher(); if (btn !btn.disabled btn.getAttribute(aria-disabled) ! true) { stop(); onClick(btn); return true; } return false; }; const stop () { if (timer) { clearTimeout(timer); timer null; } observer.disconnect(); }; const observer new MutationObserver(() { if (timer) return; timer setTimeout(() { timer null; check(); }, debounceMs); }); observer.observe(document.body, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: [disabled, class] }); // 立即扫描一次避免错过脚本注入前就已可点击的状态 check(); return stop; }逻辑说明MutationObserver 的回调在 DOM 变化后异步触发同一轮渲染可能连续触发多次回调。代码里的if (timer) return把多次回调合并成一次检查debounceMs默认 100ms可以在 50ms 到 150ms 之间调。窗口太短会提高查询频率占用 CPU窗口太长则延迟增大。attributeFilter只监听disabled和class因为票档按钮的禁用状态通常由这两个属性表达没必要监听全部属性。参数说明check里同时判断disabled和aria-disabled因为很多组件库会把禁用状态写到两处。初次调用check()很有必要——如果脚本注入时按钮已经可点击再等 MutationObserver 就没有意义了。注意buttonMatcher必须返回单个按钮元素或null不要在多元素场景下直接取第一个。3.3 自动选择票档与观演人定位到按钮之后下一个问题是选对票档和观演人。按固定下标选择容易出错页面渲染顺序一变就失效。常见做法是把目标票档文字和观演人姓名做成配置用文本匹配来查找节点。// 在指定容器内查找文本匹配的叶子节点 function findLeafByText(root, text) { const nodes Array.from(root.querySelectorAll(button, div, span, label, li)); return nodes.find((el) { if (el.children.length 0) return false; const t (el.textContent || ).replace(/\s/g, ); return t.includes(text); }); }逻辑说明限制children.length 0是这段代码的关键。外层 div 的textContent会包含整个票档列表的文本直接对它做includes匹配非常容易误命中。只保留叶子节点后匹配到的必然是真正显示文字的末端节点语义更准确。这个函数既可以用来找票档也可以用来找观演人所在的 label传入不同的 root 即可。选观演人时注意勾选框是 checkbox外面通常包一层 label。找到 label 后调用.click()不要直接修改checked属性——Vue 或 React 应用里属性值和框架状态可能不一致直接赋checked会被框架的渲染机制覆盖只有模拟点击才能触发事件绑定。如果页面存在多个相同姓氏的观演人建议在配置里写全名而不是子串。3.4 提交订单与验证码分流点击提交订单之后的反馈有三种常见情况跳转支付、排队等待、校验弹窗。脚本能自动处理前两种遇到验证码应该立即停下来通知人工。给脚本加一个最小状态机idle表示等待监听fighting表示已提交等待响应blocked表示需要人工处理。let state idle; function submitOrder() { if (state ! idle) return; state fighting; const btn findLeafByText(document, 提交订单); if (btn) btn.click(); setTimeout(() { if (state ! fighting) return; if (document.querySelector([class*captcha], [class*verify])) { state blocked; GM_notification({ title: 需要人工验证, text: 检测到验证码请切回页面处理, timeout: 10000 }); return; } state idle; }, 2000); }逻辑说明点击后 2 秒内如果页面出现验证码特征节点状态置为blocked并通知用户没有出现则认为是排队中状态回到idle让上层监听逻辑继续运行。这个 2 秒窗口来自订单提交反馈的最长等待时间网络状况差时可以调到 3 秒。状态机避免了极端情况下同一个提交按钮被连续触发两次。提示使用GM_notification需要在元数据块补充grant GM_notification。一旦声明了任意 GM API脚本进入沙箱模式之前用window直接访问页面全局变量的代码会失效要改用unsafeWindow。这是脚本从开发切到带 GM 功能后最容易踩的坑。4. 抢票时序与参数设计为什么更快不总是更优很多第一次写抢票脚本的人把全部精力放在缩短点击时间上最后发现要么页面卡死要么触发了验证码。抢票成功率里时序准确比点击速度更重要。本机时钟和服务器时钟通常存在几秒偏差倒计时一旦提前或延后之前的监听和点击逻辑全都会错位。4.1 用服务器时间校准倒计时通用做法是在脚本启动时向同源接口发一个轻量请求读取响应头里的Date字段与本地时间做差得到偏移量。之后所有离开始还有多久的计算都基于本地时间 offset不再依赖本机时钟。// 校准本地时间与服务器时间的偏移量单位为毫秒 async function calibrateOffset() { const resp await fetch(location.origin /, { method: HEAD, cache: no-store }); const serverTime new Date(resp.headers.get(Date)).getTime(); return serverTime - Date.now(); }逻辑说明同源请求不需要额外授权HEAD方法不下载 body流量开销小。cache: no-store是为了避免浏览器命中本地缓存保证Date头来自服务端而不是缓存副本。校准函数的返回值是毫秒偏移量使用时serverTime Date.now() offset。如果页面里有更直接的时间接口也可以换成那个接口核心是拿到服务端的当前时间。校准应在脚本注入后立刻执行一次开票前几分钟再校准一次避免时钟漂移。4.2 关键参数表参数建议值说明提前入场5-10 分钟给页面数据和登录态留足加载时间防抖窗口80-120 msMutationObserver 合并检查的窗口提交等待1.5-3 s提交订单后等待反馈的时间重试次数2-3 次超过阈值容易触发风控多场次轮询间隔1-2 次/秒切换候选场次的频率这些值不是常量应该作为参数暴露在配置里。电脑 CPU 性能较弱时防抖窗口往 150ms 调网络延迟高时提交等待往 3s 调。照搬他人的参数没有意义参数必须匹配自己的运行环境和网络状况否则要么延迟偏高要么频繁触发热点限制。4.3 风控边界频率、指纹与验证码票务平台的网络请求带有行为特征。固定间隔轮询、毫秒级连续点击、异常的用户代理都会让请求指纹和其他用户重叠多用户共用同一份脚本时重叠更明显。设计脚本时要让点击节奏接近真实用户防抖窗口不要低于 50ms重试之间加随机延迟比如在 500ms 到 1500ms 之间取随机值。这会让整体慢几十毫秒但能显著降低被识别为自动化的概率。验证码是平台的最终防线。滑块或点选出现后脚本自动识别并解决的情况在真实环境里很少而且针对验证码的自动化绕过属于平台明确禁止的行为。正确做法是感知到验证码后立即停止让用户人工接手。此时继续点击只会增加行为和网络层面的异常特征抢到票的概率反而下降——脚本让位越早风险越小。4.4 把参数做成独立配置把所有参数收拢到一个config对象放在脚本开头换场次时只改文本不碰业务逻辑。const config { targetSeat: 看台A-580, audience: [张三], advanceMs: 300000, // 提前入场时间单位毫秒 debounceMs: 100, // 监听防抖窗口 submitTimeoutMs: 2000, // 提交等待时间 maxRetries: 2, // 提交重试次数 notifyOnBlocked: true // 遇到验证码时通知 };参数说明targetSeat和audience走文本匹配必须与页面显示完全一致或至少保持连续子串advanceMs控制进入抢票状态的时间maxRetries控制提交失败后的重试上限。配置项需要持久化时把可变部分用GM_setValue存储脚本启动时读回并合并到config上避免每次开票前改代码。5. 把抢票 user.js 做成可观测的工具分级日志与多场次轮询5.1 先给脚本加上分级日志抢票过程中页面反馈极快出错时往往已经过去好几秒没有日志很难定位是没监听到、没点上还是点了没生效。常见做法是自建一个带级别的log函数info记录流程切换debug记录每次检测结果同时把最近 100 条日志持久化到GM_setValue。脚本卡住时直接看管理面板的存储页就能回查不用重新挂页面。const logs []; function log(level, msg) { const line [${level}] ${Date.now()} ${msg}; logs.push(line); if (level info) console.log(line); if (logs.length 100) logs.splice(0, logs.length - 100); GM_setValue(damai_logs, logs); }逻辑说明GM_setValue的写入是同步的但频繁写入会有性能损耗所以只在info级别或状态变化时持久化debug级别只往内存数组里追加避免每次MutationObserver回调都触发存储写入。加日志的成本很低排查问题的收益很高。5.2 多场次轮询的思路一场演出往往有多个日期场次热门场次抢不到时相邻场次的票通常还有余。脚本可以维护一个候选场次列表按优先级排序每秒轮询一到两次各场次目标票档的可购买状态发现可点就切换过去并执行 3.2 节的监听逻辑。轮询频率比单页面的 mutation 监听低得多每秒 1-2 次足够加上各场次间 500ms 的随机等待可以避免请求节奏过于规律。5.3 一个能省一档延迟的细节最后一个可以立刻落地的技巧等待支付跳转时不要轮询按钮是否变灰而是点击后立刻把状态置为fighting把定时器挂在一个已缓存的计时器实例上两秒后状态没有变化再回退。跳转本身由框架内的路由事件触发轮询 DOM 只是多等一个 setInterval 周期。把这个状态机搬进自己的脚本先确认注入再用日志核对每次点击的触发时机脚本里的每个按钮点击都会变成可回查的事件记录。本文还有配套的精品资源点击获取