TG商户采集系统-需求方案书.md 28 KB

TG 商户采集系统 — 需求方案书(v2)

本文档描述系统要实现的功能和业务逻辑,供开发者从零设计和实现。 版本: v2(精简版,砍掉低 ROI 模块,强调模块化隔离)


一、系统目标

一句话:用关键词去 Google 搜,把搜到的网页里的商户联系方式扒下来,清洗后输出一张可以直接联系的客户表。

什么是"商户"

系统要找的是在 TG 上提供产品或服务的人或组织

判定标准(满足任意一条即算商户):

  • 有 TG 联系方式(@xxx 或 t.me/xxx)并且有商业意图(接单、代理、价格、购买、咨询、客服、官网、订阅)
  • 被导航站收录(导航站本身就是商户目录)

不算商户的:聊天用户、新闻频道、系统 bot。

目标行业(当前)

当前只做机场 / VPN / 科学上网。行业规则可配置,以后可扩展。

输入

  • 一组关键词(比如"机场推荐"、"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。


三、采集插件 A:网页采集(优先开发)

这是最高优先级的插件,也是系统的核心价值。

为什么优先

  • 一个导航站几秒出 50 个商户,效率最高
  • 没有限速问题,想跑多快跑多快
  • 导航站上的商户是别人已经整理好的,质量高

流程

关键词 → Google 搜索 → 拿到 URL 列表
                              │
                    ┌─────────┴─────────┐
                    ↓                   ↓
              URL 是 t.me/xxx      URL 是网页
              直接提取 username     打开网页读 HTML
                    │                   │
                    │             ┌─────┴──────┐
                    │             ↓            ↓
                    │        找 t.me 链接   找联系方式
                    │        提取 username  (邮箱/电话/网址)
                    │             │            │
                    └──────┬──────┘            │
                           ↓                   │
                    写入 merchants_raw ←────────┘

详细逻辑

第一步:关键词搜索

  1. 从关键词表拿关键词(比如"机场推荐 telegram")
  2. 调搜索 API(Serper 或 Brave Search),拿搜索结果
  3. 每个关键词搜 3-5 页,每页 10 条
  4. 关键词之间等几秒,避免被封

第二步:URL 分拣

拿到的 URL 分三种:

URL 类型 怎么判断 怎么处理
t.me/xxx URL 以 t.me/ 开头 直接提取 username,写 raw 表
导航站/有用网页 不在黑名单里的网页 打开网页,进入第三步
垃圾 在黑名单里(twitter/google/youtube 等 80 个域名) 丢弃

第三步:网页解析

  1. 用 HTTP 请求抓网页 HTML
  2. HTML 前 3000 字不含中文 → 跳过(不是中文站)
  3. 解析 HTML,找所有外链:
    • t.me/xxx 链接 → 提取 username
    • mailto:xxx → 提取邮箱
    • 电话号码正则 → 提取电话
  4. 如果页面上有很多 t.me 链接(>5 个),说明这是个导航站,每个链接都是一个商户
  5. 每个提取到的商户按标准格式写入 raw 表

HTTP 请求失败时的 fallback(按顺序尝试):

  1. 标准 net/http(或 colly 爬虫框架)
  2. 带自定义 TLS 指纹的 HTTP 客户端(绕反爬,如 utls 库)
  3. chromedp / rod(Go 原生浏览器引擎,处理 JS 渲染页面)

搜索 API 选择

方案 免费额度 付费 推荐
Brave Search API 5000 次/月 $5/1000 次 先用这个测试
Serper.dev 2500 次(一次性) $50/50000 次 Google 结果最准
DuckDuckGo 无限 免费 开源库,稳定性差

建议:先用 Brave 免费额度测试,结果不够好再切 Serper。代码层面做成可配置,换 API 只改配置不改代码。


四、采集插件 B:TG 频道采集(第二优先级)

等插件 A 跑稳了再开发这个。 这是锦上添花,不是核心。

为什么第二优先级

  • TG 限速严,一天最多出几十个商户
  • 需要手机号注册账号,被封就废
  • 开发和维护成本比网页采集高得多

流程

种子频道列表 → 进频道 → 读历史消息(最近 500 条)
                              │
                    每条消息看有没有联系方式
                    正则快扫 → 有 → AI 精确提取
                              │
                    写入 merchants_raw(标准格式)

详细逻辑

  1. 从种子列表拿频道(比如 @bbs3000),种子由用户手动添加
  2. 用 TG 客户端库(Go: gotd/td)登录 TG 账号,进入频道
  3. 读最近 500 条消息(支持断点续传,记住上次读到哪条)
  4. 每条消息:
    • 系统消息 / 非中文 → 跳过
    • 正则快速扫描有没有 @xxxt.me/xxx、邮箱、网址
    • 有联系方式 → 调 AI 精确提取商户信息
    • AI 超时(>5 秒)或失败 → 用正则兜底
  5. 提取到的商户按标准格式写入 raw 表

TG 账号管理(重要)

TG 账号是稀缺资源,需要专门的调度器:

entity ID 缓存(必须做)

  • 第一次 ResolveUsername 拿到频道的数字 ID 后存到本地
  • 以后直接用数字 ID 访问,不再调 ResolveUsername
  • 这样同一个频道只消耗 1 次 resolve 额度,之后无限次不限速

限速处理

  • 全局请求频率控制(所有模块共享,不超过 30 次/分钟)
  • FloodWait < 60 秒 → 等完继续
  • FloodWait > 60 秒 → 切账号
  • FloodWait > 300 秒 → 停止,下次再来
  • 没有可用账号 → 排队等待,不崩溃

每个 TG 账号需要的信息

字段 说明
手机号 注册 Telegram 用的
api_id https://my.telegram.org 申请
api_hash 同上
session 文件 首次登录后生成

五、采集插件 C/D/E...:未来扩展

以下是以后可能加的插件,现在不做,但架构要能支持

插件 数据源 什么时候加
GitHub 搜索 GitHub README 里的 t.me 链接 网页+TG 都稳定后
TG 频道裂变 从种子频道滚雪球发现新频道 TG 采集稳定后
百度搜索 百度搜索结果 如果 Google 覆盖不够
Twitter/X 推文里的 t.me 链接 如果有需求

加新插件的步骤(这是模块化的价值):

  1. 新建一个目录/文件
  2. 实现 run()stop() 接口
  3. 按标准格式 callback 产出商户
  4. 在配置里注册插件名
  5. 不改任何已有代码

六、处理端:清洗流程

所有插件的产出都进 merchants_raw 表,然后统一过清洗流程。

清洗流水线(4 步,按顺序执行)

merchants_raw → [死号预检] → [黑名单过滤] → [去重合并] → [打标签分等级] → merchants_clean

第一步:t.me 死号预检(免费,无限速)

  • 用 HTTP 请求访问 https://t.me/{username}
  • 看返回 HTML 里有没有 tgme_page_photo_image 标记
  • 有头像 = 活号 → 继续
  • 没头像 = 死号 → 标记 invalid,不进后面的步骤
  • 准确率 100%,不花钱,不限速
  • 建议并发 10 个,每分钟能检 600 个

第二步:黑名单过滤(本地,秒级)

规则 处理
TG 用户名是系统 bot(@BotFather@SpamBot、以 bot 结尾) 标记 bot
TG 用户名像邀请链接哈希(16-24 位随机字符串) 标记 invalid
原始文本不含中文(如果有原始文本的话) 标记 invalid

第三步:去重合并(本地,秒级)

  • 同一个 tg_username 可能被多个插件多次发现
  • 按信息丰富度保留最好的一条(有网站 > 没网站,有邮箱 > 没邮箱)
  • 其他标记为 duplicate
  • 合并所有来源信息到保留的那条

第四步:打标签 + 分等级

行业标签:用关键词匹配(商户名/原始文本里包含"机场""节点""VPN"→ 打标签)。只做机场/VPN 一个行业时,关键词匹配完全够用,不需要 AI。

等级划分(3 个桶,不打分):

等级 条件 含义
Hot 行业匹配 + 有网站或邮箱 优先联系,信息最全
Warm 行业匹配 + 只有 TG 号 可以联系,但信息少
Cold 行业不匹配 / 信息太少 暂不联系

为什么不用 0-100 打分

  • 100 分制需要成员数、Premium、活跃度等数据,但这些要调 TG API 才能拿到
  • TG API 是最大的瓶颈,为了打分去调 API 得不偿失
  • 3 个桶简单直观,销售拿到手就能用

可选增强:TG 验证(需要 TG 账号)

如果有 TG 账号且未被限速,可以在第三步和第四步之间加一步:

  • ResolveUsername 验证账号真实性
  • 拿到:显示名、是否 Premium、最后在线时间
  • 有了这些数据可以更精准地分等级

但这不是必须的。 没有 TG 账号,系统照样能跑(靠 t.me 预检 + 黑名单就能过滤大部分垃圾)。


七、数据模型(5 张表)

表 1:关键词表 (keywords)

字段 类型 说明
id int 主键
keyword string 搜索关键词
industry_tag string 行业标签(机场/VPN)
enabled bool 是否启用
created_at datetime 创建时间

种子频道也放这个表(industry_tag = 'seed'),不单独建表。

表 2:商户表 — 原始 (merchants_raw)

所有插件的产出统一写这张表。

字段 类型 说明
id int 主键
tg_username string 必填,TG 用户名
tg_link string t.me 链接
merchant_name string 商户名
website string 官网
email 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 允许多条(去重在清洗阶段做)。

表 3:商户表 — 已清洗 (merchants_clean)

清洗通过的商户。

字段 类型 说明
id int 主键
tg_username string TG 用户名
tg_link string t.me 链接
merchant_name string 商户名
website string 官网
email 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 首次发现时间

表 4:频道表 (channels)

只有启用了 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 入库时间

表 5:任务日志表 (task_logs)

字段 类型 说明
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 个页面)

