# 政务网站信息采集系统 设计文档

> 版本：v2.1　　日期：2026-04-22
> 目标：**多站点、定时、可持续、低调**地采集政务公开信息，结构化保存到服务器目录与数据库，作为下游已有 RAG 系统的数据源。
> 规模：**20–30 个政府门户网站**，单机部署即可长期运行。
> **核心范围**（v2.1 收敛）：
> - ✅ 做：站点/栏目配置化、增量抓取、反爬绕过、元数据入库、原件落盘
> - ❌ 不做：向量化/切片/Embedding（已有 RAG 系统负责）
> - ❌ 不做：**附件内容解析**（PDF/DOC/ZIP 原件保存即可，由后续系统处理）
> - ❌ 不做：实时抓取（**可滞后 1 天，T+1 即可**）
>
> **对目标站点"无感"是硬性要求**：低频、夜间、严格频控、UA 标识来源，不给对方系统添负担。
>
> **许可证约束**：所有组件必须是**开源许可且允许商业使用**，不得引入 GPL/AGPL/SSPL 等传染性或限制性许可污染主链路。

---

## 1. 背景与目标

### 1.1 背景
- 从多个政府门户网站（如 `www.gdqy.gov.cn` 等）的多个栏目定时采集**政务动态、政策法规、最新文件**等公开信息。
- 采集字段：标题、发布时间、发布主体（来源）、正文、附件（PDF/DOC/图片等）、原文 URL、所属栏目、抓取时间。
- 采集结果**归类落地**到服务器文件目录 + 数据库，供已有 RAG 系统按需拉取。
- 主要挑战：政务站普遍部署**专业反爬**（瑞数、知道创宇 ctct 盾、顶象等）。实测 `gdqy.gov.cn` HTTP 请求返回 412 + 滑块挑战；**真实 Chrome 内核可自动过挑战**。

### 1.2 设计目标
| 维度 | 目标 |
|---|---|
| 覆盖 | 任意政务站点，新站点只需 YAML 配置，不需改代码 |
| 稳定 | 单站成功率 ≥ 95%；自动降级 + 重试 |
| 可持续 | 分层抓取、Cookie 复用，资源成本可控 |
| 合规 | 遵守 robots、低频访问、UA 标识来源 |
| 易对接 RAG | 结构化元数据 + 原始文件 + 纯文本并存，三种消费方式 |

### 1.3 非目标（明确不做）
- ❌ **向量化 / Embedding / 切片**（已有 RAG 系统负责）
- ❌ 登录态抓取、付费内容抓取
- ❌ 破解验证码（真滑块触发时跳过或人工介入）
- ❌ 实时抓取（分钟/小时/日级定时即可）
- ❌ 在线问答、全文检索（RAG 系统做，本系统只保证"存好、能拉"）

---

## 2. 关键实测结论

| 方式 | 对 `gdqy.gov.cn` 的表现 |
|---|---|
| WebFetch / httpx + 真实 UA | ❌ HTTP 412，返回 `请稍候…` 挑战页（ctct 盾 + 滑块） |
| **真实 Chrome 内核（Playwright）** | ✅ 自动过 JS 挑战，拿到完整正文与元数据 |

**结论**：
1. 政务站反爬主要是 JS 挑战 + Cookie 种值，**浏览器可自动过**，多数不触发真滑块。
2. 浏览器抓取资源成本是 HTTP 的 20–50 倍，**不能当主力**，必须分层调度。
3. 过挑战后的 Cookie 可复用 2–6 小时，**Cookie 池**是降本关键。

---

## 3. 总体架构

