【FDE系列】阶段3:Day 63:文档解析 — 把真实 PDF 手册变成可用文本

📚前言

📒FDE系列内容总纲:

【大纲】FDE 前沿部署工程师学习系列教程-CSDN博客

🚄前置课程列表:

见文档结尾附录。


🚀阶段3·Day 63:文档解析 --- 把真实 PDF 手册变成可用文本

FDE 学习系列教程 · 第三阶段 · 第 9 周 · Day 3 预计时长:3 小时 | 难度:★★★★☆ | 前置知识:Day 61-62(RAG 链路、Embedding)、命令行基础 对标大纲课时:3.2.2 文档解析 ------ PDF/Word/PPT/HTML/邮件解析;OCR;表格提取;扫描件处理(MinerU / Docling / Unstructured)
📌 一句话目标:认清 PDF 的五类坑,用 MinerU 和 Docling 把一份真实设备手册解析成干净 Markdown,掌握表格质检与产物清洗。


🧑‍🤝‍🧑 开场:你的资料柜,机器打不开

先看看你这两天攒下的东西:

复制代码
  Day61:跑通了 RAG 八步链路(用的 5 条手写资料)
  Day62:BGE-M3 把文字变成向量,检索从"字面"升级到"语义"

  现在的知识库:
  ┌────────────────────────────┐
  │ DOCS = [ 5 条我手敲的资料 ]   │  ← 三百页的手册呢?
  └────────────────────────────┘

第 7-8 周冻结的智能工单助手 v0.1 长在第二阶段的设备告警工单闭环系统(FastAPI + MySQL + Docker + 飞书)之上,它会分类、会提取、会写报告------但那些能力都建立在"输入已经是结构化数据"的前提上。而客户真正想让我们回答的"这台设备怎么修",答案在资料柜里,不在数据库里。

现实是:客户的知识根本不是 txt,更不是 MySQL 里的一行记录,而是这些东西:

复制代码
  📄 设备操作与维护手册.pdf          (300 页,扫描版,有些页是图片)
  📄 注塑车间作业指导书.docx         (带表格、带批注)
  📄 设备点检标准.xlsx              (合并单元格一大堆)
  📄 工艺变更通知.pptx              (字都在图形框里)
  📧 邮件往来:张工说 A3 的那个参数改了   (正文里夹着关键结论)
  🖼️ 现场拍的铭牌照片.jpg            (型号在图片里)

更要命的是,这些文件里最值钱的内容往往是最难提取的:

复制代码
  · 参数表(一张表 50 个型号的额定值)  → 表格提取不行,全变成乱码
  · 安全警示框("警告:必须先断电")   → 被当成普通段落,重要性丢失
  · 双栏排版的小字注释                → 左右两栏被读成"一句话交叉拼接"

在客户现场我见过太多次这样的翻车:RAG 系统上线了,师傅问"液压油的更换周期",系统答"请咨询供应商"------因为手册里那个参数在表格里 ,而解析工具把表格读成了 液压油 更换周期 | | | 500 | 小时 这种散架的字符流,向量检索根本匹配不上。

RAG 的效果上限,是由解析质量决定的。 后面分块再巧、向量模型再强、重排再精,原料是垃圾,出来的还是垃圾(garbage in, garbage out)。

今天就是"把原料准备好"的一天。三件事:

  1. 认清 PDF 的五类坑(知道坑在哪,才能验收解析结果)

  2. 上手两个当下最强的开源解析工具:MinerU 和 Docling

  3. 写出清洗脚本,把解析产物收拾成能直接分块的干净 Markdown


📖 一、PDF 的五类坑

PDF 这个格式,生来就不是为了"被提取内容"设计的------它是为了打印效果固定设计的。一个 PDF 里存的其实是一堆"把字符画在坐标 (x, y)"的绘图指令,不是"段落/表格/标题"这种结构信息。

这就导致解析 PDF 像是在从一张打印好的纸上反推它的排版结构。坑主要有五个:

坑 1:扫描件 ------ 整页就是一张图

复制代码
  ┌──────────────────────────┐
  │                          │
  │   (一整页的照片)         │  ← PDF 里根本没有文字层
  │                          │     只有一个 image 对象
  │                          │
  └──────────────────────────┘

  直接提取文本 → 得到空字符串 ""

  ✅ 解法:OCR(光学字符识别),先把图片里的字认出来
     代价:慢(一页几秒)、可能认错(0↔O、1↔l、8↔B)

怎么判断是不是扫描件:用 PDF 阅读器试着选中一段文字,选不中就说明没有文字层。

复制代码
# 快速判断:用 PyMuPDF 看每页的文字量
import fitz  # pip install pymupdf

doc = fitz.open("手册.pdf")
for i, page in enumerate(doc):
    text = page.get_text().strip()
    flag = "🖼️ 疑似扫描页" if len(text) < 20 else "📝 有文字层"
    print(f"第 {i+1:>3} 页:{len(text):>5} 字  {flag}")

坑 2:双栏排版 ------ 左右两栏被读串行

复制代码
  论文/手册常见双栏:

  ┌───────────────┬───────────────┐
  │ 4.1 冷却系统   │ 4.3 电气系统   │
  │ 冷却水压力应   │ 主电机功率为   │
  │ 保持在 0.3MPa │ 22kW,启动电   │
  └───────────────┴───────────────┘

  ❌ 朴素提取(按字符坐标从上到下扫):
     "4.1 冷却系统 4.3 电气系统 冷却水压力应 主电机功率为
      保持在 0.3MPa 22kW,启动电......"
     ↑ 左右两栏被交织成一段,语义完全破坏

  ✅ 解法:版面分析(Layout Analysis)------
     先用模型识别出"这里有两个栏",再按栏分别读取。
     MinerU / Docling 都内置了版面分析,这是它们比 PyMuPDF 强的核心原因。

