电商网站是互联网上最丰富的公开结构化数据来源之一。每条商品列表都包含标题、价格、评分、库存状态和图片,这些数据驱动着价格追踪、竞争对手研究、库存监控以及市场趋势分析。问题在于,现代电商平台大多通过 JavaScript 渲染商品详情,并对自动化流量设有防护措施,因此普通 HTTP 请求往往只能返回一个几乎空白的页面框架,而非你所需的商品目录。
本指南是一份逐步讲解的 Node.js 实战教程,教你以可靠的方式从电商网站抓取商品数据。你将构建一个小型的可运行爬虫,通过 Crawling API 获取已渲染的商品列表页,使用 Cheerio 解析每个商品卡片,遍历分页,并将干净的结构化数据保存为 JSON 或 CSV 格式。整个教程仅限于公开商品数据的抓取,结尾处的合法性说明并非套话,请在对任何真实目标发起大规模请求前认真阅读。
你将构建什么
一个 Node.js 脚本,接收一个公开的电商搜索或分类 URL,通过 Crawling API 获取已渲染的 HTML,并为结果页中的每件商品提取一条结构化记录。我们以一个搜索查询为贯穿示例,每条记录提取以下字段:
- 标题:商品卡片上显示的商品名称,例如"男士模拟腕表"。
- 价格:页面上显示的挂牌价格,如"Rs. 1,299"。
- 评分:商品旁显示的评论数量或星级评分。
- 库存:卡片上呈现的库存或位置信息(如有)。
- 图片:商品的缩略图 URL。
- 商品链接:指向该商品详情页的链接。
为什么普通请求在电商网站上会失败
如果你用一个普通的 HTTP 客户端请求电商搜索 URL,通常会收到状态码 200 的响应,但响应体中只有部分商品数据。有两个因素对你不利。第一,大多数电商网站在浏览器端通过 JavaScript 和 AJAX 渲染价格、评分以及每个商品卡片的大部分内容,因此在页面脚本执行完成之前,初始 HTML 是不完整的。第二,零售平台能快速识别自动化流量:数据中心 IP 以及不像真实浏览器的请求模式,在到达渲染内容之前就会遭到挑战、限速或屏蔽。
因此,一个有效的电商爬虫需要在单次请求中同时具备两个能力:真正渲染页面的浏览器,以及平台视为真实访客的 IP 地址。你可以自行组合无头浏览器和轮换住宅代理池,但将两者整合在一起并保持其正常运行才是大部分工作量所在。Crawling API 将两者合并为单次调用:你发送带有 JavaScript token 的 URL,它在可信 IP 后面渲染页面,并将完整的 HTML 返回供你解析。若想深入了解客户端渲染目标的行为原理,请参阅如何抓取 JavaScript 网站。
Crawlbase 提供两种 token 类型。普通 token(TCP)获取静态 HTML;JavaScript(JS)token 先在真实浏览器中渲染页面。电商搜索页面的关键字段在客户端加载,因此 JS token 能给你最完整的页面内容。使用它时,还可以传递 ajax_wait 和 page_wait 等等待参数来处理 AJAX 加载。使用普通 token 可能返回不完整的结果,价格或评分缺失,使解析无从下手。
前提条件
在编写任何代码之前,你需要准备好以下几项内容,都不需要太长时间。
基本的 JavaScript 和 Node.js 知识。你应该能够编写和运行 Node 脚本,并使用 npm 安装包。如果你是 Node 新手,官方文档和任何入门课程都能让你达到本教程所要求的水平。更完整的教程请参阅我们关于如何使用 Node.js 构建网络爬虫的指南。
Node.js 16 或更高版本。使用 node --version 确认你的版本。如果没有,请从 Node.js 官网或通过 nvm 等版本管理工具安装最新的 LTS 版本。npm 与 Node.js 捆绑发布,安装一个即可同时获得两者。
Crawlbase 账号和 JS token。注册账号,打开控制台,从账号文档页面复制你的 JavaScript(JS)token。Crawlbase 提供两种 token:用于静态页面的普通 token 和用于动态 JavaScript 渲染页面的 JS token。请像对待密码一样保管 token:它用于验证你的请求,因此不要将其纳入版本控制。
设置项目
创建项目文件夹,将其初始化为 npm 包,并安装爬虫所需的库。npm init -y 标志接受所有默认值,并为你生成 package.json。
node --version mkdir ecommerce-crawling && cd ecommerce-crawling npm init -y npm install crawlbase cheerio csv-writer
三个依赖各司其职:crawlbase 是 Crawling API 的官方 Node 客户端,cheerio 使用类 jQuery 的 API 解析返回的 HTML,让你可以通过 CSS 选择器提取各个字段,csv-writer 最终将结构化记录转换为 CSV 文件。如果你对选择器还不熟悉,可以参阅关于 XPath 和 CSS 选择器的入门教程。
步骤 1:获取已渲染的搜索页面
首先获取完整的页面。导入 CrawlingAPI 类,使用你的 JS token 初始化它,然后请求搜索 URL。在解析之前检查状态码,可以让失败信息明显暴露而不是悄悄忽略。page_wait 选项会在页面加载后等待固定的毫秒数,确保延迟渲染的商品卡片在页面被捕获前已经出现。
const { CrawlingAPI } = require('crawlbase'); // Replace with your actual Crawlbase JS token 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; } const searchUrl = 'https://example-shop.com/catalog/?q=watches+for+men'; crawl(searchUrl).then((html) => { console.log(html ? html.slice(0, 500) : 'No HTML returned'); });
这两个等待选项对于客户端渲染的电商平台至关重要。ajax_wait 告诉 API 等待异步内容加载完成,page_wait 在加载后等待 5000 毫秒(5 秒),确保延迟渲染的元素在捕获前已经出现。五秒是一个合理的起点,如果价格或评分返回为空,可以适当增加。JS token 还会在后台自动进行 IP 轮换,使请求看起来像真实访客。使用 node scraper.js 运行脚本,你应该能看到真实的商品标记,而不是一个精简的空壳。这在你编写任何选择器之前就确认了渲染有效。如需回顾请求层的基础知识,请参阅如何在 Node.js 中发起 HTTP 请求。
你刚刚获取的页面需要真实的浏览器渲染和可信 IP,而这一切只需一次调用。Crawling API 接收 JS token,在真实浏览器中运行电商平台,在服务端轮换住宅 IP,并将完整的 HTML 返回给你,省去了自己维护无头浏览器集群和代理池的麻烦。先用免费套餐在公开搜索页上试试。
步骤 2:识别选择器并解析每件商品
在提取任何内容之前,先检查一个真实的商品卡片。在浏览器中右键点击标题、价格或评分,选择"检查"打开开发者工具,然后读取包裹每个字段的类名和标签。电商网站将每条结果排列在重复的块中,所以你选取所有卡片,再从其内部读取标题、价格、评分、图片和商品链接。对每个字段进行防御性读取,可以防止单个缺失值导致整个运行崩溃。
const cheerio = require('cheerio'); function parseSearch(html) { const $ = cheerio.load(html); const results = []; $('div[data-qa-locator="general-products"] div[data-qa-locator="product-item"]').each((index, element) => { const card = $(element); const product = {}; product.productPageUrl = card.find('.mainPic--ehOdr a').attr('href') || null; product.thumbnailImage = card.find('.mainPic--ehOdr img').attr('src') || null; product.title = card.find('.info--ifj7U .title--wFj93 a').text().trim() || null; product.price = card.find('.info--ifj7U .price--NVB62 span').text().trim() || null; product.noOfReviews = card.find('.info--ifj7U .rateAndLoc--XWchq .rating__review--ygkUy').text().trim() || null; product.location = card.find('.info--ifj7U .rateAndLoc--XWchq .location--eh0Ro').text().trim() || null; if (product.title) results.push(product); }); return results; }
有几个细节使代码更加健壮。当元素缺失时每个字段都回退为 null,这很常见,因为并非每张卡片都显示评分或位置信息。商品 URL 和图片从链接的 href 和图片的 src 读取,而非文本内容,因此使用 attr 而非 text。外层选择器通过 data-qa-locator 属性定位商品网格,这些属性通常比哈希类名更稳定,最后的 if (product.title) 检查会跳过任何空白或占位块。
上述哈希类名(.mainPic--ehOdr、.title--wFj93、.price--NVB62 等)由电商平台的构建工具生成,可能随时变更且不另行通知。将这里的选择器视为起始模板,而非固定合同。当某个字段返回 null 时,在浏览器开发者工具中重新检查线上页面并更新选择器。定期维护选择器对任何生产爬虫来说都是正常的,并不意味着出了什么问题。
步骤 3:组合完整爬虫
现在将获取和解析串联成一个可运行的脚本。获取已渲染的 HTML,传给解析器,并打印结构化记录。这是爬取单个搜索页面的最小端到端版本。
const { CrawlingAPI } = require('crawlbase'); const cheerio = require('cheerio'); 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 parseSearch(html) { const $ = cheerio.load(html); const results = []; $('div[data-qa-locator="general-products"] div[data-qa-locator="product-item"]').each((index, element) => { const card = $(element); const product = { productPageUrl: card.find('.mainPic--ehOdr a').attr('href') || null, thumbnailImage: card.find('.mainPic--ehOdr img').attr('src') || null, title: card.find('.info--ifj7U .title--wFj93 a').text().trim() || null, price: card.find('.info--ifj7U .price--NVB62 span').text().trim() || null, noOfReviews: card.find('.info--ifj7U .rateAndLoc--XWchq .rating__review--ygkUy').text().trim() || null, location: card.find('.info--ifj7U .rateAndLoc--XWchq .location--eh0Ro').text().trim() || null, }; if (product.title) results.push(product); }); return results; } async function main() { const searchUrl = 'https://example-shop.com/catalog/?q=watches+for+men'; const html = await crawl(searchUrl); if (!html) return; const results = parseSearch(html); console.log(JSON.stringify(results.slice(0, 3), null, 2)); } main();
main 函数构造查询的搜索 URL,通过 Crawling API 发送,等待 5 秒以完成 JavaScript 渲染,然后使用 Cheerio 解析返回的 HTML。提取的记录(包括商品 URL、图片、标题、价格、评论数量和位置)被收集到一个数组并打印以供检查。运行 node scraper.js 确认各字段有内容返回。
输出结果示例
运行完整脚本后,你将得到一个干净的记录数组,每件商品一条,可以写入 JSON、CSV 或数据库。
[ { "productPageUrl": "https://example-shop.com/products/mens-analog-watch-1.html", "thumbnailImage": "https://img.example-shop.com/p/mens-analog-watch-1.jpg", "title": "Men's Analog Wrist Watch Stainless Steel", "price": "Rs. 1,299", "noOfReviews": "(128)", "location": "Karachi" }, { "productPageUrl": "https://example-shop.com/products/sport-digital-watch-2.html", "thumbnailImage": "https://img.example-shop.com/p/sport-digital-watch-2.jpg", "title": "Sport Digital Watch Waterproof", "price": "Rs. 899", "noOfReviews": "(54)", "location": "Lahore" } ]
步骤 4:处理商品列表页的分页
一页结果只是演示;真实任务需要遍历所有分页。电商搜索结果分布在多个页面中,所以要收集完整目录,你需要先读取总页数,然后从第 1 页迭代到第 N 页,逐一获取和解析。总页数通常由第一页底部的分页控件公开,你可以用选择器读取并解析为数字。每个分页 URL 只需在搜索 URL 后附加 &page= 参数。
async function getTotalPages(query) { const searchUrl = `https://example-shop.com/catalog/?q=${encodeURIComponent(query)}`; const html = await crawl(searchUrl); if (!html) return 0; const $ = cheerio.load(html); const totalPages = parseInt($('ul.ant-pagination li:nth-last-child(2)').attr('title'), 10); return Number.isNaN(totalPages) ? 1 : totalPages; } async function crawlPage(query, page) { const searchUrl = `https://example-shop.com/catalog/?q=${encodeURIComponent(query)}&page=${page}`; const html = await crawl(searchUrl); return html ? parseSearch(html) : []; } async function crawlAll(query) { const totalPages = await getTotalPages(query); const results = []; for (let page = 1; page <= totalPages; page++) { const pageResults = await crawlPage(query, page); results.push(...pageResults); } return results; }
流程分为三个小函数。getTotalPages 获取第一个搜索页并从分页控件读取总页数,如果该值缺失则回退为 1。crawlPage 通过在 URL 后附加 page 参数来获取和解析单个页面。crawlAll 将两者结合:确定总页数,从第 1 页循环到第 N 页,将每一页的结果汇总到一个数组中。由于每个结果页具有相同的卡片结构,你已经写好的 parseSearch 函数无需修改即可适用所有页面。对于非常大的目录,请将循环限制在合理的页数范围内,而不要在一次运行中爬取数千页。
步骤 5:将数据保存为 CSV
收集好记录后,将它们写入 CSV 文件,以便在电子表格中打开数据或将其加载到其他工具中。csv-writer 库允许你定义映射到字段名的表头,并在一次调用中写入所有记录。JSON 和 CSV 各有其适用场景,电商网络抓取指南介绍了何时选择哪种格式。
const createCsvWriter = require('csv-writer').createObjectCsvWriter; const csvWriter = createCsvWriter({ path: 'ecommerce_products.csv', header: [ { id: 'productPageUrl', title: 'Product Page URL' }, { id: 'thumbnailImage', title: 'Thumbnail Image URL' }, { id: 'title', title: 'Title' }, { id: 'price', title: 'Price' }, { id: 'noOfReviews', title: 'Number of Reviews' }, { id: 'location', title: 'Location' }, ], }); async function saveToCsv(data) { await csvWriter.writeRecords(data); } (async () => { const products = await crawlAll('watches for men'); await saveToCsv(products); console.log(`Saved ${products.length} products to ecommerce_products.csv`); })();
表头数组将每个字段 id 映射到可读的列名,使生成的 CSV 在 Excel 或 Google Sheets 中打开时列名标注清晰。如果要将相同的记录存入数据库,结构化对象可以直接映射为表行:保持相同的字段名作为列名,每件商品插入一行。数据的形状不变,只是目标不同,因此你可以先从 CSV 开始,之后无需重写解析器即可迁移到数据库。
扩展规模与保持不被封禁
即使渲染已经处理好,零售平台仍然会监视爬虫特征的流量。一些好习惯可以保持运行顺畅,这些习惯适用于任何强防护的商业目标。
- 控制请求节奏。在紧密循环中猛打页面是最快触发限速的方式。分散请求并变换查询,而不是以全速爬取单一路径。
- 依靠轮换。住宅 IP 池将请求分散到许多真实用户地址,使任何单个 IP 都不会触发速率限制。Crawling API 为你处理这个问题;如果你自己搭建堆栈,这是最需要做好的部分。
- 关注状态码。如果运行开始返回挑战或错误,说明当前的速率或 IP 层级已经不够了。将其视为需要降速的信号,而不是可以忽略的噪声。
- 缓存和去重。存储已渲染的 HTML 或解析好的行,这样重新运行时不会重新获取你已经拥有的页面,既降低成本又减少请求量。
更广泛的策略请参阅如何在不被封禁的情况下抓取网站。如果你更倾向于通过轮换池路由自己的流量而不是使用托管 API,Smart AI Proxy 以直接代理端点的形式提供与 Crawling API 相同的住宅 IP 轮换。本指南中的"获取再解析"模式适用于大多数电商平台:从一个网站到另一个网站,只需更改选择器和分页参数。
抓取电商网站合法吗?
爬取电商网站是否被允许,取决于该网站的服务条款、你所在的司法管辖区以及你如何使用这些数据。许多电商平台在其条款中限制自动化访问,因此无论你的工具多么谨慎,爬取都可能违反这些条款。本文的代码不会改变这一点,它只是让技术层面的工作得以实现。请阅读网站的服务条款及其 robots.txt,并将两者视为你收集内容和请求频率的边界。
有几条原则值得坚守。只收集公开的商品数据:任何人无需账户即可看到的标题、价格、评分、库存状态、图片和商品链接。将请求量保持在不给网站服务器造成压力的水平,并遵守其声明的任何速率预期。远离登录后的任何内容,包括账户页面、购物车和订单历史,避免触及与可识别买家或卖家相关的个人数据。不要以未经许可的方式重新分发受版权保护的媒体,如商品摄影或描述;商业性地重用这些内容是与读取公开价格不同的另一个问题。
对于大规模或商业用途,如果存在官方渠道,优先使用官方渠道。许多大型零售商和市场平台发布了官方商品或联盟 API,为你提供有保障的数据结构和明确的使用权,这些才是你需要大量数据或商业复用时应该使用的工具。本指南刻意将范围限定在公开商品列表和搜索页面,因为这是让工作有据可依的界限。它不涵盖任何需要身份验证的内容、个人数据、私人账户或订单信息,也不涵盖任何绕过登录的尝试。如果你的项目需要的不止公开列表,官方 API 或数据协议才是正确的途径,而不是更聪明的爬虫。
核心要点
- 电商网站在客户端渲染商品列表。普通请求返回不完整的页面,因此在解析之前必须先渲染它。
-
你需要在同一次请求中同时具备渲染能力和可信 IP。带有 JS token 的 Crawling API 在一次调用中实现两者,
ajax_wait和page_wait控制等待内容的时长。 - Cheerio 负责提取。选取每个商品卡片,然后将标题、价格、评分、图片和商品 URL 映射到当前选择器,并预计这些哈希选择器会漂移。
- 通过遍历分页来扩展规模。读取总页数,循环从第 1 页到第 N 页追加页面参数,以合理的节奏在每一页复用同一个解析器。
- 坚守公开数据。遵守每个网站的服务条款和 robots.txt,对于大规模或商业用途优先使用官方商品 API,切勿触及登录、个人数据或你无权使用的受版权保护媒体。
常见问题
网络爬取和网络抓取有什么区别?
网络爬取是系统地浏览网站并跨多个页面收集数据、跟随链接和分页的过程。网络抓取是从给定页面中提取特定字段(如价格或标题)的过程。实际上两者协同工作:爬虫遍历列表页,抓取步骤从每个页面提取结构化字段。本指南中的脚本两者兼而有之。
为什么普通请求从电商网站返回不完整的数据?
因为大多数电商平台通过 JavaScript 和 AJAX 在客户端渲染价格、评分以及每个商品卡片的大部分内容。在页面脚本在浏览器中运行之前,初始 HTML 是不完整的,因此原始 HTTP 请求返回状态码 200,但关键字段缺失或为空。要获取完整页面,必须先渲染它,这正是 Crawling API 的 JS token 为你处理的事情。
我需要普通 token 还是 JS token?
对于电商搜索和列表页,请使用 JS token。普通 token 获取静态 HTML,对于客户端渲染的电商平台,返回的页面可能缺少价格或评分。JS token 在返回 HTML 之前先在真实浏览器中渲染页面,并且允许你传递 ajax_wait 和 page_wait 等等待参数,确保动态加载的商品卡片在 Cheerio 解析时已经存在。
我的选择器返回 null。是什么变了?
几乎肯定是网站的标记结构变了。电商平台构建工具生成的哈希类名会在不通知的情况下更改,而且搜索页和单个商品页之间也有所不同,所以上个月有效的选择器可能已经失效。在浏览器开发者工具中重新检查线上页面并更新选择器,优先使用 data-qa-locator 等稳定属性(如果存在)。定期维护选择器对任何生产爬虫来说都是正常的。
如何存储抓取到的商品数据?
快速获得结果时,可以使用 csv-writer 将记录写入 CSV 文件,这样数据可以在任何电子表格中打开。对于重复或较大规模的运行,将同样的结构化对象插入数据库,保持字段名作为列名,每件商品一行。无论哪种方式,记录形状不变,只是目标不同,因此你可以先从 CSV 开始,之后无需重写解析器即可迁移到数据库。
爬取电商网站时如何避免被封禁?
保持每个 IP 的请求速率较低,变换查询而不是循环同一路径,并通过轮换住宅 IP 路由,确保没有单个地址触发速率限制。Crawling API 为你管理轮换和可信 IP 池;如果你自己搭建堆栈,这是值得投入的部分。关注状态码,当你开始看到挑战时降速,而不是强行穿越。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
