Web Scraping 架构设计:队列、Worker、浏览器与数据质量

发布时间
阅读时长约 5 分钟

Key Takeaways

面向生产环境的 Web Scraping 系统设计指南,重点拆分任务所有权、HTTP/浏览器抓取边界、代理与会话策略、有限重试、数据验证、证据留存和可观测性。

抓取系统真正需要设计的是“失败边界”

一个生产级 Web Scraping 系统,不应该只是“拿到 URL → 发请求 → 解析 HTML”的循环。

当任务规模扩大后,最容易失控的通常不是 HTTP 客户端本身,而是这些问题:

  • 谁拥有当前任务?Worker 崩溃后谁接手?
  • 哪些失败允许重试,哪些应该立即停止?
  • HTTP 抓取和浏览器渲染在哪里分流?
  • Cookie、浏览器 Context 和 Sticky 代理会话如何绑定?
  • 200 OK 但拿到登录页、挑战页、错误地区页面时,算成功还是失败?
  • 重复投递是否会产生重复数据?
  • 原始页面、截图、HAR、日志要保留多久,如何避免泄露敏感信息?

所以架构目标不是“服务越多越专业”,而是让任务所有权、状态、重试、数据质量和停止条件都能被明确解释。

本文只讨论获得授权的数据采集、监控和测试场景。代理改变的是网络出口,不会自动获得访问权限,也不会消除 Cookie、账户状态、浏览器指纹、隐私要求或站点策略。

一套可落地的参考架构

这里真正重要的是边界,不是具体选 Redis、SQS、Kafka 还是某个爬虫框架。

Scrapy 的官方架构同样把 Scheduler、Downloader、Spider 和 Item Pipeline 分成不同职责;Item Pipeline 常用于清洗、验证、去重和持久化。这种分层思想比“所有逻辑塞进一个 spider”更容易测试和演进。

参考:Scrapy Architecture · Item Pipeline

1. 先定义 Job Contract,再谈 Worker 数量

每个入队任务都应该包含足够的信息,让任何合格 Worker 都可以独立执行和审计,而不是依赖某个进程里的隐藏状态。

json
{
  "job_id": "crawl-01JEXAMPLE",
  "url": "https://example.com/products/42",
  "method": "GET",
  "fetch_mode": "auto",
  "market": "US",
  "session_key": null,
  "attempt": 0,
  "max_attempts": 3,
  "deadline_at": "2026-08-13T05:30:00Z",
  "schema_version": "product-v3"
}

这里的 3 次尝试只是示例,不是推荐默认值。真正重要的是:

  • 有稳定任务 ID
  • 有明确 target
  • 有 fetch policy
  • 有 attempt budget
  • 有 deadline
  • 有输出 schema version

如果同一任务可能被重复消费,写入端要有幂等键。例如:

plain text
idempotency_key = job_id + canonical_url + schema_version

AWS SQS 的官方文档明确说明标准队列采用至少一次投递语义,因此消费者仍需要处理重复消息。即使你不用 SQS,这也是设计任务队列时应主动考虑的故障模型。

参考:Amazon SQS Visibility Timeout

2. 队列不只是 Buffer,而是任务所有权协议

队列至少要回答四个问题:

  1. 当前谁拥有这个任务?
  2. Worker 死掉后,所有权何时失效?
  3. 什么错误可以重新入队?
  4. 什么错误必须进入 dead-letter / quarantine?

常见模型是:

plain text
claim/lease → execute → validate → acknowledge

Worker 在 ack 前崩溃,lease 到期后任务可以被重新领取。

建议监控的不只是 queue depth,还包括:

  • oldest job age
  • in-flight jobs
  • lease expiration count
  • retry count by reason
  • dead-letter count
  • 每个 target 的排队年龄

“队列只有 20 条”并不代表健康。如果最老任务已经卡了很久,可能是某个 target、partition 或依赖持续失败。

3. HTTP 与 Browser Worker 要明确分流

不要默认把所有 URL 都扔给 Playwright。

