16 KiB
资讯采集模块技术文档
1. 概述
资讯采集模块允许管理员从外部网页采集文章内容,自动转换为 Markdown 格式存入草稿,经人工编辑后发布。当前系统支持 两种采集方式:
| 采集方式 | 适用场景 | 入口 |
|---|---|---|
| 普通采集 | 技术博客、新闻网站等一般网页 | POST /api/crawl |
| 微信采集 | 微信公众号文章(含搜狗链接) | POST /api/wechat-crawl/* |
此外,batch_crawl.py 提供针对特定站点(niaodaifu.cn)的批量采集脚本,采用 Playwright 直接翻页抓取全站文章。
2. 架构总览
┌──────────────────────┐
│ 管理员输入URL │
└──────────┬───────────┘
│
┌────────────────┼────────────────┐
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 普通采集 /api/crawl │ │ 微信采集 /api/ │
│ │ │ wechat-crawl/* │
│ Requests 优先 │ │ │
│ Playwright 兜底 │ │ Playwright + │
│ + Stealth 可选 │ │ Stealth 必需 │
└────────┬──────────┘ │ + Sogou 会话预热 │
│ └────────┬──────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ HTML → Markdown │ │ HTML → Markdown │
│ 图片下载到临时目录│ │ 图片下载到正式目录│
│ 数学公式修复 │ │ 直接创建文章入库 │
└────────┬──────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ 返回前端预览 │
│ 用户编辑后保存 │
└─────────────────┘
3. 普通采集 (crawl.py)
文件: backend/app/routers/crawl.py
3.1 API 接口
POST /api/crawl
{
"url": "https://juejin.cn/post/xxx",
"use_playwright": false
}
返回采集结果(标题、Markdown 正文、已下载的图片路径),不直接入库,由前端展示预览后用户决定是否保存。
3.2 双模式采集策略
| 模式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Requests | 普通静态网站(博客园、菜鸟教程等) | 速度快、资源占用低 | 无法执行 JS |
| Playwright | SPA / 反爬网站 | 可执行 JS、绕过 WAF | 启动慢、内存占用高 |
强制使用 Playwright 的平台:
- 微信公众号 (
mp.weixin.qq.com) — Requests 会被拦截 - 掘金 (
juejin.cn) — 返回混淆代码 - 阿里云开发者社区 (
developer.aliyun.com) — 语雀编辑器懒加载
Playwright 自动兜底触发条件:
- HTTP 状态码 468 / 403 / 429 / 503(被 WAF 拦截)
- 页面文本内容 < 200 字符(检测到 SPA 骨架)
is_spa_site()检测到#root、#app、data-reactroot等特征
3.3 反爬虫配置
Playwright 启动时注入以下配置绕过常见反爬:
browser = p.chromium.launch(
headless=True,
args=[
'--disable-blink-features=AutomationControlled',
'--disable-web-security',
'--disable-features=IsolateOrigins,site-per-process',
'--no-sandbox',
'--disable-setuid-sandbox',
]
)
context.add_init_script("""
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
Object.defineProperty(navigator, 'plugins', {get: () => [1, 2, 3, 4, 5]});
window.chrome = { runtime: {} };
""")
若安装了 playwright-stealth,自动启用 stealth_sync(page) 进一步增强隐匿。
注意:普通采集使用同步 Playwright API(
sync_playwright),避免 Windows 下 asyncio 子进程兼容问题。
3.4 各平台特殊处理
| 平台 | 等待策略 | 标题选择器 | 正文选择器 |
|---|---|---|---|
| 掘金 | article 出现(15s) |
article h1 → h1 |
.markdown-body → article |
| 阿里云 | .article-content + 滚动5次 |
h1 |
.article-content → .article-detail |
| SegmentFault | networkidle + 8s |
h1.article-title → h1 |
.article-content → .markdown-body → article |
| 腾讯云 | [class*="markdown"] |
h1 → .article-title |
.mod-content__markdown |
3.5 标题提取策略
按优先级依次尝试(extract_title_from_html):
- Open Graph
og:titlemeta 标签 - 博客园
h1.postTitle/a#cb_post_title_url - 人人都是产品经理
.article--title - 菜鸟教程
.article-intro h1 <title>标签(清理| 站点名等后缀)<h1>标签- 内容区
<h2>/<h3> - Twitter Card
twitter:title - 回退
"未命名文章"
3.6 内容提取策略
BeautifulSoup 优先选择器(按优先级):
.article--content.grap # 人人都是产品经理
#cnblogs_post_body # 博客园
.article-intro # 菜鸟教程
article, main # 通用
.post-content / .article-content / .entry-content
#content / #main-content
BS4 不可用时回退正则匹配。
3.7 HTML 转 Markdown
<img>→(URL 自动补全为绝对路径,负责处理//、/前缀)<h1~h6>→# ~ ######<pre><code>→ 代码块(自动识别language-xxx/lang-xxx类名)<code>→ 行内代码<strong>/<b>→**粗体**<em>/<i>→*斜体*- 空行合并、HTML 实体解码
3.8 数学公式修复
fix_math_formulas() 处理掘金等平台采集到的 LaTeX 公式格式混乱:
- 检测块级公式关键词(
\begin{array}、\sum_、\int_等),自动包裹$$...$$ - 行内公式检测
\[a-zA-Z]+模式,包裹\(...\) - 规范化反斜杠(双重转义 → 单重)
- 清理已渲染的数学符号(上标、下标 Unicode 字符)
3.9 图片处理
下载流程:
- 从 Markdown 提取
中的图片 URL - 下载到临时目录(Linux:
/tmp/news_crawl/) - 防盗链检测:文件 < 1KB 则丢弃
- 图片完整性校验:PIL
verify() - 生成缩略图到临时目录
thumbnails/子目录 - 替换 Markdown 中的远程 URL 为临时路径
关键请求头:
headers = {
'User-Agent': 'Mozilla/5.0 ...',
'Accept': 'image/avif,image/webp,image/apng,image/*,*/*;q=0.8',
'Referer': request.url, # 模拟来源页面
}
# 微信图片特化处理
if 'mmbiz.qpic.cn' in img_url:
headers['Referer'] = 'https://mp.weixin.qq.com/'
保存时迁移:调用 create_post 时,临时图片被识别并迁移到正式目录 storage/admin/images/。
4. 微信公众号采集 (wechat_crawl.py)
文件: backend/app/routers/wechat_crawl.py
微信采集与普通采集是两个独立的路由模块。微信采集使用异步 Playwright + Stealth,并提供完整的预览 → 保存工作流,直接创建文章入库。
4.1 API 接口
| 端点 | 方法 | 说明 |
|---|---|---|
/api/wechat-crawl/preview |
POST | 预览文章(采集内容但不入库) |
/api/wechat-crawl/save |
POST | 保存文章(入库 + 下载图片) |
/api/wechat-crawl/columns |
POST | 获取可用栏目列表 |
4.2 支持的链接格式
- 微信公众号直链:
https://mp.weixin.qq.com/s/... - 搜狗微信链接:
https://weixin.sogou.com/...(自动会话预热绕反爬)
4.3 搜狗链接反爬绕过机制
搜狗对直接访问的请求会 302 跳转到验证码页面。绕过方法如下:
# 同一浏览器会话:先访问搜狗搜索页面激活 session
await page.goto(
"https://weixin.sogou.com/weixin?type=2&query=test",
wait_until="networkidle", timeout=30000,
)
await asyncio.sleep(2)
# 再访问目标链接(此时携带有 SNUID/SUID cookie)
await page.goto(url, wait_until="networkidle", timeout=30000)
搜狗链接最终会重定向到 mp.weixin.qq.com 的真实文章页面,后续提取流程与直链一致。
4.4 Stealth 配置
微信采集对反爬要求更高,强制使用 playwright_stealth:
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True, args=[...])
context = await browser.new_context(
viewport={"width": 1920, "height": 1080},
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
locale="zh-CN",
)
page = await context.new_page()
if STEALTH_AVAILABLE:
await stealth_async(page) # 异步 Stealth
4.5 内容提取流程
- 处理弹窗: 检测微信 PC 端二维码弹窗
#js_pc_qr_code_img,自动点击关闭按钮 - 标题:
page.title(),去除- 公众号名后缀 - 正文选择器:
#js_content→.rich_media_content→#js_article→body - 作者:
#js_name/.rich_media_meta_nickname/.profile_nickname - 摘要:
#js_description/.rich_media_meta_desc - 封面图:
meta[property="og:image"]→#js_cover→.rich_media_cover img
4.6 HTML 修复 (fix_wechat_html)
微信公众号 HTML 有特殊格式问题,需要专项修复:
- 图片
data-src: 微信用data-src存真实 URL,src常为占位图,需对调 - 代码块:
<pre class="code-snippet__js">内每行是一个<span class="code-snippet__line">,需合并为纯文本 - 空标签清理: 移除不含文本也不含图片的空
<p>/<div>/<section>
4.7 HTML → Markdown 转换
使用 markdownify 库:
md = markdownify.markdownify(
html,
heading_style="ATX",
bullets="-",
strip=["script", "style"],
)
4.8 保存流程
POST /api/wechat-crawl/save 一次性完成:
- 校验栏目是否存在
- 下载封面图 →
storage/admin/images/{YYYYMM}/{uuid}.jpg - 从 Markdown 提取所有图片 URL,逐一下载并替换为本地路径
- 图片记录写入
images表 - 生成 slug(
pypinyin中文转拼音,回退hashlib.md5) - 创建
Article记录(状态draft)
图片存储:统一按年月子目录存储(如 storage/admin/images/202605/xxx.jpg),不直接放根目录。
5. 批量采集 (batch_crawl.py)
文件: backend/batch_crawl.py
独立的命令行脚本,用于批量采集 niaodaifu.cn 健康资讯栏目的全部文章。
5.1 工作原理
Playwright 打开首页 → 遍历栏目列表页 → 翻页获取全部文章 URL
→ 逐篇打开详情页 → 提取标题/正文/图片 → 下载图片 → HTML→MD
→ 直接写入 SQLite (news_posts / images 表)
5.2 栏目映射 (MENU_TO_CATEGORY)
MENU_TO_CATEGORY = {
15: (2, "科普文章"),
7: (3, "肾友"),
8: (4, "糖友"),
6: (5, "健康"),
}
5.3 关键差异
与普通采集的区别:
- 直接操作
news_postsSQLite 表(不走 FastAPI ORM) - 无需用户交互,全自动翻页抓取
- 图片存储在
storage/admin/images/{YYYYMM}/ - slug 用
hashlib.md5(url)生成,支持去重 - 包含专用
html_to_markdown()递归实现(处理嵌套 HTML 结构)
6. 前端 Markdown 渲染
6.1 访客端渲染架构
访客端文章详情页 (/news/:slug) 采用 纯手写正则替换 + KaTeX 公式渲染 的轻量方案。
Markdown 原文
│
▼
renderMarkdown() ──▶ HTML 字符串
│
▼
dangerouslySetInnerHTML ──▶ DOM 插入
│
▼
KaTeX auto-render ──▶ 数学公式渲染
6.2 renderMarkdown 处理顺序
关键:代码块在行内代码之前处理,图片在链接之前处理:
```lang\ncode``` → <pre><code>
#### ~ # → <h4> ~ <h1>
**bold** → <strong>
*italic* → <em>
~~del~~ → <del>
`code` → <code>
> quote → <blockquote>
 → <img> (必须在 [link] 之前)
