ISSUE-0080:phone 正则把 GPS 坐标当电话号码抓¶
现象(用户截图 v0.10.114)¶
Homer Veterinary Clinic 行:
- HTTP chip tooltip:「已采到:邮箱 0 个、电话 44 条(来自他人共享)/ mstage:success — success: 44 contacts (emails=0 phones=44) (pool-hit)」
- 但电话列只显示 +1 907-235-896…(1 个)+ 邮箱列 info@homervet.com(1 个)
用户问:"为什么显示 44 条,后边却只有 1 个手机呢?"
诊断(日志验证)¶
pageLogs[homervet]:
emails=1, emailList=['info@homervet.com'] ← 真邮箱 1 个
phone="0.5 15.0234, 11.5916 59.989, 11.8529 60.2645"
phoneList(len)=10
phoneList sample: ['0.5 15.0234', '11.5916 59.989', '11.8529 60.2645',
'12.7165 60.5762', '13.2591 60.3604']
error: mstage:success — 44 contacts (emails=1 phones=35) (pool-hit)
44 条 phones 里 ~43 条是 GPS 经纬度坐标对(lat lng 格式),只有 ~1 个真号码。
根因 — phone 过滤太弱¶
src/utils/scraper.ts:177 PHONE_TEXT_REGEX:
测试 "11.5916 59.989":
- "11" → \d{1,3} ✓
- "." → 分隔符 [\s.\-] ✓
- "5916" → \d{1,4} ✓
- " " → 分隔符 ✓
- "59" → \d{1,4} ✓
- "." → 分隔符 ✓
- "989" → \d{2,9} ✓
- 匹配成功 ❌
后续 addPhone(line 442) 过滤:
11.5916 59.989 → digits "1159165989" (10 位) → 过 8-15 检查 ❌
addPhone 过滤层只看「数字位数」,没看分隔符模式。
数据流污染¶
- 网页含 Google Maps embed / SVG path / canvas → 大量浮点对
- scraper 误抓 → 写入
MapTaskData.emails/phones字段 - 写入 ContactPool 共享池 — 这是最严重的(全球用户都受影响)
- 其他用户 pool-hit → 拿到同样垃圾 44 条
- 如云端同步开启 → 垃圾传播到云端
修复(v0.10.118)¶
#1 提取公用 helper isLikelyCoordinateNoise()¶
src/utils/scraper.ts:
export function isLikelyCoordinateNoise(rawPhone: string): boolean {
const norm = String(rawPhone || '').replace(/\s+/g, ' ').trim();
// 特征 1:≥ 2 个小数点(浮点对 / SVG path)
if ((norm.match(/\./g) || []).length >= 2) return true;
// 特征 2:以 ≤2 位数字 + 点开头("0.5" "11.59")— 电话不会 N.N 开头
if (/^\d{1,2}\./.test(norm)) return true;
return false;
}
#2 三层防御¶
| 层 | 时机 | 文件 |
|---|---|---|
| 写入侧 | scraper addPhone() 提取后 | scraper.ts |
| 读取侧 | ContactPool 查询命中后 | contact-pool.ts queryContactPool* |
| 存量迁移 | engine session 启动跑一次 | contact-pool.ts cleanPollutedContactPool() |
三层之间互为兜底: - 写入侧防新数据污染 - 读取侧防云端拉来的脏数据 - 存量迁移修历史污染
#3 sysLog 可观测¶
appendSysLog('contact-pool', 'cleaned-polluted', {
scanned: N, cleaned: M, totalPhonesRemoved: K
});
appendSysLog('contact-pool', 'hit', {
..., cleanedDirty: 5, // 这次 pool-hit 过滤掉 5 个坐标
});
教训¶
- 正则宽 + 过滤弱 = 字段被污染
- 共享数据是 product surface — 写入前必须 validate,比单用户 UI 显示重要 10×
- 三层防御是数据质量必备模式:写入 / 读取 / 存量迁移
- count tooltip 是真理之口:用户问"44 条但只有 1 个"是黄金信号 — UI 数字差距是 data quality 的早期预警
- v0.10.10 加 phoneTier1/2/3 时设计的「分隔符要求」是对的,但 Tier 3 自由文本只看长度不看格式 — 这次补上
通用 rule¶
加 docs/rules/phone-extraction-quality.md(待写):phone 提取的多层质量过滤规范。