Files
nanocode/new-media-platform/website-new/docs/CRAWL_MODULE.md
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

16 KiB
Raw Blame History

资讯采集模块技术文档

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):

  1. Open Graph og:title meta 标签
  2. 博客园 h1.postTitle / a#cb_post_title_url
  3. 人人都是产品经理 .article--title
  4. 菜鸟教程 .article-intro h1
  5. <title> 标签(清理 | 站点名 等后缀)
  6. <h1> 标签
  7. 内容区 <h2> / <h3>
  8. Twitter Card twitter:title
  9. 回退 "未命名文章"

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> → ![alt](url)(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 图片处理

下载流程:

  1. 从 Markdown 提取 ![alt](url) 中的图片 URL
  2. 下载到临时目录(Linux: /tmp/news_crawl/)
  3. 防盗链检测:文件 < 1KB 则丢弃
  4. 图片完整性校验:PIL verify()
  5. 生成缩略图到临时目录 thumbnails/ 子目录
  6. 替换 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 内容提取流程

  1. 处理弹窗: 检测微信 PC 端二维码弹窗 #js_pc_qr_code_img,自动点击关闭按钮
  2. 标题: page.title(),去除 - 公众号名 后缀
  3. 正文选择器: #js_content → .rich_media_content → #js_article → body
  4. 作者: #js_name / .rich_media_meta_nickname / .profile_nickname
  5. 摘要: #js_description / .rich_media_meta_desc
  6. 封面图: 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 一次性完成:

  1. 校验栏目是否存在
  2. 下载封面图 → storage/admin/images/{YYYYMM}/{uuid}.jpg
  3. 从 Markdown 提取所有图片 URL,逐一下载并替换为本地路径
  4. 图片记录写入 images 表
  5. 生成 slug(pypinyin 中文转拼音,回退 hashlib.md5)
  6. 创建 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_posts SQLite 表(不走 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>
![alt](url)        → <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链接支持、批量采集章节)