文档解析大PDF乱码、表格丢失,LangChain文档加载器
用 LangChain 做文档问答或 RAG 知识库时,一旦传入几百页、几十 MB 的大 PDF,最容易遇到两类问题:文字变成乱码,表格里的数据被拆得七零八落。
这通常不是模型的问题,而是文档加载器没选对或参数没调好。
本文按零基础可操作的方式,讲清楚如何通过替换加载器、指定语言、调整解析边界来改善 PDF 提取效果,最终得到更干净的文本切片。
大 PDF 乱码和表格丢失先从加载器找原因
LangChain 默认的 PyPDFLoader 依赖 pypdf 提取文本。
这种方案速度快,但对扫描版 PDF、带嵌入字体的 PDF 支持很差,容易出现中文变成方框、数字错位。
表格在 pypdf 眼里只是一堆坐标和字符流,行列边界经常丢失,所以表格数据混成一大段是常事。
如果你只是做简单文本检索,PyPDFLoader 够用;
一旦需要保留表格结构,或 PDF 带复杂排版,就要换成基于 pdfplumber 的加载器。
原因很简单:pdfplumber 能识别字符坐标、表格线和区域,可以按表格结构输出内容。
第一阶段:替换加载器并打开语言修正
安装依赖:
pip install langchain-community pdfplumber
LangChain 社区包已经提供了 PDFPlumberLoader,直接使用:
from langchain_community.document_loaders import PDFPlumberLoader
loader = PDFPlumberLoader(
"large_report.pdf",
extract_tables=True, # 提取表格结构
keep_blank_chars=False, # 不保留多余空格
use_text_flow=False # 让表格按行列输出
)
docs = loader.load()
for doc in docs:
print(doc.page_content[:500])
这里 extract_tables=True 会强制启用表格识别。
如果表格仍然丢失,可以改用 pdfplumber 原生 API 手动提取表格,再与文本拼接。
下面是一个组合示例:
import pdfplumber
with pdfplumber.open("large_report.pdf") as pdf:
text_parts = []
for page in pdf.pages:
text = page.extract_text()
tables = page.extract_tables()
if text:
text_parts.append(text)
for table in tables:
for row in table:
text_parts.append(" | ".join(cell.replace("\n", " ") if cell else "" for cell in row))
full_text = "\n".join(text_parts)
这种方式不会漏掉表格,代价是解析速度会慢一些,适合离线预处理。
第二阶段:处理乱码必须解决字体和编码
加载器选对了还是乱码,问题经常出在 PDF 本身的字体编码。
中文 PDF 常见两种乱码:一是文字显示正常但提取出来是 Unicode 乱码;
二是提取出来是符号或空白。
先检查环境能否正常显示中文:
python -c "print('测试中文')"
如果终端正常,而 PDF 内容乱码,则尝试指定 pdfplumber 的 charset 猜测参数:
import pdfplumber
with pdfplumber.open("large_report.pdf") as pdf:
page = pdf.pages[0]
text = page.extract_text()
if text and "�" in text:
# 回退到 OCR,见下一步
pass
对于扫描版 PDF,不管哪个加载器都没法直接提取文字,必须走 OCR。
先安装 OCR 工具:
pip install rapidocr-onnxruntime langchain-community
然后把每一页转成图片再做识别,这一步可以在 LangChain 外完成,把识别后的文本作为 Markdown 文件再交给 LangChain 加载。
第三阶段:切分方式也会影响表格保留效果
文本提取干净了,LangChain 默认的 RecursiveCharacterTextSplitter 仍然可能把表格行切碎。
原因是它按换行符和空格切分,遇到表格中的短行会频繁断句。
更好的做法是先把表格区域用特殊符号隔离,再切分。
例如在拼接表格时,每行前加 TABLE_ROW: 标记:
row_text = "TABLE_ROW: " + " | ".join(cells)
然后使用 MarkdownHeaderTextSplitter 或带有分隔符列表的切分器:
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
separators=["\n\n", "TABLE_ROW: ", "\n", "。", " "],
chunk_size=800,
chunk_overlap=150
)
chunks = splitter.split_text(full_text)
这样表格行会被保留在同一片段里,不会因为换行而被拆开。
避坑:这些情况别硬调加载器
以下几种 PDF 格式问题不是换加载器能解决的,提前判断能省很多时间:
- 扫描版 PDF:没有文字层,任何文本加载器都无效,必须 OCR。
- 加密 PDF:加载时会提示密码错误,需要先用工具解密,例如用
pikepdf去除限制。 - 超大 PDF:一次性读入全部页面容易内存溢出,建议按页流式处理,每 20 页保存一次结果。
- 表格带合并单元格:pdfplumber 会输出空串或 None,需要自己补齐父单元格值。
如果在云服务器上跑解析任务,建议预留至少 2GB 空闲内存,并设置文件上传大小限制。
解析过程对 CPU 和内存的占用都比较高,使用正规云厂商的机器即可,不必追逐高配置。
验证解析效果的方法
不要只看 print 前 500 字就认为成功。
最好做两个检查:
- 检查乱码字符比例。统计提取文本中
�或□等替换字符数量,超过 1% 就要考虑 OCR。
bad_chars = sum(1 for c in full_text if c in "�□")
print(f"异常字符数:{bad_chars}")
- 检查表格行数。对照原始 PDF 表格,看提取出的
TABLE_ROW数量是否接近一致。
python -c "import re; text=open('extracted.txt',encoding='utf-8').read(); print(len(re.findall('TABLE_ROW', text)))"
如果两个检查都通过,再把切分后的片段带入 LangChain 的向量库测试问答,确认答案能定位到正确段落。
常见疑问快答
为什么换用 PDFPlumber 后速度变慢?
pdfplumber 要计算每个字符和线的坐标,处理大 PDF 比 pypdf 慢 5-10 倍。建议只对表格密集的页面启用表格提取,普通文本页继续用 PyPDFLoader,分层处理效率更高。
所有 PDF 都能调好吗?
不能。图片型 PDF、手写扫描件、复杂图文混排的 PDF,调加载器只能部分改善,最终效果取决于 PDF 导出时的文字层是否完整。制作 PDF 时尽量用 Word 或 LaTeX 导出,避免用图片拼接。
如果你正在处理 LangChain 解析大 PDF 乱码、表格丢失的问题,建议先按本文步骤换掉默认加载器,再处理字体和切分。
遇到扫描版 PDF 就直接转 OCR,不要浪费时间反复调参数。
解析完成后,务必用异常字符统计和表格行数两个方法验证结果,确认无误后再进入 RAG 流程。