From 8800d4b2b125ff3de2cb0061ba1c45cebf988d15 Mon Sep 17 00:00:00 2001 From: v6ole Date: Sat, 9 May 2026 12:52:48 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=E7=88=AC=E8=99=AB?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=8C=96=E8=AE=BE=E8=AE=A1=EF=BC=88Spider=20?= =?UTF-8?q?=E5=9F=BA=E7=B1=BB=20+=20Pipeline=20=E7=AD=96=E7=95=A5=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...6-05-09-fastapi-docker-migration-design.md | 184 +++++++++++++++--- 1 file changed, 155 insertions(+), 29 deletions(-) diff --git a/docs/superpowers/specs/2026-05-09-fastapi-docker-migration-design.md b/docs/superpowers/specs/2026-05-09-fastapi-docker-migration-design.md index 8ec327f..125ea52 100644 --- a/docs/superpowers/specs/2026-05-09-fastapi-docker-migration-design.md +++ b/docs/superpowers/specs/2026-05-09-fastapi-docker-migration-design.md @@ -42,12 +42,14 @@ gx-gp-notify/ │ │ └── schemas.py # Pydantic 请求/响应模型 │ ├── crawler/ │ │ ├── __init__.py -│ │ ├── spider.py # 爬虫核心 -│ │ ├── dahuagov_spider.py # 大化县专用爬虫 +│ │ ├── base.py # Spider 抽象基类 + Pipeline 配置 +│ │ ├── gxgp_spider.py # 广西政府采购网爬虫 (API 模式) +│ │ ├── dahuagov_spider.py # 大化县政府网爬虫 (HTML 模式) │ │ └── parsers.py # 数据解析器 │ ├── services/ │ │ ├── __init__.py -│ │ ├── crawl_service.py # 爬取业务流程 +│ │ ├── crawl_service.py # 爬取编排器 (注册/调用所有 Spider) +│ │ ├── pipeline.py # 统一后处理管道 (存储→筛选→推送) │ │ ├── filter_service.py # 筛选逻辑 │ │ └── notification_service.py # 通知服务 │ ├── wechat/ @@ -78,7 +80,142 @@ gx-gp-notify/ --- -## 3. API 端点设计 +## 3. 爬虫模块化设计 + +### 3.1 设计动机 + +系统爬取两个不同来源的公告:广西政府采购网(API 模式,13 种公告类型)和大化县政府网(HTML 解析,单一来源)。两个爬虫的后处理流程不同: + +| 对比 | 广西政府采购网 | 大化县政府网 | +|------|---------------|-------------| +| 爬取方式 | POST JSON API | GET HTML + BeautifulSoup | +| 反爬机制 | 敏感词检查 | 无 | +| 分页 | 多页轮询 | 单页 | +| 筛选策略 | 关键词 + 日期 + 去重 | 不过滤 | +| 推送策略 | 只推送关键词匹配的 | 全部推送 | +| 已发送标记 | is_new 标记 | is_new → mark_sent | + +为避免两个爬虫各自维护一套存储/筛选/推送逻辑,采用 **Spider 接口 + Pipeline 策略模式** 分离关注点。 + +### 3.2 架构 + +``` +┌─────────────────────────────────────────────────┐ +│ CrawlOrchestrator │ +│ (注册所有 Spider,统一调度爬取) │ +└─────────────────────┬───────────────────────────┘ + │ + ┌─────────────┼─────────────┐ + │ │ │ +┌───────▼──────┐ ┌────▼──────┐ ┌────▼──────────┐ +│ GXGPSpider │ │ Dahuagov │ │ (FutureSpider) │ +│ (API 模式) │ │ Spider │ │ │ +│ 13 sources │ │ (HTML模式) │ │ │ +└───────┬──────┘ └────┬──────┘ └────┬───────────┘ + │ │ │ + └─────────────┼─────────────┘ + │ List[Announcement] + │ +┌─────────────────────▼───────────────────────────┐ +│ PostCrawlPipeline │ +│ │ +│ 每个 Spider 声明 PipelineConfig: │ +│ - filter_enabled: 是否启用关键词筛选 │ +│ - keywords: 筛选关键词列表 │ +│ - dedup_enabled: 是否去重 │ +│ - notify_mode: "filtered" | "all" │ +│ - mark_sent: 推送后是否标记已发送 │ +│ │ +│ 统一处理流程: │ +│ 存储 → 去重 → 筛选(可选) → 推送(可选) → 标记(可选) │ +└──────────────────────────────────────────────────┘ +``` + +### 3.3 Spider 基类 + +```python +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import List + +@dataclass +class PipelineConfig: + """Spider 后处理策略配置""" + filter_enabled: bool = True + keywords: List[str] = None + dedup_enabled: bool = True + notify_mode: str = "filtered" # "filtered" | "all" + mark_sent: bool = False + + +class BaseSpider(ABC): + """爬虫基类 — 只负责爬取+解析,不管后续如何处理""" + + name: str # spider 名称 + source_code: str # 来源代码 + source_name: str # 来源名称 + + @abstractmethod + async def crawl(self) -> CrawlResult: + """执行爬取,返回包含 Announcement 列表的 CrawlResult""" + ... + + def get_pipeline_config(self) -> PipelineConfig: + """返回后处理策略(子类可覆盖)""" + return PipelineConfig() +``` + +### 3.4 两个 Spider 的 PipelineConfig + +```python +# GXGPSpider — 关键词筛选模式 +PipelineConfig( + filter_enabled=True, + keywords=["大化"], # 从配置读取 + dedup_enabled=True, + notify_mode="filtered", # 只推送匹配关键词的 + mark_sent=False, # 用 is_new 标记,不单独 mark_sent +) + +# DahuagovSpider — 全量推送模式 +PipelineConfig( + filter_enabled=False, + keywords=[], + dedup_enabled=True, + notify_mode="all", # 全部推送 + mark_sent=True, # 推送后标记已发送 +) +``` + +### 3.5 统一后处理管道 + +```python +class PostCrawlPipeline: + """统一后处理管道 — 所有 Spider 共用""" + + async def process(self, announcements: List[Announcement], + config: PipelineConfig) -> PipelineResult: + # 1. 统一存入 announcements 表 (按 source_code 区分) + # 2. 去重 (基于 content_hash) + # 3. 根据 config.filter_enabled 决定是否筛选 + # 4. 根据 config.notify_mode 决定推送策略 + # - "all": 推送全部新公告 + # - "filtered": 只推送匹配关键词的 + # 5. 根据 config.mark_sent 标记已发送 + # 6. 生成 Markdown (可选) + ... +``` + +### 3.6 优势 + +- **新增爬虫源**只需实现 `BaseSpider.crawl()` + 声明 `PipelineConfig`,推送/存储逻辑零改动 +- **推送逻辑单一入口**,修改一处对所有源生效 +- 每个 Spider 文件只关注"怎么爬"和"怎么解析",不再包含存储/筛选/推送代码 +- `dahuagov_announcements` 独立表可合并到统一的 `announcements` 表,用 `source_code='dahuagov'` 区分 + +--- + +## 4. API 端点设计 ### 3.1 公告查询 @@ -127,11 +264,11 @@ gx-gp-notify/ --- -## 4. 数据模型 +## 5. 数据模型 -### 4.1 公告表 (announcements) +### 5.1 公告表 (announcements) -合并现有的 `announcements`、`auto_announcements`、`manual_announcements` 三张表为一张,通过 `crawl_mode` 字段区分自动/手动。 +合并现有的 `announcements`、`auto_announcements`、`manual_announcements`、`dahuagov_announcements` 四张表为一张,通过 `source_code` 字段区分来源,通过 `PipelineConfig` 控制每个来源的推送策略。 | 字段 | 类型 | 说明 | |------|------|------| @@ -140,41 +277,30 @@ gx-gp-notify/ | publish_date | DateTime | 发布时间 | | purchase_name | String(200) | 发布单位 | | content_url | Text | 内容链接 | -| source_code | String(50) | 来源代码 | +| source_code | String(50) | 来源代码(ZcyAnnouncement1 / dahuagov 等) | | source_name | String(100) | 来源名称 | | announcement_type | String(50) | 公告类型枚举值 | -| content_hash | String(64) | SHA256 内容哈希,UNIQUE | +| content_hash | String(64) | SHA256 内容哈希,UNIQUE INDEX | | crawl_mode | String(20) | "auto" / "manual" | | is_new | Boolean | 是否新公告 | -| is_today | Boolean | 是否今日公告 | +| is_sent | Boolean | 是否已推送 | +| keyword_matched | Boolean | 是否匹配关键词 | | created_at | DateTime | 创建时间 | | updated_at | DateTime | 更新时间 | -### 4.2 大化县公告表 (dahuagov_announcements) +### 5.2 设计决策 -保留独立表,因为推送逻辑不同(全部推送,不筛选关键词)。 +- **四表合一**:不再为不同来源或抓取模式创建独立表。`source_code` 区分来源(`ZcyAnnouncement1` ~ `dahuagov`),`crawl_mode` 区分自动/手动 +- **is_sent 字段**:替代原来大化县的 `is_new → mark_sent` 模式,同时为广西采购网的公告提供统一的已推送标记 +- **keyword_matched 字段**:在筛选阶段标记,持久化到数据库方便后续查询 -| 字段 | 类型 | 说明 | -|------|------|------| -| id | Integer | 主键 | -| title | String(500) | 公告标题 | -| publish_date | DateTime | 发布时间 | -| purchase_name | String(200) | 发布单位 | -| content_url | Text | 内容链接 | -| source_code | String(50) | 默认 "dahuagov" | -| source_name | String(100) | 默认 "大化县政府网采购公告" | -| content_hash | String(64) | SHA256 内容哈希,UNIQUE | -| is_new | Boolean | 是否新公告(未推送) | -| created_at | DateTime | 创建时间 | -| updated_at | DateTime | 更新时间 | - -### 4.3 变更说明 +### 5.3 变更说明 - `content_hash` 从 MD5 (32位) 改为 SHA256 (64位) - 去掉 `announcement_sources` 表,来源信息从环境变量配置动态读取 - 去掉 `crawl_results` 表,爬取结果通过日志记录 -- 去掉多余的索引表(keyword_matched / date_filtered 字段),筛选在应用层处理 - 使用 Alembic 管理数据库迁移 +- 旧数据迁移:通过 Alembic migration 脚本将现有四张表的数据合并到新表 --- @@ -387,4 +513,4 @@ __pycache__/ | 数据库迁移丢失数据 | Alembic 自动生成迁移脚本,先在测试环境验证 | | 异步爬虫被目标站限流 | 保留现有延迟/重试机制,httpx 支持同样的超时配置 | | 企业微信回调兼容性 | 回调路径不变,加解密逻辑完全复用现有代码 | -| Docker 网络访问 10.10.10.14 | 确认 Docker 宿主机可访问该 IP,必要时用 `host.docker.internal` | +| Docker 网络访问 10.10.10.14 | 确认 Docker 宿主机可访问该 IP,必要时用 `host.docker.internal` | \ No newline at end of file