Amazon 的搜索结果页面是电商领域最丰富的公开数据集之一:每次查询都会返回一个按排名排列的商品网格,包含标题、价格、评分、评价数量以及指向各商品列表的链接。追踪这个网格随时间的变化,能让你了解哪些商品正在热销、竞争对手在某个关键词下的排名情况,以及某个品类内价格的走势。这些数据是公开的,但通过脚本可靠地抓取它才是难点所在,因为 Amazon 使用 JavaScript 渲染页面的大部分内容,并且能快速识别并拦截自动化流量。

本指南将向你展示如何以全栈方式使用 Next.js 抓取 Amazon SERP。你将构建一个可运行的小型 Next.js 应用,其服务端 Action 调用 Crawling API 获取已渲染的 Amazon 搜索页面,用 Cheerio 解析每个商品,并在 React 网格中渲染结果。Crawlbase 的 token 始终保留在服务端,绝不暴露给浏览器。本演示仅限于公开搜索数据,文末的合法性部分并非样板文字,请在将此方案用于实际大规模请求前仔细阅读。如果你想要纯脚本版本,请参阅如何使用 Crawling API 抓取 Amazon 搜索页面

你将构建什么

一个使用 App Router 的 Next.js 应用,包含一个服务端 Action:接受搜索关键词,通过 Crawling API 获取已渲染的 Amazon SERP,用 Cheerio 解析,并将每个商品的结构化记录返回给负责渲染网格的客户端组件。每个商品提取以下字段:

  • 名称(Name):商品卡片上显示的标题,例如"Apple iPhone 15 Pro Max 256GB"。
  • 价格(Price):显示的标价,例如"$1,199.00"。
  • 图片(Image):用于渲染商品卡片的缩略图 URL。
  • 评分(Rating):存在时显示的星级评分文本,例如"4.7 out of 5 stars"。
  • 评价数(Reviews):评分旁显示的用户评价数量。
  • 链接(URL):指向对应商品详情页的绝对链接。

为什么普通请求在 Amazon 上会失败

如果你使用普通 HTTP 客户端请求 Amazon 搜索 URL,很少能获得在浏览器中看到的整洁商品网格。有两个原因使你处于不利地位。首先,Amazon 使用 JavaScript 在浏览器中渲染价格、评分和商品卡片的部分内容,因此原始 HTML 返回时可能并不完整。其次,Amazon 会快速标记自动化流量:数据中心 IP 和不像真实浏览器的请求模式,在你访问商品页面之前就会收到 CAPTCHA、"机器人检查"插页广告或直接封禁。

因此,一个可用的 Amazon SERP 爬虫需要在单次请求中同时具备两点:一个能真正渲染页面的浏览器,以及一个平台视为真实访客的 IP。你可以自行组合无头浏览器和轮换住宅代理池,但将它们整合在一起并保持健康运行才是绝大部分工作所在。Crawling API 将两者合并为一次调用:你发送带有 JavaScript token 的 URL,它在可信 IP 后面渲染页面,并将完整 HTML 返回给你进行解析。在 Next.js 服务端 Action 中执行此操作,可以让 token 远离客户端,让请求在服务端进行,这正是它应有的方式。

为什么使用 JS token

Crawlbase 提供两种 token 类型。普通 token 获取静态 HTML;JavaScript(JS)token 会先在真实浏览器中渲染页面。Amazon 的关键结果字段是在客户端加载的,因此 JS token 能为你提供最完整的页面。使用普通 token 可能返回不完整的网格,缺少价格或评分,导致没有可靠内容可供解析。

前提条件

在编写任何代码之前,你需要准备几样东西。这些都不需要太长时间。

Node.js 18.17 或更高版本。Next.js 需要较新的 Node 运行时。使用 node --version 确认你的版本。如果还没有安装,可以从 Node.js 官网或通过 nvm 等版本管理器安装。

React 和 Next.js 基础知识。你应该熟悉 React 组件、hooks 以及运行 Next.js 应用。如果 App Router 和服务端 Action 对你来说是新概念,官方 Next.js 文档涵盖了这两者,我们关于如何使用 Node.js 构建网页爬虫的指南也是抓取部分的好参考。

