1. 从一个抢购场景说起:为什么要自己动手做库存监控
iPhone 新机首发那几天,官网的购买按钮基本就是一场手速和运气的博弈。我身边不少朋友定了闹钟、开着五台设备同时刷,结果还是"暂无供应"。更让人抓狂的是,有时候明明页面显示有货,点进去付款环节又提示库存不足——这种"薛定谔的库存"体验,相信抢过首发的人都懂。
我这次盯的是 iPhone18 的首发。连续蹲了三个晚上,手动刷新到手指发酸,最后还是一台没抢到。痛定思痛,我决定换个思路:与其拼手速,不如让程序替我盯着。于是我用 Codex 配合 Node.js 写了一个轻量的苹果官网库存监控工具,核心逻辑就是定时轮询官方商品页面的库存接口,一旦检测到目标机型有货,立刻通过本地通知和声音提醒我,让我能在第一时间手动下单。
这个工具解决的核心问题很明确:把"人盯着页面"变成"程序盯着接口"。人不可能 24 小时不睡觉地刷新,但程序可以。它适合所有需要蹲守限量商品、又不想被页面刷新折磨的人。整个方案不复杂,一台常开的电脑、一个 Node.js 环境、几十行核心代码就能跑起来。下面我把从思路设计到落地踩坑的全过程拆开讲,尽量让没有编程基础的朋友也能照着做出来。
需要提前说明的是,这个工具只做"监控和提醒",不涉及任何自动下单、绕过验证的操作。所有购买动作仍然由我本人在官方页面手动完成,这样既合规,也避免了账号风险。这一点在后面会反复强调,因为它直接决定了工具的设计边界。
2. 整体设计思路:为什么选 Codex + Node.js + 浏览器扩展这套组合
2.1 需求拆解:监控工具到底要干哪几件事
在动手写代码之前,我先把需求拆成了四个环节,这样选型的时候心里有数:
- 数据获取:定时向苹果官网的商品页面或库存接口发起请求,拿到当前库存状态。
- 状态判断:解析返回的数据,判断目标机型(比如 iPhone18 Pro 256G 深空黑)是否有货。
- 触发提醒:一旦状态从"无货"变成"有货",立刻发出强提醒,确保我能被叫醒。
- 辅助操作:提醒之后,能一键跳转到购买页面,减少我手动找链接的时间。
这四个环节里,最难的是第一个。苹果官网的库存数据并不是一个公开的、稳定的 REST 接口,它更多是嵌在页面渲染逻辑里的。所以"怎么拿到准确的库存状态"成了整个项目的技术核心,也是我后面反复调试的地方。
2.2 为什么用 Codex 来辅助开发
Codex 这类 AI 编程助手在这个项目里的价值,主要体现在"快速试错"上。库存监控这种小工具,逻辑不复杂但细节琐碎:请求头怎么构造、返回的 JSON 结构长什么样、状态字段叫什么名字,这些都需要反复试。如果全靠自己查文档、翻源码,可能要花大半天;用 Codex 我可以直接把抓到的返回片段贴进去,让它帮我分析字段含义、生成解析代码,效率高很多。
举个具体的例子。我一开始不知道库存状态藏在哪个字段里,就把浏览器开发者工具里 Network 面板抓到的响应内容复制给 Codex,问它"这段 JSON 里哪个字段表示库存数量"。它很快帮我定位到了类似availability和stockStatus这样的字段,并给出了对应的解析思路。这种"贴数据问结构"的用法,比让它凭空写代码靠谱得多。
提示:用 AI 助手分析接口数据时,一定要贴真实的返回片段,而不是描述"大概长这样"。真实数据里的字段名、嵌套层级、大小写,往往和想象中差很远。
2.3 为什么主体用 Node.js 而不是 Python
很多人做这类脚本第一反应是 Python,毕竟 requests + BeautifulSoup 是经典组合。我这次选 Node.js,主要基于三点考虑:
第一,和浏览器扩展的配合更顺。我的提醒环节用了一个 Chrome 扩展来做页面跳转和桌面通知,Node.js 和扩展之间通过本地 HTTP 服务通信,两边都是 JavaScript 生态,数据格式(JSON)和异步模型一致,联调时少了很多心智负担。
第二,定时任务写起来简洁。Node.js 的setInterval配合async/await,处理"每隔几秒请求一次、请求失败要重试"这种逻辑非常自然,代码量比 Python 的多线程方案更少。
第三,部署轻。Node.js 装完就能跑,不需要额外配虚拟环境。对于这种"扔在后台常驻"的小工具,越简单越好。
当然,Python 也完全能做,如果你更熟悉 Python,把核心逻辑平移过去没有任何障碍,思路是一样的。
2.4 浏览器扩展在这里扮演什么角色
有人会问:既然 Node.js 已经能发提醒了,为什么还要一个 Chrome 扩展?原因是提醒的触达率。Node.js 在后台跑,它的系统通知容易被忽略,尤其是深夜。而 Chrome 扩展可以直接在浏览器里弹出醒目通知、播放声音,甚至自动打开一个新的标签页定位到购买链接。浏览器是我抢购时本来就开着的窗口,提醒放在这里,触达最直接。
另外,扩展还能帮我处理一个细节:官网的购买页面有时候需要登录态和特定的 Cookie,用扩展在已登录的浏览器环境里打开页面,比 Node.js 单独维护一套登录态要省事得多。这也是"监控归监控、操作归操作"这个设计原则的体现。
3. 核心细节解析:库存数据到底怎么拿、怎么判断
3.1 先搞清楚数据从哪来:页面渲染 vs 接口请求
这是整个项目最关键的一步,也是最容易走弯路的地方。我一开始的思路是"用 Node.js 请求商品页面的 HTML,然后正则匹配'有货'两个字"。实测下来,这条路基本走不通,原因有两个:
一是苹果官网的商品页面大量使用前端渲染,直接请求 HTML 拿到的往往是骨架,真正的库存信息是页面加载后由 JavaScript 再发请求获取的。二是页面上的文案会做多语言、A/B 测试,今天写"有货",明天可能写"现货供应",正则匹配非常脆弱。
正确的做法是:打开浏览器的开发者工具,切到 Network 面板,手动刷新一次商品页面,观察页面发出了哪些请求,从中找到真正返回库存数据的那个接口。这个接口通常返回的是结构化的 JSON,字段稳定,解析起来可靠得多。
具体操作步骤:
- 在 Chrome 里打开目标商品的页面。
- 按 F12 打开开发者工具,切到 Network 标签。
- 勾选 "Fetch/XHR" 过滤,只显示数据请求。
- 刷新页面,观察请求列表,逐个点开看 Response 内容。
- 找到那个返回内容里包含库存、可购买状态等信息的请求,记下它的 URL、请求方法、请求头。
这一步是整个项目的"地基",地基打准了,后面就顺了。
3.2 请求头里哪些字段不能少
找到接口之后,直接拿 Node.js 去请求,大概率会失败或者返回空数据。原因是苹果的接口会校验请求头,缺少关键字段就会被拒。根据我的实测,以下几个头信息比较重要:
| 请求头字段 | 作用 | 是否必需 |
|---|---|---|
User-Agent | 标识客户端类型,缺失容易被判定为异常请求 | 必需 |
Accept | 声明期望的返回格式,一般设为application/json | 必需 |
Referer | 标识请求来源页面,部分接口会校验 | 建议带上 |
Accept-Language | 语言偏好,影响返回文案 | 建议带上 |
Cookie | 部分接口需要登录态或会话标识 | 视接口而定 |
我的经验是,最省事的办法是把浏览器里抓到的完整请求头原样复制过来,先保证能跑通,再逐个删减,看哪些是真正必需的。这样比从零猜要快得多。
注意:请求头里的 Cookie 属于敏感信息,不要提交到代码仓库,也不要在公开场合分享。建议用环境变量或本地配置文件管理,代码里只引用变量名。
3.3 返回数据的结构长什么样
不同接口返回的 JSON 结构差别很大,但库存相关的信息通常集中在几个字段上。以我抓到的接口为例,返回内容大致是这样的结构(字段名做了简化处理):
{ "products": [ { "partNumber": "XXXXXX", "name": "iPhone18 Pro 256GB", "color": "深空黑", "availability": { "status": "IN_STOCK", "quantity": 3 }, "pickupMessage": "今日可到店取货" } ] }判断有货的核心逻辑就是看availability.status这个字段。常见的取值有IN_STOCK(有货)、OUT_OF_STOCK(无货)、PREORDER(预售)等。我只需要在代码里判断它是否等于IN_STOCK,就能确定目标机型当前能不能买。
这里有个细节值得说:同一个商品可能有多个 partNumber,对应不同的容量、颜色、版本。我一开始只监控了一个 partNumber,结果发现另一个颜色有货时没提醒到。后来改成遍历整个 products 数组,只要目标型号的任意一个 SKU 有货就触发提醒,覆盖率就上去了。
3.4 轮询频率怎么定:太快被封、太慢错过
轮询频率是个需要权衡的参数。设得太快(比如每秒一次),请求量太大,容易被限流甚至临时封禁;设得太慢(比如五分钟一次),可能刚补货就被别人抢走了。
我的实测经验是:平时用 30 到 60 秒一次比较稳妥,临近发售或补货高峰期可以临时调到 10 到 15 秒一次。这个频率既能保证一定的灵敏度,又不至于给服务器造成明显压力。
另外,我加了一个"随机抖动"的处理:每次请求的间隔不是固定的 30 秒,而是在 25 到 35 秒之间随机。这样做是为了避免请求节奏过于规律,看起来更像正常用户行为。这个技巧在很多轮询场景里都适用。
// 带随机抖动的轮询间隔 function getNextInterval(baseSeconds) { const jitter = Math.random() * 10 - 5; // -5 到 +5 秒 return (baseSeconds + jitter) * 1000; }3.5 状态变化才提醒,避免通知轰炸
如果每次轮询到有货都提醒一次,那在有货的时段里我会被通知淹没。所以代码里要维护一个"上一次的状态",只有当状态从"无货"变成"有货"的那一刻,才触发提醒。这个逻辑用一个变量记录上次状态就能实现:
let lastStatus = 'OUT_OF_STOCK'; function checkAndNotify(currentStatus) { if (currentStatus === 'IN_STOCK' && lastStatus !== 'IN_STOCK') { triggerAlert(); // 只在状态跃迁时提醒 } lastStatus = currentStatus; }这个"边沿触发"的思路,和数字电路里的上升沿检测是一个道理,用在这里能有效减少无效通知。
4. 实操过程:从零把工具跑起来
4.1 环境准备:Node.js 安装与版本选择
第一步是把 Node.js 装好。我推荐用 LTS(长期支持)版本,稳定性最好。写这篇文章时,Node.js 20 及以上的 LTS 版本是比较稳妥的选择。安装方式按系统分:
- Windows:去 Node.js 官网下载 LTS 版本的安装包(.msi),一路下一步即可。安装完成后打开命令提示符,输入
node -v和npm -v,能显示版本号就说明装好了。 - macOS:可以用官网的 .pkg 安装包,也可以用 Homebrew,命令是
brew install node。 - Linux(以 Ubuntu 为例):建议通过 NodeSource 的源安装,能拿到较新的 LTS 版本,避免系统自带源里版本过旧的问题。
安装完成后,验证一下:
node -v # 期望输出类似 v20.x.x npm -v # 期望输出类似 10.x.x提示:如果
node -v报"命令未找到",多半是环境变量没配好。Windows 重装一次安装包通常能解决;Linux 检查一下 PATH 里有没有 Node 的安装路径。
4.2 初始化项目与安装依赖
新建一个文件夹作为项目目录,进去之后初始化:
mkdir apple-stock-monitor cd apple-stock-monitor npm init -y然后安装两个核心依赖:
npm install axios npm install node-notifieraxios:用来发 HTTP 请求,比 Node 自带的 http 模块好用,支持 Promise 和超时设置。node-notifier:用来发系统级桌面通知,跨平台支持不错。
如果你还想加声音提醒,可以再装一个播放音频的库,或者直接用系统命令调用本地播放器,这个后面细说。
4.3 核心监控脚本的编写
下面是我实际在用的核心脚本结构,做了简化,保留了关键逻辑。你可以直接拿去改:
const axios = require('axios'); const notifier = require('node-notifier'); // 目标商品接口地址(替换成你自己抓到的) const API_URL = 'https://example-apple-api.com/stock'; // 目标型号关键词,用于筛选 const TARGET_KEYWORDS = ['iPhone18 Pro', '256GB', '深空黑']; // 轮询基础间隔(秒) const BASE_INTERVAL = 30; let lastStatus = 'OUT_OF_STOCK'; // 构造请求头,尽量贴近真实浏览器 const headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...', 'Accept': 'application/json', 'Referer': 'https://www.example-apple.com/', 'Accept-Language': 'zh-CN,zh;q=0.9', // Cookie 从环境变量读取,不硬编码 'Cookie': process.env.APPLE_COOKIE || '' }; async function fetchStock() { try { const res = await axios.get(API_URL, { headers, timeout: 10000 // 10 秒超时 }); return res.data; } catch (err) { console.error('请求失败:', err.message); return null; } } function isTargetInStock(data) { if (!data || !data.products) return false; return data.products.some(p => { const nameMatch = TARGET_KEYWORDS.every(kw => (p.name + p.color).includes(kw) ); const inStock = p.availability && p.availability.status === 'IN_STOCK'; return nameMatch && inStock; }); } function triggerAlert() { notifier.notify({ title: '有货了!', message: '目标 iPhone18 已补货,快去下单!', sound: true, wait: true }); console.log('[' + new Date().toLocaleString() + '] 检测到有货,已提醒'); } async function loop() { const data = await fetchStock(); const inStock = isTargetInStock(data); if (inStock && lastStatus !== 'IN_STOCK') { triggerAlert(); } lastStatus = inStock ? 'IN_STOCK' : 'OUT_OF_STOCK'; // 带抖动的下一次轮询 const jitter = Math.random() * 10 - 5; const next = (BASE_INTERVAL + jitter) * 1000; setTimeout(loop, next); } loop();这段代码跑起来之后,控制台会持续输出检测日志,一旦命中就弹通知。你可以用node monitor.js启动它。
4.4 用 Chrome 扩展做二次提醒和快速跳转
Node.js 的通知在后台容易被忽略,所以我加了一个 Chrome 扩展做补充。扩展的核心是一个后台脚本,它监听本地 Node 服务推送过来的"有货"事件,然后:
- 弹出浏览器通知(比系统通知更醒目)。
- 播放一段提示音。
- 自动打开一个新的标签页,定位到购买链接。
扩展的manifest.json大致如下:
{ "manifest_version": 3, "name": "库存提醒助手", "version": "1.0", "permissions": ["notifications", "tabs", "storage"], "background": { "service_worker": "background.js" }, "action": { "default_title": "库存监控" } }background.js里用chrome.notifications.create发通知,用chrome.tabs.create打开购买页。Node 服务和扩展之间通过一个本地 HTTP 接口通信,Node 检测到有货时向这个接口发一个 POST,扩展收到后执行提醒动作。
注意:自己写的扩展在本地加载时,需要在
chrome://extensions/页面打开"开发者模式",然后点"加载已解压的扩展程序"。如果扩展没有上架应用商店,浏览器会提示"该扩展程序未列在 Chrome 应用商店中",这是正常的,本地开发都会遇到,选择保留即可。
4.5 让脚本常驻后台运行
脚本写好了,总不能一直开着终端窗口。让它常驻后台有几种办法:
- Windows:可以用任务计划程序,设置开机自启,或者用
pm2这类进程管理工具。 - macOS / Linux:推荐用
pm2,一条命令就能守护进程、自动重启、查看日志。
npm install -g pm2 pm2 start monitor.js --name stock-monitor pm2 save pm2 startuppm2的好处是脚本崩了会自动拉起来,日志也能随时用pm2 logs stock-monitor查看。对于这种需要长时间运行的监控任务,非常合适。
5. 常见问题与排查技巧实录
5.1 请求返回空数据或 403
这是最常见的问题,八成是请求头不对。排查顺序:
- 先用浏览器抓到的完整请求头原样复制,确认能跑通。
- 再逐个删减字段,找出哪个是必需的。
- 检查
User-Agent是不是太"假",用真实的浏览器 UA 字符串。 - 检查 Cookie 是否过期,重新登录后更新。
如果还是 403,可能是接口加了额外的校验(比如某个自定义头或签名参数),这时候需要回到 Network 面板,仔细对比浏览器请求和你的请求差在哪。
5.2 状态判断不准,明明有货却没提醒
多半是筛选逻辑写得太死。比如型号名称里多了个空格、颜色字段单独存放、容量写法是 "256G" 而不是 "256GB",都会导致匹配失败。我的做法是先把返回数据完整打印出来,肉眼确认目标商品的字段长什么样,再写匹配规则。匹配时尽量用"包含"而不是"等于",容错性更高。
5.3 通知不弹出来
分两种情况。如果是 Node.js 的node-notifier不弹,检查系统是否开启了"专注模式"或"勿扰模式",这类模式会屏蔽通知。如果是 Chrome 扩展的通知不弹,检查扩展的通知权限有没有授予,以及浏览器设置里有没有屏蔽该站点的通知。
5.4 脚本跑一段时间就停了
常见原因是未捕获的异常导致进程退出。解决办法有两个:一是用try/catch把请求逻辑包起来,出错不要让整个进程挂掉;二是用pm2守护,崩了自动重启。我两个都做了,双保险。
5.5 常见问题速查表
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| 请求 403 / 空数据 | 请求头缺失或 Cookie 过期 | 复制完整请求头,更新登录态 |
| 有货不提醒 | 筛选关键词不匹配 | 打印数据,放宽匹配规则 |
| 通知不弹 | 系统勿扰模式 / 权限未授予 | 关闭勿扰,检查通知权限 |
| 脚本自动退出 | 未捕获异常 | 加 try/catch,用 pm2 守护 |
| 被限流 | 轮询太快 | 降低频率,加随机抖动 |
5.6 几条踩坑心得
第一,不要一上来就追求全自动。我见过有人想直接做自动下单,结果账号被风控。监控 + 手动下单这个边界,既安全又够用。
第二,日志一定要打全。每次请求的时间、返回状态、判断结果都记下来,出问题的时候能快速定位。我一开始日志打得太少,排查一个匹配问题花了半小时。
第三,Cookie 和敏感信息用环境变量管理。别图省事硬编码在代码里,万一代码分享出去就麻烦了。
第四,先在小范围验证再扩大。比如先只监控一个 SKU,跑通了再扩展到多个型号,避免一开始就面对一堆变量。
6. 关于合规与边界的几点个人体会
这个工具从设计之初,我就把它定位成"辅助提醒",而不是"抢购外挂"。所有购买动作都由我手动完成,程序只负责在第一时间告诉我"有货了"。这样做有几个好处:一是完全符合平台规则,不用担心账号风险;二是逻辑简单,维护成本低;三是不涉及任何绕过验证、批量下单的操作,用起来心里踏实。
我在实际使用中发现,真正决定能不能抢到的,往往不是手速,而是信息差——谁先知道有货,谁就占先机。这个工具的价值恰恰在于抹平信息差,把"运气"变成"准备"。至于下单那一下,还是得靠自己。
最后分享一个小技巧:如果你不想写代码,其实用浏览器自带的页面监控扩展也能实现类似效果,只是灵活性和可控性差一些。但如果你想真正理解"数据是怎么来的、状态是怎么判断的",自己动手写一遍,收获会大得多。这个项目后续还可以扩展的方向不少,比如把提醒接到手机上、增加多商品并行监控、做一个简单的网页看板展示历史库存曲线,都是很自然的延伸。