Skip to content

Latest commit

 

History

History
328 lines (242 loc) · 23.3 KB

File metadata and controls

328 lines (242 loc) · 23.3 KB

Amazon Product Scraper — 技术方案

状态:草稿 v0.1(架构主干)。反爬细节与选型依据待两份调研回填。

1. 项目定位

一个生产可用的 Amazon 商品数据抓取器,Python 实现,同时可作为库、CLI 和 Docker 镜像使用。

差异化定位(相对 GitHub 上现有同类项目):

现有项目的通病 本项目的做法
选择器硬编码在解析函数里,Amazon 一改版就全线崩 选择器外置为 YAML registry,多级候选链 + 漂移告警
抓取和解析耦合,没有真实 HTML 就没法测 解析层是纯函数,fixture 驱动的离线回归测试
反爬 = 换个 User-Agent 分层对抗:TLS 指纹 → header 一致性 → 代理调度 → 自适应限速 → 浏览器降级
封了就报错,跑到一半崩了从头再来 封锁分类 + 断路器 + 检查点续跑
只支持 amazon.com marketplace 配置化,货币/日期/数字格式本地化归一
无可观测性,不知道为什么失败 结构化指标:成功率、封锁率、各字段抽取命中层级

2. 分层架构

                    ┌──────────────┐
                    │   CLI / API  │
                    └──────┬───────┘
                           │
                    ┌──────▼───────┐
                    │   Pipeline   │  调度 · 去重 · 重试 · 检查点
                    └──────┬───────┘
                           │
         ┌─────────────────┼─────────────────┐
         │                 │                 │
   ┌─────▼─────┐    ┌──────▼──────┐   ┌─────▼─────┐
   │   Fetch   │◄───┤   Antibot   │   │   Parse   │
   │           │    │             │   │           │
   │ http      │    │ fingerprint │   │ extractor │
   │ browser   │    │ ratelimit   │   │ registry  │
   └─────┬─────┘    │ detect      │   │ normalize │
         │          │ backoff     │   └─────┬─────┘
   ┌─────▼─────┐    │ session     │         │
   │   Proxy   │◄───┤ captcha     │   ┌─────▼─────┐
   │   pool    │    └─────────────┘   │  Storage  │
   └───────────┘                      └───────────┘

关键约束:Parse 层不知道 Fetch 层存在。解析器签名恒为 (html: str, ctx: ParseContext) -> Model,纯函数,无 IO。这让整个解析层可以用冻结的 HTML fixture 完整测试,也让 DOM 漂移能在 CI 里被抓到而不是在生产里。

3. 数据获取策略:按内容类别路由,不按失败信号降级

这一节的设计被实测推翻过一次,详见 EVIDENCE.md §1

为什么不是「失败了再降级」:实测显示纯 HTTP 抓 JS 注入页时,返回的是 HTTP 200 + 一个看起来正常的 348 字节导航壳,不是错误、不是异常、不是 4xx。 「HTTP 失败就升级到浏览器」这个直觉设计根本不会被触发。同时另一组实测证明 headless 不是 HTTP 的超集(headless_jc_covers_both: false),两者是互换关系, 而 headless 贵 5.1× 且区间不重叠。

所以路由规则是:按字段的渲染来源静态分类,用内容断言兜底。

第 1 级 — 复现底层 JSON 请求(首选) 实测中性价比最高的路径:直打页面背后的 JSON 端点,与渲染整页几乎同耗时(0.416s vs 0.412s), 但拿到的是全量结构化数据而非 0 个卡片。能走这条就绝不解析 HTML。

第 2 级 — HTTP + 页面内嵌结构化数据 ⚠️ 实测:Amazon 商品页上没有 JSON-LDapplication/ld+json 出现次数为 0)。 所有声称可以用 JSON-LD 的教程都是错的。实际可用的三条通道是 <script type="a-state"> 状态注水块、dimensionValuesDisplayData 变体映射、 colorImages 图片清单 —— 详见 AMAZON-RECON.md §1.4

这些服务于页面自身的 JS 逻辑,改版频率远低于视觉层 DOM。 实测:对服务端渲染的内容,HTTP 的输出与浏览器逐字节相同

注意后两者是 JS 对象字面量而非合法 JSON(有尾逗号和单引号 key), json.loads 会直接失败 —— 需要括号配平扫描器取片段后规范化。

第 3 级 — HTTP + 选择器解析 主力路径的兜底。成本最低、吞吐最高。

第 4 级 — 浏览器 仅用于确知由客户端 JS 注入的字段,以及第 2/3 级的内容断言失败时。 浏览器实例昂贵(内存、启动耗时、进程泄漏风险),必须是显式路由目标而非默认路径。

