文档处理实战:PDF和Word怎么变成高质量知识库

文档处理实战:PDF和Word怎么变成高质量知识库

上篇我们用 PyMuPDF 三行代码解析了 PDF,看起来很简单对不对?

但那是"教科书级"的 PDF------纯文字、单栏、无表格。现实中你收到的 PDF 可能是这样的:扫描件歪七扭八、表格嵌套表格、双栏排版混着图片、甚至第 3 页和第 15 页方向都不一样。

文档处理是 RAG 的第一个大坑,也是最重要的坑。 这篇我把踩过的坑全倒出来。

大家好,我是黒漂技术佬。


一、先给你泼盆冷水:真实文档长什么样

企业知识库的文档来源通常就这几个:

来源 典型文件 质量
产品/技术文档 PDF(导出自 Word / Markdown) ⭐⭐⭐⭐ 较好
制度/合规文件 PDF(扫描件或打印后扫描) ⭐⭐ 差
会议纪要/方案 Word / 飞书文档 ⭐⭐⭐ 一般
培训材料 PPT 导出 PDF ⭐⭐ 差
历史归档 各种格式混杂,甚至有 .wps ⭐ 噩梦级

处理这些文档时,你会遇到:

  • 扫描件:每一页是一张图,文字是"画上去"的,需要 OCR
  • 表格:解析后变成「姓名部门职位张三技术部工程师」,糊成一坨
  • 双栏布局:解析器按行读,左边半句右边半句混着来
  • 页眉页脚/水印:每页都带"XX公司 · 内部机密 · 2024 V1.0",全被当成正文
  • 嵌入图片:架构图、流程图、截图,直接丢弃或变成乱码占位符

二、PDF 解析:三种武器,各有所长

武器 1:PyMuPDF(fitz)------ 通用首选

python 复制代码
import fitz  # PyMuPDF

doc = fitz.open("document.pdf")
for page in doc:
    text = page.get_text()          # 提取文本
    images = page.get_images()      # 提取图片引用
    tables = page.find_tables()     # 检测表格(新版支持)

优点 :速度快,对现代生成的 PDF 效果好,直接支持表格检测。

缺点:遇到扫描件就废,需要配合 OCR。

武器 2:pdfplumber ------ 表格之王

python 复制代码
import pdfplumber

with pdfplumber.open("document.pdf") as pdf:
    for page in pdf.pages:
        text = page.extract_text()          # 文本提取
        tables = page.extract_tables()      # 表格提取(比PyMuPDF更稳)

pdfplumber 的表格提取远超 PyMuPDF。它通过分析 PDF 的线条和字符位置来推断表格结构,对于有边框线和无线条的表格都能处理。

这两者不是二选一的关系,而是配合使用

python 复制代码
def extract_page(page_fitz, page_plumber):
    """融合两个解析器的结果"""
    text = page_fitz.get_text()              # 先从 PyMuPDF 拿文本
    
    tables = page_plumber.extract_tables()   # 再从 pdfplumber 拿表格
    if tables:
        for table in tables:
            # 把表格转成 Markdown 格式,保留结构
            md_table = convert_table_to_markdown(table)
            text += "\n\n" + md_table
    return text

武器 3:Tesseract OCR ------ 扫描件的救星

对于扫描件,必须上 OCR(光学字符识别,Optical Character Recognition):

python 复制代码
import fitz
from PIL import Image
import pytesseract
import io

def ocr_scanned_pdf(file_path):
    """处理扫描件 PDF,逐页 OCR"""
    doc = fitz.open(file_path)
    all_text = []
    
    for page_num, page in enumerate(doc):
        # 把 PDF 页面渲染成图片(300 DPI 是 OCR 的最佳分辨率)
        pix = page.get_pixmap(dpi=300)
        img = Image.open(io.BytesIO(pix.tobytes("png")))
        
        # OCR 识别,中英文混合
        text = pytesseract.image_to_string(img, lang='chi_sim+eng')
        all_text.append(f"--- 第 {page_num+1} 页 ---\n{text}")
    
    return "\n\n".join(all_text)

关键参数解读

  • dpi=300:渲染分辨率。低于 200 的话小字糊成一片,OCR 准确率断崖式下降
  • lang='chi_sim+eng':中英文混合识别。需要提前安装中文语言包

OCR 不是万能药

别对 OCR 抱太高期望。以下场景 OCR 基本歇菜:

  • 手写体(尤其是医生的字,懂的都懂)
  • 印章遮盖的文字
  • 严重倾斜的页面(需要先做透视校正)
  • 低分辨率的老旧档案扫描件

实战经验:扫描件先整体做预处理------灰度化、二值化、去噪------能提升 OCR 准确率 10%~20%。


三、Word 文档处理:比 PDF 简单,但也有坑

python 复制代码
from docx import Document

def parse_docx(file_path):
    doc = Document(file_path)
    content = []
    
    for para in doc.paragraphs:
        # 根据段落样式判断层级
        if para.style.name.startswith('Heading'):
            level = int(para.style.name.split()[-1])  # Heading 1 → 1
            prefix = '#' * level + ' '                # 转成 Markdown 标题
            content.append(f"{prefix}{para.text}")
        else:
            content.append(para.text)
    
    # 处理表格
    for table in doc.tables:
        md_table = docx_table_to_markdown(table)
        content.append(md_table)
    
    return "\n\n".join(content)

