跳转至

域名状态机

网站抓取多阶段决策的智能记忆层 — 每个 domain 独立持久化状态,按行为自动转换,按 TTL 自动重置。

6 种状态

                  ┌─────────────┐
                  │  unknown     │   (默认 / 首次)
                  │  完整 pipeline│
                  └──────┬──────┘
        ┌────────────────┼────────────────┬──────────────┐
        ↓                ↓                ↓              ↓
  ┌──────────┐    ┌─────────────┐  ┌──────────┐  ┌──────────┐
  │   dead    │   │ antibot-soft │  │ friendly │  │   cold   │
  │ DNS/SSL/404│   │ challenge 偶发│  │ 高 fetch ok│  │ 真无 contact│
  │ 永远 SKIP  │   │ fetch+兜底 tab│  │ 永远 fetch│  │ 永远 SKIP │
  └──────────┘    └──────┬──────┘  └──────────┘  └──────────┘
                         │ × 3
                  ┌─────────────┐
                  │antibot-hard │
                  │ 永远 tab     │
                  └─────────────┘

状态详解

状态 触发条件 决策行为 TTL 重置后状态
unknown 首次访问 / TTL 重置 完整 pipeline (HEAD → GET → tab fallback) 7d 重新评估
dead HEAD: DNS fail / SSL invalid / 404 / 410 永远 SKIP(不抓取) 30d unknown
antibot-soft HEAD: cf-mitigated:challenge / 403+cf-ray / body 含 challenge marker fetch 试 + 失败 tab fallback 7d (有衰减) unknown
antibot-hard antibot-soft 触发 × 3 连续 / cf-mitigated:block 永远 tab fallback(不再 fetch 浪费) 90d antibot-soft
friendly fetch 成功率 > 70%(最近 10 次) 永远 fetch(不 fallback tab) 无 TTL(行为驱动) fetch fail × 5 → unknown
cold fetch+tab 都拿不到 contact × 5 次 永远 SKIP(除非用户强制 force) 90d unknown

转换规则

进入 dead

HEAD response:
  - DNS resolution fail
  - TCP connection refused
  - SSL certificate invalid/expired
  - HTTP 404 / 410 (Gone)

进入 antibot-soft

HEAD response:
  - cf-mitigated: challenge
  - status 403/503 + cf-ray header present
  - status 429 (rate limit)
GET body:
  - isChallengeBody(html) === true SPEC-004 关键词列表

升级 antibot-soft → antibot-hard

连续 3 次抓取都触发 antibot不限 HEAD/GET layer
 HEAD response: cf-mitigated: block (永久标记)

进入 friendly

最近 10  method='fetch'  outcome === 'ok-contact' 占比  70%
且总 fetch 次数  5防小样本误判

进入 cold

连续 5 任何 methodoutcome === 'ok-empty'
站点活能抓 HTML 里就是没有 email/phone/social

重置(TTL 到期)

任何状态 TTL 到期 → 自动重置为 unknown,清空 recent 历史。 (除 friendly:永远不主动重置,但 fetch fail × 5 自动降级)

数据结构

interface DomainStat {
  domain: string;                              // 主键
  state: DomainState;                          // 当前状态
  recent: RecentEntry[];                       // FIFO max 20
  // 派生指标(cron 每日重算)
  fetchTotal: number;
  fetchOk: number;                             // outcome='ok-contact' 计数
  fetchOkEmpty: number;                        // outcome='ok-empty' 计数
  tabTotal: number;
  tabOk: number;
  // 时间戳
  firstSeen: number;
  lastSeen: number;
  stateChangedAt: number;
  stateExpiresAt: number;
}

interface RecentEntry {
  at: number;
  method: 'head' | 'fetch' | 'tab';
  outcome: 'ok-contact' | 'ok-empty' | 'http-error' | 'network-error' | 'antibot' | 'dead';
  // 可选 evidence(便于事后分析)
  status?: number;        // HTTP status
  errorClass?: string;
}

type DomainState =
  | 'unknown'
  | 'dead'
  | 'antibot-soft'
  | 'antibot-hard'
  | 'friendly'
  | 'cold';

存储

  • Key: local:domainStats:v1
  • Value: Record<domain, DomainStat>
  • Max 5,000 domains(LRU 淘汰按 lastSeen
  • 序列化大小:每条 ~ 500 bytes → 5k 条 ~ 2.5 MB

与云端协同(Phase 3)

时机 客户端动作
首次见 domain(unknown) 先 GET /api/scrape/domain-state?domains=... 拿云端 state(如有)
本地确认 dead / antibot-hard POST /api/scrape/domain-state 上报 + 积分奖励
云端 state 与本地不一致 优先用更新的(lastSeen 新的)
云端共识强度高(consensus > 0.8) 客户端直接采纳云端 state,不必本地实验

详见 SPEC-004-网站采集多阶段优化-云端协同 Phase 3.4。

调试 / 可观测

日志字段

PageLogEntry {
  domainState: DomainState;          // 决策时的状态
  domainStateExpiresAt?: number;     // TTL
  method: 'head' | 'fetch' | 'tab' | 'cache' | 'skip';
}

Settings 调试入口(未来)

「高级 → 域名状态管理」: - 查看当前所有域名状态分布 - 手动强制某 domain 状态(如 friendly 强制 fetch) - 一键 reset all states(数据迁移用) - 导出 domainStats CSV

FAQ

Q: domain 颗粒度 vs URL 颗粒度? A: 状态机用 domain 级(一个站通常状态一致)。但 dead 例外 — 同 domain 的不同 url 可能各自 404,所以也维护 urlContactCache 标 dead url。

Q: 子域名怎么算? A: 默认按 eTLD+1(如 mail.example.comwww.example.com 都算 example.com)。除非用户开启「子域独立追踪」。

Q: 状态错误怎么纠正? A: Settings UI 提供「reset domain state」按钮,强制回到 unknown 重新评估。

Q: 状态机会让 22w 网站越抓越慢吗? A: 不会。LRU 5k 上限 + 大部分 lookup 是 O(1) hash map。每次抓取额外开销 < 1ms。

相关