Crawlbase 账户和 JS token。注册账户,打开控制台,从账户文档页面复制你的 JavaScript(JS)token。像对待密码一样对待这个 token:它用于验证你的请求,因此请将其保留在服务端,不要纳入版本控制。我们将从环境变量中读取它,绝不将其硬编码在客户端组件中。

设置项目

创建一个 Next.js 应用脚手架,然后安装爬虫所需的两个库。当创建向导询问时,选择 App Router;其他默认选项保持不变即可。

bash
node --version

npx create-next-app@latest amazon-serp-scraper
cd amazon-serp-scraper

npm install crawlbase cheerio

两个依赖完成核心工作:crawlbase 是 Crawling API 的官方 Node 客户端,cheerio 使用类 jQuery 的 API 解析返回的 HTML,让你能通过 CSS 选择器提取各个字段。如果选择器对你来说还比较陌生,关于 XPath 和 CSS 选择器的入门指南是很好的参考。现在,在项目根目录下将 token 放入环境文件,以便它保留在服务端:

bash
# .env.local (never commit this file)
CRAWLBASE_JS_TOKEN=YOUR_CRAWLBASE_TOKEN

由于 CRAWLBASE_JS_TOKEN 没有 NEXT_PUBLIC_ 前缀,Next.js 会将其保留在服务端,永不将其打包进客户端 JavaScript。这正是在服务端 Action 中执行请求的全部意义所在。

步骤 1:在服务端 Action 中获取已渲染的 SERP

创建 app/actions.js。顶部的 'use server' 指令将文件中的所有内容标记为服务端 Action,因此这段代码只会在服务端运行,token 在那里是安全的。导入 CrawlingAPI 客户端,从环境变量中读取 token,并请求由关键词构建的 Amazon 搜索 URL。在解析之前检查状态码,能让失败情况清晰呈现,而不是悄无声息地通过。

javascript
'use server';

const { CrawlingAPI } = require('crawlbase');

const api = new CrawlingAPI({ token: process.env.CRAWLBASE_JS_TOKEN });

async function fetchSerpHtml(keyword) {
  const query = encodeURIComponent(keyword.trim());
  const pageUrl = `https://www.amazon.com/s?k=${query}`;
  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;
}

两个等待选项对于像这样的客户端渲染目标非常重要。ajax_wait 告诉 API 等待异步内容加载完成,page_wait 在加载后保持固定的毫秒数,以便延迟渲染的元素在页面被捕获前出现。五秒是一个合理的起点;如果价格或评分仍然为空,可以适当增加。此时你已经手握渲染好的 Amazon HTML,通过服务端获取,token 未暴露给浏览器。在编写任何选择器之前,这一步确认了渲染工作正常。

Crawlbase Amazon Scraper

那个 api.get 调用正在处理 Amazon 对你发起的难题:它接受你的 JS token,在真实浏览器中运行搜索页面以渲染价格和评分,在服务端通过住宅 IP 轮换让请求看起来像真实访客,并将完整 HTML 交给你的服务端 Action。你无需自己运行无头浏览器集群和代理池。先在免费层级将其指向公开搜索页面进行测试。

步骤 2:使用 Cheerio 解析每个商品

手握渲染好的 HTML,将其加载到 Cheerio 中并遍历结果卡片。Amazon 将每个搜索结果布局在标有 div[data-component-type="s-search-result"] 的重复块中,因此你选择每张卡片,然后从中读取名称、价格、图片、评分、评价数量和商品链接。以防御性方式读取每个字段,可以防止一个缺失值导致整个运行崩溃。将此函数添加到同一个 app/actions.js 文件中。

javascript
const cheerio = require('cheerio');

function parseProducts(html) {
  const $ = cheerio.load(html);
  const products = [];

  $('div[data-component-type="s-search-result"]').each((_, el) => {
    const card = $(el);
    const name = card.find('h2 span').first().text().trim();
    if (!name) return;

    const path = card.find('h2 a').attr('href');

    products.push({
      name,
      price: card.find('.a-price .a-offscreen').first().text().trim() || null,
      image: card.find('img.s-image').attr('src') || null,
      rating: card.find('.a-icon-alt').first().text().trim() || null,
      reviews: card.find('.a-size-base.s-underline-text').first().text().trim() || null,
      url: path ? `https://www.amazon.com${path}` : null,
    });
  });

  return products;
}