```
                    ┌─────────────────────────────────────┐
                    │          管理后台 (Admin UI)         │
                    │   站点/栏目配置 · 任务监控 · 数据浏览  │
                    └────────────────┬────────────────────┘
                                     │
            ┌────────────────────────┼────────────────────────┐
            ↓                        ↓                        ↓
     ┌──────────────┐        ┌──────────────┐         ┌──────────────┐
     │ 调度层        │        │ 采集层        │         │ 存储层        │
     │ APScheduler  │ ─任务─▶│  Fetcher     │ ─内容─▶ │ PG + 本地目录 │
     │ (Cron, T+1)  │        │  ├ httpx     │         │              │
     └──────────────┘        │  ├ playwright │         └──────┬───────┘
            ▲                │  └ drission  │                │
            │                └──────┬───────┘                │
            │                       ↓                        ↓
            │                ┌──────────────┐         ┌──────────────┐
            │                │ 解析层        │         │  对外输出     │
            │                │ XPath + GNE  │         │ REST / 文件   │
            │                └──────┬───────┘         │ → RAG 系统    │
            │                       ↓                 └──────────────┘
            │                ┌──────────────┐
            └──── 日志/状态 ── │ 去重 + 入库   │
                             └──────────────┘
```

**数据流向**：
- 列表页 → 详情页 URL → 抓取/解析 → 元数据入 PG + 原始 HTML + 附件落文件系统
- RAG 系统通过 **REST 接口 或 直接读共享目录 / 直接查库** 获取增量数据

### 3.1 模块划分

| 模块 | 职责 |
|---|---|
| **站点配置中心** | YAML + DB 管理站点、栏目、选择器、抓取策略、频率 |
| **调度器** | 按 Cron 触发，支持手动触发与补抓 |
| **Fetcher（三档）** | httpx（首选）→ Playwright stealth（强反爬）→ DrissionPage（兜底） |
| **Cookie 池** | 每域名共享过挑战 Cookie（Valkey），命中走 HTTP，未命中才起浏览器 |
| **解析器** | XPath/CSS 主选 + GNE（通用新闻抽取）兜底 |
| **附件处理** | **下载原文件到本地目录即可**（PDF/DOC/ZIP 等），**不做任何解析/OCR/抽文本**，交由下游系统处理 |
| **去重** | URL 归一化 + 正文 SimHash 双重去重 |
| **存储** | PG（元数据）、本地文件系统（原件/附件，按站点/栏目/日期归档） |
| **对外接口** | REST API 提供增量列表 + 详情 + 附件下载；RAG 侧也可直接读 PG |
| **管理后台** | 站点/栏目 CRUD、运行监控、数据浏览、失败重放 |
| **监控告警** | 成功率、耗时、封禁率、存储水位、飞书/企微告警 |

---

## 4. 核心设计

### 4.1 分层抓取策略（降本核心）

```
┌─────────────────────────────────────────────────────┐
│ Tier 1: 官方接口（RSS / JSON / 开放平台）           │  优先级最高
├─────────────────────────────────────────────────────┤
│ Tier 2: 静态 HTTP（httpx + 真实 UA + 频控）         │  日常主力
│   - 无反爬站点；Cookie 已种值的域名                  │
├─────────────────────────────────────────────────────┤
│ Tier 3: Playwright stealth（headless Chromium）     │  挑战破冰
│   - 遇 412/403/滑块时切换                           │
├─────────────────────────────────────────────────────┤
│ Tier 4: DrissionPage（真实浏览器内核）              │  兜底
└─────────────────────────────────────────────────────┘
```

**降级规则**：每个 URL 默认 Tier 2；非 200 或正文为空 → Tier 3；Tier 3 失败 → Tier 4；全部失败 → 标记 `failed`，指数退避重试。

### 4.2 Cookie 池

- 浏览器过挑战成功后，Cookie 写入 **Valkey**：`cookie:gdqy.gov.cn`，TTL 4 小时。
- 同域名后续请求优先 Tier 2 + 该 Cookie，省下浏览器启动成本。
- Cookie 失效（再次 412）→ 清掉 → 起浏览器"充值"。

> **为什么用 Valkey 不是 Redis**：Redis 自 7.4 起转为 SSPL/RSALv2 许可，**不再是开源 OSI 意义上的开源**，商用存在限制。Valkey 是 Linux 基金会托管的 Redis 7.2 BSD 分支，协议/命令完全兼容，**BSD-3 许可，商用无限制**。客户端代码无需修改。