坑 3:表格 ------ 最值钱也最容易散架

复制代码
  PDF 里的表格,在文件层面其实【不存在】。
  它只是一堆"画在这几个坐标上的文字" + "画在这几个位置的线条"。

  原表:
  ┌────────┬──────────┬────────┐
  │ 设备   │ 冷却水压力 │ 换油周期 │
  ├────────┼──────────┼────────┤
  │ A3     │ 0.3MPa   │ 500h   │
  │ B2     │ 0.35MPa  │ 400h   │
  └────────┴──────────┴────────┘

  ❌ 朴素提取:
     "设备 冷却水压力 换油周期 A3 0.3MPa 500h B2 0.35MPa 400h"
     ↑ 丢掉了"哪一行属于哪台设备"的对应关系
       向量检索后,模型可能把 B2 的 500h 说成 A3 的

  ✅ 解法:表格结构识别(Table Structure Recognition)
     还原成 Markdown 表格或 CSV/DataFrame,行列的对应关系才保得住

坑 4:图片里的字 ------ 流程图、铭牌、曲线图

复制代码
  · 设备铭牌照片(型号/功率/出厂编号在图上)
  · 流程图里的文字标注
  · 曲线图的坐标轴刻度与图例

  ❌ 这些字不在文字层里,提取不到
  ✅ 解法:OCR + 版面分析(把图片区域也纳入识别范围)
     进阶:图片描述(用多模态模型给图生成文字说明,第 11 周高级 RAG 会提)

坑 5:页眉页脚 ------ 噪声的主要来源

复制代码
  每一页顶部:"XX型注塑机操作与维护手册        第 47 页"
  每一页底部:"公司内部资料 严禁外传        版本 V2.3  2026-03"

  ❌ 它们会被重复几十上百次地塞进你的知识库
     → 一来浪费存储与 token
     → 二来"XX型注塑机操作与维护手册"这句在每个 chunk 里都出现,
       会拉高所有 chunk 之间的相似度,把检索分数搅浑

  ✅ 解法:解析后统一清洗(今天第六节给脚本)

五坑速查

坑 表现 解法 工具支持
扫描件 提取文本为空 OCR MinerU -m ocr、Docling 内置 OCR
双栏 左右栏串行 版面分析 MinerU、Docling 都支持
表格 行列关系丢失 表格结构识别 MinerU、Docling(强项)
图内文字 内容缺失 OCR 图片区域 MinerU、Docling
页眉页脚 噪声重复 后处理清洗 自写脚本(今天给)

📖 二、工具定位:四个选手该选谁

大纲 3.2.2 点名了三个:MinerU (~79K Star)、Docling (~59K Star)、Unstructured (~28K Star)。再加个你一定会用到的底层库 PyMuPDF。

工具 定位 强项 弱项 什么时候用
MinerU 一站式 PDF→Markdown 转换器 中文文档效果极佳;自带版面分析+OCR+公式/表格识别;命令行开箱即用;国内模型源友好 Python API 相对命令行弱一些;依赖较重 中文 PDF 首选,批量转换场景
Docling IBM 出品的文档解析库 Python API 优雅;表格提取质量顶级(能直接出 DataFrame);格式支持广(PDF/DOCX/PPTX/XLSX/HTML) 中文 OCR 需额外配模型;体积大 表格多、要程序化集成的场景
Unstructured 通用文档 ETL 框架 格式覆盖最广(含邮件 .eml);云 API + 本地两种模式;生态集成多(LangChain 等) 中文效果与表格质量不如前两个;分区(chunking)逻辑偏英文 格式杂、需要接现成生态
PyMuPDF 底层 PDF 操作库 极快、极轻、零模型依赖;能取坐标/画框/切页 不做版面分析,双栏会串行;不 OCR 快速判断文档类型、做预处理、切页范围
复制代码
  FDE 现场决策树:

  文档是中文 PDF?
      ├─ 是 → MinerU(命令行一把梭,出 full.md)
      └─ 否 → 往下看
              │
  表格/结构化数据多?
      ├─ 是 → Docling(export_to_dataframe 直接拿表)
      └─ 否 → 往下看
              │
  要不要写进自己的 Python 服务?
      ├─ 要 → Docling(API 更好用)或 MinerU 的 mineru-api(Docker)
      └─ 只是批量转一次 → MinerU 命令行

  需要快速判断某份 PDF 是不是扫描件 / 切几页试试?
      └─ 永远是 PyMuPDF(3 秒出结果)

💡 我的建议:两个都装,两个都会用 。现场常见打法是------先用 PyMuPDF 快速摸清文档底细,再用 MinerU 批量转,遇到表格关键页就用 Docling 单独再跑一遍做交叉验证。解析这活,交叉验证比迷信单一工具靠谱。


🖥️ 三、实操:MinerU 命令行一把梭

实操步骤 1:安装 MinerU

复制代码
cd fde-ai
.\.venv\Scripts\Activate.ps1

pip install -U "mineru[core]"

⚠️ MinerU 依赖较多(torch、paddle 相关组件等),安装可能需要几分钟。如果与 FlagEmbedding 的 torch 版本冲突,建议为 MinerU 单独建一个虚拟环境(解析是离线一次性任务,不需要和在线服务同环境):

复制代码
python -m venv .venv-parser
.\.venv-parser\Scripts\Activate.ps1
pip install -U "mineru[core]"

顺便装上 PyMuPDF 用于文档体检:

复制代码
pip install pymupdf

实操步骤 2:准备一份真实 PDF

