# KB-AMAZON-003：Amazon US Review 页面与翻页接口技术实录

| 项目 | 内容 |
|---|---|
| 平台/站点 | Amazon 美国站 `www.amazon.com` |
| 页面 | Review 页 `/portal/customer-reviews/{ASIN}/`；商品页 `/dp/{ASIN}` |
| 接口 | `/portal/customer-reviews/ajax/reviews/get/ref=…`（翻页 XHR） |
| 对应 Tool | `amazon_collect_review_content` |
| 对应脚本 | `amazon-review-collector-demo.user.js` 0.8.1 |
| 字段 | Review ID、采集/子 ASIN、星级、标题、正文、评论者及主页、日期及原文、国家、变体、已验证购买、Vine、Helpful 数量、Review/图片/视频链接、页面 Review 数量、Global Ratings |
| 整理日期 | 2026-09-15 |
| 当前证据等级 | 登录态真实页面结构 + 真机抓包逐字重放 + 合成 HTML fixture 单测；飞书写入见 KB-FEISHU-002 |

## 1. 不能越过的证据边界

本文结论全部来自**已登录会话的真实页面**（作者本机 Chrome，经 CDP 附着）与 Amazon 自己发出的请求。文中只保留 DOM 结构、请求形状、HTTP 状态码、响应字节数和字段名，**不保存 Cookie、CSRF token 值或账号信息**。

页面结构不是 Amazon 的公开承诺。selector、token 位置和请求头都会随改版变化；任何一条结论过期都必须重新实测，不能用推断替代。

## 2. URL 与原生筛选参数

Review 页的标准形态：

```text
https://www.amazon.com/portal/customer-reviews/{ASIN}/?_encoding=UTF8&reviewerType=all_reviews&sortBy=recent
```

| 参数 | 取值 | 说明 |
|---|---|---|
| `sortBy` | `recent` / `helpful` | 最新 / 热门 |
| `reviewerType` | `all_reviews` / `avp_only_reviews` | 全部 / 仅已验证购买 |
| `filterByStar` | `all_stars`、`five_star` … `one_star`、`positive`、`critical` | 星级 |
| `mediaType` | `all_contents` / `media_reviews_only` | 全部 / 仅图片和视频 |
| `formatType` | `current_format` | 仅看当前子体；不需要限定子体时不要带 |

`_encoding=UTF8` 无害；`ref=cm_cr_arp_…` 只是来源追踪，不是必需参数。传入任一**子 ASIN**，Amazon 会把它落到该 Listing 的 Review 页，因此「输入 ASIN 采整个 Listing」是默认行为。

## 3. 分页：不是 URL 翻页，必须走 XHR

- Review 页**每页固定 10 条**。
- **纯 GET `&pageNumber=2` 不翻页**：实测返回的评论集合与第 1 页完全相同（同一批 Review ID），只是页面外壳重新渲染。想靠改 URL 翻页的思路是错的。
- 下一页参数在 `data-reviews-state-param` 属性里（JSON）。**同一页面会出现多个**，其中若干是 `pageNumber: 1` 且没有 token 的噪声项；必须取 **`pageNumber` 最大且带 `nextPageToken`** 的那一个。
- 真正翻页的请求：

```text
POST https://www.amazon.com/portal/customer-reviews/ajax/reviews/get/ref=cm_cr_getr_d_paging_btm_{page}
```

| Body 字段 | 值 | 备注 |
|---|---|---|
| `sortBy` / `reviewerType` | 同 URL 参数 | |
| `filterByStar` / `mediaType` | 不筛选时传空串 | |
| `formatType` | 不限定子体时传空串 | |
| `pageNumber` | 目标页码 | |
| `nextPageToken` | 上一页给的 token | 缺了会被拒 |
| `shouldAppend` | `true` | |
| `deviceType` / `canShowIntHeader` / `reviewsShown` | `desktop` / `true` / `undefined` | 与页面一致即可 |
| `reftag` | `cm_cr_getr_d_paging_btm_{page}` | 与 URL 的 `ref=` 一致 |
| `pageSize` | `10` | |
| `asin` | 目标 ASIN | |
| `scope` | `reviewsAjax1` | |

