Walmart 是网络上最大的零售商之一,每个公开产品页面都包含一系列用户评论:星级评分、简短标题、评论正文、评论者昵称以及日期。这些文本对情感分析、产品研究和竞争对标极具价值,因为它们用购物者自己的语言告诉你一款产品的优劣所在。
本指南介绍如何使用 JavaScript 和 Node.js 配合 cheerio 抓取 Walmart 评论。你将构建一个小型可运行的爬虫,通过 Crawling API 获取 Walmart 产品页面,将每条评论解析为干净的记录,翻页遍历评论分页,并将结果导出为 JSON 和 CSV。整个流程仅涉及公开评论文本,文末的合法性部分不是套话,请在将此工具指向真实规模目标前仔细阅读。
你将构建什么
一个 Node.js 脚本,接受一个 Walmart 产品的公开 URL,通过 Crawling API 获取渲染后的 HTML,并为页面上的每条评论提取一条结构化记录。我们以单个产品页面作为示例,每条评论提取以下字段:
- 评论者 评论上显示的昵称,例如"Optimistic"或"First Time Shopper"。
- 评分 Walmart 显示的星级,如"5 out of 5 stars review"。
- 标题 评论者给自己评论起的简短标题。
- 正文 评论的书面反馈文本。
- 日期 评论发布日期(当该信息出现在卡片上时)。
为何普通请求在 Walmart 上失效
如果你用裸 HTTP 客户端请求 Walmart 产品 URL,你会得到状态 200 的响应,但正文中只有部分评论数据。有两个原因让你陷入困境。第一,Walmart 在浏览器中用 JavaScript 渲染价格、评分和评论列表,初始 HTML 在页面脚本运行之前是不完整的。第二,Walmart 能快速识别自动化流量:来自数据中心 IP 且请求模式不像真实浏览器的请求,会在抵达渲染内容之前就遭到挑战、限速或封锁。
因此,一个有效的 Walmart 评论爬虫需要在一次请求中同时具备两样东西:能真正渲染页面的浏览器,以及平台认为是真实访客的 IP。你可以自己组合一个无头浏览器加一批轮换住宅代理,但将它们整合在一起并持续维护才是大头工作。Crawling API 将两者合并为一次调用:你发送 URL,它在受信任的 IP 后渲染页面,并返回完整 HTML 供你解析。
Crawlbase 提供两种令牌类型。普通令牌获取静态 HTML;JavaScript (JS) 令牌先在真实浏览器中渲染页面。Walmart 在客户端加载评论列表,因此 JS 令牌能给你最完整的页面。使用普通令牌可能返回评论区为空的页面,让你无从解析。
前置条件
在编写任何代码之前,你需要准备好以下几样东西,每项都不需要太长时间。
基础 JavaScript 和 Node.js。 你应该能够编写和运行 Node 脚本,并使用 npm 安装包。如果你是 Node 新手,官方文档和任何入门课程都能让你达到本教程所需的水平。更完整的演练请参阅我们关于如何用 Node.js 构建网页爬虫的指南。
Node.js 16 或更高版本。 用 node --version 确认你的版本。如果还没有安装,请从 Node.js 官网下载,或通过版本管理工具(如 nvm)安装。
Crawlbase 账号和令牌。 注册后打开控制台,从账号文档页面复制你的令牌。免费层提供 1,000 次请求,无需绑定信用卡,足以跟完本教程。请像对待密码一样保管令牌:它验证你的请求,不要将其提交到版本控制系统。
搭建项目
创建一个项目文件夹,初始化,并安装爬虫所需的两个库。
node --version mkdir walmart-reviews-scraper && cd walmart-reviews-scraper npm init -y npm install crawlbase cheerio
两个依赖各司其职:crawlbase 是 Crawling API 的官方 Node 客户端,cheerio 用类似 jQuery 的 API 解析返回的 HTML,让你可以通过 CSS 选择器提取各个字段。如果你对选择器还不熟悉,XPath 和 CSS 选择器入门指南是不错的补充读物。
步骤 1:获取渲染后的产品页面
首先获取完整页面。导入 CrawlingAPI 类,用你的令牌初始化,并请求产品 URL。我们以一个 Walmart 产品作为示例,任何包含评论区的公开产品页面都可以使用。在解析前检查状态码,让失败保持可见而非静默。
const { CrawlingAPI } = require('crawlbase'); const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_TOKEN' }); const productUrl = 'https://www.walmart.com/ip/Straight-Talk-Apple-iPhone-14-128GB-Midnight-Prepaid-Smartphone-Locked-to-Straight-Talk/1381920049'; async function crawl(pageUrl) { const options = { ajax_wait: 'true', page_wait: 5000 }; const response = await api.get(pageUrl, options); if (response.statusCode === 200) { return response.body; } console.error(`Request failed: ${response.statusCode}`); return null; } crawl(productUrl).then((html) => { console.log(html ? html.slice(0, 500) : 'No HTML returned'); });
两个等待选项对像这样的客户端渲染目标很重要。ajax_wait 告知 API 等待异步内容加载完成,page_wait 在页面加载后再等待固定毫秒数,以确保延迟渲染的元素在页面被截取前出现。5 秒是合理的起点;如果评论列表回来是空的,可以适当增加。用 node scraper.js 运行脚本,你应该能看到真实的产品标记,而非一个剥离后的空壳。这就在你编写任何选择器之前确认了渲染是正常工作的。
Walmart 需要在受信任的 IP 后渲染页面,才能在一次调用中让评论列表出现。Crawling API 在真实浏览器中运行页面,在服务端通过住宅 IP 轮换,并将完整 HTML 返回给你,省去了自己运行无头浏览器集群和代理池的麻烦。先在免费层将其指向一个公开产品页面试试。
步骤 2:用 cheerio 解析每条评论
拿到渲染后的 HTML,将其加载到 cheerio 中并遍历评论卡片。Walmart 将每条评论布局在评论区内的一个重复块中,因此你选取所有卡片,然后从每张卡片内读取评论者昵称、评分、标题、正文和日期。防御性地读取每个字段,避免单个缺失值导致整个运行崩溃。
const cheerio = require('cheerio'); function parseReviews(html) { const $ = cheerio.load(html); const reviews = []; $('#item-review-section li.dib').each((_, el) => { const card = $(el); const rating = card.find('.w_iUH7').text().trim(); const body = card.find('.lh-copy').text().trim(); if (!rating && !body) return; reviews.push({ reviewer: card.find('.f7.gray').first().text().trim() || null, rating: rating || null, title: card.find('.w_kV33.w_Sl3f.w_mvVb.f5.b').text().trim() || null, body: body || null, date: card.find('.f7.gray.mt1').text().trim() || null, }); }); return reviews; }
几个细节让这段代码更健壮。容器选择器 #item-review-section li.dib 以及内部的 .w_iUH7(评分)和 .lh-copy(评论正文)选择器直接来自 Walmart 的实际评论标记。评论者昵称、标题和日期各自有独立的小包装器,因此每个字段用独立的选择器读取。当元素缺失时,每个字段都回退到 null,这很常见,因为不是每条评论都有独立的标题或可见的日期。提前 return 跳过既没有评分也没有正文文本的列表项,以避免布局占位符污染输出。
Walmart 的类名(w_iUH7、lh-copy、dib 等)是生成的,随时可能在不通知的情况下更改,且在不同产品布局之间可能有所不同。请将上述选择器视为起始模板,而非固定合约。当字段返回 null 时,请在浏览器开发者工具中重新检查实时页面并更新选择器。定期维护选择器对任何生产爬虫来说都是正常操作,而非故障的表现。
步骤 3:处理评论分页
一页评论只是演示;真实任务需要遍历分页。Walmart 通过产品 URL 上的 page 查询参数暴露评论页码,因此你可以在循环中构建每页 URL,通过 Crawling API 获取,用同一个函数解析,并收集所有行。由于每个评论页面共享相同的卡片结构,你已编写的解析器无需任何修改即可在所有页面上工作。
async function scrapeAllReviews(baseUrl, totalPages) { const all = []; for (let page = 1; page <= totalPages; page++) { const sep = baseUrl.includes('?') ? '&' : '?'; const url = `${baseUrl}${sep}page=${page}`; const html = await crawl(url); if (html) all.push(...parseReviews(html)); } return all; }
每次迭代在产品 URL 后追加 page=N,获取渲染后的页面,并将解析出的评论展开合并到一个数组中。测试期间请保守设置 totalPages,避免发出不必要的请求;确认输出正确后再调大。
步骤 4:导出为 JSON 和 CSV
现在将获取、解析和分页组合成一个可运行的完整脚本,然后将收集到的评论写入 JSON 和 CSV 文件。JSON 是喂给情感模型的天然格式;CSV 可直接在电子表格中打开,便于快速的产品研究。
const { CrawlingAPI } = require('crawlbase'); const cheerio = require('cheerio'); const fs = require('fs'); const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_TOKEN' }); async function crawl(pageUrl) { const options = { ajax_wait: 'true', page_wait: 5000 }; const response = await api.get(pageUrl, options); if (response.statusCode === 200) return response.body; console.error(`Request failed: ${response.statusCode}`); return null; } function parseReviews(html) { const $ = cheerio.load(html); const reviews = []; $('#item-review-section li.dib').each((_, el) => { const card = $(el); const rating = card.find('.w_iUH7').text().trim(); const body = card.find('.lh-copy').text().trim(); if (!rating && !body) return; reviews.push({ reviewer: card.find('.f7.gray').first().text().trim() || null, rating: rating || null, title: card.find('.w_kV33.w_Sl3f.w_mvVb.f5.b').text().trim() || null, body: body || null, date: card.find('.f7.gray.mt1').text().trim() || null, }); }); return reviews; } async function scrapeAllReviews(baseUrl, totalPages) { const all = []; for (let page = 1; page <= totalPages; page++) { const sep = baseUrl.includes('?') ? '&' : '?'; const html = await crawl(`${baseUrl}${sep}page=${page}`); if (html) all.push(...parseReviews(html)); } return all; } function toCsv(rows) { const headers = ['reviewer', 'rating', 'title', 'body', 'date']; const escape = (v) => `"${(v ?? '').replace(/"/g, '""')}"`; const lines = [headers.join(',')]; for (const row of rows) { lines.push(headers.map((h) => escape(row[h])).join(',')); } return lines.join('\n'); } async function main() { const productUrl = 'https://www.walmart.com/ip/Straight-Talk-Apple-iPhone-14-128GB-Midnight-Prepaid-Smartphone-Locked-to-Straight-Talk/1381920049'; const reviews = await scrapeAllReviews(productUrl, 3); fs.writeFileSync('walmart_reviews.json', JSON.stringify(reviews, null, 2)); fs.writeFileSync('walmart_reviews.csv', toCsv(reviews)); console.log(`Saved ${reviews.length} reviews to JSON and CSV`); } main();
用 node scraper.js 运行完整脚本。它遍历三页评论,将所有评论收集到一个数组,并将 walmart_reviews.json 和 walmart_reviews.csv 写入项目文件夹。CSV 转义将每个值用引号括起,并将内部引号加倍,因此包含逗号或引号的评论正文不会破坏列结构。
输出结果示例
JSON 文件每条评论对应一个对象,可直接喂给情感分析流水线或加载到 notebook 中。
[ { "reviewer": "Optimistic", "rating": "5 out of 5 stars review", "title": "Great camera and speed", "body": "Pictures are coming out great and the speed is what I've been needing", "date": "October 2, 2023" }, { "reviewer": "First Time Shopper", "rating": "5 out of 5 stars review", "title": "Awesome phone", "body": "Bought this last month and it's been awesome. The camera quality is perfect especially with the action mode.", "date": "September 18, 2023" } ]
CSV 包含相同的字段和一行标题,营销或产品团队可以在电子表格中打开,按评分排序,快速浏览最低分评论。有了这些数据,你可以进行情感评分、统计评分分布,或追踪反馈如何随版本迭代而变化。关于分析层面,我们关于如何抓取用户评论的指南介绍了如何将原始评论文本转化为结构化洞察,同样的评论数据流水线也适用于Amazon 评论(如果你需要对比两个平台)。
保持不被封锁
即便渲染问题已经解决,Walmart 仍然会监控爬虫特征的流量。以下几个习惯能保持运行健康,适用于任何高防御商业目标。
-
控制请求速率。 在紧密循环中快速请求评论页面是被限速的最快方式。分散请求,将
totalPages控制在你真正需要的范围内,而非全速爬取每一页。 - 依赖轮换。 住宅 IP 池将请求分散到众多真实用户地址,使任何单个地址都不会触发速率限制。Crawling API 为你处理这些;如果你自己搭建,这是最需要做好的部分。
- 关注状态码。 运行中开始返回挑战或错误,说明当前速率或 IP 层级已不够用。将其视为回退信号,而非可忽略的噪音。
关于更全面的应对方案,请参阅如何在不被封锁的情况下抓取网站。如果你还需要产品标题、价格或搜索列表而不只是评论,关于如何用 Python 抓取 Walmart 搜索的配套指南涵盖了这一方面;Walmart 也是更广泛的电商网页抓取工作中的常见目标,相同的先获取后解析模式可跨站点复用。
抓取 Walmart 评论合法吗?
抓取 Walmart 评论是否被允许取决于 Walmart 的服务条款、你所在的司法管辖区以及你如何使用这些数据。Walmart 的条款限制自动化访问,因此无论你的工具多么谨慎,抓取行为都可能与这些条款相悖。此处的代码不会改变这一点,它只是让技术部分得以运作。请阅读 Walmart 的使用条款及其位于 https://www.walmart.com/robots.txt 的 robots.txt,并将两者都视为你可以收集哪些内容的边界。
以下几条值得遵守。只收集公开的评论文本:任何人无需账户即可在卡片上看到的星级评分、评论标题、正文、显示的昵称以及发布日期。不要建立单个评论者的个人档案,不要跨产品或跨站点关联他们的评论,也不要试图找出昵称背后的真实人物。昵称和公开评论正文是公开内容;将其作为追踪特定购物者的原材料则是你不应跨越的隐私界限。遵守 Walmart 规定的速率预期,并将请求量控制在不对其服务器造成压力的范围内。
本指南刻意将范围限定在公开产品和评论页面,因为这是让工作保持可辩护的界限。它不涵盖任何需要登录的内容、受登录门控的账户或订单数据、评论者超出其公开发布文本之外的个人数据,以及任何绕过身份验证的尝试。如果你的项目需要超出公开评论的内容,与 Walmart 的正式数据协议才是正确的途径,而非更聪明的爬虫。有疑问时,优先考虑聚合分析(评分分布、常见主题),而非任何针对单个评论者的操作。
核心要点
- Walmart 在客户端渲染评论。 普通请求返回不完整的页面,因此在解析之前必须先渲染。
-
你需要同时具备渲染和受信任的 IP。 Crawling API 在一次调用中完成两者;
ajax_wait和page_wait控制等待评论列表加载的时间。 -
cheerio 负责提取。 选取所有
#item-review-section li.dib卡片,然后将评论者、评分、标题、正文和日期映射到当前选择器,并预期这些选择器会漂移。 -
通过遍历页面扩展规模,导出为 JSON 和 CSV。
page参数遍历评论页面,同一解析器适用于所有页面,两个文件均可用于情感分析和产品研究。 - 只收集公开评论文本。 遵守 Walmart 的服务条款和 robots.txt,不要对单个评论者建档,远离任何需要登录的内容。
常见问题
为何普通请求会从 Walmart 返回不完整的数据?
因为 Walmart 用 JavaScript 在客户端渲染价格、评分摘要和完整评论列表。初始 HTML 在页面脚本在浏览器中运行之前是不完整的,因此裸 HTTP 请求返回状态 200,但评论区是空的或只有占位符。要获得完整页面,必须先渲染,这正是 Crawling API 为你处理的部分。
我可以从 Walmart 评论中提取哪些字段?
评论者昵称、星级评分(Walmart 的表述方式为"5 out of 5 stars review")、简短的评论标题、正文文本,以及卡片上显示的发布日期。本指南中的爬虫使用 .w_iUH7 和 .lh-copy 选择器以及昵称、标题和日期的小包装类,从 #item-review-section li.dib 评论卡片中提取这五个字段。
如何抓取所有页面的评论,而不只是第一页?
Walmart 通过产品 URL 上的 page 查询参数来分页评论。本指南中的 scrapeAllReviews 函数从第 1 页循环到你设定的页数,在 URL 后追加 page=N,通过 Crawling API 获取每个渲染后的页面,并在合并结果之前对每页运行同一解析器。
我的选择器返回 null。是什么变了?
几乎可以肯定是 Walmart 的标记发生了变化。其生成的类名(w_iUH7、lh-copy、dib 及昵称、标题、日期的包装类)随时可能在不通知的情况下更改,且在不同产品布局之间可能有所不同,因此上个月有效的选择器可能已失效。请在浏览器开发者工具中重新检查实时评论页面并更新选择器。定期维护选择器对任何生产爬虫来说都是正常操作。
我可以抓取有关 Walmart 评论者的个人数据吗?
不可以,本指南也不涵盖这一内容。公开的昵称和评论正文是公开内容,但对特定评论者建档、关联其跨产品或跨站点的活动,或试图找出昵称背后的真实人物,都逾越了隐私界限。将你的分析保持在聚合层面(评分分布、常见主题),并远离任何需要登录的内容。
我可以分析抓取到的 Walmart 评论吗?
可以。一旦评论以 JSON 或 CSV 格式存储,你就可以对每条评论进行情感分析,将其分类为积极、消极或中性,统计评分分布,找出最受称赞和最受批评的功能,并追踪反馈如何随时间变化。这种聚合视角正是评论数据对产品研究和竞争对标有价值的原因所在。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
