news 2026/10/15 17:26:16

京东商品库存监控脚本原理与实战:Node.js轻量轮询方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
京东商品库存监控脚本原理与实战:Node.js轻量轮询方案

简介:这是一份基于 Node.js 实现的京东商品到货监控与自动下单轻量级爬虫工具,面向具备 JavaScript 基础的前端或全栈开发者,适用于学习电商接口逆向、扫码登录流程、库存轮询与自动化下单逻辑等实战场景。资源包共11个文件,含5个核心 JS 脚本(如 index.js 入口、utils.js 工具函数、log.js 日志模块)、README.md 说明文档、LICENSE 协议文件、.gitignore 与 package.json 等工程配置,以及展示扫码登录与下单流程的 jd-happy.gif 动图,整体压缩包仅1.7MB,结构简洁、依赖明确,便于快速阅读与本地调试。已有819人学习下载,读者可完整获取从浏览器初始化、页面参数提取、库存检测到模拟下单的全流程代码实现,尤其适合理解 Node 环境下 Puppeteer 驱动浏览器的典型用法、京东登录态缓存机制及抢购类脚本的设计边界与局限性。

1. 这不是个“自动下单神器”:一个被标记为 DEPRECATED 的 Node.js 京东监控爬虫,它真实解决的是「人工盯盘疲劳」和「页面结构突变响应滞后」问题

你搜到这个项目时,大概率正卡在这样一个场景里:某款显卡/新机/限量手办上架京东,你手动刷新了 47 分钟,F5 键按到发烫,页面还是“缺货”,而朋友圈已有人晒单。这时候点开 GitHub 上标着[DEPRECATED]的jd-happy仓库,第一反应是失望——连作者都放弃维护了,还能用?但实际拆进去你会发现,它压根没想做全自动下单黑盒。它的核心价值,是把「人盯页面 → 发现有货 → 手动点击」这个链路里最耗神的前两步,压缩成一个可观察、可调试、可快速修复的 Node.js 脚本。它不依赖 Puppeteer 全浏览器渲染,而是直击京东商品页的 Ajax 接口与 DOM 结构规律;它不追求秒级抢购,而是以 3~8 秒轮询节奏,稳住状态机,把“有货”信号变成终端里一行醒目的✅ In stock!。适合谁?适合懂基础 JavaScript、能看懂 Chrome DevTools Network 面板、愿意在接口字段变更后花 15 分钟改两行 JSONPath 的一线开发者或技术型消费者。它不是玄学抢购工具,而是一份写给自己的、带日志和断点的「人工下单增强说明书」。


2. 为什么选 Node.js 而非 Python 或 Puppeteer:轻量轮询 + 接口直调的工程权衡

2.1 从「全渲染」到「接口直取」:京东商品库存状态的真实来源

京东商品页的“有货/缺货”状态,并非全部由前端 JS 渲染决定。当你打开 Chrome DevTools → Network → Filterxhr,反复点击“加入购物车”按钮,会捕获到类似/ware/detail.action?skuId=1000XXXXXX的请求。该接口返回的 JSON 中,skuInfo.stockInfo.stockDesc字段(值如"有货"、"缺货"、"预售中")才是后端真实库存状态。jd-happy放弃 Puppeteer 等重量级方案,正是基于这一事实:不需要加载整个页面 HTML、执行所有 JS、等待 DOM 就绪,只需精准调用这个轻量接口,解析关键字段即可。Node.js 的axios+cheerio组合,在发起 HTTP 请求、解析 JSON、提取 DOM 片段三者间取得极佳平衡——启动快、内存占用低、无浏览器进程开销。某次实测中,同一台 4C8G 云服务器上,10 个jd-happy实例常驻内存仅 120MB,而同等数量 Puppeteer 实例直接触发 OOM Killer。

2.2package.json里的技术栈选择逻辑:为什么是cheerio而非jsdom