### 4.3 站点配置（YAML 示例）

```yaml
site_id: gdqy
site_name: 清远市人民政府
base_url: https://www.gdqy.gov.cn
default_strategy: httpx
concurrency: 2
interval_sec: 3
columns:
  - column_id: szfwj
    name: 市政府文件
    category: policy_doc            # 归类（供 RAG 侧按类别筛选）
    list_url: "https://www.gdqy.gov.cn/gdqy/newxxgk/fgwj/szfwj/"
    list_selector:
      item: "ul.list_news li"
      link: "a@href"
      date:  "span.date"
    pagination:
      type: page_param
      param: page
      max_pages: 5
    detail:
      title:        "h1.article-title::text"
      publish_time: "span.time::text"
      source:       "span.source::text"
      content:      "div.article-content"
      attachments:  "div.article-content a[href$='.pdf'], a[href$='.doc'], a[href$='.docx']"
    schedule: "0 2 * * *"           # 每天 02:00
```

### 4.4 抓取流程（单栏目一轮）

```
1. 加载栏目配置
2. 抓列表页（Tier 2 → Tier 3 自动降级）
3. 解析列表 → URL 列表
4. 对每个 URL：
   a. URL 归一化 → 查去重表，已采过则跳过
   b. 增量判断：本轮文章时间 ≤ 上次最新时间 → 提前停止
   c. 抓详情页
   d. 解析字段（XPath 主 + GNE 兜底）
   e. 下载附件到本地目录
   f. 正文 SimHash 去重
   g. 元数据写 PG；原始 HTML 存文件
5. 更新栏目"最新时间戳"
6. 对外事件：新增文章 ID 推到 Valkey Stream（供 RAG 侧消费，可选）
```

### 4.5 存储布局

#### 4.5.1 文件系统目录结构

```
/data/govcrawler/
├── raw_html/                        # 原始 HTML 归档
│   └── gdqy/szfwj/2026/04/
│       └── post_2136593.html
├── articles_text/                   # 清洗后的纯文本（供 RAG 直接读）
│   └── gdqy/szfwj/2026/04/
│       └── post_2136593.txt
└── attachments/                     # 附件原件
    └── gdqy/szfwj/2026/04/
        ├── 2136593_附件1.pdf
        └── 2136593_附件2.docx
```

**设计理由**：
- 按 `站点/栏目/年/月` 分层，单目录文件数不爆。
- **纯文本与附件原件分离**：RAG 系统最简单的消费方式就是直接读 `articles_text/` 目录。
- 原始 HTML 保留用于后续重新解析（选择器修复后可重跑）。

> 本阶段仅存"正文纯文本"，**不对附件做 OCR/抽文本**。附件原件保留，RAG 侧或后续需要时再统一处理。

#### 4.5.2 数据库表（PG，不含向量表）

```sql
-- 站点
sites(id, site_id, name, base_url, config_yaml, enabled, created_at)

-- 栏目
columns(
  id, site_id, column_id, name, category,
  list_url, config_json,
  last_crawled_at, last_article_time,
  enabled
)

-- 文章（核心表）
articles(
  id BIGSERIAL PRIMARY KEY,
  site_id VARCHAR, column_id VARCHAR, category VARCHAR,
  url TEXT, url_hash CHAR(64) UNIQUE,       -- 去重键 1
  content_simhash CHAR(16),                 -- 去重键 2（语义级）
  title TEXT,
  publish_time TIMESTAMP,
  source VARCHAR,                           -- 发布主体
  content_text TEXT,                        -- 清洗后正文
  raw_html_path TEXT,                       -- 原始 HTML 文件路径
  text_path TEXT,                           -- 纯文本文件路径
  has_attachment BOOLEAN,
  status VARCHAR,                           -- raw / ready / failed
  fetch_strategy VARCHAR,                   -- httpx / playwright / drission
  fetched_at TIMESTAMP,
  exported_to_rag_at TIMESTAMP              -- 已同步到 RAG 的时间戳
);
CREATE INDEX ON articles(site_id, column_id, publish_time DESC);
CREATE INDEX ON articles(status, fetched_at);
CREATE INDEX ON articles(exported_to_rag_at);   -- 方便 RAG 侧拉增量

-- 附件
attachments(
  id BIGSERIAL PRIMARY KEY,
  article_id BIGINT REFERENCES articles(id),
  file_name TEXT,
  file_ext VARCHAR,
  size_bytes BIGINT,
  file_path TEXT,                           -- 本地路径
  file_hash CHAR(64),                       -- 内容去重
  downloaded_at TIMESTAMP
);

-- 抓取日志
crawl_logs(
  id BIGSERIAL PRIMARY KEY,
  site_id VARCHAR, column_id VARCHAR, article_url TEXT,
  strategy VARCHAR, http_status INT, duration_ms INT,
  success BOOLEAN, error_msg TEXT,
  occurred_at TIMESTAMP
);
CREATE INDEX ON crawl_logs(site_id, occurred_at DESC);
```