降级触发器 = 字段级内容断言。 每个页面类型声明一组必现字段(如商品页必有 ASIN + 标题), 断言失败才升级。绝不用 HTTP 状态码或异常作为触发条件。

反面教材:实测中某工具的默认「智能引擎路由」比纯 HTTP 慢 4.86×、比浏览器慢 2.48×, 而输出与浏览器逐字节相同 —— 智能路由是最贵且不更准的选项。本项目的路由必须显式可控、可强制指定。

每次抓取记录实际走到了哪一级。这个分布是健康度指标 —— 第 4 级占比突然上升,说明前几级正在失效。

3.1 浏览器层:等待条件才是全部

「上了无头浏览器就解决动态内容」被实测证伪:在 800ms 延迟注入的页面上, Navigate 后直接读(317ms)和等 body ready(107ms)都漏,只有等元素可见才拿到(912ms)。 另一组同构对照:无等待 0 项 / 固定 100ms 0 项 / 固定 1200ms 8 项 / 选择器等待 8 项。

→ 浏览器是必要不充分条件,等待条件是决定性的。本项目的浏览器 fetcher: 以目标内容选择器就绪为等待条件,禁止固定 sleep、禁止等 body ready。

且等待要事件驱动而非轮询。实测的 auto-wait 过冲对比:固定 0.5s 轮询的驱动上, 100ms 和 400ms 两个完全不同的注入延迟都恰好耗时 567ms;退避 sleeper 的驱动在 1500ms 延迟上过冲到 2858ms(近 2×)。 单页多等 500ms × 10 万页 = 14 小时。

4. 反爬对抗:分层设计

对抗不是单点技巧,是一组必须互相一致的信号。任何一层的不一致都会成为指纹。

4.1 传输层指纹

TLS ClientHello(JA3/JA4)、HTTP/2 SETTINGS 帧与伪头顺序。Python 默认的 requests/httpx 在这一层有极其明显的特征。

4.2 请求头一致性

声称是某个浏览器,就必须在所有维度上像它:header 的顺序sec-ch-ua 系列与 UA 的对应关系、Accept-Language 与目标站点地区的匹配、Referer 链的合理性。半套指纹比不伪装更可疑。

4.3 会话生命周期

Cookie/session 必须与代理 IP 绑定 —— 同一个 session 在多个 IP 之间跳跃是强异常信号。session 有生命周期上限,过期即整体轮换(IP + cookie + 指纹 profile 一起换)。

4.4 代理池调度

不是简单轮询。每个代理维护健康度评分(成功率、延迟、被封次数),调度时按评分加权选择,被封的进入冷却期而非直接剔除。支持住宅代理的 sticky session 语义。

4.5 自适应限速

固定 QPS 是错的 —— 站点的容忍度随时间和 IP 段变化。用类似 AIMD 的策略:连续成功则缓慢提速,遇到封锁信号立即大幅降速。限速粒度是「每代理 × 每站点」。

4.6 封锁识别与响应

必须能可靠区分这几种状态,因为它们的正确响应完全不同:

实测确认目标站点同时跑着三套互不相同的防御栈,且三种拦截页全部返回 2xx —— 状态码检测彻底无效。现有开源抓取器普遍查状态码,会把拦截页当数据记下来。

状态 识别特征(实测) 正确响应
正常响应 正向断言 #productTitle 存在 继续
AWS WAF 挑战 x-amzn-waf-action 响应头;HTTP 202 + 空 body 换代理 + 冷却
Akamai Bot Manager body 含 bm-verify / /_sec/verify(带 JS PoW 挑战) 换代理 + 换指纹
Amazon 验证码插页 body 含 /errors/validateCaptcha 换代理 + 换指纹
商品不存在 真 404(~1.1 KB) 记录为业务结果,不重试、不换代理
站点故障(5xx) 退避重试,不惩罚代理

⚠️ 两个会让检测代码静默失效的陷阱

  1. 经典文案 "Sorry, we just need to make sure you're not a robot" / "Enter the characters you see below" 在 2026-07 的实测中一次都没出现 —— 现在是 "Click the button below to continue shopping"。按旧文案写的检测器会静默漏判。
  2. 404 是真 404,不是拦截。 拿它去换代理重试,是在为一个不存在的 ASIN 烧 IP 声誉。

完整的分类器实现见 AMAZON-RECON.md §2.2,约 20 行。

免费的早期预警:Amazon 把它对你这次请求的分类结果直接写在页面里 —— <script type="a-state">social-proofing-page-state 中有 "isRobot": false。 必须记录该字段,它在你真正被封之前就会告诉你指纹正在劣化。