优先用 HTTP Client 的典型场景:

  • 数据直接存在于响应正文
  • 有公开且允许使用的接口
  • 不依赖前端 JS 状态
  • 不需要浏览器 Cookie / LocalStorage / DOM 交互

只有任务确实需要客户端渲染、浏览器状态或授权交互时,再升级到 Browser Worker。

一个实用流程:

plain text
HTTP fetch

response validation
  ├─ valid → extract
  ├─ requires browser → browser queue
  └─ terminal/policy error → stop

关键点是先验证再升级

200 可能是:

  • 登录页
  • Challenge 页面
  • 空壳 HTML
  • 错误地区/语言
  • 已经变更的页面模板

同样,浏览器也不是 403 的自动解决方案。

4. BrowserContext 是会话隔离边界

Playwright 将 BrowserContext 定义为隔离的浏览器会话。不同 Context 可以隔离 Cookie 等浏览器状态。

参考:Playwright BrowserContext

如果任务需要认证态,建议把这些状态绑定到同一个逻辑 session_key

plain text
session_key
  ├─ BrowserContext
  ├─ cookies / storage state
  ├─ application identity
  └─ sticky proxy session

认证状态要按 secret 管理。Playwright 官方也提醒,保存的 storage state 可能包含能够代表账户完成操作的敏感 Cookie 和 Header。

参考:Playwright Authentication

5. 把代理选择做成 Policy Layer

代理逻辑不要散落在 spider、parser 和业务代码中。

一个独立的 routing policy 应该能决定:

  • direct / proxy
  • country / region / city 要求
  • rotating / sticky
  • session key
  • 当前 route 是否健康或被 quarantine
  • 某类失败是否允许换 route 重试

例如:

python
from dataclasses import dataclass
from typing import Literal

SessionMode = Literal["rotating", "sticky"]

@dataclass(frozen=True)
class RoutePolicy:
    country: str | None
    mode: SessionMode
    session_key: str | None


def route_for(*, country: str | None, stateful: bool) -> RoutePolicy:
    return RoutePolicy(
        country=country,
        mode="sticky" if stateful else "rotating",
        session_key="job-session" if stateful else None,
    )

不要默认“住宅代理一定更好”。真正应该比较的是该 target 上的有效结果率、错误类型、地区准确性、字节成本和延迟

更深入的路由层设计可参考:Web Scraping Proxy Architecture

6. 先分类失败,再决定是否重试

信号主要故障层默认处理
407 Proxy Authentication Required代理认证检查凭据和代理配置,不盲目换 IP
429 Too Many Requests目标站限流降低速率;存在 Retry-After 时按其语义处理
403 Forbidden授权 / 策略 / 安全检查响应和授权边界,不把换 IP 当默认方案
Connect timeout网络 / 代理 / 目标站先做分层诊断,再进入有限重试
Schema validation failure解析 / 页面变更进入 quarantine 或更新 extractor,不用网络重试掩盖
Wrong locale / market路由或应用状态同时验证出口地区与页面级证据

RFC 6585 对 429 的定义也说明,服务端不一定按 IP 识别用户,还可以依据 credentials、cookie 或其他状态。因此“429 → 马上换 IP”不是可靠的通用策略。

参考:RFC 6585 §4

7. Retry 必须同时受次数和 Deadline 限制

只设置 max_attempts 不够,只设置 timeout 也不够。

更完整的预算是:

plain text
attempt budget + per-attempt timeout + absolute deadline

这样目标站持续退化时,不会出现大量旧任务永久占用 Worker。

示例:

python
from dataclasses import dataclass
from time import monotonic

@dataclass
class Budget:
    max_attempts: int
    deadline_monotonic: float

    def can_retry(self, attempt: int) -> bool:
        return (
            attempt < self.max_attempts
            and monotonic() < self.deadline_monotonic
        )

具体次数和时间必须根据目标延迟、任务 SLA、请求成本以及重复执行风险来确定,不应该复制某篇文章里的固定数字。

8. Validation 是写入 Canonical Data 前的门禁

抓到了 HTML,不等于拿到了正确数据。