有真实设备手册最好。没有的话,自己造一份(这个过程本身能让你理解五类坑):

  1. 打开 Word,写一页《A3 型注塑机冷却系统维护说明》

  2. 插入一个 3 行 4 列的表格(设备 / 冷却水压力 / 换油周期 / 责任人)

  3. 加页眉"XX型注塑机操作与维护手册"、页脚"第 X 页"

  4. 再写一段"警告:维护前必须切断主电源并挂牌上锁"

  5. 另存为 PDF ,命名 manual_a3.pdf

放好目录:

复制代码
fde-ai/
└── data/
    └── raw/
        └── manual_a3.pdf      ← 原始 PDF,永远保留,别改

实操步骤 3:先做个体检(30 秒摸清底细)

新建 day63_probe.py:

复制代码
"""解析前的体检:这份 PDF 到底是什么货色"""
import fitz
from pathlib import Path

pdf = Path("data/raw/manual_a3.pdf")
doc = fitz.open(pdf)

print(f"文件:{pdf.name}   页数:{doc.page_count}")
print("-" * 62)
scan_pages = 0
for i, page in enumerate(doc):
    text = page.get_text().strip()
    images = page.get_images()
    tables = page.find_tables()          # PyMuPDF 也有基础表格检测
    tag = "🖼️ 扫描页" if len(text) < 20 else "📝 文字层"
    if len(text) < 20:
        scan_pages += 1
    print(f"P{i+1:<3} {tag}  字数={len(text):<6} 图片数={len(images):<3} "
          f"表格数={len(tables.tables)}")

print("-" * 62)
print(f"疑似扫描页:{scan_pages}/{doc.page_count}")
print("→ 扫描页占比高就必须开 OCR(-m ocr)")

# 看一眼第 1 页的原始文本,感受"朴素提取"的质量
print("\n【第 1 页朴素文本提取(前 400 字)】")
print(doc[0].get_text()[:400])

python day63_probe.py

看什么:

  • 页数对不对?

  • 有多少页是扫描页 → 决定要不要 -m ocr

  • 朴素提取出来的文本是不是双栏串行、表格散架 → 记下来,等会儿跟 MinerU 的产出对比

实操步骤 4:跑 MinerU 命令行

最基础的一条命令:

复制代码
mineru -p data/raw/manual_a3.pdf -o data/parsed/mineru

国内用户强烈建议加模型源参数(避免下载模型卡住):

复制代码
mineru -p data/raw/manual_a3.pdf -o data/parsed/mineru --source modelscope

常用参数速查:

复制代码
# 纯 CPU 后端(没有 GPU 时务必加上,否则可能报 CUDA 相关错)
mineru -p 手册.pdf -o out -b pipeline --source modelscope

# 扫描件/图片型 PDF → 走 OCR
mineru -p 手册.pdf -o out -m ocr -b pipeline

# 只解析前 10 页(第 0 页到第 9 页,页码从 0 开始)------大文件先试跑
mineru -p 手册.pdf -o out -s 0 -e 9

# 指定语言,提升 OCR 与版面识别准确率
mineru -p 手册.pdf -o out -l ch

# 组合拳:CPU + 中文 + 前 20 页 + 国内源
mineru -p 手册.pdf -o out -b pipeline -l ch -s 0 -e 19 --source modelscope

💡 --source modelscope 也可以用环境变量方式设置,避免每次都敲:

复制代码
# PowerShell
$env:MINERU_MODEL_SOURCE = "modelscope"
# Linux / macOS
export MINERU_MODEL_SOURCE=modelscope

实操步骤 5:看懂产物目录

MinerU 会给每个输入文件建一个子目录:

复制代码
data/parsed/mineru/
└── manual_a3/
    ├── full.md              ★ 主产物:全文 Markdown(后面就用它)
    ├── content_list.json        结构化内容列表(含每个块的坐标、类型)
    ├── model.json               版面分析结果(调试用)
    ├── layout.pdf               版面可视化(框出了标题/表格/图片区域)
    ├── span.pdf                 文本片段可视化
    └── images/                  从 PDF 里抠出来的图片
        ├── 0.jpg
        └── 1.jpg

重点看这三样:

复制代码
  ① full.md      → 你要的干净文本,Day 64 分块的原料就是它
  ② layout.pdf   → 打开看版面分析框得准不准(质检神器)
                   标题被框成"标题"了吗?表格被框成一整块了吗?
                   如果框错了,说明这份文档这个工具吃不下,换 Docling 试试
  ③ content_list.json → 每个块的 type(text/table/image)+ bbox 坐标 + page
                   想做"只取第 3 章"或"只取表格"这类精细操作,读这个

看一眼 full.md:

复制代码
# PowerShell
Get-Content data/parsed/mineru/manual_a3/full.md -TotalCount 60 -Encoding UTF8

# Linux / macOS / Git Bash
head -n 60 data/parsed/mineru/manual_a3/full.md

跟第 3 步的朴素提取对比一下,重点看三点:

  1. 标题有没有变成 # / ##(Markdown 结构保住了吗)

  2. 表格是不是变成了 | --- | --- | 的 Markdown 表格

  3. 双栏有没有被正确分开

实操步骤 6:把 MinerU 变成服务(Docker 部署)

批量解析或要给别的系统调用时,MinerU 提供了 FastAPI 服务 mineru-api。平台类工具一律 Docker 部署(第二阶段 Day 43 的老规矩):

复制代码
# docker-compose.mineru.yml
version: "3.8"
services:
  mineru-api:
    image: mineru/mineru-api:latest
    container_name: mineru-api
    ports:
      - "8001:8000"
    environment:
      MINERU_MODEL_SOURCE: modelscope
    volumes:
      - ./data:/app/data
    restart: unless-stopped

docker compose -f docker-compose.mineru.yml up -d
docker logs -f mineru-api

不想写 compose,一条 docker run 也行:

