Wayfair 采用动态定价策略。同一张沙发早上是一个价格,傍晚可能变成另一个,第二天又是另一个,这些变化由需求、库存和平台自身的算法定价模型共同驱动。对于等待优惠的购物者,或研究家具品类的分析师而言,真正有价值的不是此刻的价格,而是价格随时间的变动趋势。要看到这种趋势,你需要按计划记录价格并保留历史。
本指南介绍如何用 JavaScript、Node.js 和 cheerio 构建一个 Wayfair 价格追踪器。你通过 Crawling API 获取 Wayfair 公开产品或分类页面,提取每个产品的标题和价格,将带时间戳的行追加到历史文件中,并按计划运行整个流程,让文件逐渐积累为可绘图的价格日志。整个演练仅限于公开列表数据,文末的合法性部分是真实的内容而非套话,请在将此工具指向真实规模目标前仔细阅读。
你将构建什么
一个 Node.js 追踪器,接受 Wayfair 公开列表 URL,通过 Crawling API 获取渲染后的 HTML,解析每张产品卡片,并将带时间戳的快照写入磁盘。运行一次你就能获得当前价格;按计划运行,历史文件就变成了可分析的时间序列。每条记录包含以下字段:
- 时间戳 标记快照采集时间的 ISO 时间字符串,这一列将行数据变成时间序列。
- 标题 从列表卡片读取的产品名称,例如"Mahwah 98'' Chenille Square Arm Sofa"。
- 价格 卡片上显示的价格,如"$689.99"。
- URL 快照来源的列表页面,用于按页面分组历史记录。
为何普通请求在 Wayfair 上失效
如果你用裸 HTTP 客户端请求 Wayfair 列表 URL,几乎拿不到产品网格。有两个原因让你陷入困境。第一,Wayfair 在浏览器中用 JavaScript 渲染其列表卡片,初始 HTML 在页面脚本运行之前几乎是一个空壳,你想要的价格就位于那段渲染后的标记中。第二,Wayfair 会识别自动化流量:来自数据中心 IP 且请求模式不像真实浏览器的请求,会在抵达渲染后的产品数据之前遭到挑战、限速或封锁。
因此,一个有效的 Wayfair 追踪器需要在一次请求中同时具备两样东西:能真正渲染页面的浏览器,以及平台认为是真实访客的 IP。你可以自己组合一个无头浏览器加一批轮换住宅代理,但将它们整合在一起并持续维护才是大头工作,而且对于你想让它无人值守地定时运行的东西来说,这种方案并不合适。Crawling API 将两者合并为一次调用:你发送 URL,它在受信任的 IP 后渲染页面,并返回完整 HTML 供 cheerio 解析。
前置条件
在编写任何代码之前,你需要准备好以下几样东西,每项都不需要太长时间。
基础 JavaScript 和 Node.js。 你应该能够编写和运行 Node 脚本,使用 npm 安装包,并能处理变量、函数和循环。如果你是 Node 新手,官方文档和任何入门课程都能让你达到本教程所需的水平。
Node.js 16 或更高版本。 用 node --version 确认你的版本。Node 在本地运行 JavaScript,这正是实现定时无人值守追踪的基础。如果还没有安装,请从 Node.js 官网下载,或通过版本管理工具(如 nvm)安装。
Crawlbase 账号和令牌。 注册后打开控制台,从账号文档页面复制你的令牌。免费层提供 1,000 次请求,无需绑定信用卡,足以构建和测试一个追踪器。请像对待密码一样保管令牌:它验证你的请求,不要将其提交到版本控制系统。
搭建项目
创建一个项目文件夹,初始化,并安装追踪器所需的库。
node --version mkdir wayfair-price-tracker && cd wayfair-price-tracker npm init -y npm install crawlbase cheerio
两个依赖各司其职:crawlbase 是 Crawling API 的官方 Node 客户端,cheerio 用类似 jQuery 的 API 解析返回的 HTML,让你可以通过 CSS 选择器提取各个字段。Node 内置的 fs 模块负责写入历史文件,无需为此额外安装任何东西。在此文件夹中创建一个名为 tracker.js 的文件,并将下面各步骤的代码添加进去。
步骤 1:获取渲染后的 Wayfair 页面
首先获取完整页面。导入 CrawlingAPI 类,用你的令牌初始化,并请求列表 URL。我们以沙发分类页面作为运行示例。在解析前检查状态码,让失败保持可见而非静默。
const { CrawlingAPI } = require('crawlbase'); const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_TOKEN' }); const wayfairPageURL = 'https://www.wayfair.com/furniture/sb0/sofas-c413892.html'; api .get(wayfairPageURL) .then((response) => { if (response.statusCode === 200) { console.log(response.body.slice(0, 500)); } }) .catch((error) => console.error('API request error:', error));
用 node tracker.js 运行脚本,你应该能在正文顶部看到真实的 Wayfair 列表标记,而非一个剥离后的空壳。这就在你编写任何选择器之前确认了渲染是正常工作的。Wayfair 的家具页面使用了大量 JavaScript 渲染,正是这个渲染步骤让价格出现在返回的 HTML 中。
第一次请求就在你这边没有无头浏览器或代理的情况下返回了完整渲染的 Wayfair 页面。Crawling API 在真实浏览器中运行页面,在服务端通过住宅 IP 轮换,并处理 Wayfair 针对爬虫的挑战,因此你从一次调用就获得了完整 HTML,这正是定时追踪器无人值守运行所需要的。先在免费层将其指向沙发页面试试。
步骤 2:用 cheerio 提取标题和价格
拿到渲染后的 HTML,将其加载到 cheerio 中并遍历产品卡片。Wayfair 将每个产品布局在标有 data-hb-id="Card" 的重复容器中,因此你选取所有卡片,然后从每张卡片内读取标题和价格。要自己在实时页面上找到这些字段,右键点击一个产品选择"检查",然后查找卡片元素以及标注名称和价格的属性。下面的字段是 Wayfair 通过 data-test-id 属性暴露的。
const cheerio = require('cheerio'); function parseProducts(html) { const $ = cheerio.load(html); const products = []; // Each listing sits in a card container $('div[data-hb-id="Card"]').each((index, element) => { const card = $(element); let productName = card .find('p[data-test-id="ListingCard-ListingCardName-Text"]') .text() .trim(); const productPrice = card .find('span[data-test-id="PriceDisplay"]') .text() .trim(); // Fall back to a label when the name is missing if (productName === '') { productName = 'Name is not available'; } if (productPrice) { products.push({ title: productName, price: productPrice }); } }); return products; }
几个细节让这段代码忠实于页面。产品名称来自 p[data-test-id="ListingCard-ListingCardName-Text"] 元素,价格来自 span[data-test-id="PriceDisplay"] 元素。.text() 方法获取内部文本,.trim() 去除两侧空白,因此你得到的是干净的值,如"$689.99"。当卡片没有可读名称时,解析器代以"Name is not available"而非丢弃该行,这与 Wayfair 偶尔渲染没有标准标题的广告或赞助位的方式一致。
Wayfair 的属性钩子(data-hb-id、各个 data-test-id 值)与其前端构建绑定,随时可能在不通知的情况下更改。请将上述选择器视为起始模板,而非固定合约。当字段返回为空时,请在浏览器开发者工具中重新检查实时页面并更新选择器。定期维护选择器对任何生产追踪器来说都是正常操作,而非故障的表现。
步骤 3:将价格记录到 JSON 历史文件
这一步将一次性抓取变成追踪器。你不是每次运行都覆盖文件,而是读取现有历史,追加带时间戳的新快照,然后将合并记录写回。每次运行都添加一批带有运行时刻时间戳的行,因此文件会积累一段可以绘图或对比的价格历史。
const fs = require('fs'); const HISTORY_FILE = 'price-history.json'; function loadHistory() { if (!fs.existsSync(HISTORY_FILE)) return []; return JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf-8')); } function appendSnapshot(products, pageUrl) { const timestamp = new Date().toISOString(); const rows = products.map((p) => ({ timestamp, title: p.title, price: p.price, url: pageUrl, })); const history = loadHistory(); history.push(...rows); fs.writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2)); console.log(`Logged ${rows.length} prices at ${timestamp}`); }
loadHistory 辅助函数在第一次运行时(文件尚不存在)返回空数组,因此其他地方无需特殊处理。appendSnapshot 用同一个 ISO 时间戳为单次运行中的所有行打上时间标记,这样后续按运行分组或对比某产品在两个时间戳之间的价格都很简单。由于每行都携带来源 url,你可以将多个 Wayfair 页面的追踪记录写入同一文件,分析时仍能分别处理。
步骤 4:组装完整追踪器
现在将获取、解析和记录连接成一个可运行的脚本。这是完整的追踪器:运行一次就会记录一次快照。
const fs = require('fs'); const { CrawlingAPI } = require('crawlbase'); const cheerio = require('cheerio'); const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_TOKEN' }); const HISTORY_FILE = 'price-history.json'; const wayfairPageURL = 'https://www.wayfair.com/furniture/sb0/sofas-c413892.html'; async function crawl(pageUrl) { const response = await api.get(pageUrl); if (response.statusCode === 200) return response.body; console.error(`Request failed: ${response.statusCode}`); return null; } function parseProducts(html) { const $ = cheerio.load(html); const products = []; $('div[data-hb-id="Card"]').each((index, element) => { const card = $(element); let productName = card .find('p[data-test-id="ListingCard-ListingCardName-Text"]') .text() .trim(); const productPrice = card .find('span[data-test-id="PriceDisplay"]') .text() .trim(); if (productName === '') productName = 'Name is not available'; if (productPrice) products.push({ title: productName, price: productPrice }); }); return products; } function loadHistory() { if (!fs.existsSync(HISTORY_FILE)) return []; return JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf-8')); } function appendSnapshot(products, pageUrl) { const timestamp = new Date().toISOString(); const rows = products.map((p) => ({ timestamp, title: p.title, price: p.price, url: pageUrl })); const history = loadHistory(); history.push(...rows); fs.writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2)); console.log(`Logged ${rows.length} prices at ${timestamp}`); } async function track() { const html = await crawl(wayfairPageURL); if (!html) return; const products = parseProducts(html); appendSnapshot(products, wayfairPageURL); } track();
用 node tracker.js 运行。第一次运行会创建 price-history.json 并记录当前沙发价格;后续每次运行都会向同一文件追加另一批带时间戳的数据。获取、解析和记录各阶段都小而独立,让脚本易于扩展,例如只需更改一个 URL 就能追踪不同分类。
输出结果示例
历史文件每次运行的每个产品对应一个对象,各自携带时间戳、标题、价格和来源 URL。两次相隔几小时的运行后,同一产品会出现两次,标题相同但价格可能不同,这正是追踪器存在的意义所在。
[ { "timestamp": "2024-03-18T09:00:00.000Z", "title": "Mahwah 98'' Chenille Square Arm Sofa", "price": "$689.99", "url": "https://www.wayfair.com/furniture/sb0/sofas-c413892.html" }, { "timestamp": "2024-03-18T09:00:00.000Z", "title": "Adelmina 88.6'' Upholstered Sofa", "price": "$444.99", "url": "https://www.wayfair.com/furniture/sb0/sofas-c413892.html" }, { "timestamp": "2024-03-18T21:00:00.000Z", "title": "Mahwah 98'' Chenille Square Arm Sofa", "price": "$649.99", "url": "https://www.wayfair.com/furniture/sb0/sofas-c413892.html" } ]
将历史数据导出为 CSV
JSON 便于运行中的日志记录,但 CSV 可以直接在 Excel 或 Google Sheets 中打开,只需点击几下就能绘制价格折线图。这个辅助函数读取 JSON 历史并写出一个平铺的 price-history.csv,每行一条快照。它保留了写入名称和价格列的传统方式,并添加了时间戳,使历史数据可绘图。
const fs = require('fs'); function exportCsv() { const history = JSON.parse(fs.readFileSync('price-history.json', 'utf-8')); const headers = ['timestamp', 'title', 'price', 'url']; const escape = (value) => `"${String(value).replace(/"/g, '""')}"`; const lines = [headers.join(',')]; for (const row of history) { lines.push(headers.map((h) => escape(row[h])).join(',')); } fs.writeFileSync('price-history.csv', lines.join('\n')); console.log(`Exported ${history.length} rows to price-history.csv`); } exportCsv();
escape 辅助函数将每个字段用引号括起,并将内嵌引号加倍,这很重要,因为 Wayfair 的产品标题很长,经常包含逗号、英寸符号和其他标点。结果是一个干净的时间序列:一列表示何时,一列表示哪个产品,一列表示价格,可按标题透视并为每个条目绘制价格折线图。这与任何价格情报工作流程中使用的模式相同。
按计划运行
追踪器只有在自动运行时才真正有用。你有两个简单的选择。最简单的是操作系统调度器:在 macOS 或 Linux 上,一条 cron 条目按固定间隔运行脚本,每次运行追加一次快照。
# Open your crontab crontab -e # Run the tracker every day at 9am and 9pm 0 9,21 * * * cd /path/to/wayfair-price-tracker && node tracker.js
如果你更想将计划放在 Node 内部,以便它随项目一起迁移并在任何有 Node 的地方运行,可以安装 node-cron,将现有的 track 函数包装在一个计划中。
// npm install node-cron const cron = require('node-cron'); // At minute 0 of hours 9 and 21, every day cron.schedule('0 9,21 * * *', () => { console.log('Running scheduled price check...'); track(); });
请将轮询间隔设置得合理。每天两次足以捕捉 Wayfair 大多数的价格变动,同时不会给站点造成不必要的负载,而平缓的请求速率也是保持不被封锁的最重要单一因素。运行几周后,历史文件就会成为每张沙发价格变化的真实记录,这正是追踪而非单次检查的全部意义所在。同样的记录序列也天然地适合输入到价格对比工具,如果你开始在 Wayfair 旁边追踪竞争对手的列表。
保持不被封锁
即便渲染问题已经解决,Wayfair 仍然会监控爬虫特征的流量,一个永远在运行的追踪器比一次性运行面临更大的暴露风险。以下几个习惯能保持它健康运行。
- 控制请求速率。 追踪器不需要频繁轮询。每隔几小时运行一次就能捕捉价格趋势,并让你远低于任何速率限制。不要抵制不住诱惑在紧密循环中抓取。
- 依赖轮换。 住宅 IP 池将请求分散到众多真实用户地址,使任何单个地址都不会触发限制或挑战。Crawling API 为你处理这些;如果你自己搭建,这是最需要做好的部分。
- 关注状态码。 开始返回非 200 响应的定时任务在告诉你当前速率或 IP 层级已不够用。记录这些响应并回退,而不是让静默失败在历史记录中留下空白。
关于保持长期运行任务正常工作的更全面应对方案,请参阅如何在不被封锁的情况下抓取网站。如果你想将同样的方法扩展到另一个市场,关于如何轻松抓取 Walmart 价格的指南对不同店铺演示了相同的先获取后解析模式。
抓取 Wayfair 合法吗?
使用 Wayfair 价格追踪器追踪公开列表信息通常是可以辩护的,但"通常"在这句话里承担了实质性的工作。你的具体使用方式是否被允许取决于 Wayfair 的服务条款、你所在的司法管辖区以及你如何使用这些数据。Wayfair 的条款限制自动化访问,因此无论你的工具多么谨慎,抓取行为都可能与这些条款相悖。此处的代码不会改变这一点,它只是让技术部分得以运作。请阅读 Wayfair 的使用条款和 robots.txt,并将两者都视为你可以收集哪些内容的边界。
以下几条值得遵守。只收集公开的产品数据:任何人无需账户即可在列表页面上看到的标题和价格。将追踪器用于个人或研究目的,轻柔地运行,将请求量控制在不对 Wayfair 服务器造成压力的范围内。避免个人数据,包括任何与可识别评论者相关的信息(超出页面上显示的公开评论文本和评分的部分除外)。不要以自己的名义重新分发 Wayfair 版权所有的媒体内容(如产品摄影)。如果你计划将数据用于商业用途,请获得许可或正式协议,而非假设沉默即同意。
本指南刻意将范围限定在公开列表价格,因为这是让工作保持可辩护的界限。它不涵盖任何需要登录的内容、客户或卖家账户数据、订单历史,以及任何绕过身份验证或你本不该通过的挑战的尝试。如果你的项目需要超出公开列表的内容,与 Wayfair 达成经授权的数据协议才是正确的途径,而非更激进的爬虫。对于商业用途有疑问时,在扩大规模之前请咨询法律建议。
核心要点
- 追踪意味着随时间记录。 Wayfair 的动态定价让当前价格的用处不及趋势,因此追踪器追加带时间戳的快照,而非覆盖单个值。
- Wayfair 在客户端渲染价格。 普通请求返回空壳,因此必须在受信任的 IP 后渲染页面,cheerio 才能读取标题和价格。
- Crawling API 在一次调用中完成两者。 它渲染页面,轮换住宅 IP,并处理挑战,返回完整 HTML,让定时追踪器得以无人值守运行。
-
cheerio 提取字段。 选取每个
data-hb-id="Card"容器,从ListingCard-ListingCardName-Text元素读取名称,从PriceDisplay读取价格,并预期这些钩子会随时间漂移。 - 按计划运行并只收集公开数据。 用 cron 或 node-cron 每天运行两次,将历史导出为 CSV 以便绘图,并将追踪器的范围限定在遵守 Wayfair 服务条款和 robots.txt 的公开列表价格。
常见问题
什么是 Wayfair 价格追踪器?
Wayfair 价格追踪器是一个小程序,它按计划记录 Wayfair 上产品的价格,并保留历史记录。它不需要你手动检查价格,而是自动获取列表页面,读取每个产品的标题和价格,并将带时间戳的行追加到文件中。随着时间积累,该文件成为一段价格日志,你可以绘制图表,发现价格波动,并在价格下跌时把握最佳购买时机。
为何普通请求会从 Wayfair 返回不完整的数据?
因为 Wayfair 在客户端用 JavaScript 渲染其产品卡片,并对自动化流量发起挑战。来自数据中心 IP 的裸 HTTP 请求通常返回接近空壳的页面或封锁页面,而非列表卡片,因此你得到的 HTML 中不包含价格。要获得完整页面,必须在受信任的 IP 后渲染,这正是 Crawling API 为你处理的部分。
Wayfair 的定价是如何运作的?
Wayfair 使用受需求、可用性、竞争和自身算法定价系统影响的动态定价模型,实时收集和分析数据。卖家自行设定价格,Wayfair 为保持竞争力而进行调整,因此同一产品在不同地区甚至同一天内都可能显示不同价格。正是这种波动性使得按计划记录价格比单次检查更有价值。
如何追踪 Wayfair 的降价?
按计划运行追踪器,让它每次都追加带时间戳的快照,然后对比历史文件中同一标题在两个时间戳上的价格。当后一个价格低于前一个时,就是降价。在电子表格中绘制 CSV 图表,降价会在价格折线中以低谷的形式清晰呈现;一旦历史数据就位,你还可以在同一数据上叠加提醒功能。
我的选择器返回空值。是什么变了?
几乎可以肯定是 Wayfair 的标记发生了变化。解析器依赖的 data-hb-id 和 data-test-id 钩子与 Wayfair 的前端构建绑定,随时可能在不通知的情况下更改,因此上个月有效的选择器可能已失效。请在浏览器开发者工具中重新检查实时列表页面并更新选择器。定期维护选择器对任何生产追踪器来说都是正常操作。
如何在追踪 Wayfair 价格时避免被封锁?
将轮询间隔保持在适度范围内,价格趋势每天几次就足够了,并通过轮换住宅 IP 路由请求,这样没有单个地址会触发速率限制或挑战。Crawling API 为你管理轮换、受信任的 IP 池和挑战处理;如果你自己搭建,那是值得投入的部分。关注定时运行的状态码,当开始看到非 200 响应时及时回退。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