- 响应体是**流式命令序列**：多段 JSON 用 `\n&&&\n` 分隔。每段若形如 `[action, selector, html]`，取索引 2 的 HTML 片段拼接后再解析；非命令行（遥测）应忽略而不是报错。

## 4. CSRF token：本文最重要的一节

请求头名是 `anti-csrftoken-a2z`，但**值必须取自评论页自己的 token**：

```html
<span id="cr-state-object" data-state="{&quot;reviewsCsrfToken&quot;:&quot;…&quot;, …}">
```

- 该值是 `reviewsCsrfToken` 字段内容（实测 **64 字符**，且**已 percent-encode**，形如 `hKvJbDQ…%2Fdv…`）。
- 页面上还散落着**多处同名的 `anti-csrftoken-a2z`**：`<meta name="anti-csrftoken-a2z">`、以及 HTML 转义 JSON 里的 `&quot;ajaxHeaders&quot;:{&quot;anti-csrftoken-a2z&quot;:…}`。**它们和 `reviewsCsrfToken` 不是同一个值。**
- **用错 token 的结果是 HTTP 403 + 响应体 "Page Not Found"（约 2296 字节）**，看起来像页面不存在，极易误判成「Amazon 改版」或「URL 写错」。

因此脚本里那条曾出现的报错「页面存在下一页但没有 anti-csrftoken-a2z」，真实原因往往是**取到了别处的同名 token**，而不是 token 消失。

`x-amzn-flow-closure-id`（取自 `<meta name="flow-closure-id">`）：Amazon 自己的翻页请求会带这个头，但 **2026-09-15 实测去掉它仍然返回 200，属于非必需**。带上更贴近真实请求，但不能把它当成 403 的原因。

### 定位这类问题的方法（可复用）

1. monkey-patch `XMLHttpRequest.prototype.open/setRequestHeader/send` 与 `window.fetch`，把 url / headers / body 记进数组；
2. 点击页面上的 “Show 10 more reviews”，读出 Amazon **自己**发出的请求；
3. **逐字重放**该请求（应得 200）——证明请求形状没问题；
4. 再把自己的请求与它做**结构化 diff**：逐个头/参数只回答「相同 / 不同 / 缺失」，**不回显 token 明文**。差异收敛到哪一项，根因就是哪一项。

本次就是靠第 4 步锁定「唯一差异是 token 值」，进而找到 `cr-state-object`。

## 5. HTML 解析坑

### 不要用「非贪婪正则 + backreference」匹配成对标签

```js
/<([a-z][\w:-]*)\b[^>]*>([\s\S]*?)<\/\1\s*>/i   // 危险
```

`([\s\S]*?)` 是最短匹配。评论正文里经常有**同名的内层标签**（最典型是 Amazon 用于高亮的 `<span class="a-text-bold">`），于是正则在**第一个内层 `</span>`** 就闭合，正文被截断。实测：`I have used this for <span class="a-text-bold">three months</span> and it still works perfectly.<br>Highly recommend.` 只解析出前半段。

正确做法是**按标签深度计数**，找到真正配对的最外层闭合标签，并处理 void 元素（`br`、`img`、`input`…）与不完整片段。

### HTML 实体转义的 JSON

Amazon 会把配置 JSON 以实体转义塞进属性（`&quot;` 代替 `"`）。直接匹配 `"key":"value"` 会失败；应先把实体反转义（至少 `&quot;` / `&#34;` / `&#x22;`）再匹配，或把 `&quot;` 纳入分隔符字符类。

## 6. 字段来源（`data-hook`）

