跳转至

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:

/(?:^|[\s>(])(\+?\d{1,3}[\s.\-](?:\d{1,4}[\s.\-]){1,3}\d{2,9})(?=[\s<)]|$)/g

测试 "11.5916 59.989": - "11" → \d{1,3} ✓ - "." → 分隔符 [\s.\-] ✓ - "5916" → \d{1,4} ✓ - " " → 分隔符 ✓ - "59" → \d{1,4} ✓ - "." → 分隔符 ✓ - "989" → \d{2,9} ✓ - 匹配成功

后续 addPhone(line 442) 过滤:

if (digits.length < 8 || digits.length > 15) return;

11.5916 59.989 → digits "1159165989" (10 位) → 过 8-15 检查

addPhone 过滤层只看「数字位数」,没看分隔符模式

数据流污染

  1. 网页含 Google Maps embed / SVG path / canvas → 大量浮点对
  2. scraper 误抓 → 写入 MapTaskData.emails/phones 字段
  3. 写入 ContactPool 共享池 — 这是最严重的(全球用户都受影响)
  4. 其他用户 pool-hit → 拿到同样垃圾 44 条
  5. 如云端同步开启 → 垃圾传播到云端

修复(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 提取的多层质量过滤规范。