数据提取质量防御 — 写入/读取/存量三层防污染¶
背景: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,用户能看到产品在自我清洁