← 返回 Day 194 正文

Day 194 · 图解

Day 194 图解 — AI 增强与数据管道

本课所有图都是文本形式(Mermaid / ASCII),不依赖图片文件。


1. 一个页面要不要继续抓:从规则到模型

flowchart TD A[响应到达] --> B{status in 403/429/503 ?} B -- 是 --> STOP[停抓, 换代理] B -- 否 --> C{captcha / js_challenge ?} C -- 是 --> STOP C -- 否 --> D[提取 10 维特征] D --> E[标准化 + 线性打分] E --> F[sigmoid 得到 p] F --> G{p >= 0.34 ?} G -- 是 --> STOP G -- 否 --> GO[继续抓取, 落库] STOP --> H[记录样本, 进入下一轮训练]
flowchart TD
    A[响应到达] --> B{status in 403/429/503 ?}
    B -- 是 --> STOP[停抓, 换代理]
    B -- 否 --> C{captcha / js_challenge ?}
    C -- 是 --> STOP
    C -- 否 --> D[提取 10 维特征]
    D --> E[标准化 + 线性打分]
    E --> F[sigmoid 得到 p]
    F --> G{p >= 0.34 ?}
    G -- 是 --> STOP
    G -- 否 --> GO[继续抓取, 落库]
    STOP --> H[记录样本, 进入下一轮训练]

规则层只覆盖显式拒绝;软封(200 + 空壳)走完整的模型分支。 阈值 0.34 来自"漏报代价 = 误报 × 5"的期望代价最小化。


2. 模型内部:一行特征怎么变成概率

status_code 403 ─┐
body_len   1200 ─┤    ┌─────────────┐   ┌─────────┐   ┌────────┐
latency_ms  380 ─┼──► │ 标准化 x'    │──►│ w·x' + b │──►│sigmoid │──► p=0.87
captcha       1 ─┤    │ (训练集 μσ) │   └─────────┘   └────────┘
header_count  5 ─┘    └─────────────┘
                                        系数(标准化后):
                                        body_len     -1.289  (越小越可疑)
                                        status_code  +0.985
                                        header_count -0.759

为什么必须标准化:body_len 在 10⁵ 量级,status_code 只有 3 位, 同一个学习率下小量纲特征根本不动。


3. SimHash:64 位逐位投票

token          哈希(64bit)                     权重
"新能"   →  1011 0010 ... 0110   w=1.0
"能源"   →  0011 1010 ... 1101   w=1.0
"汽车"   →  1110 0011 ... 0101   w=1.0
              │
              ▼  逐位求和(+w / -w)
votes[i] = Σ (+w if bit==1 else -w)
              │
              ▼
fingerprint = 每一位取符号 → 0/1 串(64 位)

改写少量 token 只会翻转少数位,所以"内容相近 → 汉明距离小"。


4. 分桶索引:鸽巢原理把 n² 压成候选

64 位指纹切成 bands 段(例:bands=8,每段 8 位)

 ┌────┬────┬────┬────┬────┬────┬────┬────┐
 │ b0 │ b1 │ b2 │ b3 │ b4 │ b5 │ b6 │ b7 │
 └─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┴─┬──┘
   │    │    │    │    │    │    │    │
   ▼    ▼    ▼    ▼    ▼    ▼    ▼    ▼
  桶(0,v) (1,v) ...                        查 8 个桶

若两篇距离 ≤ 7,则至少一段 8 位完全相同
→ 必落在同一个桶里 → 召回有保证(实测 454 对候选 = 全量的 0.06%)
bands 每段位宽 保证 k 平均候选/篇
4 16 3 1.5
8 8 7 1.8

段越多,保证阈值越高;但每段取值空间越小,桶越挤。


5. 两阶段去重漏斗(实测数字)

719,400 对(1200 篇两两组合)
        │
        │  SimHash 汉明距离 ≤ 7(8 段索引)
        ▼
   454 对候选  (0.06%)
        │
        │  Jaccard(token 集合) ≥ 0.6
        ▼
    真实重复 451 对
    precision 1.000 / recall 0.853
    漏报 78(改写幅度大,指纹已超阈值)

单阶段做不到:k=3 召回 0.539,k=10 精度掉到 0.923,k=15 精度只剩 0.153。


6. 管道:五阶段 + 双出口

        ┌────────────┐
输入 ──►│ S1 采集     │  3000
        └─────┬──────┘
              ▼
        ┌────────────┐
        │ S2 清洗     │  去脚本/标签/实体/零宽
        │ S3 抽取     │  title/price/date/body
        └─────┬──────┘
              ▼
        ┌────────────┐
        │ S4 去重     │  SimHash 粗筛 + Jaccard 精排
        └─────┬──────┘   丢 349 (11.6%)
              ▼
        ┌────────────┐
        │ S5 质检     │  8 条可判定规则
        └──┬──────┬──┘
           │      └──────────────► quarantine 表(885 条,带 reason code)
           ▼
        ┌────────────┐
        │ S6 入库     │  UPSERT(url_key)  2115 行
        └────────────┘

关键设计:脏数据不静默丢,进隔离区并记 reason code;入库用 UPSERT 保证重跑幂等。


7. 幂等 ≠ 正确(真实事故)

第一次跑:  2709 条 URL ──► 401 行   ← 日志无报错
重跑一遍:  2709 条 URL ──► 401 行   ← "幂等"✅

但真实页面有 820 个:
  /product/106?sku=red  ┐
  /product/106?sku=blue ┴─► 同一个 key → 后写的覆盖先写的
                                     ↑
                          规范化删掉了 sku,业务主键消失

断言(CI 里应该有的):
  distinct(url_key) >= 原始 URL 数 / 10
  count(rows) == count(distinct 真实页面)   ← 需要一份黄金样本