SPEC-004 — 网站采集多阶段优化 + 云端协同¶
背景¶
当前网站抓取(contact info / emails / phones)每条 URL 都开 chrome.tabs 实抓: - ~ 5-10 秒/站 - 大批量任务(如 22w 网站)需 ~ 30 天 - 浪费场景:DNS 死、404 死、anti-bot 拒、真无联系信息的站
用户提出多阶段筛选 + 云端协同 + 贡献度三层方案。
设计目标¶
- 多阶段筛选:分级判断(黑名单 → HEAD → GET → 解析 → tab 兜底)— 跳过明确无价值的站
- 域名记忆:每域名独立状态(dead / antibot / friendly / unknown),持久化 + 自动衰减
- 云端协同:先查云端 → 命中直接用 → 不命中本地抓 + 回写
- 贡献度激励:抓新数据 +X 分,查云端消耗 -Y 分(防搭便车)
- 可观测性:每条 URL 完整记录路径与结果,便于分析
整体架构(4 层)¶
┌─────────────────────────────────────────────────────────────┐
│ 用户输入:URL │
└──────────────────────────────┬──────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ [Layer 1] 本地缓存查询 (0ms) │
│ - domainStats: dead / antibot / friendly │
│ - urlContactCache: 本地已抓过的 url + contact 数据 │
└──┬──────────────────────────────────────────────────────────┘
│ miss
↓
┌─────────────────────────────────────────────────────────────┐
│ [Layer 2] 云端查询 (200-500ms) │
│ - 按 url hash 查云端 contact 表 │
│ - 命中 + 未过期 → 拉数据 + 消耗贡献度 (-1) │
│ - 顺便拿到 domainState 加速本地判断 │
└──┬──────────────────────────────────────────────────────────┘
│ miss / expired
↓
┌─────────────────────────────────────────────────────────────┐
│ [Layer 3] 本地多阶段抓取 │
│ ① HEAD probe (1s): dead/404/anti-bot 早杀 │
│ ② GET fetch (5s): 静态 HTML 抓 │
│ ③ Body 双层 anti-bot 判断 │
│ ④ Regex 解析 emails/phones/socials │
│ ⑤ 失败 → tab fallback (旧路径) │
└──┬──────────────────────────────────────────────────────────┘
│ 抓到 contact
↓
┌─────────────────────────────────────────────────────────────┐
│ [Layer 4] 回写云端 + 贡献度奖励 │
│ - 上传 contact info(去重)+ 获得贡献度 (+5/+3/+1) │
│ - 更新 domainState(供其他客户端用) │
└─────────────────────────────────────────────────────────────┘
Phase 1 — 客户端多阶段抓取(独立可上线)¶
Phase 1.1 决策树(已与用户对齐)¶
URL
↓
[① 黑名单] (0ms)
├─ deadDomains → SKIP
└─ antiBotDomains → tab fallback
↓
[② HEAD probe] (1s timeout)
├─ DNS/TCP/SSL fail → mark dead, SKIP
├─ 404/410 → mark dead, SKIP
├─ 5xx → 24h retry queue
├─ cf-mitigated: challenge/block → mark antiBot, tab fallback
├─ 403/503 + cf-ray → mark antiBot, tab fallback
├─ 429 → retry queue
└─ 200/204 + 其它 → 进入 ③
↓
[③ GET fetch] (5s timeout, custom UA + Referer)
├─ network timeout/error → tab fallback
├─ Content-Type 非 HTML → SKIP
├─ Body 长度 < 1KB → tab fallback
├─ isChallengeBody(body) → mark antiBot, tab fallback
└─ 正常 HTML → 进入 ④
↓
[④ Regex 解析]
├─ ≥ 1 email/phone → SUCCESS
├─ HTML 含 contact 关键词但抽不到 → tab fallback(JS 渲染)
└─ HTML 无 contact 关键词 → mark 'no contact', SKIP
↓
[⑤ tab fallback] (旧路径)
Phase 1.2 关键代码模块¶
| 文件 | 职责 | 工程量 |
|---|---|---|
src/utils/website-probe.ts |
HEAD probe + 分类(dead/antibot/ok/try) | 1.5h |
src/utils/website-fetcher.ts |
GET fetch + body anti-bot 双层判断 | 2h |
src/utils/contact-extractor.ts |
regex 提取(复用现有 isEmailLikelyBroken) | 1.5h |
src/utils/website-scrape-pipeline.ts |
多阶段主入口 + 路由 | 2h |
集成 scrape-executor.ts |
替换旧入口 | 2h |
| 单元测试 + dogfood | - | 2h |
Phase 1 工程量 ≈ 11h(不含云端协同)
Phase 1.3 anti-bot 双层判断¶
HEAD response 信号(明确):
- cf-mitigated: challenge/block → 直接 antiBot
- 403/503 + cf-ray → 直接 antiBot
- 429 → retry queue
GET body 关键词(HEAD 通过但 body 是 challenge page):
const CHALLENGE_MARKERS = [
'/cdn-cgi/challenge-platform', 'cf-browser-verification', 'cf-im-under-attack',
'cf-challenge-running', 'just a moment', 'checking your browser',
'enable javascript and cookies', 'window._cf_chl_opt', '__cf_chl_jschl_tk__',
'sucuri_cloudproxy_uuid', '__incapsula__', 'awswafcaptchacdk',
];
Server: cloudflare 单独不路由(70% CF 站点是纯 CDN,fetch OK)— 只作 domainStats 标签。
Phase 2 — 域名状态本地管理¶
Phase 2.1 状态机¶
详见 域名状态机 wiki。简版:
| 状态 | 含义 | 行为 | TTL |
|---|---|---|---|
dead |
DNS/SSL/404 永久失败 | 永远 SKIP | 30d(之后重试) |
antibot-hard |
cf-mitigated:block / 永久 | 永远 tab fallback | 90d |
antibot-soft |
challenge 偶发 | fetch 试 + tab 兜底 | 7d 衰减 |
friendly |
fetch 成功率 > 70% | 永远 fetch, 不 fallback | 长期 |
cold |
fetch+tab 都拿不到 contact | 永远 SKIP(除非用户强制) | 90d |
unknown |
默认 / 首次访问 | 完整 pipeline | 7d 后重评估 |
Phase 2.2 数据结构¶
interface DomainStat {
domain: string;
state: 'dead' | 'antibot-hard' | 'antibot-soft' | 'friendly' | 'cold' | 'unknown';
// 最近 N 次(FIFO, max 20)
recent: Array<{
at: number; // timestamp
method: 'head' | 'fetch' | 'tab';
outcome: 'ok-contact' | 'ok-empty' | 'http-error' | 'network-error' | 'antibot' | 'dead';
}>;
// 派生指标(cron 每天重算)
fetchTotal: number;
fetchOk: number; // fetch 拿到 contact
tabTotal: number;
tabOk: number;
// 时间戳
firstSeen: number;
lastSeen: number;
stateExpiresAt: number;
}
存储:storage.local:domainStats:v1 (Record
Phase 2.3 状态转换规则¶
unknown ─(fetch ok-contact × 3 连续)─→ friendly
unknown ─(fetch ok-empty × 5 连续)─→ cold
unknown ─(head dead)─→ dead
unknown ─(head antibot)─→ antibot-soft
antibot-soft ─(antibot × 3 连续)─→ antibot-hard
antibot-soft ─(fetch ok × 2)─→ unknown (恢复)
friendly ─(fetch fail × 5)─→ unknown (重新评估)
任何状态 ─(TTL 到期)─→ 重置 recent + state='unknown'
Phase 3 — 云端协同 + 贡献度¶
⚠️ 该阶段涉及产品/法律/商业决策,本 spec 仅给设计草案,需 PM/法务 review。
Phase 3.1 数据复用模型¶
云端表 domain_contact_pool(按 url hash 主键):
interface CloudContactRecord {
urlHash: string; // sha256(normalized url) primary key
domain: string; // 索引
emails: string[]; // 去脏 + 去重
phones: string[];
socials: Record<string, string>;
// 抓取来源(统计 + 反作弊)
contributorCount: number; // N 个用户独立抓到(相同结果)
firstContributedAt: number;
lastVerifiedAt: number; // 最后一次验证(任何人抓到相同)
// 衰减信号
verifyTotal: number; // 总验证次数
verifyMatch: number; // 数据匹配的次数(用于一致性评分)
}
interface CloudDomainState {
domain: string; // 主键
state: DomainStat['state'];
consensus: number; // 0-1,N 个客户端共识强度
updatedAt: number;
expiresAt: number;
}
Phase 3.2 客户端 → 云端协议¶
| 时机 | 动作 |
|---|---|
| 客户端抓到新 contact | POST /api/scrape/contribute(url + emails + phones + extraction method) |
| 客户端确认 dead/antibot | POST /api/scrape/domain-state(domain + state + evidence) |
| 客户端查询 url | GET /api/scrape/lookup?hashes=... (batch) |
| 客户端批量预热 | POST /api/scrape/prefetch-states body 含 100-1000 domains |
Phase 3.3 数据时效(TTL 设计)¶
| 数据类型 | TTL | 重验证策略 |
|---|---|---|
| emails | 90d | 用户使用后报"无效"→ 立即标 stale |
| phones | 180d (电话号变化少) | 同上 |
domainState=dead |
30d (域名可能被买回) | TTL 到期后任一客户端重新探测 |
domainState=antibot-hard |
90d | 同上 |
domainState=friendly |
长期(无 TTL) | fetch 失败 3 次自动降级 unknown |
domainState=cold |
90d | 周期性给小批量客户端 retry(10%) |
Phase 3.4 贡献度机制(防作弊)¶
积分汇率¶
| 行为 | 贡献度 | 备注 |
|---|---|---|
| 上传新 url contact(云端不存在) | +5 | 真贡献 |
| 上传已存在 url contact,且数据一致 | +1 | 验证贡献(共识增强) |
| 上传已存在 url contact,数据不一致 | 0 | 不奖不罚(可能是脏数据 / 真有更新) |
| 上传 dead/antibot state | +0.5 | 状态贡献,少量奖励 |
| 查询云端拿到 contact | -1 | 消耗 |
| 用户已是 VIP / Pro 用户 | 免费查 | (已付费替代积分) |
| 新用户注册赠 50 分 | +50 | 让用户能立刻用 |
防作弊¶
| 风险 | 防御 |
|---|---|
| 用户随便造假数据上传刷分 | "上传 url 前必须客户端真实抓过" — 服务端校验抓取路径(method 字段)+ 跟其他客户端共识强度比对,超过 2σ 直接拒收 + 标用户 |
| 拿到云端数据后批量倒卖 | rate limit + 单日下载量 cap + 异常用量预警 |
| 同 IP / 同账号刷 | bind device fingerprint + 每日 free 抓取额度 |
| 上传数据正确但用户没真用 | 不直接奖励上传,奖励"被其他用户验证一致" — 验证次数累积才结算 |
积分等级(VIP/付费用户绕开)¶
免费用户:50 起步,0-100 区间,需查询前先贡献
活跃用户(每日 50+ 贡献):自动累积可观分数
重度用户(每日 1000+ 贡献):考虑升 VIP(不限)
付费 VIP:完全免费查(积分不影响)
Phase 3.5 URL 归一化(去重 key)¶
function normalizeUrl(url: string): string {
// 1. lowercase scheme + host
// 2. 去掉 www. 前缀
// 3. 去掉 trailing slash
// 4. 去掉 query/hash(contact 信息通常不依赖 query)
// 5. 去掉端口(如果是 80/443 默认)
// 例:https://www.Example.com/contact/?utm=x → example.com/contact
}
function urlHash(url: string): string {
return sha256(normalizeUrl(url)).slice(0, 16);
}
Phase 3.6 隐私 / 合规风险¶
| 风险 | 应对 |
|---|---|
| GDPR / 个人信息保护法:email/phone 是 PII | 1. 服务条款明确声明用户上传 = 同意共享公开 contact 数据;2. 不上传 user-input form data(只抓公网 HTML 提取);3. 提供"反向请求删除"接口 |
| 网站 robots.txt 禁止抓取 | 客户端 fetch 时 honor robots.txt(先 GET /robots.txt 看 Disallow) |
| 数据所有权:抓的是网站公开数据,但聚合算我们的? | 类比 Common Crawl,公开数据聚合本身合法。用户上传的是用户已抓的数据,所有权问题在用户 vs 网站,平台聚合只是路由 |
| 被反爬大规模封 IP | 客户端分散抓取 → 单点封不杀全局,反而比单服务器抓取健康 |
| 垃圾数据攻击 | 共识机制 + 异常上传检测(见 3.4) |
Phase 3.7 工程量¶
| 模块 | 工程量 |
|---|---|
| 客户端:lookup API client + cache + retry | 4h |
| 客户端:contribute API client + 反作弊签名 | 3h |
| 客户端:积分 UI + 余额查询 + 历史记录 | 5h |
| 服务端:DB schema (postgres + redis cache) | 3h |
| 服务端:lookup/contribute API + rate limit | 8h |
| 服务端:共识算法 + 异常用户检测 cron | 8h |
| 服务端:积分账户 + 流水 + 后台运营 | 12h |
| 法务/产品:服务条款更新 + 隐私政策 | (外部) |
Phase 3 总计 ≈ 40h+ 服务端工作,且需法务 review。
日志可观测性(贯穿所有 Phase)¶
page-log 字段扩展:
interface PageLogEntry {
// 现有
type: 'maps' | 'website' | 'social';
url: string;
status?: number;
emails?: number;
phone?: string;
socials?: string[];
// SPEC-004 新增
method?: 'head' | 'fetch' | 'tab' | 'cloud' | 'cache' | 'skip';
outcome?: 'ok-contact' | 'ok-empty' | 'dead' | 'antibot' | 'cold-skip' | 'error';
fetchMs?: number; // 客户端耗时
bodySize?: number; // GET body 字节数
cloudHit?: boolean; // 是否走云端命中
contribPoints?: number; // 本次产生 +/- 积分
domainState?: DomainStat['state']; // 决策时的 domain state
errorClass?: 'dns' | 'ssl' | 'http' | 'timeout' | 'parse' | 'cf-challenge' | 'wider';
}
日志面板(log-view)加 filter:按 method / outcome / domainState 切分,做 funnel 分析。
分阶段路线图¶
| Phase | 内容 | 工程量 | 风险 | 收益 |
|---|---|---|---|---|
| 1.0 | 客户端 HEAD probe + dead/404 早杀 | 4h | 低 | 5-15% URL 立即 SKIP(节约 5-10s/站) |
| 1.5 | 客户端 GET fetch + body anti-bot + 解析 | 6h | 中 | 50-60% URL 跳过 tab(3-5x 加速) |
| 2.0 | 域名状态本地持久化 + LRU + 衰减 | 6h | 低 | 长期使用累积智能(友好域名直接 fetch) |
| 2.5 | 日志 method/outcome 字段 + funnel 分析 | 3h | 低 | 可观测性 |
| 3.0 | 云端 lookup(只读,免费)+ 命中复用 | 8h 客户端 + 服务端 schema | 中 | 与已有用户库重叠时收益大 |
| 3.5 | 云端 contribute(写) + 共识机制 | 12h 客户端 + 20h 服务端 | 高(法务/隐私/反作弊) | 用户量级越大越值 |
| 4.0 | 贡献度机制 + 积分账户 | 20h 服务端 + UI | 高 | 商业模型变化,需 PM/产品决策 |
推荐执行顺序¶
- 立即上:Phase 1.0 + 1.5(v0.10.88+)— 客户端独立可见效
- 下个 sprint:Phase 2.0 + 2.5 — 智能记忆 + 日志
- 暂缓:Phase 3.x — 等 Phase 1+2 稳定后再评估
- 最远:Phase 4.0 — 积分商业模型重大变化
决策记录(2026-05-28 用户拍板)¶
| Phase | 状态 | 说明 |
|---|---|---|
| Phase 1 | ✅ approved | 本地多阶段抓取,v0.10.88+ 起步 |
| Phase 2 | ✅ approved | 域名状态机本地化,Phase 1 之后接力 |
| Phase 3 | ⏸️ parked | 云端 + 贡献度暂不做。触发条件:本地 Phase 1+2 上线后稳定运行 ≥ 2 周,再评估 |
| Phase 4 | ⏸️ parked | 同 Phase 3 |
暂缓 Phase 3 的理由¶
- 本地优化(Phase 1+2)单独就能解决 80% 痛点:跳过死站 / 跳过 antibot / 复用域名状态
- 云端涉及服务端工程(~30h+)+ 法务合规(数据共享 GDPR)+ 业务决策(贡献度商业模型),先用本地版本拿真实数据再评估投入产出
- Phase 3 下面 7 个待决策项自然进入冷藏(不必现在拍板)
Phase 3+ 冷藏待决策项¶
待 Phase 1+2 稳定后再处理:
- 🤔 客户端是否要 honor robots.txt?(合规 vs 效率)
- 🤔 用户上传 contact 数据 = 默认同意共享?还是 opt-in?
- 🤔 免费查 vs 积分查的免费额度(用户感受 vs 商业模型)
- 🤔 数据时效(90d 是否合理?需 dogfood 校准)
- 🤔 共识算法门槛(N 个用户独立抓到才信?N=2 还是 3)
- 🤔 服务端栈(postgres vs ClickHouse;命中查询性能要求)
- 🤔 老用户已有的本地数据,是否回填云端(一次性迁移)