有几个细节保证了代码的健壮性。价格位于 .a-price 块内的隐藏 .a-offscreen span 中,这是显示金额最干净的单一来源,因此我们直接读取它,而不是拼接符号和数字。当元素缺失时,每个字段回退到 null,这很常见,因为赞助卡片和一些商品列表会省略评分或评价数量。商品链接是相对路径,所以我们在其前面加上 Amazon 域名,得到绝对的可点击 URL。

选择器会漂移

Amazon 的类名和布局(s-search-resulta-pricea-offscreens-image 等)会在没有通知的情况下更改,并且因地区和查询类型而有所不同。将上面的选择器视为起始模板,而非固定契约。当某个字段返回 null 时,在浏览器开发者工具中重新检查实时页面并更新选择器。定期维护选择器对于任何生产级爬虫来说都是正常的,这并不意味着出了什么问题。

步骤 3:暴露一个服务端 Action 和数据结构

现在,将获取和解析逻辑整合成一个单独导出的服务端 Action,供 UI 调用。它接受关键词,返回解析后的商品,由于所有内容都保留在 'use server' 文件中,token 永远不会泄露给客户端。将其添加到 app/actions.js 的底部。

javascript
export async function scrapeAmazonProducts(keyword) {
  if (!keyword || !keyword.trim()) return { products: [], error: 'Empty search' };
  try {
    const html = await fetchSerpHtml(keyword);
    if (!html) return { products: [], error: 'Fetch failed' };
    const products = parseProducts(html);
    return { products, error: null };
  } catch (err) {
    console.error(err);
    return { products: [], error: 'Something went wrong' };
  }
}

返回简单的 { products, error } 对象而不是抛出异常,使客户端组件保持简洁:当 products 有数据时渲染网格,当 error 被设置时显示提示信息,UI 中无需散布 try/catch。以上所有代码都在服务端运行,因此 token、Crawling API 调用和 Cheerio 解析全部不会进入浏览器包。

步骤 4:在 React 网格中渲染结果

客户端部分很简洁。一个客户端组件持有搜索状态,在提交时调用服务端 Action,并将返回的商品映射成卡片。使用 'use client' 指令创建 app/page.jsx,以便使用 React hooks,然后直接导入并调用 Action;Next.js 为你处理服务端往返。

javascript
'use client';

import { useState } from 'react';
import { scrapeAmazonProducts } from './actions';

export default function Home() {
  const [keyword, setKeyword] = useState('');
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  async function handleSubmit(event) {
    event.preventDefault();
    setLoading(true);
    setError(null);
    const result = await scrapeAmazonProducts(keyword);
    setProducts(result.products);
    setError(result.error);
    setLoading(false);
  }

  return (
    <main className="container">
      <form onSubmit={handleSubmit}>
        <input
          value={keyword}
          onChange={(e) => setKeyword(e.target.value)}
          placeholder="Search Amazon (iPhone, laptop, headphones...)"
        />
        <button type="submit" disabled={loading}>
          {loading ? 'Searching...' : 'Search'}
        </button>
      </form>

      {error && <p className="error">{error}</p>}

      <div className="grid">
        {products.map((product, i) => (
          <a key={i} href={product.url} target="_blank" className="card">
            {product.image && <img src={product.image} alt={product.name} />}
            <h3>{product.name}</h3>
            <p className="price">{product.price}</p>
            <p className="rating">{product.rating} ({product.reviews || 0})</p>
          </a>
        ))}
      </div>
    </main>
  );
}

这就是整个前端部分。表单在每次按键时更新 keyword;提交时翻转加载标志,等待服务端 Action,并存储返回的商品和错误。网格将每个商品映射成一张卡片,在新标签页中链接到 Amazon 商品列表。可以用 Tailwind 或普通 CSS 进行样式设计;数据流才是这里的重点。用 npm run dev 启动应用,打开 localhost:3000,搜索类似"iPhone 15 Pro Max"的内容,已渲染并解析的商品便会出现在网格中。

输出结果示例

服务端 Action 在到达网格之前,就已返回一个干净的记录数组,每个商品对应一条记录。打印 result.products,你会得到如下结构,可以直接渲染、写入 JSON 或持久化到数据库。