项目依赖中明确列出"cheerio": "^1.0.0-rc.12",而非更接近浏览器环境的jsdom。原因在于任务边界清晰:我们只读,不交互。cheerio是服务器端的 jQuery 实现,它将 HTML 字符串解析为内存中的 DOM 树,支持 CSS 选择器,但不执行 script 标签、不处理事件绑定、不模拟 window 对象。这恰恰规避了京东页面中大量反爬 JS 注入(如动态生成 token、混淆 DOM 结构)带来的干扰。例如,京东商品页常通过 JS 动态插入<div class="stock-status">,其文本内容由 AJAX 返回后填充。若用jsdom,需模拟完整执行环境,极易因缺失window.crypto或document.hidden等属性而报错;而jd-happy直接请求/ware/detail.action接口获取原始 JSON,再用cheerio.load(html).find('.price')提取价格——HTML 来源是接口返回的wareInfo.wareDetail字段,该字段为静态 HTML 片段,无运行时依赖。这种「接口优先、DOM 辅助」的分层策略,让代码健壮性大幅提升。

# 查看项目核心依赖(来自 package.json) "dependencies": { "axios": "^0.21.1", "cheerio": "^1.0.0-rc.12", "node-schedule": "^2.1.0", "dotenv": "^10.0.0" }

提示:node-schedule用于实现轮询调度,而非setInterval。后者在 Node.js 事件循环阻塞时可能失准(如某次请求耗时 12 秒,下次执行将延迟),而node-schedule基于系统时间触发,保证每 5 秒真正发起一次请求,这对监控时效性至关重要。

2.3 环境变量驱动配置:.env文件如何解耦敏感信息与业务逻辑

项目使用dotenv加载.env,将京东账号、密码、商品 SKU、Cookie 等全部外置。这不是为了“安全”,而是为了可复现性与协作隔离。例如,A 同学监控显卡,B 同学监控耳机,两人共用同一份代码,只需各自维护.env:

# .env 示例 JD_USERNAME=your_username JD_PASSWORD=your_password SKU_ID=100012345678 POLLING_INTERVAL=5000 NOTIFY_WEBHOOK=https://your-webhook.com/alert

关键点在于JD_COOKIE字段。jd-happy不走登录流程,而是要求用户手动抓包获取有效 Cookie(pt_key、pt_pin等)。这是因为京东登录体系复杂,涉及滑块验证、短信验证码等,自动化登录极易触发风控。项目设计者选择「信任用户一次手动操作,换取长期稳定轮询」,这是对现实约束的诚实妥协。Cookie 有效期通常 30 天,过期后只需重新抓包更新.env,无需修改任何代码。


3. 核心流程拆解:从启动脚本到库存判断的六步闭环

3.1 入口文件index.js:状态机初始化与调度注册

index.js是整个服务的起点,它不做具体业务,只负责组装与启动:

// index.js require('dotenv').config(); const schedule = require('node-schedule'); const { checkStock } = require('./lib/stock-checker'); // 从 .env 读取轮询间隔,默认 5000ms const interval = parseInt(process.env.POLLING_INTERVAL) || 5000; // 每隔 interval 毫秒执行一次 checkStock const job = schedule.scheduleJob(`*/${Math.ceil(interval / 1000)} * * * * *`, () => { checkStock() .then(result => { if (result.inStock) { console.log(`✅ [${new Date().toLocaleTimeString()}] 商品 ${process.env.SKU_ID} 有货!`); // 触发通知(微信/钉钉/webhook) require('./lib/notifier').sendAlert(result); } else { console.log(`⏳ [${new Date().toLocaleTimeString()}] 仍缺货,继续轮询...`); } }) .catch(err => { console.error(`❌ 轮询失败:`, err.message); // 失败时不退出,继续下一轮,避免单次网络抖动导致服务中断 }); }); console.log(`🚀 jd-happy 已启动,轮询间隔 ${interval}ms`);

逻辑说明:

  • scheduleJob的 Cron 表达式*/5 * * * * *表示“每 5 秒执行一次”,Math.ceil(interval / 1000)将毫秒转为秒并向上取整,确保表达式合法;
  • checkStock()返回 Promise,封装了完整的 HTTP 请求、JSON 解析、状态提取流程;
  • .catch()中不process.exit(1),而是记录错误后静默继续——这是生产环境必备习惯,单次请求失败不应导致整个监控服务雪崩。

3.2 库文件lib/stock-checker.js:接口调用与状态提取双路径

checkStock()函数是真正的业务核心,它采用「主接口 + 备用 DOM」双校验机制:

