跳转至

数据提取质量防御 — 写入/读取/存量三层防污染

背景:v0.10.118 发现 phone 正则把 GPS 坐标 11.5916 59.989 当电话号码抓, 写入 ContactPool 共享池后全球用户 pool-hit 都拿到同样垃圾。 单层过滤一旦失守 → 共享池被污染 → 用户感知"产品质量差"。

任何抽取 + 共享类字段都要按"三层防御"设计。

三层防御模板

正则匹配 → [Layer 1 写入侧过滤] → 本地存 → [Layer 2 读取侧过滤] → UI 显示
                ↓                              ↑
            写入共享池 → 云端 → 别的用户读 → [Layer 2]
                              [Layer 3 存量迁移] 修历史污染

Layer 1 — 写入侧过滤(防新数据污染)

// scraper.ts addPhone()
const addPhone = (raw: string) => {
  if (basic length check fail) return;
  if (blacklist hit) return;
  if (isLikelyNoise(raw)) return;     // ← 业务特征过滤
  phones.push(raw);
};
  • 在数据进入本地 store 前过滤
  • 业务特征校验(坐标对 / 时间戳 / 邮编 / 测试号 等)

Layer 2 — 读取侧过滤(兜底老污染 + 云端脏数据)

// contact-pool.ts queryContactPool()
const hit = await fetchRecord(hash);
return { ...hit, phones: cleanPhonesForPool(hit.phones) };  // ← 再过滤
  • pool-hit / 云端 pull 后再过一遍同样规则
  • 兜底场景:升级前的存量数据 / 老版本写入的脏数据 / 云端别人传来的脏数据

Layer 3 — 存量迁移(修历史污染)

// engine 启动时跑一次
async function cleanPollutedX(): Promise<{ scanned, cleaned, removed }> {
  const rows = await selectAll();
  for (const r of rows) {
    const filtered = applyFilter(r.field);
    if (filtered.length < r.field.length) {
      await update(r.id, { field: filtered, syncedAt: 0 });  // 重置 sync
    }
  }
  appendSysLog('cleaned-polluted', { scanned, cleaned, removed });
}
  • 一次性处理升级前留下的污染
  • 重置 syncedAt: 0 让云端拿到清洗版(覆盖云端脏数据)
  • sysLog 记录 scanned / cleaned / removed 三个数

决策表 — 何时必须三层

数据流 需要几层 备注
仅本地使用 1(写入侧)够 错了重抓即可
写入本地表,UI 显示 1 + UI render 兜底 UI render 加 isLikelyBroken 检查
写入共享池(ContactPool) 必须 3 层 一旦污染传染全球
写入云端(cloud-sync) 必须 3 层 + 限速 比共享池更严重
写 page-log / sys-log 1 层即可 日志一次性,过期淘汰

isLikely* helper 命名规范

所有过滤 helper 都用 isLikelyXxx 命名

Helper 模块 过滤什么
isEmailLikelyBroken scraper.ts URL 编码 / 控制字符 / 超长本地部分
isLikelyCoordinateNoise scraper.ts GPS 坐标 / 浮点对
isLikelyTestNumber 待加 9999... / 0000... 测试号
isLikelyZipCode 待加 5 位纯数字 + 上下文 zip

好处: - 同一 helper 三层都用 — 一致性保证 - 单元测试容易(pure function) - 修一处全生效


必问清单

设计新字段抽取时:

[ ] 这个字段会写到共享池 / 云端 / 公开 UI 吗?
[ ] 抽取用什么正则?正则有没有"恰好满足但语义错"的 false positive?
[ ] Layer 1(写入侧)isLikely* helper 写了吗?
[ ] Layer 2(读取侧)pool-hit / cloud-pull 后再过一遍吗?
[ ] Layer 3(存量迁移)engine session 启动跑一次了吗?
[ ] 有 sysLog 可观测 cleaned 数吗?(用户能看到"清洗了 N 条")

当前合规处

字段 写入侧 读取侧 存量迁移
emails (scraper) isEmailLikelyBroken (v0.10.33) 写库前 dedup + 黑名单 (N/A,靠用户重抓)
phones (scraper) isLikelyCoordinateNoise (v0.10.118 新) cleanPhonesForPool 在 pool query cleanPollutedContactPool() (v0.10.118)
socials (scraper) 内置 path 排除 (v0.10.x)

教训

  • 正则宽 + 单层过滤 = 共享池污染(ISSUE-0080)
  • 共享数据的 validation 责任 10× 于单用户 UI
  • 三层防御不冗余 — 每一层守不同的场景(新写入 / 老存量 / 云端脏)
  • isLikely* helper 必须 pure function 才能 3 层复用
  • 可观测最重要:sysLog 出 cleaned: N,用户能看到产品在自我清洁