Word 的主要坑在嵌入对象:Visio 图、Excel 内嵌表、OLE 对象------这些 python-docx 解不出来,需要额外处理。


四、文本分块:最被低估的技术活

分块看似简单------切一刀就行。但实际上,chunk 的大小和策略直接影响检索质量。

分块的黄金法则

复制代码
chunk 太大(1000+ 字)→ 语义太杂,检索命中率低
chunk 太小(100 字以下)→ 缺少上下文,LLM 看不懂
chunk 刚好(300~500 字)→ 黄金区间

三种分块策略对比

python 复制代码
from langchain_text_splitters import (
    RecursiveCharacterTextSplitter,
    MarkdownHeaderTextSplitter,
    SemanticChunker,
)

# 策略1:递归字符分块(最常用,适合绝大多数场景)
splitter_recursive = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=100,
    separators=["\n\n", "\n", "。", ".", "!", "?", ";", ";", " ", ""]
)

# 策略2:Markdown 按标题分块(适合结构化文档)
headers_to_split_on = [
    ("#", "h1"),
    ("##", "h2"),
    ("###", "h3"),
]
splitter_md = MarkdownHeaderTextSplitter(headers_to_split_on)
# 这样每个 chunk 自动带上 "h1: 第三章", "h2: 3.1 安全要求" 等元数据

# 策略3:语义分块(需要额外的 Embedding 开销,适合高质量要求场景)
splitter_semantic = SemanticChunker(embeddings=my_embeddings)
# 它会用 Embedding 模型计算句子间的语义相似度,
# 在语义"跳变"的地方切分

企业级分块的额外要求

只切块不够,每个 chunk 必须带上元数据(Metadata)

python 复制代码
def chunk_with_metadata(document, file_name, file_path):
    chunks = splitter.split_documents([document])
    for i, chunk in enumerate(chunks):
        chunk.metadata.update({
            "source": file_name,          # 文件名:员工手册.pdf
            "file_path": file_path,       # 完整路径
            "chunk_index": i,             # 块序号
            "page_number": chunk.metadata.get("page", 1),
            "doc_type": "policy",         # 文档类型:制度/技术/产品
            "department": "HR",           # 归属部门
            "updated_at": "2024-03-15",   # 更新时间
        })
    return chunks

这些元数据在检索时可以用于过滤------比如"只看技术部的文档"、"只看 2024 年更新的内容"。没有元数据,RAG 就是个瞎子。


五、文档清洗流水线:一把梭

把我以上说的串成一个完整的流水线:

python 复制代码
class DocumentPipeline:
    """企业文档处理流水线"""
    
    def process(self, file_path: str) -> list:
        ext = file_path.suffix.lower()
        
        # Step 1: 解析
        if ext == '.pdf':
            raw_text = self._parse_pdf(file_path)
        elif ext == '.docx':
            raw_text = self._parse_docx(file_path)
        elif ext == '.md':
            raw_text = file_path.read_text(encoding='utf-8')
        else:
            raise ValueError(f"unsupported format: {ext}")
        
        # Step 2: 清洗
        clean_text = self._clean(raw_text)
        
        # Step 3: 分块
        chunks = self._split(clean_text)
        
        # Step 4: 注入元数据
        enriched = self._add_metadata(chunks, file_path)
        
        return enriched
    
    def _clean(self, text: str) -> str:
        """清洗文本噪音"""
        import re
        # 去掉多余空行(连续 3+ 个换行 → 2 个)
        text = re.sub(r'\n{3,}', '\n\n', text)
        # 去掉页眉页脚常见的页码标记
        text = re.sub(r'^\d+\s*/\s*\d+$', '', text, flags=re.MULTILINE)
        # 合并被换行打断的句子(中文段落内不应有单换行)
        # 这个需要根据实际情况调整正则
        text = re.sub(r'(?<=[\u4e00-\u9fff])\n(?=[\u4e00-\u9fff])', '', text)
        return text.strip()

那个中文换行合并的正则是关键

很多 PDF 提取出来的文本长这样:

复制代码
本系统采用微服务架构设计,
将不同业务模块拆分为独立的
服务单元,通过API网关进行
统一调度。

这在中文里是一个完整段落,但提取出来被硬换行了。正则 (?<=[\u4e00-\u9fff])\n(?=[\u4e00-\u9fff]) 的意思是:如果换行符前后都是中文字符,就把这个换行干掉,连成一句话。


六、实操建议(踩坑总结)

  1. 先做文档盘点:把你要入库的所有文档列出来,标注格式、页数、质量。扫一眼就知道坑在哪。
  2. 不要追求 100% 解析完美:表格乱一点、排版歪一点,只要 80% 的信息能提取出来,RAG 就能用。追求 100% 的 ROI 极低。
  3. 扫描件该放弃就放弃:如果一份扫描件占比不到 5%,且 OCR 质量实在太差,宁可手动录入关键内容到 Markdown,也不要在 OCR 参数上调一整天。
  4. chunk_size 不是拍脑袋的 :拿你的典型文档做实验,同一组问题,不同 chunk_size 下的检索 mAP 对比。我的数据:中文技术文档,512 字 + 64 字 overlap 综合最优。
  5. 元数据就是钱:花 10% 的额外时间给每个 chunk 打上完善的元数据,检索效果能提升 30% 以上。投入产出比极高。

💬 你的文档里有什么奇葩格式?遇到过什么解析难题?评论区发张截图(打码敏感信息),我帮你看看怎么解!