26 KiB
文章采集方案详解
本文档详细介绍个人博客 CMS 系统中的三种文章采集方案,包括技术选型、实现原理、各平台适配策略,以及前端交互流程。
一、总体架构
系统提供 三种采集入口,对应不同的技术策略和适用场景:
| 采集方案 | 前端入口 | 后端路由 | 核心引擎 | 适用场景 |
|---|---|---|---|---|
| 通用网页采集(方案一) | "采集文章" | /api/crawl/ |
curl_cffi + Playwright |
传统网站:人人都是产品经理、博客园、菜鸟教程等 |
| 微信公众号采集(方案二) | "公众号采集" | /api/wechat-crawl/ |
Playwright + playwright_stealth |
微信公众号文章(mp.weixin.qq.com) |
| 通用采集(方案三) | "通用采集" | /api/universal-crawl/ |
curl_cffi + Playwright |
新一代技术社区:InfoQ、掘金、CSDN、51CTO、博客园等 |
三种方案的前端交互流程完全一致(输入 URL → 预览 → 编辑 → 保存入库),区别仅在于后端采集引擎和平台适配策略。
二、方案一:通用网页采集(crawl.py)
这是系统最早实现的采集方案,采用多引擎组合策略,根据目标站点的特征选择最合适的请求方式。
2.1 技术栈
- HTTP 请求:
curl_cffi(默认,带浏览器指纹伪装)+requests(备选) - 浏览器渲染:
Playwright(用于 SPA/JS 渲染型站点) - HTML 解析:
BeautifulSoup(lxml加速) - Markdown 转换:
markdownify(备选)+ 自研html_to_markdown(主用)
2.2 核心流程
1. 提取标题 → extract_title_from_html(html)
2. 判断站点类型 → is_spa_site(html) ? SPA : 传统站点
3. 获取页面 HTML → curl_cffi / Playwright
4. 提取主要内容 → extract_main_content(html) / convert_spa_html_to_markdown()
5. 修复编码乱码 → fix_encoding(text)
6. HTML 转 Markdown → html_to_markdown(html, url)
7. 提取文章图片 → extract_images(html, url)
8. 下载图片到本地 → download_image() / download_all_images()
9. 替换 Markdown 中的图片 URL 为本地路径
10. 保存到数据库
2.3 引擎选择策略
系统根据目标域名自动判断使用哪个引擎获取 HTML:
# 强制使用 Playwright 的域名(SPA / JS 渲染 / 反爬严格)
browser_domains = [
'mp.weixin.qq.com', # 微信公众号
'juejin.cn', # 掘金
'zhihu.com', # 知乎
'segmentfault.com', # SegmentFault
'infoq.cn', # InfoQ
]
# 默认使用 curl_cffi(高性能、带浏览器指纹)
# 备选 requests(纯文本站点)
2.4 平台专用适配
extract_main_content() 内置了多个平台的 CSS 选择器优先级列表:
| 平台 | 选择器 | 说明 |
|---|---|---|
| 人人都是产品经理 | .article--content.grap |
文章正文区域 |
| 博客园 | #cnblogs_post_body |
博客正文容器 |
| 菜鸟教程 | .article-intro |
教程内容区域 |
| 掘金 | article |
文章标签 |
| 通用 | article, main, .post-content, .article-content, .entry-content, #content |
回退选择器 |
如果 BeautifulSoup 解析失败,系统会回退到正则表达式匹配。
2.5 上层业务逻辑
2.5.1 编码修复(fix_encoding)
处理 UTF-8 内容被错误解码为 Latin-1 的情况(常见于老旧网站或代理转发),支持单层修复和双重编码修复:
# 检测乱码特征:é, è, Ã, Â, Ã¥, â€, æ, å, è, § 等
garbled_patterns = ['é', 'è', 'Ã', 'Â', 'Ã¥', 'ü', 'ö', 'ä', 'ß', 'â€', 'æ', 'å', 'è', '§']
# 单层修复:latin1 → utf-8
text.encode('latin1').decode('utf-8')
# 双重编码修复:utf-8 → latin1 → utf-8 → latin1 → utf-8
text.encode('utf-8').decode('latin1').encode('latin1').decode('utf-8')
2.5.2 数学公式修复(fix_math_formulas)
检测 Markdown 中混排的 LaTeX 数学公式,进行规范化包裹:
- 块级公式(含
\begin{array},\sum_,\int_,\frac,\prod,\lim,\mathbf,\alpha,\beta,\gamma等)→ 用$$...$$包裹 - 行内公式(含
_{,^{,\mathbf,\alpha,\sum,\int,\frac,\sqrt,\lim,\prod,\binom,\overline,\underline,\widehat,\widetilde,\mathcal,\mathbb等)→ 用\(...\)包裹 - 清理已渲染的 Unicode 数学运算符
- 规范化双重反斜杠为单重
2.5.3 HTML → Markdown 转换(html_to_markdown)
自研转换器,支持以下标签:
<img>→,自动将相对 URL 转为绝对 URL<h1>~<h6>→#~######<p>→ 段落文本<pre><code>→ 代码块(自动识别language-xxx类名)<code>→ 行内代码<strong>,<b>→**粗体**<em>,<i>→*斜体*- 修复混合内容问题(HTTPS 页面中的 HTTP 图片自动升级)
- 处理 HTML 实体(
,<,>等)
2.5.4 图片下载(download_image)
- 支持
curl_cffi和requests双引擎下载 - 限制最大 10MB,超时 15 秒
- 生成唯一文件名:
{timestamp}_{hash[:8]}{ext} - 保存到
uploads/目录 - 保存后返回本地 URL 映射表
2.6 已知局限
- 对 InfoQ 的
.ProseMirror容器支持不够精细,会混入导航噪声 - 对掘金、CSDN、51CTO 等平台无专用提取逻辑,正文可能夹杂大量 UI 元素
- 对知乎无登录态支持,无法采集
三、方案二:微信公众号文章采集(wechat_crawl.py)
专门针对微信公众号文章(mp.weixin.qq.com)设计的采集方案,处理微信特有的反爬机制和页面结构。
3.1 技术栈
- 浏览器引擎:
Playwright+playwright_stealth(反检测) - HTML 解析:
BeautifulSoup - Markdown 转换:
markdownify+ 自研微信专用修复函数
3.2 核心流程
1. Playwright 打开微信文章 URL(wait_until="networkidle", 60s 超时)
2. 注入反检测脚本(隐藏 webdriver、伪造插件列表、模拟 chrome.runtime)
3. 启用 playwright_stealth(如果可用)
4. 提取标题(优先 page.title(),回退到 #activity_name)
5. 提取公众号名称(#js_name)
6. 提取正文 HTML(#js_content)
7. 提取封面图(og:image meta)
8. 提取文章内图片(data-src 属性)
9. 微信专用 HTML 修复
10. 转换为 Markdown
11. 预览 → 下载图片(带 Referer 防盗链) → 入库
3.3 微信专用处理
3.3.1 data-src 修复(fix_wechat_images)
微信文章使用 data-src 存放真实图片 URL,src 往往是 SVG 占位图或空值:
for img in soup.find_all('img'):
data_src = img.get('data-src', '')
src = img.get('src', '')
# data-src 是真实 URL,且 src 不是有效的 http URL
if data_src and data_src.startswith('http') and (
not src or src.startswith('data:image') or not src.startswith('http')
):
img['src'] = data_src
# 清理无意义的占位属性
del img['data-src']
3.3.2 代码块修复(fix_wechat_code_blocks)
微信代码块使用 <pre class="code-snippet__js"> + <code> + <span class="code-snippet__line"> 结构,需要按行重组:
for pre in soup.find_all('pre', class_=re.compile('code-snippet')):
code_tag = pre.find('code')
lines = []
for span in code_tag.find_all('span', class_='code-snippet__line'):
lines.append(span.get_text(separator='', strip=False))
code_tag.clear()
for line in lines:
code_tag.append(NavigableString(line + '\n'))
3.3.3 空段落清理(remove_empty_paragraphs)
移除没有文本内容也没有图片的空 <p> 标签。
3.3.4 微信图片防盗链绕过
微信图片服务器(mmbiz.qpic.cn)对直接访问会返回 403,必须在请求头中带上 Referer:
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
"Referer": "https://mp.weixin.qq.com/", # 关键!
}
下载时通过 curl_cffi 或 requests 发送带 Referer 的请求,成功后将图片保存到本地。
3.4 测试结果
微信公众号文章采集测试通过,标题、正文、代码块、图片均能正常采集。腾讯的反爬策略主要集中在异常流量检测,Playwright + stealth 组合可以稳定绕过。
四、方案三:通用采集(universal_crawl.py)
这是为新一代技术社区专门设计的采集方案,核心理念是**"专用选择器提取 + 复用上层业务逻辑"**。
4.1 设计目标
- 针对各平台页面结构差异大、通用提取效果差的问题
- 为每个主流平台编写专用 CSS 选择器,精准提取正文
- 复用
crawl.py中已验证成熟的上层业务逻辑(编码修复、数学公式、HTML→Markdown、图片下载) - 保持与方案一/方案二完全一致的前端交互流程
4.2 技术栈
- HTTP 请求:
curl_cffi(默认)+Playwright(SPA / 反爬严格站点) - HTML 解析:
BeautifulSoup(lxml加速) - 上层逻辑:全部从
crawl.py复用:extract_title_from_html、fix_encoding、fix_math_formulas、html_to_markdown、extract_images、extract_images_from_markdown、download_image、download_all_images
4.3 引擎选择策略
browser_domains = [
'mp.weixin.qq.com', # 微信公众号
'juejin.cn', # 掘金(JS 渲染)
'zhihu.com', # 知乎(未登录无法采集)
'segmentfault.com', # SegmentFault(WAF 拦截)
'infoq.cn', # InfoQ(JS 渲染)
'51cto.com', # 51CTO(JS 渲染)
]
对于非 browser_domains 的站点,默认使用 curl_cffi 获取 HTML,性能更好。
4.4 Playwright 反爬配置
通用采集的 Playwright 配置比方案一更完善:
browser = await p.chromium.launch(
headless=True,
args=[
"--disable-blink-features=AutomationControlled",
"--disable-web-security",
"--disable-features=IsolateOrigins,site-per-process",
"--disable-dev-shm-usage",
"--no-sandbox",
]
)
context = await browser.new_context(
user_agent="...Chrome/120.0.0.0 Edg/120.0.0.0...",
viewport={"width": 1920, "height": 1080},
locale="zh-CN",
timezone_id="Asia/Shanghai",
extra_http_headers={
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Sec-Ch-Ua": '"Not_A Brand";v="8", "Chromium";v="120", "Microsoft Edge";v="120"',
"Sec-Ch-Ua-Mobile": "?0",
"Sec-Ch-Ua-Platform": '"Windows"',
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"Upgrade-Insecure-Requests": "1",
},
)
对于知乎,采用先访问首页建立 Cookie,再跳转目标页面的策略:
if 'zhihu.com' in url:
await page.goto("https://www.zhihu.com", wait_until="domcontentloaded", timeout=15000)
await asyncio.sleep(2)
await page.goto(url, wait_until="domcontentloaded", timeout=30000)
await asyncio.sleep(3)
# 等待正文加载
await page.wait_for_selector(".Post-RichTextContainer, .RichContent-inner", timeout=10000)
4.5 平台专用适配
4.5.1 专用内容提取
| 平台 | 主选择器 | 备选选择器 | 说明 |
|---|---|---|---|
| InfoQ | .ProseMirror |
article |
InfoQ 使用 ProseMirror 编辑器渲染正文,包含干净的结构化内容 |
| 掘金 | article.article |
.article-viewer, #article-root |
掘金文章正文在 <article class="article"> 中 |
| CSDN | #content_views |
.article_content |
CSDN 正文主容器,排除侧边栏、评论等噪声 |
| 博客园 | #cnblogs_post_body |
.postBody |
博客园经典正文容器 |
| 51CTO | .article-content |
article |
51CTO 文章正文区域 |
4.5.2 专用标题提取
各平台 <title> 标签通常包含站点后缀,需要清洗:
| 平台 | 清洗规则 |
|---|---|
| InfoQ | 去掉 " - InfoQ" 后缀 |
| 掘金 | 去掉 " - 掘金" 后缀 |
| CSDN | 去掉 "-CSDN博客" / " - CSDN博客" 后缀 |
| 博客园 | 去掉 " - 博客园" 后缀 |
| 51CTO | 去掉 "-51CTO.COM" 后缀 |
4.5.3 图片过滤
各平台正文容器中常常混入 UI 图标(收藏、点赞、logo、二维码等),需要过滤:
| 平台 | 过滤规则 |
|---|---|
| CSDN | 跳过 newHeart2023、tobarCollect、tobarCollection、copyright、avatar、blogv2/dist/pc/img 等关键词的图片 |
| 博客园 | 跳过 assets.cnblogs.com/logo、assets.cnblogs.com/icons、skins/custom/images/logo |
| 51CTO | 跳过公众号矩阵二维码(oss/202302)、软考二维码(oss/202408)、顶部横幅(oss/202506/06)、APP 下载图(oss/202302/07) |
4.6 测试结果汇总
| 平台 | 测试链接 | 结果 | 正文长度 | 图片数 | 说明 |
|---|---|---|---|---|---|
| 微信公众号 | mp.weixin.qq.com/s/... |
✅ 正常 | ~6000 | 24 | Playwright + stealth 稳定采集,防盗链通过 |
| InfoQ | infoq.cn/article/... |
✅ 正常 | ~9000 | 2 | .ProseMirror 精准提取,无导航噪声 |
| 掘金 | juejin.cn/post/... |
✅ 正常 | ~6253 | 26 | article.article 精准提取,JS 渲染正常 |
| CSDN | blog.csdn.net/... |
✅ 正常 | ~9678 | 91 | #content_views 精准提取,过滤 UI 图标 |
| 博客园 | cnblogs.com/... |
✅ 正常 | ~17092 | 17 | #cnblogs_post_body 精准提取,过滤 logo |
| 51CTO | 51cto.com/article/... |
✅ 正常 | ~14177 | 36 | .article-content 精准提取,过滤广告二维码 |
| 知乎 | zhuanlan.zhihu.com/... |
❌ 失败 | — | — | 强制登录,未登录跳转登录页,无法采集 |
| SegmentFault | segmentfault.com/... |
❌ 失败 | — | — | 雷池 WAF 拦截,返回 "客户端异常,请确认您是合法用户" |
详细说明
✅ 微信公众号
- 使用 Playwright +
playwright_stealth模拟真实浏览器 - 注入反检测脚本隐藏
navigator.webdriver - 图片使用
data-src修复,下载时带Referer: https://mp.weixin.qq.com/绕过防盗链 - 代码块、空段落、标题均处理正常
✅ InfoQ
- InfoQ 页面使用 JS 渲染,必须走 Playwright
- 通用提取会把导航栏、推荐阅读等混入正文(37265 字符)
- 使用
.ProseMirror专用选择器后,正文精简到 9000 字符,完全无噪声
✅ 掘金
- 掘金是 SPA,必须走 Playwright
- 通用提取会混入头部导航和底部推荐
- 使用
article.article专用选择器后,正文从导航处开始,干净整洁
✅ CSDN
- CSDN 页面为服务端渲染,可用 curl_cffi 获取
- 通用提取会混入收藏按钮、版权声明、专栏信息、相关推荐等
- 使用
#content_views专用选择器 + UI 图标过滤后,正文从第一张图开始
✅ 博客园
- 博客园页面为服务端渲染,可用 curl_cffi 获取
- 通用提取混入博客园 logo、会员周边、搜索栏、用户头像等
- 使用
#cnblogs_post_body专用选择器 + logo 过滤后,正文干净
✅ 51CTO
- 51CTO 使用 JS 渲染,必须走 Playwright
- 通用提取混入首页导航、AI 社区、博客学堂、公众号矩阵二维码等
- 使用
.article-content专用选择器 + 二维码过滤后,正文从编辑信息开始
❌ 知乎
- 知乎对所有未登录的浏览器请求强制跳转登录页
- 即使先访问首页建立 Cookie、使用 stealth、调整 viewport 均无效
- 这是知乎的反爬策略,与采集工具无关,必须提供登录态 Cookie 才能解决
❌ SegmentFault
- SegmentFault 部署了雷池 WAF(SafeLine Web Application Firewall)
- 无论 curl_cffi(多种浏览器指纹)还是 Playwright(先访问首页建立信任)均被拦截
- 返回 HTTP 468 + "安全检测能力由 雷池 WAF 驱动 / 客户端异常,请确认您是合法用户"
- 可能需要更换代理 IP 或使用更高级的设备指纹模拟才能绕过
4.7 扩展新平台的方法
要为新平台添加支持,只需在 universal_crawl.py 中完成四步:
- 判断域名:在
fetch()中添加is_xxx = 'xxx.com' in url - 提取标题:编写
_extract_xxx_title(html)函数,清洗<title>后缀 - 提取正文:编写
_extract_xxx_content(html)函数,返回精准容器的 HTML - 过滤图片(可选):编写
_filter_xxx_images(images)函数,过滤 UI 图标 - 接入主流程:在
fetch()的标题提取、正文转换、图片过滤三个分支中加入新平台
不需要修改 html_to_markdown、fix_encoding、fix_math_formulas 等上层逻辑,它们对所有平台通用。
⚠️ 注意:上述「四步接入新平台」的方式正在被第八章的策略化重构取代。新方案下新增平台只需写一个策略类并注册,不再需要修改
fetch()主流程。
五、三种方案对比
| 维度 | 方案一(通用网页) | 方案二(公众号) | 方案三(通用采集) |
|---|---|---|---|
| 目标站点 | 人人都是产品经理、博客园、菜鸟教程 | 微信公众号 | InfoQ、掘金、CSDN、博客园、51CTO |
| 核心引擎 | curl_cffi / Playwright | Playwright + stealth | curl_cffi / Playwright |
| 平台适配 | 通用选择器(CSS + 正则回退) | 微信专用(data-src、代码块、防盗链) | 各平台专用选择器 |
| 编码修复 | ✅ | ❌(微信无此问题) | ✅(复用方案一) |
| 数学公式 | ✅ | ❌(微信无此问题) | ✅(复用方案一) |
| 图片下载 | ✅ 通用 | ✅ 微信防盗链 | ✅(复用方案一) |
| SPA 支持 | ✅ | ✅(必须) | ✅ |
| 扩展性 | 修改通用选择器列表 | 固定(仅微信) | 新增平台只需四步 |
六、前端交互流程
三种采集方案的前端页面结构完全一致(CrawlEdit.jsx / WechatCrawlEdit.jsx / UniversalCrawlEdit.jsx),交互流程为:
┌─────────────────────────────────────────┐
│ 步骤1:输入 URL │
│ ├─ 文本框输入文章链接 │
│ └─ "采集预览" 按钮 │
├─────────────────────────────────────────┤
│ 步骤2:预览采集结果 │
│ ├─ 显示文章标题(可编辑) │
│ ├─ 显示 Markdown 正文(可编辑) │
│ ├─ 显示图片列表(可删除) │
│ └─ 侧边栏显示文章元信息 │
├─────────────────────────────────────────┤
│ 步骤3:编辑与调整 │
│ ├─ 修改标题 │
│ ├─ 修改正文(Markdown 编辑器) │
│ ├─ 删除不需要的图片 │
│ └─ 选择文章分类 │
├─────────────────────────────────────────┤
│ 步骤4:保存入库 │
│ ├─ "保存文章" 按钮 │
│ ├─ 后端下载图片到本地 │
│ ├─ 替换 Markdown 中的图片 URL │
│ └─ 写入 SQLite 数据库 │
└─────────────────────────────────────────┘
API 调用时序:
POST /api/{scheme}/preview → 采集并返回预览数据(标题、正文、图片列表)
POST /api/{scheme}/save → 确认保存(下载图片、替换 URL、入库)
其中 {scheme} 对应三种方案:crawl(方案一)、wechat-crawl(方案二)、universal-crawl(方案三)。
七、总结与建议
7.1 使用建议
| 场景 | 推荐方案 |
|---|---|
| 微信公众号文章 | 方案二(公众号采集) |
| InfoQ、掘金、CSDN、博客园、51CTO | 方案三(通用采集) |
| 人人都是产品经理、菜鸟教程 | 方案一(通用网页采集) |
| 其他未知站点 | 先尝试方案三,失败再尝试方案一 |
7.2 未来优化方向
- 知乎登录态支持:通过 Cookie 池或扫码登录获取知乎登录态,解决知乎采集问题
- SegmentFault 绕过:尝试更换出口 IP 或更高级的设备指纹模拟
- 自动平台识别:通过页面特征自动判断站点类型,减少人工选择采集方案的步骤
- 智能摘要生成:接入大模型 API,对采集内容自动生成摘要和标签
- 更多平台适配:根据使用需求,逐步添加开源中国(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) 接口思想:
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 是否需要更新?
不需要(本阶段)。原因:
wechat_crawl.py是独立路由/api/wechat-crawl/,有自己的 preview/save 两阶段流程,是「微信专用采集器」而非「散落的站点特判逻辑」,不存在重复规则问题- 微信的提取逻辑会以
WechatExtractor策略类形式注册到crawl_strategies.py,供通用入口(universal_fetch/crawl_article)遇到微信 URL 时兜底复用——这是新增复用,不改动 wechat_crawl.py 本体 - 若未来想让 wechat_crawl.py 内部也复用策略类,属于可选优化(消除与 crawl.py 微信段的重复),不在本次重构范围