复制代码
docker run -d --name mineru-api -p 8001:8000 \
  -e MINERU_MODEL_SOURCE=modelscope \
  -v ${PWD}/data:/app/data \
  mineru/mineru-api:latest

# 用 httpx 调用解析服务(Day 47 学过的技能直接用上)
import httpx

r = httpx.post("http://localhost:8001/parse",
               json={"pdf_path": "/app/data/raw/manual_a3.pdf"},
               timeout=300)
print(r.json()["markdown"][:500])

⚠️ 说明:MinerU 的镜像标签与 API 路径会随版本变化,以上为通用写法。实际使用时先 docker run 起来后打开 http://localhost:8001/docs 看它自动生成的接口文档确认路径------第二阶段的 OpenAPI 技能在这里又派上用场了。


🖥️ 四、实操:Docling 的 Python API

MinerU 胜在命令行一把梭和中文效果,Docling 胜在 Python API 与表格质量。两个都学,现场按需切换。

实操步骤 1:安装

复制代码
pip install docling

实操步骤 2:命令行快速验证

复制代码
docling data/raw/manual_a3.pdf

默认会在当前目录输出 Markdown。先跑一次确认安装没问题。

实操步骤 3:Python API 完整用法

新建 day63_docling.py:

复制代码
"""Day63:Docling Python API ------ 转 Markdown + 提取表格"""
from pathlib import Path
from docling.document_converter import DocumentConverter

SRC = Path("data/raw/manual_a3.pdf")

# ① 转换(一个入口吃 PDF/DOCX/PPTX/XLSX/HTML)
converter = DocumentConverter()
result = converter.convert(str(SRC))
doc = result.document          # DoclingDocument 对象

# ② 导出 Markdown
md = doc.export_to_markdown()
out = Path("data/parsed/docling/manual_a3.md")
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(md, encoding="utf-8")
print(f"✅ Markdown 已写入 {out}({len(md)} 字符)")

# ③ 提取表格(Docling 的王牌:直接给你 DataFrame)
tables = [t.export_to_dataframe() for t in doc.tables]
print(f"\n共识别到 {len(tables)} 个表格")
for i, df in enumerate(tables, 1):
    print(f"\n------ 表格 {i}({df.shape[0]} 行 × {df.shape[1]} 列)------")
    print(df.to_string(index=False))
    df.to_csv(f"data/parsed/docling/table_{i}.csv", index=False, encoding="utf-8-sig")

# ④ 看看文档骨架(标题层级是否保住了)
print("\n------ 文档标题结构 ------")
for item in doc.texts:
    if getattr(item, "label", None) in ("title", "section_header"):
        print(f"  [{item.label}] {item.text}")

运行:

复制代码
python day63_docling.py

输出示例:

复制代码
✅ Markdown 已写入 data/parsed/docling/manual_a3.md(3821 字符)

共识别到 1 个表格

------ 表格 1(3 行 × 4 列)------
设备    冷却水压力   换油周期  责任人
A3     0.3MPa      500h     张工
B2     0.35MPa     400h     李工

------ 文档标题结构 ------
  [title] A3 型注塑机操作与维护手册
  [section_header] 4.3 冷却系统维护
  [section_header] 4.4 加热系统

实操步骤 4:交叉验证 MinerU 与 Docling

同一份 PDF、两个工具,写个小脚本对比:

复制代码
"""Day63:MinerU vs Docling 产出对比"""
from pathlib import Path

mineru_md = Path("data/parsed/mineru/manual_a3/full.md").read_text(encoding="utf-8")
docling_md = Path("data/parsed/docling/manual_a3.md").read_text(encoding="utf-8")

for name, md in [("MinerU", mineru_md), ("Docling", docling_md)]:
    n_table = md.count("|---")                      # Markdown 表格分隔行数
    n_head = sum(1 for ln in md.splitlines() if ln.startswith("#"))
    print(f"{name:<8} 字符数={len(md):<6} 标题数={n_head:<4} 表格分隔行={n_table}")

# 找出只在其中一个里出现的关键词(差异点)
kws = ["0.3MPa", "500h", "挂牌上锁", "警告"]
for kw in kws:
    print(f"\n关键词「{kw}」:",
          f"MinerU={'✅' if kw in mineru_md else '❌'}",
          f"Docling={'✅' if kw in docling_md else '❌'}")

📌 这个"关键词是否出现在产物里"的检查,是最朴素也最有效的解析质检方法。后面做分块、做向量库之后,你还会反复用到它------因为它直接对应"用户问到这个关键词时能不能被召回"。


📖 五、表格提取质量检查清单

表格是工程资料里最容易出事的部分。解析完必须逐项检查,别偷懒。

✅ 七项检查清单

# 检查项 怎么查 不合格的表现
1 行列数是否正确 数 ` ` 分隔的列数,与原 PDF 对比
2 表头是否被识别为表头 Markdown 表格第一行是不是列名 表头变成了普通数据行
3 合并单元格是否还原 看跨行/跨列的值有没有被复制填充 合并格只填了第一个,其余空
4 数字与单位是否分离 0.3MPa 有没有被拆成 0.3 和 MPa 两列 数值列里混进了单位
5 跨页表格是否连续 表格被分页截断时,两页是否拼回一个表 变成两个残缺的表,第二页没表头
6 是否混入页眉页脚 表格里有没有出现"第 X 页" 表里有噪声行
7 中文是否乱码 有没有 \ufffd 或方框符号 编码问题

检查脚本:自动跑一遍

新建 day63_table_qc.py:

python 复制代码
"""表格质检:把七项检查自动化"""
import re
from pathlib import Path

md = Path("data/parsed/docling/manual_a3.md").read_text(encoding="utf-8")
lines = md.splitlines()