验证层至少可以检查:

  • 必填字段是否存在
  • 类型、币种、单位是否正确
  • 时间是否可解析和标准化
  • 是否命中登录/挑战/空页特征
  • 当前 market 是否符合请求
  • schema version 是否一致
  • 是否是重复记录

一个结果可以明确拆为:

json
{
  "transport_ok": true,
  "http_status": 200,
  "content_valid": false,
  "failure_class": "login_page",
  "schema_version": "product-v3"
}

这种表达比一个全局 success=true/false 更容易排错。

9. Raw Evidence 和业务数据分开保存

推荐拆成四类:

  • Raw response / browser evidence → 有生命周期策略的对象存储
  • Canonical extracted data → 数据库 / Warehouse
  • Job & attempt state → 运行状态存储
  • Metrics → 可观测性系统

不要默认永久保存所有 HTML、Screenshot、HAR、Trace。

它们可能包含:

  • Cookie
  • Token
  • 用户数据
  • Query 参数中的签名
  • 账户信息

需要明确 retention、访问权限和 secret redaction。

10. 指标要围绕“可用结果”,而不是 HTTP 200

更有价值的指标包括:

  • usable-result rate
  • validation-failure rate
  • challenge/login/empty-page rate
  • attempts per usable result
  • bytes per usable result
  • browser escalation rate
  • p50 / p95 job duration
  • queue oldest age
  • lease expiration rate
  • 407 / 403 / 429 / timeout 分类计数
  • requested market vs observed market mismatch

如果一个请求返回 200,但产出的 SKU、价格、职位或 SERP 数据无法通过验证,它就不应计入业务成功率。

11. 上线前主动注入失败

在 staging 或明确授权的测试环境里,至少验证这些场景:

  • Worker fetch 完成后、ack 前退出
  • 队列重复投递
  • 目标站 200 返回登录页
  • DOM / JSON schema 发生变化
  • 代理返回 407
  • 目标返回 429,分别包含和不包含 Retry-After
  • BrowserContext / browser process 崩溃
  • sticky session 中途消失
  • 数据库或对象存储短时不可用
  • 任务在 retry 中超过 deadline

每一种情况都应有可预测结果:

plain text
retry / quarantine / dead-letter / stop

如果值班人员无法判断系统下一步会怎么做,失败策略就还没有设计完成。

生产检查清单

每个 Job 有稳定 ID、deadline 和有限 retry budget。
队列按“可能重复投递”设计。
Canonical 写入具备幂等或去重机制。
HTTP 与 Browser fetch 有明确升级条件。
BrowserContext 生命周期和认证状态隔离清晰。
Proxy / Sticky session 由统一 policy 控制。
407、403、429、timeout、parser、validation 分开统计。
Raw evidence 有 retention 和 secret redaction 策略。
Metrics 衡量 usable output,而不是只看 HTTP 200。
每个 target 可独立配置 concurrency / rate policy。
明确授权、隐私、安全和停止条件。

FAQ

所有生产爬虫都需要分布式队列吗?

不需要。小规模任务可能一个进程内 scheduler 就足够。只有在任务需要持久化、跨 Worker、故障接管或独立扩缩容时,分布式队列才值得引入。

Playwright 是否应该成为默认抓取方式?

通常不应该。只有当正确数据依赖浏览器渲染或浏览器状态时才升级到浏览器;否则 HTTP 路径更容易调试和控制资源成本。

返回 200 是否代表抓取成功?

不代表。还要验证 body、schema、market、页面类型和业务字段。

遇到 403/429 是否应该立即切换住宅代理?

不应该作为默认动作。先确认授权和错误语义,再按失败分类决定是否重试、降速、停止或变更路由。

相关 BytesFlows 工程指南

AV
技术团队背书同行架构师审查 & 实测校验

Alex Vance

核心基础设施架构团队

本文由 BytesFlows 工程团队审查。代码示例适用于合规的公开网页数据采集、QA 自动化、SEO 监测与市场研究工作流。实际基准表现可能因目标站点防爬策略、地理位置、客户端运行环境及请求频率而异。