LangChain-Chatchat 开发与应用(七) 自定义文档解析器-搞定那些不听话的PDF
自定义文档解析器:搞定那些"不听话"的 PDF
标签:文档解析 | PDF | OCR | 自定义 Loader | 二次开发
一、文档解析的"噩梦"
做知识库的同学,最头疼的往往不是系统配置,而是文档本身。
你遇到过这些情况吗?
- 上传了一个 PDF,解析出来是空的——因为它其实是扫描件(图片)
- 一个表格 PDF,解析后数据全乱了,列对不上行
- 图文混排的文档,文字顺序是乱的,读起来前言不搭后语
- 几百页的 PDF,解析到一半内存溢出,进程挂了
Chatchat 默认的文档解析能处理大部分情况,但遇到"硬骨头"就不够用了。
今天这篇,咱们就深入文档解析,看看怎么搞定这些"不听话"的文档。
二、Chatchat 的文档加载体系
2.1 默认的加载流程
用户上传文件 ↓ 根据文件后缀选择 Loader ↓ Loader 解析文件 → Document 对象列表 ↓ 每个 Document 包含:page_content(文本)+ metadata(元数据) ↓ 交给 TextSplitter 分块 ↓ 向量化入库2.2 默认 Loader 映射
Chatchat 内置的 Loader 选择逻辑:
| 文件后缀 | 使用的 Loader | 依赖库 |
|---|---|---|
.txt | TextLoader | 内置 |
.md | UnstructuredMarkdownLoader | unstructured |
.pdf | PyPDFLoader / UnstructuredPDFLoader | pypdf / unstructured |
.docx | Docx2txtLoader / UnstructuredWordLoader | docx2txt |
.csv | CSVLoader | 内置 |
.html | BSHTMLLoader | beautifulsoup4 |
2.3 默认 Loader 的局限
# PyPDFLoader 的问题示例fromlangchain.document_loadersimportPyPDFLoader loader=PyPDFLoader("scan_document.pdf")# 扫描件docs=loader.load()print(docs[0].page_content)# 输出:"" ← 空的!因为 PyPDF 只能提取文本层,不能 OCR三、扫描件 PDF 的 OCR 处理
3.1 问题本质
扫描件 PDF 的每一页都是图片,没有文本层。需要 OCR(光学字符识别)把图片里的文字提取出来。
3.2 方案一:基于 pytesseract(开源免费)
# 自定义 OCR PDF Loaderimportpytesseractfrompdf2imageimportconvert_from_pathfromlangchain.schemaimportDocumentclassOCRPDFLoader:"""支持 OCR 的 PDF 加载器"""def__init__(self,file_path,language="chi_sim+eng"):self.file_path=file_path self.language=language# 中文+英文defload(self):# 1. PDF 转图片images=convert_from_path(self.file_path,dpi=300)documents=[]fori,imageinenumerate(images):# 2. OCR 识别文字text=pytesseract.image_to_string(image,lang=self.language)# 3. 创建 Documentdoc=Document(page_content=text,metadata={"source":self.file_path,"page":i+1})documents.append(doc)returndocuments# 使用loader=OCRPDFLoader("scan_document.pdf")docs=loader.load()print(docs[0].page_content)# 现在有内容了!优点:完全免费,本地运行
缺点:速度慢(逐页 OCR),中文识别准确率一般
3.3 方案二:基于 Marker(推荐)
Marker 是一个专门做 PDF 解析的开源工具,对扫描件和复杂排版支持很好。
# 安装pipinstallmarker-pdf# 使用marker_single input.pdf output.md--langsChinese# 集成到 Chatchatimportsubprocessfromlangchain.schemaimportDocumentclassMarkerPDFLoader:"""基于 Marker 的 PDF 加载器"""def__init__(self,file_path):self.file_path=file_pathdefload(self):# 调用 Marker 转换output_dir="/tmp/marker_output"subprocess.run(["marker_single",self.file_path,output_dir,"--langs","Chinese"],check=True)# 读取转换后的 Markdownmd_file=f"{output_dir}/input.md"withopen(md_file,"r",encoding="utf-8")asf:text=f.read()return[Document(page_content=text,metadata={"source":self.file_path})]优点:排版还原好,支持表格、公式
缺点:需要额外安装,首次运行会下载模型
3.4 方案三:基于 MinerU(阿里出品,效果最佳)
MinerU 是阿里开源的 PDF 解析工具,对中文文档支持非常好。
# 安装pipinstallmagic-pdf# 下载模型(首次)wgethttps://github.com/opendatalab/MinerU/raw/master/scripts/download_models.py python download_models.pyfrommagic_pdf.data.data_reader_writerimportFileBasedDataWriterfrommagic_pdf.data.datasetimportPymuDocDatasetfrommagic_pdf.model.doc_analyze_by_custom_modelimportdoc_analyzefrommagic_pdf.config.enumsimportSupportedPdfParseMethodclassMinerUPDFLoader:"""基于 MinerU 的 PDF 加载器"""def__init__(self,file_path):self.file_path=file_pathdefload(self):# 读取 PDFwithopen(self.file_path,"rb")asf:pdf_bytes=f.read()# 解析dataset=PymuDocDataset(pdf_bytes)# 判断是文本型还是扫描型ifdataset.classify()==SupportedPdfParseMethod.OCR:result=dataset.apply(doc_analyze,ocr=True)else:result=dataset.apply(doc_analyze,ocr=False)# 提取 Markdownmd_text=result.get_markdown()return[Document(page_content=md_text,metadata={"source":self.file_path})]优点:中文效果最好,支持 OCR,排版还原优秀
缺点:模型较大,首次加载慢
四、表格数据的特殊处理
4.1 表格解析的难点
PDF 里的表格解析出来往往是这样的:
产品名称 价格 库存 iPhone 15 5999 100 iPad Pro 8999 50 MacBook 12999 30看起来是表格,但其实是纯文本,丢失了表格结构。后续分块和检索时,行与行之间的关系就断了。
4.2 方案:保留表格结构
# 使用 tabula-py 或 camelot 提取表格importcamelot# 提取 PDF 中的所有表格tables=camelot.read_pdf("document.pdf",pages="all")fori,tableinenumerate(tables):# 转成 DataFramedf=table.df# 转成 Markdown 表格格式(保留结构)md_table=df.to_markdown(index=False)print(f"表格{i+1}:")print(md_table)输出:
| 产品名称 | 价格 | 库存 | |---------|------|------| | iPhone 15 | 5999 | 100 | | iPad Pro | 8999 | 50 | | MacBook | 12999 | 30 |Markdown 表格格式的好处:
- 保留了行列结构
- TextSplitter 按行分割时,每行包含完整的表头信息
- LLM 理解 Markdown 表格很容易
4.3 集成到自定义 Loader
classTableAwarePDFLoader:"""能识别并保留表格结构的 PDF 加载器"""def__init__(self,file_path):self.file_path=file_pathdefload(self):# 1. 先用 Marker / MinerU 提取文本和表格# 2. 表格部分保留 Markdown 格式# 3. 普通文本正常处理# 4. 合并输出full_text=self._extract_with_tables()return[Document(page_content=full_text,metadata={"source":self.file_path})]def_extract_with_tables(self):# 具体实现...pass五、自定义 Loader 接入 Chatchat
5.1 找到扩展点
Chatchat 的文档加载在server/knowledge_base/utils.py中:
# 原始逻辑(简化)defget_loader(file_path):ext=os.path.splitext(file_path)[1].lower()loaders={".txt":TextLoader,".pdf":PyPDFLoader,# ← 这里可以替换".docx":Docx2txtLoader,# ...}returnloaders.get(ext,TextLoader)(file_path)5.2 修改 Loader 映射
# 在 server/knowledge_base/utils.py 中修改fromchatchat.document_loadersimport(# 自定义的 loaderMinerUPDFLoader,TableAwarePDFLoader,OCRPDFLoader)defget_loader(file_path,loader_type="default"):ext=os.path.splitext(file_path)[1].lower()# 根据配置或文件特征选择 loaderifext==".pdf":ifloader_type=="mineru":returnMinerUPDFLoader(file_path)elifloader_type=="table":returnTableAwarePDFLoader(file_path)elifloader_type=="ocr":returnOCRPDFLoader(file_path)else:returnPyPDFLoader(file_path)# 其他文件类型...5.3 在配置中指定 Loader
# kb_settings.yamlDOCUMENT_LOADERS:pdf:default:"PyPDFLoader"# 默认scan:"OCRPDFLoader"# 扫描件complex:"MinerUPDFLoader"# 复杂排版table:"TableAwarePDFLoader"# 表格密集5.4 WebUI 中选择解析方式
可以在上传文档时,让用户选择解析方式:
# 前端增加选择框parser_type=st.selectbox("文档解析方式",["默认","扫描件 OCR","复杂排版","表格优化"])# 传给后端upload_file(file,parser_type=parser_type)六、性能优化:大文档处理
6.1 问题:大 PDF 内存溢出
几百页的 PDF,一次性加载到内存,很容易 OOM。
6.2 方案:分页流式处理
classStreamingPDFLoader:"""流式处理大 PDF,避免内存溢出"""def__init__(self,file_path,batch_size=10):self.file_path=file_path self.batch_size=batch_size# 每批处理 10 页defload(self):# 获取总页数total_pages=self._get_total_pages()forstartinrange(0,total_pages,self.batch_size):end=min(start+self.batch_size,total_pages)# 分批处理batch_docs=self._process_pages(start,end)# 立即分块入库,不保留在内存fordocinbatch_docs:yielddocdef_process_pages(self,start,end):# 只加载 start 到 end 页# 处理并返回 Document 列表pass6.3 异步处理 + 进度反馈
# 上传大文档时,异步处理并返回进度asyncdefprocess_large_document(file_path):total=get_total_pages(file_path)processed=0fordocinStreamingPDFLoader(file_path).load():# 分块、向量化、入库awaitprocess_and_index(doc)processed+=1yield{"progress":processed/total,"status":f"已处理{processed}/{total}页"}七、小结
这篇咱们深入文档解析,解决了几个核心问题:
✅ 扫描件 PDF:OCR 方案对比(pytesseract / Marker / MinerU)
✅ 复杂排版:MinerU 和 Marker 的排版还原能力
✅ 表格数据:保留 Markdown 表格结构的方法
✅ 自定义 Loader:从开发到接入 Chatchat 的完整流程
✅ 大文档处理:流式处理避免内存溢出
文档解析是 RAG 的"地基",地基不牢,上面再花哨也白搭。
建议根据你的文档类型选择合适的方案:
| 文档类型 | 推荐方案 |
|---|---|
| 普通文本 PDF | PyPDFLoader(默认) |
| 扫描件 | MinerU 或 Marker |
| 图文混排 | MinerU |
| 表格密集 | TableAwarePDFLoader |
| 超大文档 | StreamingPDFLoader |
你在处理文档时遇到过什么"奇葩" PDF?扫描件、表格错乱、还是超大文件?用了什么方案解决的?欢迎分享!