// lib/stock-checker.js const axios = require('axios'); const cheerio = require('cheerio'); async function checkStock() { const skuId = process.env.SKU_ID; const cookie = process.env.JD_COOKIE; // Step 1: 调用京东商品详情接口(主路径) const detailRes = await axios.get( `https://item.jd.com/${skuId}.html`, { headers: { 'Cookie': cookie, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }, timeout: 10000 } ); // Step 2: 从 HTML 中提取初始数据(京东将关键数据注入 window.__INITIAL_STATE__) const html = detailRes.data; const $ = cheerio.load(html); const scriptContent = $('script:contains(__INITIAL_STATE__)').html(); let initialState; try { // 提取 window.__INITIAL_STATE__ = {...} 中的 JSON 字符串 const jsonStr = scriptContent.match(/window\.__INITIAL_STATE__\s*=\s*({[\s\S]*?});/)[1]; initialState = JSON.parse(jsonStr); } catch (e) { throw new Error('无法解析 __INITIAL_STATE__ 数据,请检查 Cookie 是否有效或页面结构是否变更'); } // Step 3: 从 initialState 中提取库存状态(主字段) const stockDesc = initialState?.wareInfo?.stockInfo?.stockDesc || ''; const inStock = ['有货', '立即购买', '加入购物车'].includes(stockDesc.trim()); // Step 4: DOM 备用校验(防主字段为空) const domStockText = $('.product-intro .stock').text().trim() || $('.summary .stock').text().trim(); const domInStock = domStockText.includes('有货') || domStockText.includes('现货'); return { inStock: inStock || domInStock, // 只要任一路径确认有货即返回 true stockDesc, domStockText, skuId }; } module.exports = { checkStock };

参数说明:

  • timeout: 10000设为 10 秒,避免因京东 CDN 延迟导致请求挂起过久;
  • scriptContent.match(...)使用正则提取__INITIAL_STATE__,这是京东 React 应用的 SSR 数据注入点,比 DOM 更可靠;
  • inStock判断采用白名单['有货', '立即购买', '加入购物车'],覆盖京东不同 SKU 的文案差异(如自营 vs 第三方店铺);
  • DOM 备用路径.product-intro .stock和.summary .stock是京东 PC 端历史结构,当__INITIAL_STATE__字段名变更时,此路径可作为降级方案。

3.3 通知模块lib/notifier.js:如何让「有货」消息真正触达你

sendAlert()并非发送邮件或短信,而是调用 Webhook。这是因为它足够通用、开发成本低、且能对接主流 IM 工具:

// lib/notifier.js const axios = require('axios'); function sendAlert(result) { const webhook = process.env.NOTIFY_WEBHOOK; if (!webhook) return; // 未配置则跳过 const message = { msgtype: 'text', text: { content: `🔔 京东监控提醒\n商品ID: ${result.skuId}\n状态: ${result.stockDesc}\n时间: ${new Date().toLocaleString()}\n👉 快去下单:https://item.jd.com/${result.skuId}.html` } }; return axios.post(webhook, message, { headers: { 'Content-Type': 'application/json' } }).catch(err => { console.error('通知发送失败:', err.response?.status, err.message); }); } module.exports = { sendAlert };