[text](url) → <a>
- / 1. list → <ul>/<ol>
6.3 KaTeX 数学公式
通过 CDN 加载,支持的格式:
| 格式 | 类型 | 示例 |
|---|---|---|
$$...$$ |
块级居中 | $$E = mc^2$$ |
\[...\] |
块级居中 | \[\sum_{i=1}^n x_i\] |
\(...\) |
行内嵌入 | \(\alpha + \beta\) |
$...$ |
行内嵌入 | $x^2 + y^2 = r^2$ |
渲染时机:DOM 插入后 100ms + 500ms 重试,确保 KaTeX 库加载完毕。
6.4 样式适配
使用 Tailwind CSS prose 类 + 自定义内联样式:
- 代码块:
bg-slate-900 text-slate-100 - 行内代码:
bg-emerald-50 text-emerald-700 border-emerald-200 - 引用块:
border-l-4 border-emerald-400 bg-emerald-50/50 - 链接:
text-emerald-600 hover:text-emerald-700
7. 关键文件清单
| 文件 | 职责 |
|---|---|
backend/app/routers/crawl.py |
普通采集核心逻辑(双模式、HTML→MD、图片下载、公式修复) |
backend/app/routers/wechat_crawl.py |
微信采集(Playwright+Stealth、Sogou会话预热、预览→保存工作流) |
backend/batch_crawl.py |
批量采集脚本(niaodaifu.cn 整站抓取) |
backend/app/main.py |
挂载 /storage/tmp 静态目录、启动时清理过期临时文件 |
frontend/src/pages/admin/news/CrawlNews.jsx |
普通采集界面(URL输入、结果预览、编辑) |
frontend/src/pages/admin/news/WechatCrawlEdit.jsx |
微信采集界面(URL输入、预览、栏目选择、保存) |
frontend/src/pages/NewsDetail.jsx |
访客端文章详情(Markdown渲染 + KaTeX公式) |
frontend/index.html |
KaTeX CDN 引入 |
8. 常见问题
Q1: 普通采集和微信采集有什么区别?
| 维度 | 普通采集 | 微信采集 |
|---|---|---|
| API 端点 | /api/crawl |
/api/wechat-crawl/* |
| Playwright | 同步 API,条件启用 | 异步 API,始终启用 |
| Stealth | 可选 | 必需 |
| 保存流程 | 返回预览,编辑器手动保存 | 预览后一键保存入库 |
| 图片目录 | 临时目录 → 迁移 | 直接存入正式目录 |
| 支持链接 | 一般 URL | mp.weixin.qq.com + weixin.sogou.com |
Q2: 搜狗链接为什么需要"会话预热"?
搜狗对直接访问的链接会 302 到验证码页面。通过先在同一个浏览器会话中访问一次搜狗搜索页面,浏览器获得 SUID / SNUID cookie,之后再跳转目标链接就能正常通过反爬检测。
Q3: 为什么掘金采集必须使用 Playwright?
掘金返回的 HTML 是经过混淆的 JavaScript 代码,Requests 无法解析。必须用浏览器执行 JS 后才能得到真实内容。
Q4: 图片为什么先放临时目录(普通采集)?
- 避免未确认的文章污染正式图库
- 不保存的文章不会留下垃圾文件
- 保存时一次性迁移并入库,保证数据一致性
Q5: 如何支持更多平台?
普通采集:在 crawl_with_playwright() 中新增平台检测变量和对应的等待选择器、标题/正文选择器。
微信采集:在 crawl_wechat_article() 中新增正文选择器或链接格式支持。
文档版本: 2.0 创建时间: 2026-04-21 更新时间: 2026-05-18(新增微信采集独立模块、Sogou链接支持、批量采集章节)