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

发布时间:2026/7/4 2:55:42

文档处理实战: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 解析三种武器各有所长武器 1PyMuPDFfitz—— 通用首选importfitz# PyMuPDFdocfitz.open(document.pdf)forpageindoc:textpage.get_text()# 提取文本imagespage.get_images()# 提取图片引用tablespage.find_tables()# 检测表格新版支持优点速度快对现代生成的 PDF 效果好直接支持表格检测。缺点遇到扫描件就废需要配合 OCR。武器 2pdfplumber —— 表格之王importpdfplumberwithpdfplumber.open(document.pdf)aspdf:forpageinpdf.pages:textpage.extract_text()# 文本提取tablespage.extract_tables()# 表格提取比PyMuPDF更稳pdfplumber 的表格提取远超 PyMuPDF。它通过分析 PDF 的线条和字符位置来推断表格结构对于有边框线和无线条的表格都能处理。这两者不是二选一的关系而是配合使用defextract_page(page_fitz,page_plumber):融合两个解析器的结果textpage_fitz.get_text()# 先从 PyMuPDF 拿文本tablespage_plumber.extract_tables()# 再从 pdfplumber 拿表格iftables:fortableintables:# 把表格转成 Markdown 格式保留结构md_tableconvert_table_to_markdown(table)text\n\nmd_tablereturntext武器 3Tesseract OCR —— 扫描件的救星对于扫描件必须上 OCR光学字符识别Optical Character RecognitionimportfitzfromPILimportImageimportpytesseractimportiodefocr_scanned_pdf(file_path):处理扫描件 PDF逐页 OCRdocfitz.open(file_path)all_text[]forpage_num,pageinenumerate(doc):# 把 PDF 页面渲染成图片300 DPI 是 OCR 的最佳分辨率pixpage.get_pixmap(dpi300)imgImage.open(io.BytesIO(pix.tobytes(png)))# OCR 识别中英文混合textpytesseract.image_to_string(img,langchi_simeng)all_text.append(f--- 第{page_num1}页 ---\n{text})return\n\n.join(all_text)关键参数解读dpi300渲染分辨率。低于 200 的话小字糊成一片OCR 准确率断崖式下降langchi_simeng中英文混合识别。需要提前安装中文语言包OCR 不是万能药别对 OCR 抱太高期望。以下场景 OCR 基本歇菜手写体尤其是医生的字懂的都懂印章遮盖的文字严重倾斜的页面需要先做透视校正低分辨率的老旧档案扫描件实战经验扫描件先整体做预处理——灰度化、二值化、去噪——能提升 OCR 准确率 10%~20%。三、Word 文档处理比 PDF 简单但也有坑fromdocximportDocumentdefparse_docx(file_path):docDocument(file_path)content[]forparaindoc.paragraphs:# 根据段落样式判断层级ifpara.style.name.startswith(Heading):levelint(para.style.name.split()[-1])# Heading 1 → 1prefix#*level # 转成 Markdown 标题content.append(f{prefix}{para.text})else:content.append(para.text)# 处理表格fortableindoc.tables:md_tabledocx_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 字→ 黄金区间三种分块策略对比fromlangchain_text_splittersimport(RecursiveCharacterTextSplitter,MarkdownHeaderTextSplitter,SemanticChunker,)# 策略1递归字符分块最常用适合绝大多数场景splitter_recursiveRecursiveCharacterTextSplitter(chunk_size500,chunk_overlap100,separators[\n\n,\n,。,.,,,,, ,])# 策略2Markdown 按标题分块适合结构化文档headers_to_split_on[(#,h1),(##,h2),(###,h3),]splitter_mdMarkdownHeaderTextSplitter(headers_to_split_on)# 这样每个 chunk 自动带上 h1: 第三章, h2: 3.1 安全要求 等元数据# 策略3语义分块需要额外的 Embedding 开销适合高质量要求场景splitter_semanticSemanticChunker(embeddingsmy_embeddings)# 它会用 Embedding 模型计算句子间的语义相似度# 在语义跳变的地方切分企业级分块的额外要求只切块不够每个 chunk 必须带上元数据Metadatadefchunk_with_metadata(document,file_name,file_path):chunkssplitter.split_documents([document])fori,chunkinenumerate(chunks):chunk.metadata.update({source:file_name,# 文件名员工手册.pdffile_path:file_path,# 完整路径chunk_index:i,# 块序号page_number:chunk.metadata.get(page,1),doc_type:policy,# 文档类型制度/技术/产品department:HR,# 归属部门updated_at:2024-03-15,# 更新时间})returnchunks这些元数据在检索时可以用于过滤——比如只看技术部的文档、“只看 2024 年更新的内容”。没有元数据RAG 就是个瞎子。五、文档清洗流水线一把梭把我以上说的串成一个完整的流水线classDocumentPipeline:企业文档处理流水线defprocess(self,file_path:str)-list:extfile_path.suffix.lower()# Step 1: 解析ifext.pdf:raw_textself._parse_pdf(file_path)elifext.docx:raw_textself._parse_docx(file_path)elifext.md:raw_textfile_path.read_text(encodingutf-8)else:raiseValueError(funsupported format:{ext})# Step 2: 清洗clean_textself._clean(raw_text)# Step 3: 分块chunksself._split(clean_text)# Step 4: 注入元数据enrichedself._add_metadata(chunks,file_path)returnenricheddef_clean(self,text:str)-str:清洗文本噪音importre# 去掉多余空行连续 3 个换行 → 2 个textre.sub(r\n{3,},\n\n,text)# 去掉页眉页脚常见的页码标记textre.sub(r^\d\s*/\s*\d$,,text,flagsre.MULTILINE)# 合并被换行打断的句子中文段落内不应有单换行# 这个需要根据实际情况调整正则textre.sub(r(?[\u4e00-\u9fff])\n(?[\u4e00-\u9fff]),,text)returntext.strip()那个中文换行合并的正则是关键很多 PDF 提取出来的文本长这样本系统采用微服务架构设计 将不同业务模块拆分为独立的 服务单元通过API网关进行 统一调度。这在中文里是一个完整段落但提取出来被硬换行了。正则(?[\u4e00-\u9fff])\n(?[\u4e00-\u9fff])的意思是如果换行符前后都是中文字符就把这个换行干掉连成一句话。六、实操建议踩坑总结先做文档盘点把你要入库的所有文档列出来标注格式、页数、质量。扫一眼就知道坑在哪。不要追求 100% 解析完美表格乱一点、排版歪一点只要 80% 的信息能提取出来RAG 就能用。追求 100% 的 ROI 极低。扫描件该放弃就放弃如果一份扫描件占比不到 5%且 OCR 质量实在太差宁可手动录入关键内容到 Markdown也不要在 OCR 参数上调一整天。chunk_size 不是拍脑袋的拿你的典型文档做实验同一组问题不同 chunk_size 下的检索 mAP 对比。我的数据中文技术文档512 字 64 字 overlap综合最优。元数据就是钱花 10% 的额外时间给每个 chunk 打上完善的元数据检索效果能提升 30% 以上。投入产出比极高。 你的文档里有什么奇葩格式遇到过什么解析难题评论区发张截图打码敏感信息我帮你看看怎么解

相关新闻