json
[
  {
    "name": "Apple iPhone 15 Pro Max 256GB Natural Titanium",
    "price": "$1,199.00",
    "image": "https://m.media-amazon.com/images/I/81fxjeu8fdL._AC_UL320_.jpg",
    "rating": "4.7 out of 5 stars",
    "reviews": "1,284",
    "url": "https://www.amazon.com/dp/B0CHX1W1XY"
  },
  {
    "name": "Samsung Galaxy S24 Ultra 256GB Unlocked",
    "price": "$1,099.99",
    "image": "https://m.media-amazon.com/images/I/71CXi9gZ4mL._AC_UL320_.jpg",
    "rating": "4.5 out of 5 stars",
    "reviews": "912",
    "url": "https://www.amazon.com/dp/B0CMDRCZBP"
  }
]

跨结果页面扩展

一页结果只是演示;真实任务需要遍历分页。Amazon 通过 page 查询参数暴露页码,因此你可以通过相同的服务端 Action 获取每一页,用相同的函数解析,并收集所有行。由于每个结果页面共享相同的卡片结构,你已经编写好的解析器无需任何更改即可跨所有页面工作。将分页变体添加到 app/actions.js

javascript
export async function scrapeAmazonPages(keyword, totalPages) {
  const all = [];
  const query = encodeURIComponent(keyword.trim());
  for (let page = 1; page <= totalPages; page++) {
    const url = `https://www.amazon.com/s?k=${query}&page=${page}`;
    const response = await api.get(url, { ajax_wait: 'true', page_wait: 5000 });
    if (response.statusCode === 200) {
      all.push(...parseProducts(response.body));
    }
  }
  return all;
}

若要用完整商品详情(完整描述、所有图片、购买框、完整评价数据)丰富每一行,可以从每张卡片中获取 url,通过同一个 API 客户端获取该独立商品页面,然后为商品布局编写一个小型解析器。模式是相同的:渲染,然后解析。有关渲染密集型目标的更多信息,请参阅如何爬取 JavaScript 网站

保持畅通无阻

即使渲染已经处理好,Amazon 仍会监视爬虫形状的流量。一些习惯可以让运行保持健康,它们适用于任何难以攻克的商业目标。

  • 控制请求节奏。在紧密循环中猛烈轰炸页面是获得 CAPTCHA 最快的方式。分散请求并变换关键词,而不是以最高速度爬取同一路径。
  • 善用轮换。住宅 IP 池将请求分散到许多真实用户地址,避免任何单一地址触发速率限制。Crawling API 为你处理这些;如果你自己搭建技术栈,这是最需要做好的部分。
  • 读取状态码。当运行开始返回挑战或非 200 响应时,说明当前速率或 IP 层级已不再足够。将其视为退出信号,而非可以忽略的噪声。

更广泛的操作手册,请参阅如何在不被封锁的情况下抓取网站。如果你宁愿通过自己的流量路由轮换池,而非使用托管 API,Smart AI Proxy 提供与之相同的住宅 IP 轮换,可作为直接替换的代理端点。

抓取 Amazon 合法吗?

是否允许抓取 Amazon,取决于 Amazon 的使用条款、你所在的司法管辖区以及你如何使用数据。Amazon 的条款限制自动访问,因此无论你的工具多么谨慎,抓取行为可能都违反这些条款。这里的任何代码都不会改变这一点;它只是让技术层面的工作能够运转。请阅读 Amazon 的使用条款及其 robots.txt,并将两者视为你收集内容的边界。

有几条值得遵守的准则。只收集公开搜索数据:任何人无需账户即可看到的商品名称、价格、评分、评价数量、缩略图和商品链接。尊重 Amazon 说明的速率预期,将请求量保持在足够低的水平,以免给其服务器造成压力。避免个人数据,包括搜索结果页面上显示的公开评价文字和数量之外的任何可识别评论者相关信息。不要大量转发受版权保护的商品图片或描述;引用它们,不要将其作为自己的内容重新发布。如果你计划出于商业目的重用数据,请获得许可或正式协议,而不是假设沉默即是同意。