识别方式:签名 + 结构特征联合判定,绝不用文本量启发式。 实测反面案例:某工具用「小页面 + 可见文本少」的结构启发式,在全链路无任何反爬的本地环境下 把一个 HTTP 200 的正常 JS 页标成「Blocked by anti-bot protection」。 正面案例:用 title/body 匹配挑战页特征文案的签名门,真实抓出了一次挑战页并剔除了脏数据。 详见 EVIDENCE.md §3

验证码处理边界:本项目实现验证码页面的识别、退避和代理轮换,并提供一个可选的第三方 solver 接入接口。不内置任何验证码破解实现。

关于本节的证据强度(必须诚实标注):TLS/JA3 指纹、HTTP/2 帧序、header 顺序、代理轮换、 CAPTCHA、各大反爬服务的绕过率 —— 这些在现有 benchmark 资料中全部无实测覆盖 (该项目刻意不测绕过率)。唯一的一手指纹数据表明:反检测库与原生栈的可测默认差异只有一个 navigator.webdriver 布尔值 + 窗口几何,且 headless UA token 默认不遮蔽。 另一条有价值的实测:webdriver 在所有栈上都是 Navigator.prototype原生 getter, 值在启动期决定 —— 靠 Object.defineProperty 打补丁是可被检出的民间偏方。 本项目对这一层的一切设计都标注为待自行验证,不假装有依据。详见 EVIDENCE.md §4

4.7 断路器

当整体封锁率超过阈值,停止发起新请求进入观察期,而不是继续撞墙把所有代理都烧掉。

5. 解析层:抗漂移设计

5.0 解析引擎选型

实测暴露了五个「不崩溃、只产脏数据」的坑,直接决定了选型(详见 EVIDENCE.md §5):

主引擎用 lxml,且必须开 huge_tree 理由:

  • XPath 不可替代 —— 「找到兄弟 <th> 文本是 Price 的那个 <td>」在纯 CSS 里不可表达, 实测 10 个此类目标中 7 个只能靠 XPath。商品规格表正是这个形态。selectolax 和 soupsieve 都没有 XPath。
  • 默认 parser 在 253 层深度处静默截断且不报错(libxml2 的 DoS 保护), 现代组件框架产的商品页轻易超过。huge_tree=True 救到 999 层。
  • 畸形 HTML(手写的遗留规格表)上,lxml 15/15 正确,而 BeautifulSoup 的默认 html.parser 会让表格单元格互相吞并['abcd','bcd','cd','d'] 而非 ['a','b','c','d']),15 个样本只对 12 个。

lxml 的两个已知缺陷必须在代码层规避

  • :has() 伪类能解析但求值错,静默返回错集合。「支持但有 bug」比「不支持」更危险 —— 选择器 registry 里禁用。
  • [attr="x" i] 大小写不敏感属性匹配完全不支持 —— 需要时用 XPath 的 translate() 或后处理。

编码在入口显式处理,不依赖解析器猜。 实测:某些引擎的 .text()静默把非 UTF-8 字节 损坏成 U+FFFD 甚至直接吞掉,而同一对象的 .html 属性会抛 UnicodeDecodeError —— 报错的那条路是响亮的,取数的那条路返回看起来正常的错数据。

不引入正文抽取器(readability / trafilatura 类)。实测:它们在商品目录页上 products_named_in_text_output: 12/12structured_rows_returned: 0 —— 给你文本、拿走结构。而选择器类工具在同一 fixture 上确定性拿到 12/12。

性能不照抄任何公开说法。 「selectolax 比 lxml 快 4×」这个流行结论在 macOS arm64 / Python 3.14 上实测反转(lxml 的 parse 步骤反而快 1.5×)。 本项目在自己的目标平台上复跑后再定,并把 benchmark 脚本放进 repo。

5.1 多级候选链

每个字段配置一组按优先级排列的抽取策略:

price:
  - type: json_path      # 第 1 优先:内嵌结构化数据
    source: inline_state
    path: ...
  - type: css            # 第 2 优先:稳定属性选择器
    selector: "[data-...]"
  - type: css            # 第 3 优先:视觉层选择器
    selector: "..."
  - type: regex          # 兜底
    pattern: ...

抽取结果附带 provenance —— 记录命中的是第几级。这是漂移的早期信号:某字段的一级命中率下降、三级命中率上升,说明 Amazon 改了结构,此时功能还没坏,但需要更新 registry 了。