早期只需要 2 个页面,其他的等有需求了再加。

页面 1:商户列表

  • 显示 merchants_clean 表的数据
  • 按等级筛选(Hot / Warm / Cold)
  • 按行业筛选
  • 按来源筛选
  • 搜索(按商户名、TG 用户名)
  • 排序(按发现时间、来源数)
  • 导出 CSV / Excel
  • 点击 TG 链接可以直接跳转

页面 2:任务管理

  • 选择插件启动任务(网页采集 / TG 采集 / 清洗)
  • 显示当前运行中的任务
  • 停止任务
  • 查看历史任务和结果

不做的(延后)

功能 为什么不做
仪表盘 数据量小时看列表就够了
配置管理 改配置文件比写前端快
实时日志流 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

可选的(TG 采集插件启用后才需要)

服务 用途 备注
gotd/td TG 频道采集 Go 原生 MTProto 库,替代 Python Telethon
AI 大模型 API 联系方式提取 智谱 GLM 或 DeepSeek,仅 TG 采集时用,HTTP 调用即可

AI 使用策略

规则优先,AI 只在一个地方用。

环节 方法 说明
网页联系方式提取 纯正则 网页上的 t.me 链接、邮箱、电话,正则就能 100% 提取
TG 消息联系方式提取 正则 + AI 非标准格式("加V:xxx")需要 AI
行业分类 纯关键词匹配 只做机场/VPN,关键词够用
导航站识别 纯规则(黑名单 + 正向关键词) 不需要 AI

