资讯动态

浏览器自动化脚本实战:定时触发、元素定位与秒杀抢购技术解析

发布时间:2026/10/20 12:38:44 来源:尧图企业网站定制
1. 从想法到落地浏览器脚本的运行形态和适配思路先说个场景。你在某个购物网站上看中一件限量商品页面写着x月x日 10:00 开抢你定了闹钟守到点狂点鼠标结果一刷新就已售罄。旁边有人就抢到了。这中间的差距往往是手速和网络延时更可能是对方用了脚本。这个标题里提到的js 脚本 各种网站定点 秒杀 抢购商品本质上是前端自动化脚本技术的应用场景之一。听上去有点神秘拆开来看核心就三件事定时触发、页面元素定位、模拟用户操作。本文不讨论绕过平台规则的黑产手段而是从技术原理出发讲清楚这类脚本的构成、实现细节和边界。先明确一下运行形态。JavaScript 脚本要在浏览器里跑常见有三种方式第一种是浏览器控制台粘贴执行。F12 打开开发者工具在 Console 里粘贴一段代码回车脚本立刻在当前页面上下文运行。优点是零依赖、适合快速验证逻辑缺点是页面一刷新就没了无法持久化。第二种是浏览器扩展Extension。通过 manifest.json 声明权限在后台页或内容脚本里注入逻辑能跨页面保持运行适合需要长期监听、多标签页协同的场景。第三种是用户脚本管理器如 Tampermonkey。它本质上是扩展的简化版提供 match、run-at 等注解让脚本在特定网站加载时自动执行。对于抢购这种提前挂好、到点触发的需求这种方式最合适脚本随页面加载注入逻辑独立维护改起来也方便。我个人的建议是如果只是自用、临时用Tampermonkey 足够如果做成了稳定流程、想精细控制页面生命周期用扩展更踏实。标题里的各种网站四个字意味着脚本必须具备适配性后面我会专门讲怎么处理不同网站的差异。// 一个最小可用的用户脚本骨架 // UserScript // name 定点抢购工具学习用途 // namespace // version 0.1 // description 定时触发模拟点击 // match https://example.com/* // run-at document-idle // /UserScript (function() { use strict; console.log(脚本已注入等待定时任务启动...); })();注意 run-at document-idle意思是等 DOM 基本就绪后再执行避免页面还没渲染完脚本就去找元素、结果找不到。2. 定时触发机制用本地时钟校准取代傻等抢购脚本最容易翻车的地方就是定时不准。你可能会想不就是 setInterval 每秒检查一次时间吗到点执行不就行了实际跑起来会发现两个问题第一浏览器对 setInterval 有最小间隔限制和节流策略后台标签页可能被挂起第二脚本里拿到的本地时间跟服务器时间存在偏差而商品上架时间以服务器为准。比较稳的方案是先做时间校准再做倒计时轮询。时间校准的思路如下向目标网站发起一个请求从响应头里读取 Date 字段。HTTP 响应头的 Date 是服务器生成响应时的 UTC 时间精度在秒级。用这个值和本地时间做差得到一个偏移量。此后所有现在几点的判断都用本地时间加偏移量来算。async function getServerTimeOffset() { const response await fetch(/api/health, { method: GET, cache: no-store }); const serverDate new Date(response.headers.get(Date)); return serverDate.getTime() - Date.now(); }这里有几个注意点请求路径要挑不带缓存的接口最好是一个返回很轻的 API避免服务器渲染页面耗时导致时间失真。Response Headers 里必须能读到 Date 字段。同源请求下肯定有跨域的话需要服务端开 CORS 暴露该头。偏移量建议取三次请求的中位数避免单次网络抖动污染。校准完之后轮询可以这样写每秒检查一次当前时间与目标时间差小于某个阈值比如 300ms时立刻执行抢购逻辑。不要提前太早因为页面上的按钮可能是未开始状态模拟点击了也没响应。为什么轮询要用 requestAnimationFrame 而不是 setInterval这不是必须的但如果抢购那一刻需要极高时间精度rAF 有两个好处一是跟随浏览器刷新率帧率 60Hz 时约 16.7ms 一次比 setInterval 的毫秒级精度更平滑二是页面处于可见状态时 rAF 才触发天然避免了后台节流导致的睡过头。代价是 CPU 占用略高不过只在抢购前几秒启动就行。let serverOffset 0; let rafId null; function checkAndStart(targetTime) { const now Date.now() serverOffset; if (now targetTime) { cancelAnimationFrame(rafId); executePurchaseFlow(); return; } rafId requestAnimationFrame(() checkAndStart(targetTime)); } // 使用方式提前 5 秒启动 setTimeout(() { checkAndStart(targetTimestamp); }, 5000);关于时间同步还有一个细节电脑的系统时间可能被 NTP 同步过也可能没有。哪怕校准了一次服务器时间本地时钟之后也可能漂移。稳妥做法是每 30 秒重新校准一次偏移量或者至少在目标时间前 1 分钟校准一次取最新的偏移值。另外目标时间的获取最好来自页面本身的倒计时文本。很多网站的倒计时是从服务器时间算的可以直接读取页面里的 data 属性或倒计时元素。如果页面没暴露就用接口返回时间。实在不行再用硬编码时间加校准偏移的方案。3. 页面元素定位从选择器到状态判断脚本的第二个重头戏是准确地找到抢购按钮。做过爬虫的同学一定深有体会不同网站的 DOM 结构千差万别。同一个网站页面改版一次选择器可能就全失效了。所以定位元素不能只写死一个选择器要设计成多级回退策略。我把定位策略分成三级第一级文本匹配。按钮的文本通常是立即抢购马上开抢加入购物车立即购买。用 XPath 按文本内容查找比 class 名稳定得多因为文案不会轻易变class 可能每次发布都改。function findButtonByText(keywords) { const candidates []; for (const keyword of keywords) { const nodes document.evaluate( //button[contains(normalize-space(.), ${JSON.stringify(keyword)})] | //a[contains(normalize-space(.), ${JSON.stringify(keyword)})] | //span[contains(normalize-space(.), ${JSON.stringify(keyword)})], document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null ); for (let i 0; i nodes.snapshotLength; i) { candidates.push(nodes.snapshotItem(i)); } } return candidates; }第二级属性回退。如果文本匹配失败再尝试常见的属性约定比如 id 里包含buy、class 里包含purchase、data-* 属性里有明确的业务含义。这个属于经验匹配成功率不高但在网站结构稳定时很可靠。第三级用户自定义选择器。这是各种网站适配的关键。在脚本开头留一个配置对象允许使用者针对特定站点填写自己的选择器脚本优先使用自定义选择器找不到再走文本和属性匹配。const SITE_CONFIG { example.com: { buyButtonSelector: #J_LinkBasket, countdownSelector: [data-countdown], clickDelay: 50 }, shop.example.org: { buyButtonSelector: .product-buy-btn, countdownSelector: .timer } }; function getConfigForSite() { const host location.hostname; return SITE_CONFIG[host] || {}; }定位到元素只是第一步状态判断决定脚本会不会白点。典型的状态有三种未开始按钮 disabled 或文案是即将开始、可点击文案是立即抢购、按钮可点、已结束文案是已售罄。写脚本时必须同时检查 disabled 属性和文案因为不同网站的禁用实现方式不同有的是 HTML disabled 属性有的是 aria-disabled有的是 class 上加了一个置灰样式但没有真正禁用。有个容易被忽略的坑按钮可能被遮挡。页面弹窗、浮层、底部悬浮条都可能盖住按钮导致模拟点击虽然触发了 DOM 事件但实际用户点击位置被其他元素拦截。解决方法是先检查元素是否处于页面顶层用 elementFromPoint 验证鼠标坐标位置是否就是这个元素本身。function isElementClickable(el) { const rect el.getBoundingClientRect(); const cx rect.left rect.width / 2; const cy rect.top rect.height / 2; const topEl document.elementFromPoint(cx, cy); return el topEl || el.contains(topEl); }如果被遮挡需要先尝试关闭弹窗找到 .modal、.popup、.dialog 一类容器里的关闭按钮先点击关闭再重新判断。这个动作要放在轮询循环之外避免每帧都去关弹窗造成多余操作。4. 提高命中率的关键直接调用底层逻辑而不是模拟点击很多人以为抢购脚本就是模拟点击按钮这个理解太浅了。从技术层面看模拟点击分两种一种是派发 MouseEvent靠事件冒泡触发页面绑定的事件处理器另一种是直接调用按钮的事件处理器函数或者直接调用页面内部的业务函数。后者的成功率和速度通常远高于前者。为什么因为很多现代网站用的是 Vue、React 这类框架按钮的点击处理并不是绑定在 DOM 上的而是框架内部通过事件委托绑定在根节点上再由虚拟 DOM 机制分发。你手动 new MouseEvent(click) 在绝大多数情况下能触发但如果框架对事件做了 check比如校验 isTrusted模拟事件就会失效。isTrusted 是 Event 对象的一个只读属性。真实用户操作产生的事件 isTrusted 为 true脚本派发的事件为 false。部分页面会在事件处理里校验这个字段防范自动化点击。这就回到了标题里秒杀这个场景你模拟的点击能不能被页面认可直接决定了抢购能否成功。绕过 isTrusted 的常见思路不是伪造事件而是绕开事件系统。具体怎么绕看情况如果是原生 JS 的 onclick 属性可以直接调用 el.onclick()。如果是 jQuery 绑定的可以尝试通过 jq 内部的数据缓存找事件回调。如果是 Vue/React可以尝试获取组件的vue/ __reactInternalInstance 等内部属性找到方法直接调用。这里有一个极其重要的前提以上操作仅用于分析页面逻辑、学习前端框架内部机制且必须在你拥有合法操作权的页面中进行。千万不要用于破坏他人系统或绕过安全机制。// 一个更接近真实点击的派发方式 function simulateRealClick(el) { const rect el.getBoundingClientRect(); const opts { bubbles: true, cancelable: true, composed: true, view: window, clientX: rect.left rect.width / 2, clientY: rect.top rect.height / 2, button: 0, buttons: 1 }; el.dispatchEvent(new MouseEvent(pointerdown, opts)); el.dispatchEvent(new MouseEvent(mousedown, opts)); el.dispatchEvent(new MouseEvent(pointerup, opts)); el.dispatchEvent(new MouseEvent(mouseup, opts)); el.dispatchEvent(new MouseEvent(click, opts)); }顺序上先 pointer 再 mouse 最后 click是为了贴近真实浏览器的事件序列。如果网站绑定的是 pointer 事件系列只派发 click 就不够。不过说实话真正提高命中率的往往不是事件派发而是提前把 HTTP 请求准备好。抢购的本质是什么是向服务器提交一个购买请求服务端判断库存扣减。按钮点击只是触发这个请求的入口。如果能在点击前提前分析出请求的 URL、方法、参数然后在触发点用 fetch 直接发出请求会比等按钮响应快几十到几百毫秒。这对分秒必争的抢购场景是质的区别。怎么做用浏览器开发者工具的网络面板在手动点击一次按钮时观察发出的请求记录请求路径、参数规律。然后在脚本里模拟这个请求。// 示例提前构造请求在到达触发时间时直接发送 function firePurchaseRequest() { const formData new URLSearchParams(); formData.append(skuId, skuId); formData.append(qty, 1); formData.append(token, getTokenFromPage()); fetch(/api/order/create, { method: POST, body: formData, credentials: include, headers: { Content-Type: application/x-www-form-urlencoded, X-Requested-With: XMLHttpRequest } }) .then(r r.json()) .then(data { if (data.code 0) { // 跳转支付或提示成功 } }); }这个思路要成功前提是请求参数能从页面里拿到。常见的情况是skuId 藏在页面 data 属性里token 在 cookie 或某个全局变量里qty 固定是 1。需要提前分析写成适配层。直接请求的好处是绕开了 DOM 事件和渲染线程坏处是可能会漏掉一些前置条件——比如某些网站要求先调用一个预扣库存接口才能调用创建订单接口否则会报错。所以稳妥的流程是把请求拆成两步先分析完整链路再脚本化。5. 真实场景中的坑与注意事项写抢购脚本最容易栽跟头的地方往往是脚本本身逻辑正确但页面状态跟预期不一样。这里列几个我实际遇到过的状况。5.1 反自动化检测与行为特征很多大流量交易网站都有风控系统专门识别自动化操作。常见特征包括事件对象 isTrusted false前面提过点击坐标在按钮上的分布过于固定真实用户会有随机偏移时间间隔过于规律比如每次都精确到毫秒级请求头缺少一些浏览器特有字段如果你的脚本仅仅用于个人学习、测试自己的账户和页面这些通常不会触发风控但如果规模大了或者用于商业目的风险就完全不同了。我给的建议是脚本用于学习和自动化测试场景不要在真实交易场景里反复高频使用。5.2 多标签页协同的问题一个常见需求是同时盯着几个网站抢购。多标签页时代脚本怎么协同最粗糙的方式是每个标签页各跑各的脚本互不通信。问题在于目标时间可能不同某个标签页在后台被浏览器节流定时器不准。更合理的方式是让一个标签页作为主控负责计时和触发其他标签页通过 BroadcastChannel 接收触发信号。// 主控页面 const channel new BroadcastChannel(purchase-sync); channel.postMessage({ action: fire, site: location.hostname }); // 其他标签页 const channel new BroadcastChannel(purchase-sync); channel.onmessage (event) { if (event.data.action fire) { executePurchaseFlow(); } };BroadcastChannel 是同源页面间通信的简单方案不需要服务器中转延迟在毫秒级。注意它是同源限制的两个不同域名之间没法直接用需要借助中间页面或后台脚本转发。5.3 页面跳转导致的脚本失效点击抢购按钮后页面通常会发生跳转跳到确认订单页、跳到支付页。用户脚本的作用域是匹配的 URL跳转后如果新 URL 不匹配 match脚本就不执行了。解决办法是match写宽一点覆盖整个站点的所有路径。同时在抢购按钮点击后短时间内在新页面也要注入脚本用于自动完成下一步比如点击提交订单立即支付。这一步如果在兄弟们实际项目中做过会发现在确认订单页往往还需要重新读取一次库存状态因为部分网站会二次校验。5.4 内存泄漏与长跑任务的稳定性脚本挂在页面上后如果使用 setInterval 轮询页面长时间不刷新可能会导致内存增长。轮询里避免创建大量闭包和临时对象时间判断放在一个简单的函数里即可。另外页面如果遇到网络错误导致脚本崩溃需要自动恢复机制。// 简单的自愈机制监听错误并重新初始化 window.addEventListener(error, (event) { console.error(脚本运行错误5秒后重新初始化, event.message); setTimeout(initScript, 5000); });注意这里不要无限循环重启最好加一个重启次数上限。5.5 手机端网页的问题很多人以为脚本只在 PC 浏览器上跑但实际上手机浏览器如 Kiwi、Yandex也是可以装扩展的。手机端网页的 DOM 结构和 PC 端经常不一样按钮的渲染方式、事件绑定方式都有差异。适配移动端时先看一下元素选择器是否在两种 userAgent 下都成立不行就拆成两套配置。手机浏览器的另一个特点是触摸事件优先有时需要派发 touchstart/touchend 才能触发点击MouseEvent 在部分移动端浏览器上不好使。function simulateTouchClick(el) { const rect el.getBoundingClientRect(); const cx rect.left rect.width / 2; const cy rect.top rect.height / 2; const touch new Touch({ identifier: Date.now(), target: el, clientX: cx, clientY: cy, radiusX: 2, radiusY: 2, force: 1 }); const touchStart new TouchEvent(touchstart, { touches: [touch], targetTouches: [touch], changedTouches: [touch], bubbles: true, cancelable: true, composed: true }); el.dispatchEvent(touchStart); // 触发 click el.click(); }6. 边界与思考这些脚本技术能做什么、不该用来做什么聊到这里标题里的秒杀抢购基本拆解完了。最后我想认真聊一下边界因为这是很多文章不会写、但实际做事必须想清楚的部分。这些技术的合法用途非常广远远不止商品抢购定时自动打卡、自动签到定时刷新监控页面变化时自动通知自动化填写表单、批量上传数据竞拍/秒杀公共资源预约政务预约、图书馆抢座等在合规前提下日常页面的重复操作自动化如果把这些脚本用在虚假交易、批量注册、影响正常用户公平性的场景里性质就变了。平台有权利拒绝你的请求风控系统有权封禁账户严重情况下可能涉及不正当竞争或破坏计算机信息系统。我建议每个写自动化脚本的人先问自己三个问题这个脚本是否会让其他真实用户处于不公平的劣势这个脚本是否违反了网站明确的服务条款如果网站方看到这个脚本会不会合理地质疑你的目的如果三个问题里有任何一个让你犹豫那这个脚本就不该在真实环境里跑。留下技术本身用在合理的地方。从我实际玩脚本的经验看真正有价值的能力不是抢到商品而是分析页面逻辑、梳理事件链路、设计低耦合的自动化框架这些通用技能。今天能帮你自动抢购的脚本稍加改造就能变成自动巡检、自动报表、自动同步的工具。把这些技能用在提升个人效率上比卡着秒针抢一个便宜几十块钱的商品划算得多。最后分享一个我自己的习惯写这类脚本时永远把配置跟逻辑分离。选择器、时间、开关都放到脚本头部核心逻辑保持稳定。这样网站改版时只需要改配置不用动逻辑省下的时间够你多研究好几种反爬方案。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