**RAG 对接点**：下游只需查 `articles WHERE exported_to_rag_at IS NULL ORDER BY fetched_at` 即可拉增量，处理完回写 `exported_to_rag_at = now()`。或者通过 REST 接口订阅 Valkey Stream。

### 4.6 对外接口（给 RAG 系统）

**三种消费方式任选**：

1. **直接查 DB**（最简单，推荐内网场景）
   - RAG 系统按 `exported_to_rag_at IS NULL` 拉新增，读 `text_path` 拿正文。

2. **REST API**（若 RAG 系统独立部署）
   ```
   GET  /api/articles?since=2026-04-22T00:00:00&site_id=gdqy&limit=100
   GET  /api/articles/{id}                     # 单篇详情
   GET  /api/articles/{id}/attachments/{aid}   # 下载附件
   POST /api/articles/{id}/ack                 # 标记已消费
   ```

3. **Valkey Stream 事件推送**（可选，本项目非实时，通常不需要）
   - 每新增一篇文章，推送 `{article_id, site_id, category}` 到 `stream:new_articles`。

> 本项目允许 **T+1 滞后**，推荐 RAG 侧用 **方式 1（直接查 DB）**，每日凌晨拉前一天数据即可，架构最简单。

---

### 4.7 监控与告警

| 指标 | 告警阈值 |
|---|---|
| 单栏目成功率 | < 80% 连续 2 轮 |
| 412/403 比例 | > 30% 持续 1 小时（反爬升级信号） |
| 浏览器启动次数/天 | 超均值 2×（Cookie 池失效信号） |
| 栏目 24h 内无新文章 | 且历史通常有更新 → 选择器可能失效 |
| 磁盘使用率 | > 80% |

告警：飞书 / 企微机器人。

### 4.8 对目标站点"无感"与合规红线

本项目允许 **T+1 滞后**（发布当天抓不到没关系，第二天补上即可），因此**"对目标站点无感"是硬约束**。执行规范：

- **频率极低**：每站每栏目 **每天 1 次**（深夜），绝不高频轮询。
- **并发极低**：单站并发 = 1，请求间隔 ≥ 5 秒（比常规 3s 更保守）。
- **时段集中**：所有任务错峰分布在 **01:00–05:00**，分散到 4 小时内，避免同一时刻多任务撞上同一站点。
- **请求随机化**：每次请求间隔加 ±20% 抖动，避免固定节奏被识别为机器人。
- **遵守 robots.txt**（默认遵守，可配置豁免）。
- **UA 带身份**：`Mozilla/5.0 ... MyKnowledgeBot/1.0 (contact: xxx@example.com)`，出问题对方可联系。
- **列表页提前停止**：翻到已采过的文章就停，不做不必要的翻页。
- **Cookie 池复用**：减少浏览器挑战次数，也减少异常访问痕迹。
- **只采公开信息**，不触碰 `/admin/`、`/api/internal/` 等路径。
- **失败不硬冲**：遇 412/403 连续 3 次 → 本轮放弃，明天再试（不做连续重试刺激对方 WAF）。
- 必要时联系站点白名单。