对于大量或商业用途,Amazon 提供官方渠道,包括适用于联盟商的商品广告 API,以及适用于注册企业的 Amazon 卖家和广告 API,当你需要大量数据、保证结构或商业权利时,这些才是正确的工具。本指南有意仅限于公开搜索和商品列表页面,因为这是让工作保持合理可辩护的边界。它不涵盖登录后的任何内容、买家或卖家账户数据、订单历史、私信,也不涵盖任何试图绕过身份验证或将解决 CAPTCHA 作为访问手段的行为。如果你的项目需要公开搜索数据以上的内容,Amazon 的官方 API 或数据协议才是正确路径,而不是更聪明的爬虫。

回顾

核心要点

  • Amazon 在客户端渲染 SERP。普通请求返回不完整或受挑战的页面,因此必须先渲染再解析。
  • 在服务端 Action 中执行请求。'use server' 文件将 Crawlbase token 保留在服务端的 process.env 中,完全不进入浏览器包。
  • 一次调用处理渲染和可信 IP。使用 JS token 的 Crawling API 同时完成两者;ajax_waitpage_wait 控制等待内容的时长。
  • Cheerio 负责提取。选择每个 s-search-result 卡片,然后将名称、价格、图片、评分、评价数量和 URL 映射到当前选择器,并预期这些选择器会漂移。
  • 保持在公开数据范围内。尊重 Amazon 的条款和 robots.txt,对于大量或商业用途优先使用官方 API,永远不要触碰登录信息、个人数据或订单历史。

常见问题

为什么使用 Next.js 服务端 Action 而不是客户端请求?

因为 Crawlbase token 绝不能到达浏览器。服务端 Action 只在服务端运行,所以你可以从 process.env.CRAWLBASE_JS_TOKEN 读取 token,进行 Crawling API 调用,并用 Cheerio 解析,所有这些都不会打包进客户端 JavaScript。客户端组件只需调用 Action 并渲染返回的内容。这也让 Cheerio(一个 Node 库)保持在服务端,因为它无法在浏览器中运行。

为什么普通请求从 Amazon 返回不完整的数据?

因为 Amazon 使用 JavaScript 在客户端渲染价格、评分和每个结果卡片的部分内容,并且会用 CAPTCHA 和机器人检查来挑战自动化流量。原始 HTTP 请求可能会返回关键字段缺失的页面,或者被直接拦截。要获得完整页面,你必须在可信 IP 后面渲染它,这正是 Crawling API 的 JS token 为你处理的事情。

对于 Amazon,我需要普通 token 还是 JS token?

使用 JS token。普通 token 获取静态 HTML,而在 Amazon 上这可能导致价格或评分缺失。JS token 在将 HTML 交回之前会在真实浏览器中渲染页面,因此当 Cheerio 解析时,结果字段都是存在的。

我的选择器返回 null,发生了什么变化?

几乎可以肯定是 Amazon 的标记发生了变化。其 s-search-result 卡片、a-price 块和 a-offscreen span 会在没有通知的情况下更改,并因地区和查询类型而有所不同,因此上个月有效的选择器可能会失效。在浏览器开发者工具中重新检查实时页面并更新选择器。定期维护选择器对于任何生产级爬虫来说都是正常的。

这与纯 Python 或 Node 脚本方式有何不同?

数据和 Crawling API 调用是相同的;不同的是全栈框架。这里的请求和解析位于 Next.js 服务端 Action 中,React 网格在浏览器 UI 中渲染结果,token 安全地保留在服务端。如果你只需要一个打印 JSON 的脚本,更简单的方式是直接使用 Crawling API 抓取 Amazon 搜索页面,或者使用 Node.js 构建网页爬虫,不需要框架。

我可以从 Amazon 抓取买家或卖家的个人数据吗?

不行,本指南也不涵盖这方面内容。账户数据、订单历史和私信位于登录后,不属于公开数据。抓取登录墙后的内容、个人数据,或绕过身份验证或 CAPTCHA 来访问这些内容,超出了本指南的范围,并违反 Amazon 的条款。若需获得授权访问,正确途径是 Amazon 的官方 API 或许可协议。

开始构建

大规模爬取任何站点,无需与基础设施对抗。

Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。

自助开通 · 无需销售通话 · 提供企业级爬取量