首页 / 资讯中心 / 文章详情

Node爬虫监控商品库存:从接口探测到半自动下单实践

Node爬虫监控商品库存:从接口探测到半自动下单实践 ★ FEATURED ARTICLE
简介面向有京东商品监控与自动下单需求的开发者这份资源是基于 Node.js 实现的京东到货监控爬虫支持扫码登录、按地区查询库存、库存大于零时自动下单并包含抢购逻辑。由于京东接口更新项目已标记为 DEPRECATED但整体思路仍可作为电商自动化脚本的参考。压缩包共 11 个文件大小 1.7MB以 5 个 JavaScript 脚本为主涵盖入口参数处理、工具函数与日志模块另附 package.json、yarn.lock、README 说明、LICENSE 和演示动图结构清晰便于查阅。已有 820 人学习下载。源码演示了扫码登录、登录状态本地缓存、分地区库存检测、自动下单与抢购等关键流程配合演示动图和说明文档适合想了解 Node.js 爬虫及电商下单自动化实现的开发者学习借鉴。项目虽已废弃但其中关于接口调用、状态管理和自动化操作的思路仍对相关工具开发有参考价值。1. 一个标着 DEPRECATED 的 Node 爬虫项目为什么反而值得拆开看想买的商品一直缺货页面显示“无货”你每隔十分钟手动刷一次刷了两天这是很多人的真实经历。jd-happy 这类 Node 项目就是把这件事脚本化轮询商品详情页背后的库存接口发现状态从“无货”变成“有货”后自动加购并尝试提交订单。项目自身标了 DEPRECATED这几年类似的爬虫项目也陆续被弃但这不代表思路没有价值。真正值得拆开看的是监控逻辑到今天依然能低成本复现而“下单服务”才是被风控成本击穿的脆弱环节。这篇文章会从接口探测、轮询参数、Cookie 维护到下单链路逐个讲清最后说明为什么“半自动下单”才是这类脚本的稳定形态。适合两类人想自己做到货提醒的人以及想了解电商自动化链路深浅的开发者。2. 到货监控先找到那个藏在商品页里的库存接口2.1 从浏览器开发者工具开始把黑匣子变成可见请求很多新手拿到需求后的第一反应是抓取商品详情页的 HTML再用正则去匹配“库存”“无货”这些字样。这条路在静态页时代还能走但现在商品页的库存状态几乎都是页面加载后由 JavaScript 异步请求回来的HTML 里只有一个空壳容器。正确做法是打开商品页按 F12 进入开发者工具切到 Network 面板并勾选 Fetch/XHR然后刷新页面。你会看到一列异步请求其中某一条返回的是 JSON内容里包含库存状态、价格、促销信息——这就是你要找的库存接口。定位接口的技巧很简单在开发者工具的 Preview 面板里逐层展开 JSON直接按 CtrlF 搜索“无货”或“有货”这两个字能搜到的那条请求就是数据源。把它复制成 cURL在终端里跑一遍确认能拿到同样的返回接口探测就算完成了。我一般会顺手把请求里的地区编码、时间戳参数记下来因为这些参数直接决定返回结果是“有货”还是“无货”。这个环节最常见也最坑的操作是跳过接口探测直接解析 HTML等页面改版后整个脚本瞬间变成废铁——这不是脚本写得不好是数据源选错了。2.2 Node 轮询脚本最小可跑版本找到接口之后监控就是一个带频率控制的循环请求。下面的脚本是 Node 18 直接可跑的最小版本依赖只有 Node 自带的 fetch不需要装 axios 和 cheerio。// checkStock.js —— 监控京东商品库存状态的最小脚本Node 18 const SKU_ID 100012345678; // 商品详情页 URL 里的数字 ID const AREA_ID 1-72-4810-0; // 地区编码必须和收货地址一致 const CHECK_INTERVAL 30000; // 两轮请求的间隔单位毫秒 const HTTP_TIMEOUT 10000; // 单次请求超时时间单位毫秒 async function fetchStock(skuId, areaId) { // 注意库存接口地址和参数必须以你自己抓包的结果为准 // 这里只给出调用结构不要照抄占位地址。 const url https://api.example.com/stock?skuId${skuId}area${areaId}; const controller new AbortController(); const timer setTimeout(() controller.abort(), HTTP_TIMEOUT); try { const res await fetch(url, { headers: { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://item.example.com/${skuId}.html, Cookie: process.env.JD_COOKIE || // 敏感配置从环境变量读取 }, signal: controller.signal }); const json await res.json(); return parseStockState(json); } finally { clearTimeout(timer); } } function parseStockState(json) { // 不同账号、登录态、地区下库存字段的层级可能完全不同。 // 第一次跑的时候先 console.log(JSON.stringify(json)) 看真实结构。 const raw json?.stock?.state ?? json?.data?.stock?.state; return { inStock: raw 1, // 假设 1 表示有货按抓包结果调整 raw }; } async function loop() { const state await fetchStock(SKU_ID, AREA_ID).catch(err { console.error([${new Date().toISOString()}] 请求失败:, err.message); return { inStock: false, raw: null }; }); console.log([${new Date().toISOString()}] 有货${state.inStock}); setTimeout(loop, CHECK_INTERVAL); // 上一次请求完成后才开始计时 } loop();这里有两个必须说透的设计点。第一用 setTimeout 实现循环而不是 setInterval是为了避免一次请求因为网络延迟还没返回下一次请求又发出去导致请求堆积、并发数失控。一个慢请求拖垮整个监控进程是这类脚本最常见的死法。第二超时用 AbortController 实现10 秒拿不到结果就直接失败不会让一个悬空的 Promise 卡住后续轮询。参数建议下表按我的经验这个区间最稳参数建议值说明CHECK_INTERVAL30000~60000ms低于 20 秒容易被识别为脚本特征HTTP_TIMEOUT10000ms网络抖动时快速失败不占资源AREA_ID与收货地址绑定直接决定返回的是哪个仓库的库存User-Agent从浏览器复制完整值别用 Node 默认 UA特征太明显2.3 状态判定与通知别把“上架”当成“可下单”接口返回的库存字段不是只有“有货/无货”两种常见还有“仅剩几件”“预售”“该地区暂不售卖”等状态。如果只按“有货”一个条件判断很容易在预售商品上误报。我一般会把状态先归一化成枚举再交给下游判断const STATE { IN_STOCK: in_stock, OUT_OF_STOCK: out_of_stock, PRE_SALE: pre_sale, AREA_LIMIT: area_limit, }; function normalizeState(raw) { if (raw 1) return STATE.IN_STOCK; if (raw 4) return STATE.PRE_SALE; // 按抓包结果调整映射 if (raw 6) return STATE.AREA_LIMIT; return STATE.OUT_OF_STOCK; }通知逻辑上要做“状态翻转去重”只在“无货 → 有货”这个变化发生时推送一次而不是每次轮询到有货都推送。不然你的手机在一个小时内会被相同内容轰炸一二十次。let lastState null; function handleStockUpdate(currentState) { if (lastState ! null lastState STATE.OUT_OF_STOCK currentState STATE.IN_STOCK) { notifyWebhook(SKU_ID, AREA_ID); // 触发你的通知渠道 } lastState currentState; } function notifyWebhook(skuId, areaId) { // 通用做法是调用钉钉/企业微信/webhook传商品链接和时间戳 console.log([ALERT] 商品 ${skuId} 到货地区 ${areaId}); }还有个容易被忽略的小细节通知内容里带上商品短链接和监控到的库存状态而不只是“有货”两个字的文案。你等货等了两天早上被推送叫醒大概率想直接点链接进去而不是再打开 App 搜一遍商品。这个体验细节决定你第二天还想不想继续用这套脚本。3. 下单服务从“看到有货”到“提交订单”的五个关卡3.1 登录态与 Cookie 维护最大的不稳定源监控接口不登录也能调但下单必须要登录态。网页端下单的钥匙就是一个带登录身份的 Cookie 集合。它也是最不稳定的依赖——Cookie 的有效期跟账号活跃度、IP 纯净度、请求频率强相关有的 Cookie 能用一星期有的半天就失效这属于玄学。应对方式是把登录态做成配置文件由人工维护脚本不做自动登录更不做自动过验证码。// session.js —— Cookie 加载与过期判断 const fs require(fs); const path require(path); function loadSession() { try { return JSON.parse(fs.readFileSync(path.join(__dirname, session.json), utf8)); } catch { return null; } } function saveSession(session) { fs.writeFileSync(path.join(__dirname, session.json), JSON.stringify(session, null, 2)); } function sessionExpired(session) { if (!session?.cookie) return true; const expiredAt session.expiredAt || 0; return Date.now() expiredAt; }session.json 长这样{ cookie: thorxxx; pt_keyyyy; pt_pinzzz, userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, expiredAt: 1767225600000 }人工从浏览器登录后复制 Cookie粘贴进这个文件。脚本每次请求带上同一份 Cookie。Cookie 过期时脚本不要试图自己重登而是发一条“登录态失效请手动更新”的通知然后停摆。自动重登意味着要过验证码、短信那是一条维护成本极高的路不值得为几件商品走。如果你同时盯好几个商品建议多准备一个小号轮换使用一个账号跑太高频的请求风控概率会快速上升。3.2 下单链路拆解加购、结算、提交网页端下单链路可以拆成三步加入购物车 → 拉取结算页信息 → 提交订单。每一步都是一个普通 POST 请求但每一步都可能被风控拦截。新手最容易犯的错是直接调“提交订单”接口忽略中间确认库存和价格的步骤。下面的代码是半自动下单服务的骨架重点在错误分支处理和提交前校验。// orderService.js —— 半自动下单服务骨架 const { loadSession, sessionExpired } require(./session); async function apiCall(path, payload, session) { const res await fetch(https://api.example.com${path}, { method: POST, headers: { Content-Type: application/json, Cookie: session.cookie, User-Agent: session.userAgent, }, body: JSON.stringify(payload), }); return res.json(); } function buildRequest(skuId, count) { // 从最近一次抓包里提取完整请求体保留所有固定字段 // 只替换 skuId、count 这类变化字段。 // 不要尝试手写签名算法版本一更新你会一直追着改。 return { skuId, count, // ...固定字段从抓包复制 }; } function handleBizCode(res, stage) { const code res?.code ?? res?.result?.code; switch (code) { case 0: return; // 成功 case 101: throw new Error(SESSION_EXPIRED); case 103: throw new Error(OUT_OF_STOCK); case 106: throw new Error(NEED_MANUAL_VERIFY); default: throw new Error(UNKNOWN_CODE:${code}); } } async function submitOrder({ skuId, count, addressId, expectedPrice }) { const session loadSession(); if (!session || sessionExpired(session)) { throw new Error(SESSION_EXPIRED); } const addCartRes await apiCall(/cart/add, buildRequest(skuId, count), session); handleBizCode(addCartRes, ADD_CART); const checkoutRes await apiCall(/checkout/confirm, { addressId }, session); const latestPrice checkoutRes?.price?.total || 0; if (Math.abs(latestPrice - expectedPrice) 1) { // 价格变了别直接提交通知人去决定 throw new Error(PRICE_CHANGED); } const orderRes await apiCall(/order/submit, { addressId, ...buildRequest(skuId, count), }, session); handleBizCode(orderRes, SUBMIT_ORDER); return orderRes; }代码里的状态码 101、103、106 只是说明用的示意值真实接口的状态码要按你自己抓包看到的去映射不要照抄。这里想强调的核心是 handleBizCode 的分类能力把返回值事先分好类脚本才能知道当前该“重试”“停摆”还是“找人工”。如果不分类遇到没见过的返回码就直接抛异常日志里只有一行 undefined排错会非常痛苦。还有一个现实约束提交订单的接口通常带动态签名参数。完整的爬虫方案需要逆向前端加密逻辑但更可靠的做法是把一键下单接口的完整请求体复制下来作为模板每次只替换 skuId 和数量。这样加密逻辑一旦更新你会立刻在返回里看到未知状态码而不是下了个坏单。3.3 监控到下单的联动阈值、冷却与防抖监控模块和下单模块不能直接写成“检测到有货就立刻下单”中间还差一道防抖。原因是库存接口刷新频率和仓库真实锁定之间存在几十秒的时间差第一轮看到有货可能是“别人取消订单还没最终释放”的短暂状态。我一般会要求连续两轮都检测到有货才触发下单流程。let consecutiveHit 0; let coolingUntil 0; async function mainLoop() { const inStock await fetchStockSafely(SKU_ID, AREA_ID); if (inStock) { consecutiveHit; } else { consecutiveHit 0; } if (consecutiveHit 2 Date.now() coolingUntil) { await runHalfAutoOrder(); // 加购 预填结算页最后一步人工确认 coolingUntil Date.now() COOLING_MS; // 下单后进入冷却 consecutiveHit 0; } setTimeout(mainLoop, CHECK_INTERVAL); }连续两轮命中意味着多等一个轮询周期对“到货监控”场景完全值得。下单失败后进入冷却期比如 5 分钟内不再对同一个 SKU 发起下单反复冲击只会更快触发风控不会提高成功率。冷却期应该做成可配置项双十一这类大促场景可以适当调短但不要低于 60 秒。3.4 支付环节为什么“下单服务”到提交订单就停住很多人在搭这套系统时会问能不能把支付也自动掉技术上支付环节需要支付密码、短信验证码或扫码这些动作要么需要逆向支付通道要么需要处理验证码识别自动化成本和账号风险都极高。更现实的是这类自动化本身就处在平台协议和账号安全的灰色地带一旦触发风控轻则验证码频出重则账号受限。因此成熟的做法是“半自动下单”脚本负责监控、加购、预填结算页最后一步由人在手机上确认支付。这也是 jd-happy 这类项目普遍被标记 DEPRECATED 的一个结构性原因——全自动链路太长任何一个环节出现验证码整个流程就得人工接管。4. 避坑与排查jd-happy 从能跑到被废弃的五个现场4.1 监控说有货下单永远提示无货现象监控日志连续多轮显示“有货”但下单接口始终返回“商品无货”而且不是偶发是必现。原因监控接口和下单接口的“有货”判断维度不一致。监控接口返回的是某个默认仓库的库存状态而下单结算页的库存是跟收货地址绑定的另外 Plus 会员、预售商品、限购标识也会造成两个接口结果对不上。解决监控请求里带上与收货地址一致的地区编码让监控结果和下单结果落在同一个仓库维度下单前不要信任监控结果先拉一次结算页信息做二次确认确认有货再提交。这个多出来的一次请求是必要成本省掉它你就是在赌两个接口的判定逻辑完全一致。4.2 同一个账号刷了半小时全部请求返回登录失效现象脚本跑得好好的突然所有请求都返回“未登录”或跳转验证码页重新复制 Cookie 也只能撑一小段时间。原因请求频率太高加上请求头缺少浏览器特征被服务端识别为脚本流量强制刷新登录态或触发验证码。解决轮询间隔调到 30 秒以上并加随机抖动让请求间隔不完全固定请求头里把 Accept-Language、sec-ch-ua 等浏览器特征字段补齐最关键的是控制请求量监控只需要调库存接口不要顺手把商品页 HTML、推荐接口也抓下来——请求越少特征越接近真人。const jitter Math.floor(Math.random() * 10000); // 0~10 秒随机偏移 setTimeout(loop, CHECK_INTERVAL jitter);4.3 按教程写的接口字段第二天解析全挂现象脚本昨晚还在正常解析库存字段早上起来所有库存判断都返回“无货”但打开商品页明明有货。看了日志发现是 undefined 满天飞JSON 解析出来的字段层级跟昨天不一样了。原因接口返回结构不是永远稳定的登录态、地区、时间都可能影响字段层级更常见的是接口加了一个版本号参数没带这个参数时返回的是旧结构或直接降级。解决解析字段前先打印一次原始 JSON确认当前真实结构不要迷信任何教程里的字段路径把字段路径抽成配置常量集中管理脚本启动时先做一次结构自检关键字段缺失就直接报错退出而不是带病跑一整天。const STOCK_FIELD_PATH [stock, state]; function resolvePath(obj, path) { return path.reduce((o, key) o?.[key], obj); } // 启动自检 const sample await fetchStock(SKU_ID, AREA_ID); if (resolvePath(sample, STOCK_FIELD_PATH) undefined) { console.error([FATAL] 接口结构变化请重新抓包); process.exit(1); }4.4 半夜脚本静默死亡早上起来一条通知都没有现象睡前看着脚本在跑第二天早上发现没有任何通知进程也没了。日志里什么都没有就像被拔了电源。原因网络超时异常没有 catch 住或者进程被系统 OOM 杀掉更隐蔽的是 Promise rejection 没有处理Node 进程直接退出。脚本只处理了成功分支没给异常分支留后路。解决给所有异步请求加全局兜底异常记录同时注册进程级异常处理把崩溃现场写进日志文件用进程守护工具拉起新实例监控脚本每小时至少要有一条“心跳日志”证明自己还活着。process.on(uncaughtException, (err) { console.error([FATAL] ${err.stack || err.message}); process.exit(1); // 退出后由守护进程拉起不留半死状态 }); process.on(unhandledRejection, (reason) { console.error([REJECT] ${reason}); });4.5 DEPRECATED 的本质整个链路里最脆弱的一环是“自动”现象jd-happy 这类项目不是不能跑而是慢慢地没人维护了。提交订单接口的签名参数升级验证码策略收紧原来能用的固定请求体开始返回风控错误修好一个又冒出一个。原因接口签名和风控策略是持续演进的服务端能力而爬虫脚本是静态的客户端代码。“自动下单”每被风控打击一次就需要重新抓包、重新适配这个维护成本已经接近一个正式的逆向工程项目远超过它解决的实际问题——等一件商品到货。解决面对这个现实最有效的策略是重新划分自动化边界。把“监控库存状态并推送通知”作为核心功能把“自动下单”降级为实验功能并且只做到“加购并预填结算页”为止最后一步留给人工。这样即使下单接口升级监控和通知不受影响你的核心价值不会塌。这个结论也是后面章节要展开的半自动方案的出发点。5. 更进一步把“全自动下单”降级成“半自动预填”脚本反而能活得更久如果要把这套系统长期用下去我建议拆成三个独立小模块checker 负责轮询库存notifier 负责状态翻转通知helper 负责加购和预填结算页。模块之间用一份 JSON 状态文件通信不需要引入消息队列。checker 发现连续两轮有货写入 state.jsonhelper 守护进程读到这个信号后执行加购流程。这样某一个模块挂了其他模块还能独立工作排查问题范围也小得多。全自动下单和半自动预填的差异用一张表说清楚对比维度全自动下单半自动预填自动化走到哪一步提交订单并尝试支付加购 预填结算页人工确认支付账号风险高容易触发设备风控低行为更接近真人维护成本每次签名/验证码升级都要改只维护监控和加购接口可用周期以周计频繁翻车以月计相对稳定验证这套系统是否健康不要只看“有没有抢到货”太片面。用三个指标衡量监控成功率即一段时间内成功返回的轮询次数除以总轮询次数健康阈值在 98% 以上低于 90% 就要去查 UA、IP 和轮询间隔通知延迟从检测到有货到 webhook 推送成功的秒数目标小于 3 秒预填成功率成功生成待支付订单的次数除以尝试次数低于 90% 优先查 Cookie 是否过期。这三个指标能同时看住监控、通知、下单三段链路。日志格式建议直接用单行 JSON方便后续用 jq 统计{ts:1700000000000,sku:1000123456,state:in_stock,latency_ms:280,action:notify_ok}我后来维护类似脚本时最大的教训是把自动化边界画在了“下单”而不是“通知”上。改成半自动预填之后脚本从“每周都要修”变成“几个月不用动”账号也没再出过风控问题。如果你也想做这件事我把建议再说直白一点监控能帮你省钱省时间自动下单只会给你找事边界划在“提醒加预填”这套方案的生命周期会长得多。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站