# ① 找出所有 Markdown 表格块
tables, cur = [], []
for ln in lines:
    if ln.strip().startswith("|"):
        cur.append(ln.strip())
    else:
        if len(cur) >= 2:
            tables.append(cur)
        cur = []
if len(cur) >= 2:
    tables.append(cur)

print(f"共发现 {len(tables)} 个表格\n")

for i, t in enumerate(tables, 1):
    header = [c.strip() for c in t[0].strip("|").split("|")]
    ncol = len(header)
    body = t[2:] if len(t) > 2 and set(t[1].replace("|", "").replace("-", "").strip()) == set() else t[1:]
    print(f"------ 表格 {i} ------")
    print(f"  列数:{ncol}    表头:{header}")

    # ② 检查列数一致性
    bad = [r for r in body if len([c for c in r.strip("|").split("|")]) != ncol]
    print(f"  {'✅' if not bad else '❌'} 列数一致:"
          f"{len(body) - len(bad)}/{len(body)} 行正常")

    # ③ 检查空单元格比例(合并单元格未还原的典型症状)
    cells = [c.strip() for r in body for c in r.strip("|").split("|")]
    empty = sum(1 for c in cells if not c)
    ratio = empty / len(cells) if cells else 0
    print(f"  {'✅' if ratio < 0.2 else '⚠️'} 空格率:{ratio:.1%}(>20% 可能合并单元格未还原)")

    # ④ 检查是否混入页眉页脚噪声
    noise = [r for r in body if re.search(r"第\s*\d+\s*页|严禁外传|版本\s*V", r)]
    print(f"  {'✅' if not noise else '❌'} 页眉页脚噪声:{len(noise)} 行")

    # ⑤ 检查乱码
    garbled = len(re.findall(r"\ufffd|□", "\n".join(t)))
    print(f"  {'✅' if garbled == 0 else '❌'} 乱码符号:{garbled} 个")

    # ⑥ 检查数字与单位分离(同一格里数字后跟空格再跟单位)
    split_unit = [c for r in body for c in r.strip("|").split("|")
                  if re.search(r"\d\s+[a-zA-Z%℃]+", c.strip())]
    print(f"  {'✅' if not split_unit else '⚠️'} 数字单位分离:{len(split_unit)} 处 "
          f"{split_unit[:3]}\n")
复制代码
python day63_table_qc.py

💡 表格质检不合格怎么办?按顺序试:① 换工具(MinerU ↔ Docling)② 在 Docling 里调整表格识别模式 ③ 手工补录(关键参数表手工校对一遍是值得的 ,就几十行)④ 实在不行就把表格转成"一句话一条记录"的文本("A3 的冷却水压力是 0.3MPa"),牺牲格式换检索可用性------这招在 RAG 场景反而效果更好。


🖥️ 六、解析产物清洗脚本

解析出来的 full.md 通常还带着一堆脏东西。清洗这一步省不得------它直接决定明天分块的质量。

四类必须清洗的噪声

复制代码
  ① 页眉页脚      每页都重复的"XX手册  第 47 页"
  ② 多余空行      连续 3 个以上的空行
  ③ 断行          PDF 换行导致的句子被腰斩:
                   "冷却水进水压力应保持在\n0.3MPa 以上"  → 应合并
  ④ 水印/噪声     "严禁外传""样张""DRAFT"

清洗脚本

新建 day63_clean.py:

python 复制代码
"""Day63:解析产物清洗 ------ 去页眉页脚、去空行、修断行"""
import re
from pathlib import Path
from collections import Counter

SRC = Path("data/parsed/mineru/manual_a3/full.md")
DST = Path("data/cleaned/manual_a3.clean.md")
DST.parent.mkdir(parents=True, exist_ok=True)

raw = SRC.read_text(encoding="utf-8")
lines = raw.splitlines()

# ========== ① 自动识别页眉页脚(出现次数最多的短行)==========
# 思路:真正的页眉页脚会在几十页里重复出现,统计频率就能抓出来
short_lines = [ln.strip() for ln in lines if 0 < len(ln.strip()) <= 30]
freq = Counter(short_lines)
# 出现 ≥5 次且长度短的,判为页眉页脚
threshold = 5
header_footer = {ln for ln, c in freq.items() if c >= threshold}
# 正则兜底:带"第 X 页"、页码、日期戳的行
PAGE_PAT = re.compile(r"^\s*第?\s*\d+\s*页?\s*$|^\s*\d+\s*/\s*\d+\s*$|严禁外传|公司内部资料")

print("识别为页眉页脚的重复行(前 10 条):")
for ln, c in freq.most_common(10):
    if c >= threshold:
        print(f"   ×{c:<4} {ln[:40]}")

# ========== ② 逐行清洗 ==========
cleaned: list[str] = []
for ln in lines:
    s = ln.strip()
    if s in header_footer:            # 命中页眉页脚
        continue
    if PAGE_PAT.search(s):            # 命中页码等正则
        continue
    cleaned.append(ln.rstrip())

# ========== ③ 压缩多余空行(连续空行压成 1 个)==========
out: list[str] = []
blank = 0
for ln in cleaned:
    if ln.strip() == "":
        blank += 1
        if blank > 1:
            continue
    else:
        blank = 0
    out.append(ln)

# ========== ④ 修断行(把被腰斩的中文句子接回去)==========
# 规则:上一行以中文/数字结束(不是句末标点),且下一行以中文/数字开头
#       → 说明是 PDF 换行导致的断行,直接合并
merged: list[str] = []
END_OK = "。!?;:)"】》!?;:"
START_SKIP = "#|-|>|*"          # 标题、表格、列表、引用不参与合并