| 字段 | 来源 |
|---|---|
| Review ID | `data-hook="review"` 节点的 `id`（部分页面为 `data-review-id`）；跨页与续跑的去重键 |
| 星级 | `review-star-rating` / `cmps-review-star-rating` 的文本或 `aria-label` |
| 标题 / 链接 | `review-title`（其文本常带 `N out of 5 stars` 前缀，需去掉） |
| 日期 / 国家 | `review-date`：`Reviewed in {国家} on {月} {日}, {年}` |
| 变体 / 子 ASIN | `format-strip` 的文本与 `href`（href 路径里含子 ASIN） |
| 已验证购买 / Vine | `avp-badge` / `vine-review-badge` |
| Helpful 数量 | `helpful-vote-statement`；`one person found this helpful` 要当 1，不能用数字解析 |
| 正文 | `review-body` |
| 图片 | `review-image-tile` 的 `src` / `data-src` |
| 视频 | `<video>/<source>` 的 `src`、`data-video-url`，以及 JSON 里的 `videoUrl` |
| 页面 Review 数量 | `cr-filter-info-review-rating-count`，文案可能带 `more than`（是下限） |
| Global Ratings | 页面 `N global ratings` 文案；**与文字 Review 数量不是一回事，不能混用** |

## 7. 访问受限与失败诊断

- `429`、登录页、`/edgex/guard/`、`Robot Check` / `validateCaptcha` 一律**如实报错并保留已采集结果 + 续跑游标**，不绕过验证。
- 续跑游标**不含 Cookie 或 CSRF token**；续跑时重新加载首页取新 token，并从保存的 `nextPageToken` 继续。
- 报错信息必须带诊断（落地 URL、页面标题、已解析评论数）。只报「页面结构可能已变化」无法定位问题。

## 8. 已复现结果（2026-09-15，登录会话）

| 操作 | 结果 |
|---|---|
| GET 首页 `/portal/customer-reviews/B09LD2CD1L/` | 200；解析出 11 条卡片；取到 `reviewsCsrfToken`；取到 `pageNumber=2` 的 `nextPageToken` |
| 用页面同款 token + 空 `formatType` POST 翻页 | **200 / 76420 字节 / 10 段命令 / 含 review 卡片** |
| 同上但去掉 `x-amzn-flow-closure-id` | 仍 **200** → 该头非必需 |
| 改用页面别处的 `anti-csrftoken-a2z` | **403 + "Page Not Found"** → 根因确认 |
| 纯 GET `&pageNumber=2` | 200，但内容与第 1 页**完全相同** → 不翻页 |

该 ASIN 的页面显示 6611 条评论。实际可采范围同时受 `review_limit_per_asin`、单轮请求上限和「Amazon 前台只提供最近若干页」约束；`0` 表示翻到没有下一页为止，不等于能拿到全部历史评论。

## 9. 数据组织（与脚本约定配套）

- **本地**：一次任务一份报告（数据集 `amazon-review-reports`，主键 `任务编号`），全部 Review 明细收在「完整报告」字段。
- **飞书**：明细表，每条 Review 一行（29 列全文本）；写入前按「任务编号」查询已有 Review ID，只补写未成功项。
- 本地单条记录上限 **1 MiB**：超限要明确报错并保留报告，**不截断原文**。

## 10. 未验证范围

- 非 `amazon.com` 站点、未登录会话、移动端 UA/布局、Amazon A/B 新评论页。
- 视频 Review 只记录页面**明确披露**的 URL，不做解密或绕过站点保护。
- 本文不含飞书授权、写入与限流细节，见 [KB-FEISHU-002](/docs/knowledge/feishu-table-setup)。

对改版或未覆盖页面，请提供可公开访问的 ASIN/URL、站点与登录状态、脱敏后的评论区 DOM 或页面诊断，以及人工看到的预期字段。**不要提供 Cookie、登录凭据或订单隐私数据。**
