Auto backup \2026-\04-\20

This commit is contained in:
Ubuntu
2026-08-06 16:00:01 +08:00
parent bcc8866ad8
commit a04277e6eb
4 changed files with 555 additions and 55 deletions
+87 -1
View File
@@ -393,6 +393,8 @@ if 'zhihu.com' in url:
不需要修改 `html_to_markdown`、`fix_encoding`、`fix_math_formulas` 等上层逻辑,它们对所有平台通用。
> ⚠️ **注意**:上述「四步接入新平台」的方式正在被第八章的**策略化重构**取代。新方案下新增平台只需写一个策略类并注册,不再需要修改 `fetch()` 主流程。
---
## 五、三种方案对比
@@ -468,4 +470,88 @@ POST /api/{scheme}/save → 确认保存(下载图片、替换 URL、入
2. **SegmentFault 绕过**:尝试更换出口 IP 或更高级的设备指纹模拟
3. **自动平台识别**:通过页面特征自动判断站点类型,减少人工选择采集方案的步骤
4. **智能摘要生成**:接入大模型 API,对采集内容自动生成摘要和标签
5. **更多平台适配**:根据使用需求,逐步添加开源中国(oschina)、简书(jianshu)等平台
5. **更多平台适配**:根据使用需求,逐步添加开源中国(oschina)、简书(jianshu)等平台(见第八章策略化方案,新增平台只需注册一个策略类)
---
## 八、站点提取策略化重构(v2,进行中)
> 本文档是既有采集方案的**增量演进**,目标是把散落在三个文件里的站点特判逻辑收敛为**策略注册表**,同时保证线上行为零回归。现有逻辑全部保留,作为旧轨(Legacy)参与双轨调度。
### 8.1 背景与动机
现状盘点:站点特判逻辑分散在三个文件,且同一站点在不同入口**各写了一遍**:
| 文件 | 站点特判位置 | 涉及的站点 |
|------|------------|-----------|
| `crawl.py` | `crawl_with_playwright()`(约 796-1451 行) | 微信、腾讯云、飞书、yiigle、掘金、SegmentFault、InfoQ、CSDN、博客园 + eeworld 强制 Playwright |
| `universal_crawl.py` | `universal_fetch()`(约 378-480 行) | InfoQ、掘金、CSDN、博客园、51CTO + `_needs_browser()` 直通列表 |
| `wechat_crawl.py` | 独立路由,专用采集器 | 仅微信(独立 preview/save 流程) |
痛点:
- 同一站点(如掘金)在 `crawl.py` 和 `universal_crawl.py` 各有一套选择器和标题清洗规则,改一处忘另一处
- 新增站点要在标题提取、正文转换、图片过滤三个分支各改一次,容易漏
- 选择器、过滤规则、跳转策略(先访问首页建 cookie 等)与主流程强耦合,无法单独测试
### 8.2 设计:SiteExtractor 抽象基类
新增文件 `backend/routers/crawl_strategies.py`(纯新增,不删不改现有逻辑),借鉴 crawl4ai 的 `ContentScrapingStrategy(ABC)` / `MarkdownGenerationStrategy(ABC)` 接口思想:
```python
class SiteExtractor(ABC):
name: str # 站点名(日志/调试用)
@abstractmethod
def match(self, url: str) -> bool: ... # URL 是否命中该站点
def needs_browser(self, url: str) -> bool: return False # 是否需要 Playwright
def extract_title(self, html: str, url: str) -> Optional[str]: return None
def extract_content_html(self, html: str, url: str) -> str: return html # 返回正文容器 HTML
def filter_images(self, images: list, url: str) -> list: return images # 过滤 UI 图标
def to_markdown(self, content_html: str, url: str, images: list) -> str: return html_to_markdown(...)
```
每个策略类的内部逻辑 = **现有代码原样搬迁**(如 `JuejinExtractor` 直接搬 `_extract_juejin_title` / `_extract_juejin_content`),行为零改动。
### 8.3 策略注册表与双轨降级
`SITE_EXTRACTORS = [JuejinExtractor, CsdnExtractor, ...]` 为有序注册表,末尾挂 `GenericExtractor`(通用兜底)。
双轨调度的**优先级由配置决定**,两种顺序都支持:
```
STRATEGY_FIRST_DOMAINS = set() # 试点白名单,默认空
crawl 流程:
提取 extractor = get_extractor_for_url(url)
若 url 命中白名单 → 轨B优先(新策略)→ 质量不达标回退轨A(旧逻辑)
否则 → 轨A优先(旧逻辑)→ 失败/过短时兜底轨B(新策略)
```
| 顺序 | 配置 | 适用场景 |
|------|------|---------|
| **旧→新**(默认) | `STRATEGY_FIRST_DOMAINS` 为空 | 线上行为零回归,新策略仅作兜底 |
| **新→旧**(试点) | 白名单加入该域名,如 `{"juejin.cn"}` | 新策略优先,质量不合格自动回退旧逻辑 |
**质量门控**(fallback 判据,复用现有「内容过短」经验):
- 策略抛异常
- 标题为空或等于「未获取到标题」
- Markdown 内容 < 300 字符
- 图片数为 0(可选判据,默认关闭)
### 8.4 落地步骤与状态
| 步骤 | 内容 | 状态 |
|------|------|------|
| 1 | 新建 `crawl_strategies.py`:抽象基类 + 注册表 + GenericExtractor(纯新增,不影响线上) | ✅ 完成 |
| 2 | 原样搬迁 Juejin / CSDN 两个策略,单测断言与旧逻辑输出一致 | ✅ 完成 |
| 3 | `universal_fetch()` 接入双轨调度:旧逻辑包成 `_legacy_extract()`,加 `STRATEGY_FIRST_DOMAINS` 开关,默认走旧轨 | ✅ 完成 |
| 4 | 试点验证(掘金/CSDN 加入白名单,对比新旧采集结果),确认后推广到 `crawl.py` Playwright 轨、wechat 轨 | ⏳ 待用户确认 |
### 8.5 wechat_crawl.py 是否需要更新?
**不需要**(本阶段)。原因:
1. `wechat_crawl.py` 是独立路由 `/api/wechat-crawl/`,有自己的 preview/save 两阶段流程,是「微信专用采集器」而非「散落的站点特判逻辑」,不存在重复规则问题
2. 微信的提取逻辑会以 `WechatExtractor` 策略类形式注册到 `crawl_strategies.py`,供通用入口(`universal_fetch` / `crawl_article`)遇到微信 URL 时兜底复用——这是**新增复用**,不改动 wechat_crawl.py 本体
3. 若未来想让 wechat_crawl.py 内部也复用策略类,属于可选优化(消除与 crawl.py 微信段的重复),不在本次重构范围