---

## 5. 技术栈（全部开源，允许商用）

### 5.1 选型与许可证

| 层 | 选型 | 许可证 | 商用 | 说明 |
|---|---|---|---|---|
| 语言 | Python 3.11+ | PSF | ✅ | — |
| 调度 | APScheduler | MIT | ✅ | 单进程 Cron 够用 |
| HTTP 客户端 | httpx | BSD-3 | ✅ | 异步 + HTTP/2 |
| 浏览器框架 | Playwright | Apache-2.0 | ✅ | 内置 Chromium 也是 BSD |
| 反检测 | patchright / playwright-stealth | Apache-2.0 / MIT | ✅ | 二选一 |
| 兜底浏览器 | DrissionPage | BSD-3 | ✅ | 国产反反爬生态 |
| 通用抽取 | GNE (General News Extractor) | MIT | ✅ | 正文兜底抽取 |
| 解析 | parsel / selectolax / lxml | BSD / MIT / BSD | ✅ | XPath/CSS |
| 数据库 | PostgreSQL 16 | PostgreSQL License（类 BSD） | ✅ | 元数据 + 日志 |
| 文件存储 | **本地文件系统** | — | ✅ | 20–30 站规模足够 |
| 缓存 / 队列 | **Valkey** | BSD-3 | ✅ | **替代 Redis**（见下文） |
| Web 框架 | FastAPI | MIT | ✅ | REST API + 后台 API |
| 后台前端 | Vue 3 + Element Plus | MIT + MIT | ✅ | Ant Design Vue 也是 MIT |
| 监控 · 指标 | Prometheus | Apache-2.0 | ✅ | — |
| 监控 · 面板 | **VictoriaMetrics + Perses** 或自建 HTML 面板 | Apache-2.0 | ✅ | 替代 AGPL 的 Grafana |
| 监控 · 日志 | 直接写文件 + `lnav` 查看，或 **OpenObserve** | Apache-2.0 | ✅ | 替代 AGPL 的 Loki |
| 容器 | Docker + Docker Compose | Apache-2.0 | ✅ | — |

### 5.2 关键的"坑"与替换说明

| ⚠️ 有许可风险的常见组件 | 许可证 | 本方案采用的替代 |
|---|---|---|
| **Redis**（7.4+） | SSPL/RSALv2（非 OSI 开源，商用受限） | **Valkey**（Linux 基金会 BSD-3 分支，协议兼容，客户端零改动） |
| **MinIO**（2021+） | AGPL v3（传染性强，对外暴露服务会被波及） | **本地文件系统**（本项目规模根本不需要对象存储） |
| **Grafana** | AGPL v3（内网自用可接受，发行/SaaS 有风险） | 自建 HTML 小面板 / VictoriaMetrics Perses（Apache-2.0） |
| **Loki** | AGPL v3 | 日志落文件 + `lnav` / OpenObserve（Apache-2.0） |
| **Elasticsearch**（7.11+） | SSPL/Elastic License | **OpenSearch**（Apache-2.0）；本项目**不用 ES，PG 足够** |