注意:Webhook 地址需自行配置。例如,使用钉钉机器人,需在钉钉群中添加自定义机器人,获取 Webhook URL(形如https://oapi.dingtalk.com/robot/send?access_token=xxx),并开启「加签」或「IP 白名单」(若部署在云服务器)。此处不提供具体平台配置步骤,因各平台策略常变,且涉及账号安全。


4. 避坑指南:五个真实踩过的坑与血泪修复方案

4.1 现象:轮询日志显示✅ 有货,但手动打开商品页仍是“缺货”

原因:京东存在「库存缓存分层」。/ware/detail.action接口返回的stockDesc是 CDN 缓存结果,而页面实时状态由另一个/ware/dynamic-stock.action接口提供,后者响应更快但需额外 Header(如Referer: https://item.jd.com/)。
解决:在checkStock()中增加备用接口调用,仅当主接口返回stockDesc为"有货"时才触发:

// 在 checkStock() 内,主逻辑后追加 if (inStock) { try { const dynamicRes = await axios.get( `https://c0.3.cn/stock?skuId=${skuId}&area=1_72_2799_0&venderId=1000000000`, { headers: { 'Referer': `https://item.jd.com/${skuId}.html` } } ); const stockData = JSON.parse(dynamicRes.data.match(/try{.*?jQuery\d+\((.*?)\);}catch/)[1]); if (stockData?.stock?.StockState === 33) { // 33 表示有货 return { ...result, confirmed: true }; } } catch (e) { console.warn('动态库存接口调用失败,使用主接口结果'); } }

4.2 现象:TypeError: Cannot read property 'stockInfo' of undefined

原因:__INITIAL_STATE__结构变更。京东在 2023 年 Q3 将wareInfo改为ware,且stockInfo移至ware.stock下。cheerio提取的 JSON 本身无误,但代码路径写死。
解决:使用安全访问模式,逐层 fallback:

const stockDesc = ( initialState?.wareInfo?.stockInfo?.stockDesc || initialState?.ware?.stock?.stockDesc || initialState?.skuInfo?.stockInfo?.stockDesc || '' ).trim();

4.3 现象:Error: connect ETIMEDOUT频繁出现,轮询中断

原因:京东对高频 IP 请求限流,node-schedule的固定间隔导致请求过于规律,易被识别为爬虫。
解决:引入随机抖动(Jitter),将固定 5 秒改为4500~5500ms区间:

// 替换 index.js 中的 scheduleJob 行 const jitter = Math.floor(Math.random() * 1000) + 4500; // 4.5~5.5 秒 const job = schedule.scheduleJob(`*/${Math.ceil(jitter / 1000)} * * * * *`, () => { ... });

4.4 现象:NOTIFY_WEBHOOK发送成功,但钉钉/微信未收到消息

原因:Webhook 请求体格式不匹配。钉钉要求msgtype: 'text',而企业微信要求msgtype: 'text'且touser: '@all'。
解决:根据NOTIFY_WEBHOOK域名自动适配:

// lib/notifier.js 中 function getWebhookConfig() { const url = process.env.NOTIFY_WEBHOOK; if (url.includes('oapi.dingtalk.com')) return { platform: 'dingtalk' }; if (url.includes('qyapi.weixin.qq.com')) return { platform: 'wechat-work' }; return { platform: 'generic' }; }

再按平台生成不同结构的消息体。

4.5 现象:npm start报错ReferenceError: regeneratorRuntime is not defined

原因:async/await语法需@babel/polyfill或regenerator-runtime,但项目未声明。Node.js 14+ 原生支持,但若运行在旧版 Node(如 12.x),则需补丁。
解决:在index.js顶部添加:

// index.js 第一行 require('regenerator-runtime/runtime');

并安装依赖:npm install regenerator-runtime。


5. 进阶技巧:用curl+jq快速验证接口可用性,绕过 Node.js 环境

当你的 Node.js 服务部署在受限环境(如某公司内网 Docker 容器,无法安装 npm 包),或只想快速确认京东接口是否还活着、Cookie 是否有效,curl+jq是最轻量的验证组合。它不依赖任何 JS 运行时,纯 Shell 即可完成端到端探测。

5.1 构建最小化验证命令链

以下命令模拟jd-happy的核心请求,输出stockDesc值:

# 将 YOUR_COOKIE 替换为实际 Cookie(pt_key=xxx; pt_pin=yyy;) curl -s \ -H "Cookie: YOUR_COOKIE" \ -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \ "https://item.jd.com/100012345678.html" | \ grep -oP 'window\.__INITIAL_STATE__\s*=\s*({[^}]*?});' | \ sed 's/window\.__INITIAL_STATE__\s*=\s*//' | \ jq -r '.wareInfo.stockInfo.stockDesc // .ware.stock.stockDesc // ""'

逻辑分解:

  • curl -s:静默模式,不输出进度条;
  • -H "Cookie: ...":携带有效 Cookie,否则京东返回 302 跳转登录页;
  • grep -oP '...':用 Perl 正则提取__INITIAL_STATE__ = {...}整块;
  • sed 's/...':剥离window.__INITIAL_STATE__ =前缀,只留 JSON 字符串;
  • jq -r '...':用jq解析 JSON,//表示 fallback,当第一个路径不存在时尝试第二个,最后为空字符串。

5.2 自动化轮询脚本:Shell 版jd-happy精简内核

将上述命令封装为watch-jd.sh,实现无 Node.js 依赖的轮询:

#!/bin/bash # watch-jd.sh SKU_ID=${1:-100012345678} COOKIE=${2:-"YOUR_COOKIE"} INTERVAL=${3:-5} echo "🔍 开始监控商品 ${SKU_ID},间隔 ${INTERVAL} 秒..." while true; do STOCK_DESC=$(curl -s \ -H "Cookie: ${COOKIE}" \ -H "User-Agent: Mozilla/5.0 (X11; Linux x86_64)" \ "https://item.jd.com/${SKU_ID}.html" 2>/dev/null | \ grep -oP 'window\.__INITIAL_STATE__\s*=\s*({[^}]*?});' 2>/dev/null | \ sed 's/window\.__INITIAL_STATE__\s*=\s*//' 2>/dev/null | \ jq -r '.wareInfo.stockInfo.stockDesc // .ware.stock.stockDesc // ""' 2>/dev/null) if [[ "$STOCK_DESC" =~ ^(有货|立即购买|加入购物车)$ ]]; then echo "✅ [$(date '+%H:%M:%S')] ${SKU_ID} 有货!" # 此处可添加 notify-send、osascript(macOS)、或 curl 发送 webhook break else echo "⏳ [$(date '+%H:%M:%S')] ${SKU_ID} 缺货,${INTERVAL}s 后重试..." fi sleep $INTERVAL done

使用方式:

chmod +x watch-jd.sh ./watch-jd.sh 100012345678 "pt_key=xxx;pt_pin=yyy;" 3

血泪经验:某次线上监控失效,我就是靠这个 Shell 脚本 2 分钟内定位出是 Cookie 过期(STOCK_DESC输出为空),而非代码逻辑问题。从此我养成了一个习惯:任何 Node.js 爬虫上线前,必先用curl + jq手动跑通一次核心接口链路。这就像给汽车启动前先听引擎声——不依赖仪表盘,直击物理层。

从那以后我每次部署新的监控脚本,都强制走一遍curl验证流程,哪怕多花 30 秒。因为真正的稳定性,不来自框架的华丽,而来自对每一层协议、每一个字段、每一次网络往返的亲手触摸。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 17:25:55

pyecharts地图可视化实战:Map与Geo核心原理、动态效果及性能优化

1. 为什么地图可视化值得单独拎出来讲 做数据可视化这些年&#xff0c;我越来越觉得地图类图表是一个分水岭。折线图、柱状图这些&#xff0c;随便找个库都能画得七七八八&#xff0c;但一旦涉及地理维度&#xff0c;坑就突然多起来了——坐标系对不上、区域名称匹配不上、数据…

作者头像 李华
网站建设 2026/10/15 17:25:20

代码问世不是终点:构建年份无关的工程化交付体系

简介&#xff1a;本资源是面向Python编程学习者与算法爱好者的一站式Advent of Code历年真题实践合集&#xff0c;覆盖2017至2020年全部挑战的完整代码实现与配套输入数据&#xff0c;助力读者系统训练算法思维、提升工程化解题能力。压缩包共240个文件&#xff0c;含152个Pyth…

作者头像 李华
网站建设 2026/10/15 17:25:20

Go分布式爬虫实战:从单机到亿级并发的架构演进

1. 项目概述&#xff1a;为什么“Go分布式爬虫”正在成为工程实践中的刚需最近在几个技术社区和某高校实验室的协作项目里&#xff0c;反复看到“Go分布式爬虫”这个组合词被高频提及。它不是某个新出的框架名字&#xff0c;而是一类解决实际问题的技术方案代称——用Go语言构建…

作者头像 李华
网站建设 2026/10/15 17:25:06

2026Git 下载、安装、使用完整教程(附安装包)

Git 下载、安装、使用完整教程一、Git 简介&#xff08;与SVN区别&#xff09;二、Git 下载三、Git 安装教程1. 安装&#xff08;关键步骤已标注注意事项&#xff09;2. 验证安装是否成功四、Git 基础配置&#xff08;安装后必须配置&#xff0c;否则无法正常提交代码&#xff…

作者头像 李华