for ln in out:
    s = ln.strip()
    if (merged
            and s
            and not s.startswith(tuple(START_SKIP))
            and not merged[-1].startswith(tuple(START_SKIP))
            and merged[-1]
            and merged[-1][-1] not in END_OK
            and not merged[-1].endswith("  ")     # Markdown 硬换行(行尾两空格)
            and re.search(r"[\u4e00-\u9fa5]$", merged[-1])
            and re.search(r"^[\u4e00-\u9fa5]", s)):
        merged[-1] = merged[-1] + s              # 接到上一行末尾
    else:
        merged.append(ln)

text = "\n".join(merged)
text = re.sub(r"\n{3,}", "\n\n", text)            # 再压一次空行

DST.write_text(text, encoding="utf-8")
print(f"\n清洗前:{len(raw)} 字符 / {len(lines)} 行")
print(f"清洗后:{len(text)} 字符 / {len(text.splitlines())} 行")
print(f"✅ 已写入 {DST}")

# ========== ⑤ 抽查:打印首尾各 20 行,肉眼过一遍 ==========
print("\n【抽查:前 20 行】")
print("\n".join(text.splitlines()[:20]))

运行:

复制代码
python day63_clean.py

⚠️ 修断行这一步要小心 :合并逻辑会破坏 Markdown 表格(表格行以 | 开头,脚本里已用 START_SKIP 跳过)、也会破坏列表项。跑完务必抽查 有没有把不该合并的合并了。规则是保守的宁可少合并------断行影响检索一点点,把表格合并坏了影响很大。

清洗前后对比(记进实验记录本)

指标 清洗前 清洗后 说明
字符数 一般减少 10%~25%
行数
"第 X 页"出现次数 应为 0
连续空行数 应为 0
表格行是否完好 必须抽查

📊 文档解析速查表

MinerU 命令速查

参数 作用 示例
-p 输入路径(文件或目录) -p 手册.pdf
-o 输出目录 -o data/parsed
-b 后端,pipeline = 纯 CPU -b pipeline
-m 解析模式,ocr = 走 OCR -m ocr
-s / -e 起始页 / 结束页(从 0 计) -s 0 -e 9
-l 语言 -l ch
--source 模型源,国内用 modelscope --source modelscope

环境变量等价写法:MINERU_MODEL_SOURCE=modelscope

Docling 代码速查

复制代码
from docling.document_converter import DocumentConverter

doc = DocumentConverter().convert("手册.pdf").document
md      = doc.export_to_markdown()                        # 全文 Markdown
tables  = [t.export_to_dataframe() for t in doc.tables]   # 表格 → DataFrame
texts   = [t.text for t in doc.texts]                     # 全部文本块

产物结构速查

复制代码
mineru 输出目录/
└── <文件名>/
    ├── full.md             ★ 主产物(清洗与分块的原料)
    ├── content_list.json     结构化块列表(含 type / bbox / page)
    ├── layout.pdf            版面可视化(质检用)
    └── images/               抠出的图片

常见翻车与解法

❌ 症状 原因 ✅ 解法
解析产物是空的 扫描件没开 OCR 加 -m ocr
CUDA / 显卡相关报错 无 GPU 却用了默认后端 加 -b pipeline
下载模型卡死 HuggingFace 直连不畅 --source modelscope 或设 HF_ENDPOINT
中文乱码(\ufffd) 编码或字体问题 换工具;或另存 PDF 时嵌入字体
表格列数不对 表格结构识别失败 换 Docling;或手工补录
双栏串行 版面分析没生效 确认用了 MinerU/Docling(PyMuPDF 不解决这个)
装依赖冲突 torch 版本打架 为解析单独建虚拟环境
大文件跑很久 300 页 CPU 解析确实慢 -s/-e 先试跑 10 页;批量走 Docker 服务

📝 本课小结

知识点 一句话记住
PDF 本质 为打印设计的绘图指令,不是结构化文档
坑 1 扫描件 只有图片没有文字层 → 必须 OCR
坑 2 双栏 朴素提取左右串行 → 要版面分析
坑 3 表格 行列关系会丢失 → 要表格结构识别
坑 4 图内文字 铭牌/流程图里的字提取不到 → OCR 图片区域
坑 5 页眉页脚 重复噪声污染相似度 → 后处理清洗
MinerU 中文 PDF 首选,命令行一把梭,产物 full.md
Docling 表格质量顶级,Python API 优雅,export_to_dataframe()
Unstructured 格式覆盖最广(含邮件),适合接生态
PyMuPDF 极快极轻,不做版面分析,用于体检与预处理
质检方法 关键词是否在产物里 + layout.pdf 目视 + 表格七项检查
清洗四件事 去页眉页脚、压空行、修断行、去水印
交叉验证 两个工具跑同一份,比迷信单一工具靠谱

🧠 核心认知 :RAG 的效果上限由解析质量决定------分块再巧、向量再强,原料是垃圾出来的还是垃圾。FDE 在现场的真正工作量,往往不在模型上,而在"把客户那堆格式混乱、扫描件与表格夹杂的历史资料,整理成机器能读、能检、能溯源的干净文本"。这一步做扎实,后面所有优化才有地基;这一步偷懒,后面所有调参都是在给垃圾堆喷香水。


📋 课后练习

练习 1:给一份真实 PDF 做体检与解析(约 45 分钟)

  1. 找一份真实 PDF(设备手册、产品说明书、论文、财报都行,≥10 页),放进 data/raw/。

  2. 跑 day63_probe.py,记录:总页数、疑似扫描页数、图片数、表格数、朴素提取的乱象(截图或复制一段)。

  3. 用 MinerU 解析(按体检结果决定要不要加 -m ocr),打开 layout.pdf 看版面框得准不准,写下 2 处"框对了"和 1 处"框错了"。

  4. 对比朴素提取与 full.md,写下一句话结论。