**原则**：
1. **避开所有 AGPL/SSPL/BSL/Elastic License** 的组件进入主链路。
2. MIT / BSD / Apache-2.0 / PostgreSQL License / PSF → 安全。
3. LGPL（仅动态链接用）不进入，避免后续打包困扰。
4. 每次引入新依赖前，先查 `license` 字段 + `LICENSE` 文件确认。
5. 使用 [`pip-licenses`](https://pypi.org/project/pip-licenses/) 定期扫描 Python 依赖树，CI 阶段检查。

> **删除**：原计划的 pgvector / bge-m3 / LlamaIndex / MinerU / PaddleOCR 全部**不引入**——向量化、切片、OCR、附件解析均由下游 RAG 系统或其他系统负责。本项目**纯粹负责抓取 + 存储**。

---

## 6. 规模适配说明（20–30 站）

### 6.1 流量测算
- 站点数：20–30；栏目总数：150–300
- 日均请求：列表页 ~300 + 详情页 ~500 = **~800 次/天**
- 峰值并发：2–3

### 6.2 架构简化（已应用到上文）
| 组件 | 简化后 |
|---|---|
| 部署 | 单机 Docker Compose |
| 调度 | APScheduler 单进程 |
| 文件存储 | 本地目录（/data）即可；不用 MinIO（AGPL） |
| 代理 IP 池 | **不需要**（低频 + Cookie 池足够） |
| 分布式队列 | 不需要（内存队列 + 少量 Valkey） |
| 服务器规格 | **单台 4C8G** 全包 |
| 月成本 | **~200–400 元/月** |

### 6.3 工期与投入
- **一次性开发**：2–3 周（阶段 0–3）
- **接入站点**：每站 1–2 小时配置，20–30 站约 **1 周**
- **持续维护**：每月 2–4 小时

---

## 7. 分阶段实施路线

### 阶段 0 · 原型验证（2–3 天）
**目标**：跑通"一个站一个栏目一篇文章"端到端。
- [x] Playwright stealth 跑通 `gdqy.gov.cn` 详情页抓取
- [x] 解析出标题/时间/来源/正文/附件
- [x] 附件下载到本地
- [x] 正文纯文本 + 原始 HTML 分别落盘
- [x] 单表 PG 存元数据

**交付物**：Python 脚本 `poc_crawler.py`，跑出结果。

### 阶段 1 · 最小可用采集器 MVP（1–2 周）
**目标**：多站点配置化采集 + 持久化 + 对外接口雏形。
- [x] YAML 站点/栏目配置加载
- [x] Fetcher 三档（httpx / playwright / drissionpage，自动降级）  <!-- httpx + playwright 已上线并自动降级；drission 兜底待真实触发时启用 -->
- [x] Cookie 池（Valkey）  <!-- VALKEY_URL→ValkeyCookieStore；空值→InMemory 回退；TTL 4h；playwright 采集、httpx 注入、chain 失败自动清 -->
- [x] 解析器（XPath 主 + GNE 兜底）
- [x] PG 元数据 + 本地目录存文件（按 站点/栏目/年/月 分层）
- [x] 去重（URL hash + SimHash）
- [x] APScheduler 定时 + 增量抓取（01:00-05:00 错峰 + URL hash 遇已采即停 INC-03）
- [x] 失败重试（指数退避 + FETCH-05 连续 3 次 412/403 放弃本轮）
- [x] 最小 REST API：`/api/articles?since=...`  <!-- + /{id}, /{id}/attachments/{aid}, /ack, /health -->

- [ ] 接入 3 个典型政务站联调  <!-- 有辅助工具：`python -m govcrawler validate <site> <column> [--url <detail>] [--limit N]` 跑通后 toggle --enable -->

**交付物**：可容器化部署的采集服务；RAG 系统已可拉数据。

### 阶段 2 · 批量接入 + 稳定化（1 周）
**目标**：全部 20–30 站接入并稳定运行。
- [ ] 接入剩余站点（每站 1–2 小时写配置 + 联调）  <!-- 已写 2 份模板 (yingde, gd)，enabled: false 待真实站点验证后翻开 -->
- [x] 基础监控（成功率、耗时、失败 Top N）  <!-- /metrics Prometheus endpoint + 5 counters/histograms -->
- [x] 失败自动重试 + 告警（飞书/企微）  <!-- 重试见 Phase 1；告警见 `govcrawler/alerting/`：R1 成功率<80% / R2 1h 412/403>30% / R3 栏目 24h 零新增 -->
- [x] 站点/栏目启停开关  <!-- `python -m govcrawler toggle <site> [<col>] --enable/--disable` 写回 YAML (ruamel 保留注释) + 热重载；`list-sites` 查看状态；scheduler 跳过 disabled 栏目 -->

**交付物**：日级跑批稳定；`articles` 表持续增量。

### 阶段 3 · 管理后台（1–2 周，按需）
**目标**：非工程同学可接入新站、查数据、排障。
- [x] 站点/栏目 CRUD（启停 + 立即抓取 + 列表）  <!-- /admin/api/sites + set_enabled 回写 YAML；在线编辑 XPath 尚未做 -->
- [x] 任务运行监控面板  <!-- /admin/ 看板 + 站点/日志/文章 三 Tab，Prometheus /metrics + /admin/api/stats -->
- [x] 文章浏览与搜索（按站点/栏目/时间/关键词）  <!-- /admin/api/articles/search: q+site+status+since+limit -->
- [x] 失败重放按钮  <!-- /admin/api/logs/{id}/retry 重抓单条，异步 BackgroundTasks -->
- [x] 与 RAG 侧对接状态展示（`exported_to_rag_at` 分布）  <!-- stats.unexported_to_rag + 文章列表 RAG ack 徽章 -->

**交付物**：Admin UI；监控面板。

> **阶段 3 可选**：如果只是自用，也可以跳过前端，仅靠 SQL + 命令行管理。

---

## 8. 风险与对策

| 风险 | 概率 | 影响 | 对策 |
|---|---|---|---|
| 反爬升级致 Playwright 也失败 | 中 | 高 | 多档 Fetcher + DrissionPage 兜底 + 降频 |
| 触发真滑块 | 中 | 中 | Cookie 池降频；必要时人工介入 |
| IP 被封 | 低 | 高 | 严格频控（2 并发/3s 间隔）、夜间跑 |
| 站点改版致选择器失效 | 高 | 中 | GNE 兜底 + 成功率告警 + 后台热更新 |
| 浏览器内存泄漏 | 高 | 中 | 守护进程 + 定期重启 + 任务上限 |
| 附件重复占磁盘 | 中 | 低 | 附件按 hash 去重 |
| 法律灰度 | 低 | 高 | 仅采公开信息 + UA 标识 + 告知白名单 |

---

## 9. 成本估算（单机部署）

| 项 | 规格 | 月成本 |
|---|---|---|
| 应用服务器（采集+PG+Redis+后台） | 4C8G + 200G SSD | ~200–300 元 |
| 备份存储（对象存储，可选） | 200G | ~20 元 |
| 监控（自建 Grafana，免费） | — | 0 |
| **合计** | | **~200–400 元/月** |

---

## 10. 待决策事项

1. **管理后台做不做？** 自用可跳过阶段 3。
2. **RAG 对接方式**：直连 PG / REST API / Redis Stream —— 哪种最符合现有 RAG 系统的消费习惯？
3. **附件要不要此阶段就抽文本？** 当前方案只存原件，RAG 侧处理；若 RAG 侧不愿处理，可在本系统补一个附件文本化模块（阶段 2.5）。
4. **部署环境**：本地机房 / 云服务器 / 已有内网服务器复用？

---

## 11. 附录：`gdqy.gov.cn` 实测记录

- URL：`https://www.gdqy.gov.cn/gdqy/newxxgk/fgwj/szfwj/content/post_2136593.html`
- 文章：《清远市人民政府关于2026年第四届全国轻型飞机锦标赛航空嘉年华活动期间无人驾驶航空器安全管控的公告》
- 发布时间：2026-04-10 16:34:22
- 反爬特征：ctct 盾（知道创宇），HTTP 412 + `<title>请稍候…</title>` + `ctct-slider-canvas` 滑块
- 绕过方式：真实 Chrome 内核加载 JS 自动过挑战，未触发真滑块，正文与附件完整可见
- 推断：同集团政务站多数走相同方案，一个 Playwright stealth 策略可覆盖绝大部分

---

**文档结束。** 下一步建议：启动"阶段 0 · 原型验证"（2–3 天），验证分层 Fetcher + Cookie 池可行性后，再展开阶段 1。
