本文档描述系统要实现的功能和业务逻辑,供开发者从零设计和实现。 版本: v2(精简版,砍掉低 ROI 模块,强调模块化隔离)
一句话:用关键词去 Google 搜,把搜到的网页里的商户联系方式扒下来,清洗后输出一张可以直接联系的客户表。
系统要找的是在 TG 上提供产品或服务的人或组织。
判定标准(满足任意一条即算商户):
不算商户的:聊天用户、新闻频道、系统 bot。
当前只做机场 / VPN / 科学上网。行业规则可配置,以后可扩展。
| 字段 | 说明 |
|---|---|
| 商户名 | 显示名称 |
| TG 用户名 | @xxx |
| TG 链接 | https://t.me/xxx |
| 网站 | 商户官网 |
| 邮箱 | 联系邮箱 |
| 电话 | 联系电话 |
| 来源 | 从哪个网页/渠道发现的 |
| 行业标签 | 机场 / VPN 等 |
| 等级 | Hot / Warm / Cold |
系统分两大部分:采集端和处理端。
处理端:负责清洗、去重、验证、打标签,所有插件共用同一套
┌─────────────────────────────────────────────────────────────┐
│ 采集端(插件式) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 插件 A │ │ 插件 B │ │ 插件 C │ ← 互相 │
│ │ 网页采集 │ │ TG 频道采集 │ │ 未来新增... │ 不影响 │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 统一入口:商户表 raw │ │
│ │ 所有插件的产出格式一样,往同一张表写 │ │
│ └──────────────────────┬─────────────────────────────┘ │
└─────────────────────────┼─────────────────────────────────────┘
│
┌─────────────────────────┼─────────────────────────────────────┐
│ 处理端(固定流程) │
│ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 死号预检 │ → │ 黑名单 │ → │ 去重 │ → │ 打标签 │ │
│ │ (t.me) │ │ 过滤 │ │ 合并 │ │ 分等级 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │ │
│ ▼ │
│ 商户表 clean │
│ (Hot / Warm / Cold) │
└───────────────────────────────────────────────────────────────┘
核心问题:后期加新的采集渠道,不能把前面的东西弄坏。
解决办法:插件隔离。
规则 1: 每个采集插件是独立的代码模块,有自己的目录/文件
规则 2: 插件之间零依赖,A 插件的代码不能 import B 插件的任何东西
规则 3: 所有插件的产出格式统一(见下方"标准产出格式")
规则 4: 插件只管采集,不管清洗/去重/打分 — 那是处理端的事
规则 5: 新增插件 = 新建一个目录 + 实现标准接口,不改任何已有代码
每个插件采集到商户后,必须按这个格式写入 merchants_raw 表:
{
"merchant_name": "商户名(选填)",
"tg_username": "xxx(必填,没有就不入库)",
"tg_link": "https://t.me/xxx",
"website": "官网地址(选填)",
"email": "邮箱(选填)",
"phone": "电话(选填)",
"source_type": "web / tg_channel / github / ...",
"source_name": "具体来源(哪个网页/频道)",
"source_url": "来源 URL",
"original_text": "原始文本(留底)",
"industry_tag": "行业标签(选填)"
}
关键约束:没有 tg_username 的商户不入库。这是核心数据,其他都是锦上添花。
每个插件需要实现以下接口(伪代码):
class CollectorPlugin:
name: str # 插件名,比如 "web_collector"
async def run(config, callback):
"""
config: 该插件的配置(关键词、URL 列表等)
callback(merchant_data): 每找到一个商户就调一次,由框架写入 raw 表
"""
async def stop():
"""外部可以随时叫停"""
框架负责:调度插件、写数据库、记日志、控制并发。 插件负责:采集逻辑,只管找商户,找到就 callback。
这是最高优先级的插件,也是系统的核心价值。
关键词 → Google 搜索 → 拿到 URL 列表
│
┌─────────┴─────────┐
↓ ↓
URL 是 t.me/xxx URL 是网页
直接提取 username 打开网页读 HTML
│ │
│ ┌─────┴──────┐
│ ↓ ↓
│ 找 t.me 链接 找联系方式
│ 提取 username (邮箱/电话/网址)
│ │ │
└──────┬──────┘ │
↓ │
写入 merchants_raw ←────────┘
第一步:关键词搜索
第二步:URL 分拣
拿到的 URL 分三种:
| URL 类型 | 怎么判断 | 怎么处理 |
|---|---|---|
t.me/xxx |
URL 以 t.me/ 开头 | 直接提取 username,写 raw 表 |
| 导航站/有用网页 | 不在黑名单里的网页 | 打开网页,进入第三步 |
| 垃圾 | 在黑名单里(twitter/google/youtube 等 80 个域名) | 丢弃 |
第三步:网页解析
t.me/xxx 链接 → 提取 usernamemailto:xxx → 提取邮箱HTTP 请求失败时的 fallback(按顺序尝试):
net/http(或 colly 爬虫框架)| 方案 | 免费额度 | 付费 | 推荐 |
|---|---|---|---|
| Brave Search API | 5000 次/月 | $5/1000 次 | 先用这个测试 |
| Serper.dev | 2500 次(一次性) | $50/50000 次 | Google 结果最准 |
| DuckDuckGo | 无限 | 免费 | 开源库,稳定性差 |
建议:先用 Brave 免费额度测试,结果不够好再切 Serper。代码层面做成可配置,换 API 只改配置不改代码。
等插件 A 跑稳了再开发这个。 这是锦上添花,不是核心。
种子频道列表 → 进频道 → 读历史消息(最近 500 条)
│
每条消息看有没有联系方式
正则快扫 → 有 → AI 精确提取
│
写入 merchants_raw(标准格式)
@xxx、t.me/xxx、邮箱、网址TG 账号是稀缺资源,需要专门的调度器:
entity ID 缓存(必须做):
ResolveUsername 拿到频道的数字 ID 后存到本地ResolveUsername限速处理:
每个 TG 账号需要的信息:
| 字段 | 说明 |
|---|---|
| 手机号 | 注册 Telegram 用的 |
| api_id | 在 https://my.telegram.org 申请 |
| api_hash | 同上 |
| session 文件 | 首次登录后生成 |
以下是以后可能加的插件,现在不做,但架构要能支持:
| 插件 | 数据源 | 什么时候加 |
|---|---|---|
| GitHub 搜索 | GitHub README 里的 t.me 链接 | 网页+TG 都稳定后 |
| TG 频道裂变 | 从种子频道滚雪球发现新频道 | TG 采集稳定后 |
| 百度搜索 | 百度搜索结果 | 如果 Google 覆盖不够 |
| Twitter/X | 推文里的 t.me 链接 | 如果有需求 |
加新插件的步骤(这是模块化的价值):
run() 和 stop() 接口所有插件的产出都进 merchants_raw 表,然后统一过清洗流程。
merchants_raw → [死号预检] → [黑名单过滤] → [去重合并] → [打标签分等级] → merchants_clean
https://t.me/{username}tgme_page_photo_image 标记| 规则 | 处理 |
|---|---|
| TG 用户名是系统 bot(@BotFather、@SpamBot、以 bot 结尾) | 标记 bot |
| TG 用户名像邀请链接哈希(16-24 位随机字符串) | 标记 invalid |
| 原始文本不含中文(如果有原始文本的话) | 标记 invalid |
行业标签:用关键词匹配(商户名/原始文本里包含"机场""节点""VPN"→ 打标签)。只做机场/VPN 一个行业时,关键词匹配完全够用,不需要 AI。
等级划分(3 个桶,不打分):
| 等级 | 条件 | 含义 |
|---|---|---|
| Hot | 行业匹配 + 有网站或邮箱 | 优先联系,信息最全 |
| Warm | 行业匹配 + 只有 TG 号 | 可以联系,但信息少 |
| Cold | 行业不匹配 / 信息太少 | 暂不联系 |
为什么不用 0-100 打分:
如果有 TG 账号且未被限速,可以在第三步和第四步之间加一步:
ResolveUsername 验证账号真实性但这不是必须的。 没有 TG 账号,系统照样能跑(靠 t.me 预检 + 黑名单就能过滤大部分垃圾)。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| keyword | string | 搜索关键词 |
| industry_tag | string | 行业标签(机场/VPN) |
| enabled | bool | 是否启用 |
| created_at | datetime | 创建时间 |
种子频道也放这个表(industry_tag = 'seed'),不单独建表。
所有插件的产出统一写这张表。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| tg_username | string | 必填,TG 用户名 |
| tg_link | string | t.me 链接 |
| merchant_name | string | 商户名 |
| website | string | 官网 |
| string | 邮箱 | |
| phone | string | 电话 |
| source_type | string | 来源类型(web / tg_channel / github) |
| source_name | string | 具体来源(哪个网页/频道) |
| source_url | string | 来源 URL |
| original_text | text | 原始文本 |
| industry_tag | string | 行业标签 |
| status | string | raw / processing / done |
| created_at | datetime | 入库时间 |
入库去重规则:同 tg_username + 同 source_url 不重复插入。不同来源发现同一个 username 允许多条(去重在清洗阶段做)。
清洗通过的商户。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| tg_username | string | TG 用户名 |
| tg_link | string | t.me 链接 |
| merchant_name | string | 商户名 |
| website | string | 官网 |
| string | 邮箱 | |
| phone | string | 电话 |
| source_count | int | 被多少个来源发现 |
| all_sources | text | 所有来源列表(JSON) |
| industry_tag | string | 行业标签 |
| level | string | Hot / Warm / Cold |
| status | string | valid / invalid / bot / duplicate |
| is_alive | bool | t.me 预检结果 |
| last_checked_at | datetime | 最近一次验证时间 |
| created_at | datetime | 首次发现时间 |
只有启用了 TG 采集插件才需要这张表。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| username | string | 频道用户名(唯一) |
| channel_id | bigint | TG 数字 ID(缓存,避免重复 resolve) |
| access_hash | bigint | TG access_hash(缓存) |
| status | string | pending / scraped / skipped |
| last_message_id | int | 上次采集到哪条(断点续传) |
| merchants_found | int | 发现了多少商户 |
| source | string | seed / discovered |
| created_at | datetime | 入库时间 |
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| task_type | string | web_collect / tg_collect / clean / ... |
| plugin_name | string | 哪个插件 |
| status | string | running / success / failed / stopped |
| items_processed | int | 处理了多少条 |
| merchants_added | int | 新增了多少商户 |
| errors_count | int | 错误数 |
| started_at | datetime | 开始时间 |
| finished_at | datetime | 结束时间 |
| detail | text | 详细日志/错误信息 |
早期只需要 2 个页面,其他的等有需求了再加。
merchants_clean 表的数据| 功能 | 为什么不做 |
|---|---|
| 数据量小时看列表就够了 | |
| 改配置文件比写前端快 | |
| SSH 看日志就行 | |
| 初期手动维护,量不大 |
| 层 | 选型 | 说明 |
|---|---|---|
| 后端语言 | Go | 高并发、编译型、单二进制部署 |
| Web 框架 | Gin 或 Echo | 轻量 HTTP 框架 |
| ORM | GORM | Go 主流 ORM |
| 数据库 | SQLite(初期)/ PostgreSQL(后期) | 初期单机够用 |
| TG 客户端 | gotd/td 或 gotdlib | Go 原生 Telegram MTProto 库 |
| HTML 解析 | goquery | 类似 jQuery 的 HTML 解析 |
| HTTP 客户端 | net/http + colly | 标准库 + 爬虫框架 |
| 浏览器引擎 | chromedp 或 rod | Go 原生 Chrome DevTools Protocol(替代 Playwright) |
| 前端 | Vue 3 + Vite + TypeScript | 不变 |
| 配置 | YAML(viper 库) | Go 生态标准 |
| 日志 | zerolog 或 zap | 结构化日志 |
| 服务 | 用途 | 备注 |
|---|---|---|
| 搜索 API | 关键词搜索 | Brave(免费 5000 次/月)或 Serper($50/50000 次) |
| HTTP 客户端 | 抓网页、t.me 预检 | net/http + colly + chromedp 三层 fallback |
| 服务 | 用途 | 备注 |
|---|---|---|
| gotd/td | TG 频道采集 | Go 原生 MTProto 库,替代 Python Telethon |
| AI 大模型 API | 联系方式提取 | 智谱 GLM 或 DeepSeek,仅 TG 采集时用,HTTP 调用即可 |
规则优先,AI 只在一个地方用。
| 环节 | 方法 | 说明 |
|---|---|---|
| 网页联系方式提取 | 纯正则 | 网页上的 t.me 链接、邮箱、电话,正则就能 100% 提取 |
| TG 消息联系方式提取 | 正则 + AI | 非标准格式("加V:xxx")需要 AI |
| 行业分类 | 纯关键词匹配 | 只做机场/VPN,关键词够用 |
| 导航站识别 | 纯规则(黑名单 + 正向关键词) | 不需要 AI |
AI 只在 TG 采集插件的联系方式提取环节使用。网页采集完全不需要 AI。
每个插件可以独立跑:
也可以串起来:网页采集 → 清洗(两步就够了)
单账号一天最多几百次 ResolveUsername,之后被限速 10-24 小时。
根治方案:缓存 channel_id + access_hash,同一个频道只调一次 ResolveUsername,之后用数字 ID 直接访问。
访问 https://t.me/{username},HTML 里有 tgme_page_photo_image = 活号,没有 = 死号。准确率 100%,不限速,不花钱。在调 TG API 之前先做这一步能省 90% 的 API 调用。
AI 提取后必须用正则回原文二次验证。原文里找不到的,丢弃 AI 的结果。
用两张表(raw 和 clean),清洗通过的搬到 clean 表。raw 表保留原始数据,可以反复清洗。
系统只做中文商户,非中文的网页/消息直接跳过,节省大量处理时间。
有些网页有反爬(Cloudflare),有些是 JS 渲染。按顺序试:net/http → utls(自定义 TLS 指纹)→ chromedp/rod(浏览器引擎)。
先把一个插件(网页采集)做稳做透,再加第二个(TG)。一上来就做 7 阶段 pipeline,结果哪个都不稳。
tg-lead-scraper/
├── cmd/
│ └── server/
│ └── main.go # 程序入口
│
├── internal/
│ ├── plugin/ # 插件框架
│ │ ├── interface.go # 插件接口定义(Collector interface)
│ │ └── registry.go # 插件注册中心
│ │
│ ├── plugins/ # 采集插件目录(每个插件一个包)
│ │ ├── webcollector/ # 插件 A:网页采集
│ │ │ ├── collector.go # 实现 Collector 接口
│ │ │ ├── searcher.go # 调搜索 API
│ │ │ └── parser.go # 解析网页 HTML
│ │ ├── tgcollector/ # 插件 B:TG 频道采集
│ │ │ ├── collector.go # 实现 Collector 接口
│ │ │ ├── scraper.go # TG 消息采集
│ │ │ └── account.go # TG 账号调度
│ │ └── githubcollector/ # 插件 C:未来新增
│ │ └── ...
│ │
│ ├── processor/ # 处理端(清洗流程)
│ │ ├── pipeline.go # 清洗流水线调度
│ │ ├── tmechecker.go # t.me 死号预检
│ │ ├── blacklist.go # 黑名单过滤
│ │ ├── dedup.go # 去重合并
│ │ └── tagger.go # 打标签 + 分等级
│ │
│ ├── model/ # 数据模型
│ │ ├── merchant.go # 商户结构体 + GORM model
│ │ ├── channel.go # 频道
│ │ ├── keyword.go # 关键词
│ │ └── tasklog.go # 任务日志
│ │
│ ├── store/ # 数据访问层
│ │ ├── db.go # 数据库连接 + 初始化
│ │ ├── merchant_repo.go # 商户 CRUD
│ │ └── keyword_repo.go # 关键词 CRUD
│ │
│ ├── extractor/ # 联系方式提取器
│ │ ├── regex.go # 正则提取
│ │ └── ai.go # AI 提取(调大模型 API)
│ │
│ └── task/ # 任务调度
│ └── manager.go # 任务启停、并发控制
│
├── api/ # HTTP API
│ ├── server.go # Gin/Echo 初始化
│ ├── handler/
│ │ ├── merchant.go # 商户列表 API
│ │ └── task.go # 任务管理 API
│ └── middleware/
│ └── auth.go # 认证中间件
│
├── frontend/ # 前端(Vue 3)
│ └── ...
│
├── config/
│ └── config.yaml # 全局配置
│
├── go.mod
├── go.sum
└── Makefile
// internal/plugin/interface.go
package plugin
import "context"
// MerchantData 是所有插件的标准产出格式
type MerchantData struct {
TgUsername string `json:"tg_username"`
TgLink string `json:"tg_link"`
MerchantName string `json:"merchant_name"`
Website string `json:"website"`
Email string `json:"email"`
Phone string `json:"phone"`
SourceType string `json:"source_type"`
SourceName string `json:"source_name"`
SourceURL string `json:"source_url"`
OriginalText string `json:"original_text"`
IndustryTag string `json:"industry_tag"`
}
// Collector 是所有采集插件必须实现的接口
type Collector interface {
// Name 返回插件名,比如 "web_collector"
Name() string
// Run 启动采集,每找到一个商户就调 callback,ctx 取消时优雅退出
Run(ctx context.Context, cfg map[string]any, callback func(MerchantData)) error
// Stop 外部可以随时叫停
Stop() error
}
internal/plugins/ 下新建包(比如 baiducollector/)Collector 接口的 3 个方法registry.go 注册插件名config.yaml 加插件配置┌─────────── 采集端 ───────────┐
│ │
│ 关键词 → [网页采集插件] │
│ │ │
│ ├→ t.me 链接 │
│ └→ 网页 → 解析 │ ┌─────── 处理端 ──────┐
│ │ │ │ │
│ ↓ │ │ [死号预检] │
│ merchants_raw ←────┼──→ │ ↓ │
│ ↑ │ │ [黑名单过滤] │
│ 种子频道 → [TG 采集插件] │ │ ↓ │
│ │ │ │ [去重合并] │
│ └→ 消息 → AI提取 │ │ ↓ │
│ │ │ [打标签分等级] │
│ (未来) → [GitHub 插件] │ │ ↓ │
│ (未来) → [百度插件] │ │ merchants_clean │
│ (未来) → [Twitter 插件] │ │ (Hot/Warm/Cold) │
│ │ │ │
└────────────────────────────────┘ └───────────────────────┘
│
↓
前端:商户列表 + 导出