硬性禁止:位置索引型选择器/html[1]/body[1]/nav[1]/button[1] 这类绝对路径, 以及裸 :nth-child(N))。实测证明它是静默错而非响亮错 —— 在目标节点前插入一个兄弟节点后, 选择器解析成功、无任何异常、但指向了错误的元素,连自愈机制都不会触发(因为没报错)。 对商品页的具体后果:价格节点上方多一个促销 badge,你拿到的是错的价格,而且完全无感。 必须绑定语义锚点(data-*、id、有语义的 class、或结构关系),而非位置。 详见 EVIDENCE.md §7

5.2 选择器外置

选择器配置是数据不是代码,放在 YAML 里。用户遇到 DOM 漂移可以自己改配置修复,不必等上游发版 —— 这对开源项目的实际可用性影响很大。

5.3 归一化

价格、评分、评论数、日期在不同站点的格式差异巨大(小数点/千分位符号、货币位置、日期顺序、非阿拉伯数字)。归一化层按 marketplace 配置处理,输出统一的结构化类型。

5.4 fixture 回归测试

真实 HTML 冻结进 repo(脱敏后),覆盖:多个类目、多种布局变体、多个国家站点、各种边界情况(无价格、缺货、有变体、有优惠券)。CI 跑离线解析测试,不产生任何网络请求 —— 这意味着贡献者不需要代理就能开发和测试解析逻辑。

另配一个本地商品页仿真 server(单文件、零依赖、端口自动选),借鉴 benchmark 仓库的两个设计:

  • ground truth 由 server 自己在 /ground-truth.json 暴露,测试断言直接对它比,不用手维护期望值文件
  • 对抗路由直接对应生产事故形态:?delay_ms=N(可配延迟的 JS 注入)、/dynamic/never(等待的选择器永不出现)、/dynamic/late(首屏后追加 DOM)、/failure/500

仪器先校准再信零值:在断言「这页没有价格」之前,必须先证明抽取器在已知有价格的页上能抽到 —— 否则记录的是一个盲零。这条落实为解析层的自检测试。

5.5 归一化必须剥离展示性前缀

实测中的相邻教训:某工具照搬所选元素的渲染字符串,返回 "SKU: WM-1000" 而非 "WM-1000""Rated 4.5 / 5" 而非 4.5。抽取拿到的是展示文本,归一化层必须负责剥离标签前缀、单位后缀、千分位符号,并输出带类型的值(Decimal 价格 + 货币码,而非字符串)。

6. 可靠性

6.1 检查点续跑:必须自己在应用层做

实测:某爬虫框架的 resume 粒度是 per-seed 而非 per-page —— 11 个页面爬到第 10 个时中断,resume 后重抓全部 11 个,因为内存中的逐页去重过滤器从不持久化。对单 seed 的大批量任务,框架级 resume ≈ 全量重跑。

→ 本项目按 ASIN 粒度在应用层记账,进度写入持久化存储,中断后精确续跑。

6.2 进程生命周期:清理是承重结构

浏览器路径必须严格管理子进程。实测数据:进程退出未显式清理时,不同驱动分别残留 1–2 个孤儿进程(3/3 复现);父进程被 SIGKILL 时残留 2 个。

三重保障:上下文管理器 + finally 显式清理 + 退出钩子。但不能只靠这些 —— 实测中最像生产事故的一条:某框架在页面数触达上限且页面未关闭时永久停滞items_total: 0、优雅关闭失败、框架自带的超时机制完全无法清理,最终 required_external_kill: true

→ 必须有一个外部看门狗,在整体无进展超过阈值时强制终止,不能指望框架自己的超时。

⚠️ 另需注意:各驱动的「leakless」类兜底机制只在进程退出/崩溃时触发,不覆盖长跑进程内反复关闭/重开浏览器的场景 —— 而这恰恰是批量抓取器的形态。本项目按「无兜底」假设设计清理逻辑。

6.3 进程模型:共享浏览器 + 多 page

实测:共享浏览器开多 tab(1 进程 / 214ms)优于每任务独立浏览器(4 进程 / 264ms,某驱动上是 1302ms 约 5×)。且过度渲染的代价主要是内存不是时间 —— 选择性渲染 vs 全渲染墙钟打平(区间重叠),但峰值 RSS 低 33.1%(区间不重叠)。

6.4 内存监控用外部进程树真值

实测:框架自报的内存统计可能瞎报 11.3×(自报 ~75MB,进程树真值 ~850MB)。按框架自报值做容量规划会低配一个数量级。→ 本项目的内存指标取自 psutil 进程树。

6.5 原始响应留存

可选保存原始 HTML。用途有三:解析失败时可离线复盘、可作为新 fixture 来源、可在解析器升级后重新解析历史数据而不必重抓。