练习 2:表格质检实操(约 40 分钟)

  1. 确认你的 PDF 里至少有 1 个表格(没有就自己造一个带合并单元格的)。

  2. 用 Docling 解析并跑 day63_table_qc.py,把七项检查结果截图存档。

  3. 找出至少一项不合格的(列数不一致 / 空格率过高 / 混入页眉页脚),换另一个工具再跑一次,看是否改善。

  4. 如果两个工具都不行,手工把这张表改写成"一句话一条记录"(如 A3 的冷却水压力是 0.3MPa),思考为什么这种写法在 RAG 里反而更好检索。

练习 3:清洗脚本调优(约 35 分钟)

  1. 对你的 full.md 跑 day63_clean.py,对比清洗前后的字符数、行数、"第 X 页"出现次数。

  2. 故意制造一个破坏 :把 START_SKIP 里的 | 去掉再跑一次,观察表格被合并成什么样,体会为什么必须跳过表格行。

  3. 给脚本加一条你自己的清洗规则(比如去掉"内部资料 严禁外传"水印、或把全角数字转半角),验证生效。

  4. 把最终的 manual_a3.clean.md 保存好------这是明天 Day 64 分块的唯一原料,别丢了。


🔭 下节预告

今天你把三百页的 PDF 变成了一份干净的 Markdown。但如果现在就把它灌进向量库,你会发现一个尴尬的问题:

复制代码
  整份手册 20 万字,一次性编码成一个 1024 维向量
  → 这个向量是"整本手册的平均意思"
  → 问"A3 冷却水压力多少"时它确实能匹配上(分不低)
  → 但你拿它去生成答案,要么超长塞不下,要么里面 99% 是无关内容

所以必须切块。而且切法大有讲究:

  • 按固定字数硬切?会把"步骤 3"和"它的说明"劈成两半

  • 切多大?500 字还是 1000 字?重叠多少?

  • 按 Markdown 标题切?那遇到一章三千字怎么办?

  • 还有更高级的语义分块 和父子分块(小块检索、大块喂模型)

明天(Day 64)就用你今天清洗好的 manual_a3.clean.md,实现四种分块策略,并做一个对比实验:同一份手册、同一组问题,看哪种分块能命中答案、哪种会漏。这是本周最体现"工程判断力"的一天,明天见!


🌍附录:前置课程列表

阶段一:认知启蒙(AI 认知与 FDE 角色)
AI 认知

【FDE系列】阶段1Day 1:AI 层级关系 --- 四个嵌套的圈-CSDN博客

【FDE系列】阶段1Day 2:AI 三阶段发展史 --- 会认 → 会判断 → 会创造-CSDN博客

【FDE系列】阶段1Day 3:符号 AI vs 机器学习 --- 两条路线的本质区别-CSDN博客

【FDE系列】阶段1Day 4:Transformer 的历史意义 --- 2017 年的分水岭-CSDN博客

【FDE系列】阶段1Day 5:本周复习与自测 --- 检验你的 AI 认知地基-CSDN博客

【FDE系列】阶段1Day 6:Transformer 架构 --- 一张图纸盖出千千万万栋楼-CSDN博客

【FDE系列】阶段1Day 7:LLM 本质 --- 文字接龙机器-CSDN博客

【FDE系列】阶段1Day 8:Token --- 模型眼中的最小单位-CSDN博客

【FDE系列】阶段1Day 9:AI 幻觉 --- 为什么会一本正经地胡说八道-CSDN博客

【FDE系列】阶段1Day 10:上下文窗口 --- 模型的记忆力上限 + 本周复习-CSDN博客

【FDE系列】阶段1Day 11:Prompt --- 给模型立规矩-CSDN博客

【FDE系列】阶段1Day 12:Memory --- 让模型记住上下文

【FDE系列】阶段1Day 13:RAG --- 给模型配图书管理员-CSDN博客

【FDE系列】阶段1Day 14:Tool Use --- 让模型动手操作-CSDN博客

【FDE系列】阶段1Day 15:MCP --- 统一的工具接口标准 + 第三周复习-CSDN博客

FDE 基础概念

【FDE系列】阶段1Day 16:什么是 FDE --- 把 AI 变成客户结果的人-CSDN博客

【FDE系列】阶段1Day 17:FDE vs 传统实施 --- 三大本质区别-CSDN博客

【FDE系列】阶段1Day 18:FDE 三重身份 + C6 胜任力模型-CSDN博客

【FDE系列】阶段1Day 19:七阶段行动路径 + 行业经验的价值-CSDN博客

【FDE系列】阶段1Day 20:阶段总结与产出物 --- 第一阶段收官-CSDN博客


阶段二:技术地基(Python + FastAPI + SQL + Docker + API 集成)
Python基础

【FDE系列】阶段2:Day 21:Python 环境搭建 --- 写出你的第一行代码-CSDN博客

【FDE系列】阶段2:Day 22:变量、数据类型、条件判断 --- Python 的"记忆"和"判断"-CSDN博客

【FDE系列】阶段2:Day 23:循环与函数 --- 让代码跑 100 遍、把逻辑打包复用-CSDN博客

【FDE系列】阶段2:Day 24:数据结构 --- 列表、字典、集合、元组-CSDN博客

【FDE系列】阶段2:Day 25:文件读写与 JSON --- 让程序连通外部数据(第一周收官)-CSDN博客

【FDE系列】阶段2:Day 26:模块化编程 --- 把代码拆成"抽屉柜"-CSDN博客

【FDE系列】阶段2:Day 27:异常处理与日志 --- 让程序"摔不烂、查得到"-CSDN博客

FastAPI入门到进阶