AI 只在 TG 采集插件的联系方式提取环节使用。网页采集完全不需要 AI。


十、运行方式

单插件运行

每个插件可以独立跑:

  • 只跑网页采集 → 看搜到了什么
  • 只跑清洗 → 处理已有的脏数据
  • 只跑 TG 采集 → 从指定频道挖商户

全链路运行

也可以串起来:网页采集 → 清洗(两步就够了)

任务控制

  • 每个任务有状态:运行中 / 完成 / 失败 / 已停止
  • 支持手动停止
  • 同类型任务不能同时跑两个
  • 支持测试模式(只跑少量数据)

十一、踩过的坑(供参考)

1. TG 限速是最大坑

单账号一天最多几百次 ResolveUsername,之后被限速 10-24 小时。

根治方案:缓存 channel_id + access_hash,同一个频道只调一次 ResolveUsername,之后用数字 ID 直接访问。

2. t.me 网页可以免费检测死号

访问 https://t.me/{username},HTML 里有 tgme_page_photo_image = 活号,没有 = 死号。准确率 100%,不限速,不花钱。在调 TG API 之前先做这一步能省 90% 的 API 调用。

3. AI 会编造联系方式

AI 提取后必须用正则回原文二次验证。原文里找不到的,丢弃 AI 的结果。

4. 清洗后数据和原始数据分开存

用两张表(raw 和 clean),清洗通过的搬到 clean 表。raw 表保留原始数据,可以反复清洗。

5. 非中文内容直接跳过

系统只做中文商户,非中文的网页/消息直接跳过,节省大量处理时间。

6. 网页抓取要有 fallback

有些网页有反爬(Cloudflare),有些是 JS 渲染。按顺序试:net/http → utls(自定义 TLS 指纹)→ chromedp/rod(浏览器引擎)。

7. 不要一上来就做全链路

先把一个插件(网页采集)做稳做透,再加第二个(TG)。一上来就做 7 阶段 pipeline,结果哪个都不稳。


附录 A:模块化目录结构建议(Go)

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

插件接口定义(Go interface)

// 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
}

加新插件的步骤

  1. internal/plugins/ 下新建包(比如 baiducollector/
  2. 实现 Collector 接口的 3 个方法
  3. registry.go 注册插件名
  4. config.yaml 加插件配置
  5. 不改 internal/plugins/ 外的任何代码

附录 B:完整数据流图

┌─────────── 采集端 ───────────┐
│                                │
│  关键词 → [网页采集插件]       │
│              │                 │
│              ├→ t.me 链接      │
│              └→ 网页 → 解析    │      ┌─────── 处理端 ──────┐
│                    │           │      │                       │
│                    ↓           │      │  [死号预检]           │
│            merchants_raw  ←────┼──→   │      ↓               │
│                    ↑           │      │  [黑名单过滤]        │
│  种子频道 → [TG 采集插件]     │      │      ↓               │
│              │                 │      │  [去重合并]          │
│              └→ 消息 → AI提取 │      │      ↓               │
│                                │      │  [打标签分等级]      │
│  (未来)  → [GitHub 插件]      │      │      ↓               │
│  (未来)  → [百度插件]         │      │  merchants_clean     │
│  (未来)  → [Twitter 插件]     │      │  (Hot/Warm/Cold)     │
│                                │      │                       │
└────────────────────────────────┘      └───────────────────────┘
                                                    │
                                                    ↓
                                           前端:商户列表 + 导出