落盘前必须过一遍 _redact:递归遍历结构,把 $HOME~、临时目录 → $TMPDIR,以及代理凭证、API key 全部抹掉。⚠️ benchmark 项目的教训是这个函数曾经漏了 $TMPDIR,导致 5 处绝对路径泄漏 —— 一次性覆盖 HOME / TMPDIR / CWD / 凭证,并配单测。

7. 可观测性

结构化指标,暴露为日志和可选的 Prometheus 端点:

  • 请求成功率 / 各类封锁的分类计数
  • 抓取阶梯分布(HTTP vs 浏览器占比)
  • 各字段的抽取层级分布(漂移预警)
  • 代理池健康度分布
  • 端到端吞吐与延迟分位数

8. 合规

  • 默认限速保守,README 明确说明这是设计意图而非性能缺陷
  • 提供 robots.txt 遵守开关(默认行为待定,见调研)
  • README 包含明确的合规声明:仅抓取公开数据、遵守目标站点条款、用户对自己的使用负责
  • 不抓取、不存储个人身份信息(评论者名称等按需脱敏)
  • 对外口径:技术项目定位,不把「对抗反爬」当作卖点宣传

9. 选型结论

调研已完成,依据见 EVIDENCE.md(工具实测)和 AMAZON-RECON.md(目标站点实测)。

选型 关键理由
HTTP 客户端 curl_cffi 0.15+impersonate="chrome" 别名 唯一覆盖 TLS + HTTP/2 + HTTP/3 指纹的成熟选项;同步 + async 一个依赖搞定;MIT。钉别名不钉版本 —— 2026 年用 chrome99 不是伪装是签名
HTML 解析 lxml 6.1+,huge_tree=True,引擎可插拔 正确性优先:selectolax 无 XPath 且不下钻 <template>;lxml 默认 253 层静默截断需 huge_tree。性能在 2.28 MB 的真实 PDP 上双方都没测过,repo 带 benchmark 自行复跑
数字/货币解析 babel parse_decimal + 显式 locale 跨站点有 1.234,56 / 1 234,56 / 1,234.56 / 印度 lakh 分组四套格式,手搓 regex 必错
浏览器降级 patchright(Apache-2.0),可选额外依赖 nodriver/zendriver 是 AGPL-3.0,会传染给任何托管本工具的人。且独立 benchmark 显示 curl_cffi 打平专用反检测浏览器,浏览器本就该是边缘路径
并发 curl_cffi AsyncSession 不额外引入 aiohttp/httpx
License Apache-2.0 专利授权 + 变更说明要求;有人 fork 加上我们声明不做的规避功能时会起作用
Python 3.11+(CI 测到 3.13) 本机是 3.14.2,需单独确认 C 扩展 wheel 覆盖

明确排除

  • tls-client(弃坑 2.5 年)、undetected-chromedriver(1,142 未关 issue)
  • 正文抽取器(readability/trafilatura —— 给文本、拿走结构)
  • Scrapy 作为基础框架(Amazon robots.txt 点名 User-agent: ScrapyDisallow: /,用默认 UA 会把自己拦住)
  • 完整评论历史(2024-11 起匿名访问 302 到登录墙,抓它需要登录态 —— 那正是 hiQ 案的败诉事实模式)

10. 合规设计约束(不只是 README 措辞)

2025-10 起诉的 Reddit v. Perplexity 等案主张 DMCA §1201 反规避条款, 针对的正是「绕过速率限制和反机器人系统」—— 刻意绕开「数据是否公开」之争, 重构为「你是否击败了一项技术措施」。这直接约束本项目的功能边界:

不做
用真实浏览器执行页面自己的 JS(正常浏览行为) 重新实现 Akamai 的 PoW 算法
识别验证码页 → 退避 → 轮换 → 报错 破解或自动求解验证码
TLS 指纹伪装(灰区,保留) 集成默认开启的打码服务
默认限速、默认遵守 robots.txt 主动探测和规避速率限制

这不是免责套话,是会拒绝相应 PR 的设计约束,且写在 README 靠前位置。 其余合规清单(无关联声明、默认不输出 PII、直接引用 ToU 原文、 指向 Creators API/SP-API 而非已弃用的 PA-API 5)见 AMAZON-RECON.md §4.7

robots.txt 按 marketplace 运行时抓取,绝不硬编码 —— 实测各站点的 robots.txt 从 1,825 B 到 10,174 B 不等,规则确有差异。 默认遵守,退出开关显眼(借鉴 Scrapy「框架默认关、项目模板开」的先例,本项目直接默认开)。