自定义文档解析器搞定那些不听话的 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依赖库.txtTextLoader内置.mdUnstructuredMarkdownLoaderunstructured.pdfPyPDFLoader / UnstructuredPDFLoaderpypdf / unstructured.docxDocx2txtLoader / UnstructuredWordLoaderdocx2txt.csvCSVLoader内置.htmlBSHTMLLoaderbeautifulsoup42.3 默认 Loader 的局限# PyPDFLoader 的问题示例fromlangchain.document_loadersimportPyPDFLoader loaderPyPDFLoader(scan_document.pdf)# 扫描件docsloader.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,languagechi_simeng):self.file_pathfile_path self.languagelanguage# 中文英文defload(self):# 1. PDF 转图片imagesconvert_from_path(self.file_path,dpi300)documents[]fori,imageinenumerate(images):# 2. OCR 识别文字textpytesseract.image_to_string(image,langself.language)# 3. 创建 DocumentdocDocument(page_contenttext,metadata{source:self.file_path,page:i1})documents.append(doc)returndocuments# 使用loaderOCRPDFLoader(scan_document.pdf)docsloader.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_pathfile_pathdefload(self):# 调用 Marker 转换output_dir/tmp/marker_outputsubprocess.run([marker_single,self.file_path,output_dir,--langs,Chinese],checkTrue)# 读取转换后的 Markdownmd_filef{output_dir}/input.mdwithopen(md_file,r,encodingutf-8)asf:textf.read()return[Document(page_contenttext,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_pathfile_pathdefload(self):# 读取 PDFwithopen(self.file_path,rb)asf:pdf_bytesf.read()# 解析datasetPymuDocDataset(pdf_bytes)# 判断是文本型还是扫描型ifdataset.classify()SupportedPdfParseMethod.OCR:resultdataset.apply(doc_analyze,ocrTrue)else:resultdataset.apply(doc_analyze,ocrFalse)# 提取 Markdownmd_textresult.get_markdown()return[Document(page_contentmd_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 中的所有表格tablescamelot.read_pdf(document.pdf,pagesall)fori,tableinenumerate(tables):# 转成 DataFramedftable.df# 转成 Markdown 表格格式保留结构md_tabledf.to_markdown(indexFalse)print(f表格{i1}:)print(md_table)输出| 产品名称 | 价格 | 库存 | |---------|------|------| | iPhone 15 | 5999 | 100 | | iPad Pro | 8999 | 50 | | MacBook | 12999 | 30 |Markdown 表格格式的好处保留了行列结构TextSplitter 按行分割时每行包含完整的表头信息LLM 理解 Markdown 表格很容易4.3 集成到自定义 LoaderclassTableAwarePDFLoader:能识别并保留表格结构的 PDF 加载器def__init__(self,file_path):self.file_pathfile_pathdefload(self):# 1. 先用 Marker / MinerU 提取文本和表格# 2. 表格部分保留 Markdown 格式# 3. 普通文本正常处理# 4. 合并输出full_textself._extract_with_tables()return[Document(page_contentfull_text,metadata{source:self.file_path})]def_extract_with_tables(self):# 具体实现...pass五、自定义 Loader 接入 Chatchat5.1 找到扩展点Chatchat 的文档加载在server/knowledge_base/utils.py中# 原始逻辑简化defget_loader(file_path):extos.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_typedefault):extos.path.splitext(file_path)[1].lower()# 根据配置或文件特征选择 loaderifext.pdf:ifloader_typemineru:returnMinerUPDFLoader(file_path)elifloader_typetable:returnTableAwarePDFLoader(file_path)elifloader_typeocr: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_typest.selectbox(文档解析方式,[默认,扫描件 OCR,复杂排版,表格优化])# 传给后端upload_file(file,parser_typeparser_type)六、性能优化大文档处理6.1 问题大 PDF 内存溢出几百页的 PDF一次性加载到内存很容易 OOM。6.2 方案分页流式处理classStreamingPDFLoader:流式处理大 PDF避免内存溢出def__init__(self,file_path,batch_size10):self.file_pathfile_path self.batch_sizebatch_size# 每批处理 10 页defload(self):# 获取总页数total_pagesself._get_total_pages()forstartinrange(0,total_pages,self.batch_size):endmin(startself.batch_size,total_pages)# 分批处理batch_docsself._process_pages(start,end)# 立即分块入库不保留在内存fordocinbatch_docs:yielddocdef_process_pages(self,start,end):# 只加载 start 到 end 页# 处理并返回 Document 列表pass6.3 异步处理 进度反馈# 上传大文档时异步处理并返回进度asyncdefprocess_large_document(file_path):totalget_total_pages(file_path)processed0fordocinStreamingPDFLoader(file_path).load():# 分块、向量化、入库awaitprocess_and_index(doc)processed1yield{progress:processed/total,status:f已处理{processed}/{total}页}七、小结这篇咱们深入文档解析解决了几个核心问题✅ 扫描件 PDFOCR 方案对比pytesseract / Marker / MinerU✅ 复杂排版MinerU 和 Marker 的排版还原能力✅ 表格数据保留 Markdown 表格结构的方法✅ 自定义 Loader从开发到接入 Chatchat 的完整流程✅ 大文档处理流式处理避免内存溢出文档解析是 RAG 的地基地基不牢上面再花哨也白搭。建议根据你的文档类型选择合适的方案文档类型推荐方案普通文本 PDFPyPDFLoader默认扫描件MinerU 或 Marker图文混排MinerU表格密集TableAwarePDFLoader超大文档StreamingPDFLoader你在处理文档时遇到过什么奇葩 PDF扫描件、表格错乱、还是超大文件用了什么方案解决的欢迎分享