【FDE系列】阶段2:Day 28:FastAPI 入门 --- 把你的函数变成 API 服务-CSDN博客

【FDE系列】阶段2:Day 29:FastAPI 进阶 --- Pydantic 模型与完整 CRUD 实战-CSDN博客

【FDE系列】阶段2:Day 30:生产代码规范 --- 测试、类型注解、配置管理(第二周收官)-CSDN博客

SQL基础

【FDE系列】阶段2:Day 31:SQL 基础 --- 增删改查一把梭-CSDN博客

【FDE系列】阶段2:Day 32:多表查询 --- JOIN 与聚合-CSDN博客

【FDE系列】阶段2:Day 33:进阶查询 --- 窗口函数与 CTE-CSDN博客

【FDE系列】阶段2:Day 34:数据清洗 --- 把脏数据捋干净-CSDN博客

【FDE系列】阶段2:Day 35:Python + SQL --- 工单接入 MySQL + 本周收官-CSDN博客

Linux基础

【FDE系列】阶段2:Day 36:Linux 入门与文件操作 --- 扔掉鼠标的第一天-CSDN博客

【FDE系列】阶段2:Day 37:权限、进程与文本三剑客-CSDN博客

【FDE系列】阶段2:Day 38:Shell 脚本 --- 把命令串起来自动跑-CSDN博客

【FDE系列】阶段2:Day 39:Linux 综合实战 --- 让服务无人值守-CSDN博客

【FDE系列】阶段2:Day 40:Shell 进阶 --- 生产级脚本与本周收官-CSDN博客

Docker

【FDE系列】阶段2:Day 41:Docker 入门 --- 把环境装进盒子-CSDN博客

【FDE系列】阶段2:Day 42:Dockerfile 实战 --- 把你的应用打包成镜像-CSDN博客

【FDE系列】阶段2:Day 43:Docker Compose --- 多容器一键编排-CSDN博客

【FDE系列】阶段2:Day 44:Nginx 反向代理 + Git 版本控制-CSDN博客

【FDE系列】阶段2:Day 45:综合实战 --- Docker + Nginx + Git 完整部署与本周收官-CSDN博客

API 集成与系统对接

【FDE系列】阶段2:Day 46:RESTful 设计与认证授权-CSDN博客

【FDE系列】阶段2:Day 47:对接企业系统 --- 飞书 / 钉钉 API-CSDN博客

【FDE系列】阶段2:Day 48:Webhook 处理与数据映射-CSDN博客

【FDE系列】阶段2:Day 49:OpenAPI 文档与接口测试-CSDN博客

【FDE系列】阶段2:Day 50:综合项目 --- 设备告警工单闭环系统 & 第二阶段收官 特殊字符-CSDN博客

阶段三:AI 应用技术(含 SDD 方法论)
AI基础:Prompt Engineering 系统训练

【FDE系列】阶段3:Day 51:从聊天窗口到代码 --- 跟 LLM 的第一次握手-CSDN博客

【FDE系列】阶段3:Day 52:Prompt 三板斧 --- 角色、示例与清晰指令-CSDN博客

【FDE系列】阶段3:Day 53:结构化输出 --- 让模型的回答能进数据库-CSDN博客

【FDE系列】阶段3:Day 54:思维链与推理任务 --- 让模型一步步想清楚-CSDN博客

【FDE系列】阶段3:Day 55:综合实战 --- 巡检报告生成器与本周收官 -CSDN博客

【FDE系列】阶段3:Day 56:评测体系入门 --- 建立你的黄金评测集-CSDN博客

【FDE系列】阶段3:Day 57:Promptfoo 实战 --- A/B 对比让数据说话-CSDN博客

【FDE系列】阶段3:Day 58:Prompt 安全 --- 注入、越狱与防护-CSDN博客

【FDE系列】阶段3:Day 59:模板化与追踪 --- Jinja2 与 Langfuse-CSDN博客

【FDE系列】阶段3:Day 60:综合实战 --- 智能工单助手 v0.1 冻结-CSDN博客

RAG 知识检索系统

【FDE系列】阶段3:Day 61:RAG 全景 --- 给模型配一间资料室-CSDN博客

【FDE系列】阶段3:Day 62:Embedding --- 文字是怎么变成向量的-CSDN博客

待完成教程:

Agent 框架与开发

Tool Calling 与 MCP

LLM 推理与部署

规范驱动开发与 Agent 工程方法论

阶段四:平台与交付(含 Agent 治理)

阶段五:行业实战与认证

相关推荐
AI人工智能+3 小时前
行驶证识别技术融合计算机视觉与自然语言处理,实现对模糊、倾斜、反光等复杂场景下行驶证的高精度结构化数据提取
人工智能·深度学习·自然语言处理·ocr·行驶证识别
weixin_750330236 小时前
AI获客工具选型:从技术架构看中小企业效率提升方案
大数据·人工智能·架构·ai获客
mpp0077 小时前
36氪《2026 中国 AI Agent 行业发展报告》:当 Agent 进入「交付」主战场
大数据·人工智能
吃饱了得干活9 小时前
Agent 的决策与规划:ReAct、Plan-and-Execute、Reflexion 与 Tree of Thoughts
人工智能·llm·agent
阿明69 小时前
小白入门机器学习基础【AI】
人工智能·机器学习
java资料站9 小时前
十一、评估测试
开发语言·人工智能·python
东方佑9 小时前
从《事件发生与智能》的框架看中国古代命理学
人工智能·深度学习·语言模型·自然语言处理·架构
天远API9 小时前
零信任架构实战:基于天远运营商三要素简版V即时版查询构建自动化司乘安全绑定网关
人工智能·安全·架构·自动化
Henry-SAP10 小时前
SAP PP核心引擎计划策略业务解析
人工智能·云原生·sap·erp