docs: 添加爬虫模块化设计(Spider 基类 + Pipeline 策略模式)
This commit is contained in:
@@ -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` |
|
||||
Reference in New Issue
Block a user