状态:草稿 v0.1(架构主干)。反爬细节与选型依据待两份调研回填。
一个生产可用的 Amazon 商品数据抓取器,Python 实现,同时可作为库、CLI 和 Docker 镜像使用。
差异化定位(相对 GitHub 上现有同类项目):
| 现有项目的通病 | 本项目的做法 |
|---|---|
| 选择器硬编码在解析函数里,Amazon 一改版就全线崩 | 选择器外置为 YAML registry,多级候选链 + 漂移告警 |
| 抓取和解析耦合,没有真实 HTML 就没法测 | 解析层是纯函数,fixture 驱动的离线回归测试 |
| 反爬 = 换个 User-Agent | 分层对抗:TLS 指纹 → header 一致性 → 代理调度 → 自适应限速 → 浏览器降级 |
| 封了就报错,跑到一半崩了从头再来 | 封锁分类 + 断路器 + 检查点续跑 |
| 只支持 amazon.com | marketplace 配置化,货币/日期/数字格式本地化归一 |
| 无可观测性,不知道为什么失败 | 结构化指标:成功率、封锁率、各字段抽取命中层级 |
┌──────────────┐
│ 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 里被抓到而不是在生产里。
这一节的设计被实测推翻过一次,详见 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 + 页面内嵌结构化数据
application/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 级占比突然上升,说明前几级正在失效。
「上了无头浏览器就解决动态内容」被实测证伪:在 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 小时。
对抗不是单点技巧,是一组必须互相一致的信号。任何一层的不一致都会成为指纹。
TLS ClientHello(JA3/JA4)、HTTP/2 SETTINGS 帧与伪头顺序。Python 默认的 requests/httpx 在这一层有极其明显的特征。
声称是某个浏览器,就必须在所有维度上像它:header 的顺序、sec-ch-ua 系列与 UA 的对应关系、Accept-Language 与目标站点地区的匹配、Referer 链的合理性。半套指纹比不伪装更可疑。
Cookie/session 必须与代理 IP 绑定 —— 同一个 session 在多个 IP 之间跳跃是强异常信号。session 有生命周期上限,过期即整体轮换(IP + cookie + 指纹 profile 一起换)。
不是简单轮询。每个代理维护健康度评分(成功率、延迟、被封次数),调度时按评分加权选择,被封的进入冷却期而非直接剔除。支持住宅代理的 sticky session 语义。
固定 QPS 是错的 —— 站点的容忍度随时间和 IP 段变化。用类似 AIMD 的策略:连续成功则缓慢提速,遇到封锁信号立即大幅降速。限速粒度是「每代理 × 每站点」。
必须能可靠区分这几种状态,因为它们的正确响应完全不同:
实测确认目标站点同时跑着三套互不相同的防御栈,且三种拦截页全部返回 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) | — | 退避重试,不惩罚代理 |
- 经典文案 "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"。按旧文案写的检测器会静默漏判。
- 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。
当整体封锁率超过阈值,停止发起新请求进入观察期,而不是继续撞墙把所有代理都烧掉。
实测暴露了五个「不崩溃、只产脏数据」的坑,直接决定了选型(详见 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/12 但 structured_rows_returned: 0 ——
给你文本、拿走结构。而选择器类工具在同一 fixture 上确定性拿到 12/12。
性能不照抄任何公开说法。 「selectolax 比 lxml 快 4×」这个流行结论在 macOS arm64 / Python 3.14 上实测反转(lxml 的 parse 步骤反而快 1.5×)。 本项目在自己的目标平台上复跑后再定,并把 benchmark 脚本放进 repo。
每个字段配置一组按优先级排列的抽取策略:
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。
选择器配置是数据不是代码,放在 YAML 里。用户遇到 DOM 漂移可以自己改配置修复,不必等上游发版 —— 这对开源项目的实际可用性影响很大。
价格、评分、评论数、日期在不同站点的格式差异巨大(小数点/千分位符号、货币位置、日期顺序、非阿拉伯数字)。归一化层按 marketplace 配置处理,输出统一的结构化类型。
真实 HTML 冻结进 repo(脱敏后),覆盖:多个类目、多种布局变体、多个国家站点、各种边界情况(无价格、缺货、有变体、有优惠券)。CI 跑离线解析测试,不产生任何网络请求 —— 这意味着贡献者不需要代理就能开发和测试解析逻辑。
另配一个本地商品页仿真 server(单文件、零依赖、端口自动选),借鉴 benchmark 仓库的两个设计:
- ground truth 由 server 自己在
/ground-truth.json暴露,测试断言直接对它比,不用手维护期望值文件 - 对抗路由直接对应生产事故形态:
?delay_ms=N(可配延迟的 JS 注入)、/dynamic/never(等待的选择器永不出现)、/dynamic/late(首屏后追加 DOM)、/failure/500
仪器先校准再信零值:在断言「这页没有价格」之前,必须先证明抽取器在已知有价格的页上能抽到 —— 否则记录的是一个盲零。这条落实为解析层的自检测试。
实测中的相邻教训:某工具照搬所选元素的渲染字符串,返回 "SKU: WM-1000" 而非 "WM-1000"、"Rated 4.5 / 5" 而非 4.5。抽取拿到的是展示文本,归一化层必须负责剥离标签前缀、单位后缀、千分位符号,并输出带类型的值(Decimal 价格 + 货币码,而非字符串)。
实测:某爬虫框架的 resume 粒度是 per-seed 而非 per-page —— 11 个页面爬到第 10 个时中断,resume 后重抓全部 11 个,因为内存中的逐页去重过滤器从不持久化。对单 seed 的大批量任务,框架级 resume ≈ 全量重跑。
→ 本项目按 ASIN 粒度在应用层记账,进度写入持久化存储,中断后精确续跑。
浏览器路径必须严格管理子进程。实测数据:进程退出未显式清理时,不同驱动分别残留 1–2 个孤儿进程(3/3 复现);父进程被 SIGKILL 时残留 2 个。
三重保障:上下文管理器 + finally 显式清理 + 退出钩子。但不能只靠这些 —— 实测中最像生产事故的一条:某框架在页面数触达上限且页面未关闭时永久停滞,items_total: 0、优雅关闭失败、框架自带的超时机制完全无法清理,最终 required_external_kill: true。
→ 必须有一个外部看门狗,在整体无进展超过阈值时强制终止,不能指望框架自己的超时。
实测:共享浏览器开多 tab(1 进程 / 214ms)优于每任务独立浏览器(4 进程 / 264ms,某驱动上是 1302ms 约 5×)。且过度渲染的代价主要是内存不是时间 —— 选择性渲染 vs 全渲染墙钟打平(区间重叠),但峰值 RSS 低 33.1%(区间不重叠)。
实测:框架自报的内存统计可能瞎报 11.3×(自报 ~75MB,进程树真值 ~850MB)。按框架自报值做容量规划会低配一个数量级。→ 本项目的内存指标取自 psutil 进程树。
可选保存原始 HTML。用途有三:解析失败时可离线复盘、可作为新 fixture 来源、可在解析器升级后重新解析历史数据而不必重抓。
落盘前必须过一遍 _redact:递归遍历结构,把 $HOME → ~、临时目录 → $TMPDIR,以及代理凭证、API key 全部抹掉。$TMPDIR,导致 5 处绝对路径泄漏 —— 一次性覆盖 HOME / TMPDIR / CWD / 凭证,并配单测。
结构化指标,暴露为日志和可选的 Prometheus 端点:
- 请求成功率 / 各类封锁的分类计数
- 抓取阶梯分布(HTTP vs 浏览器占比)
- 各字段的抽取层级分布(漂移预警)
- 代理池健康度分布
- 端到端吞吐与延迟分位数
- 默认限速保守,README 明确说明这是设计意图而非性能缺陷
- 提供 robots.txt 遵守开关(默认行为待定,见调研)
- README 包含明确的合规声明:仅抓取公开数据、遵守目标站点条款、用户对自己的使用负责
- 不抓取、不存储个人身份信息(评论者名称等按需脱敏)
- 对外口径:技术项目定位,不把「对抗反爬」当作卖点宣传
调研已完成,依据见 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: Scrapy→Disallow: /,用默认 UA 会把自己拦住) - 完整评论历史(2024-11 起匿名访问 302 到登录墙,抓它需要登录态 —— 那正是 hiQ 案的败诉事实模式)
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「框架默认关、项目模板开」的先例,本项目直接默认开)。