在多模态RAG(Retrieval-Augmented Generation)系统中,PDF文件的高效、安全解析与处理是实现高质量知识检索和生成的关键环节。PDF文件通常包含丰富的文本、图像和表格信息,这些多模态数据的有效提取和整合对于提升RAG系统的性能至关重要。然而,传统的PDF解析工具往往存在解析精度不足、无法处理复杂格式(如图像和表格)等问题,尤其是在涉及私密文档时,数据安全和隐私保护也是一大挑战。
今天,我将详细介绍MinerU 的私有化部署流程、PDF 解析服务开发,以及如何通过 API 封装实现便捷的文档处理功能。
1、简介
MinerU是一款将PDF转化为机器可读格式的工具(如markdown、json),可以很方便地抽取为任意格式。 主要具有以下功能:
- 删除页眉、页脚、脚注、页码等元素,确保语义连贯
- 输出符合人类阅读顺序的文本,适用于单栏、多栏及复杂排版
- 保留原文档的结构,包括标题、段落、列表等
- 提取图像、图片描述、表格、表格标题及脚注
- 自动识别并转换文档中的公式为LaTeX格式
- 自动识别并转换文档中的表格为HTML格式
- 自动检测扫描版PDF和乱码PDF,并启用OCR功能
- OCR支持84种语言的检测与识别
- 支持多种输出格式,如多模态与NLP的Markdown、按阅读顺序排序的JSON、含有丰富信息的中间格式等
- 支持多种可视化结果,包括layout可视化、span可视化等,便于高效确认输出效果与质检
- 支持纯CPU环境运行,并支持 GPU(CUDA)/NPU(CANN)/MPS 加速
- 兼容Windows、Linux和Mac平台
项目地址:https://github.com/opendatalab/MinerU
说明文档:https://mineru.readthedocs.io/en/latest/index.html
2、私有化部署
MinerU官方提供的API,但是其API KEY需要14天要更换一次,并且在数据安全和隐私保护方面也很难控制。下面是对MinerU的私有化部署介绍:
安装magic-pdf
复制conda create -n mineru pythnotallow=3.10 conda activate mineru pip install -U"magic-pdf[full]"-i https://mirrors.aliyun.com/pypi/simple
模型权重下载
方法一:从 Hugging Face 下载模型
使用python脚本 从Hugging Face下载模型文件
复制pip install huggingface_hub wget https://gcore.jsdelivr.net/gh/opendatalab/MinerU@master/scripts/download_models_hf.py -O download_models_hf.py python download_models_hf.py
python脚本会自动下载模型文件并配置好配置文件中的模型目录。也可以将MinerU代码clone到本地,运行download_models_hf代码
方法二:从 ModelScope 下载模型
复制pip install modelscope wget https://gcore.jsdelivr.net/gh/opendatalab/MinerU@master/scripts/download_models.py -O download_models.py python download_models.py
也可以将MinerU代码clone到本地,运行download_models代码,可以通过配置一些参数,将模型下载到制定文件夹。
详细参考如何下载模型文件。
修改配置文件以进行额外配置
完成下载模型权重文件步骤后,脚本会自动生成用户目录下的magic-pdf.json文件,并自动配置默认模型路径。 可以在【用户目录】下找到magic-pdf.json文件。
windows的用户目录为 "C:\Users\用户名", linux用户目录为 "/home/用户名", macOS用户目录为 "/Users/用户名"
可以修改该文件中的部分配置实现功能的开关,如表格识别功能:
如json内没有如下项目,请手动添加需要的项目,并删除注释内容(标准json不支持注释)
复制{ // other config "layout-config": { "model": "doclayout_yolo" }, "formula-config": { "mfd_model": "yolo_v8_mfd", "mfr_model": "unimernet_small", "enable": true // 公式识别功能默认是开启的,如果需要关闭请修改此处的值为"false" }, "table-config": { "model": "rapid_table", "sub_model": "slanet_plus", "enable": true, // 表格识别功能默认是开启的,如果需要关闭请修改此处的值为"false" "max_time": 400 } }
3、解析代码
process_pdf是核心解析函数,主要功能包括:
- 自动识别PDF类型(普通文本PDF或扫描版PDF)
- 提取文本内容和图片资源
- 生成Markdown格式的输出
- 可选生成可视化分析结果
参数
参数 | 类型 | 默认值 | 描述 |
pdf_file_name | str | 无 | 要解析的PDF文件路径 |
output_dir | str | "output" | 输出文件的主目录 |
image_subdir | str | "images" | 存放图片的子目录名称 |
simple_output | bool | True | 是否使用简单输出模式(True时只输出Markdown和内容列表) |
代码
复制import os from magic_pdf.data.data_reader_writer import FileBasedDataWriter, FileBasedDataReader from magic_pdf.data.dataset import PymuDocDataset from magic_pdf.model.doc_analyze_by_custom_model import doc_analyze from magic_pdf.config.enums import SupportedPdfParseMethod def process_pdf(pdf_file_name, output_dir="output", image_subdir="images", simple_output=True): """ 处理PDF文件,将其转换为Markdown格式并保存相关资源 :param pdf_file_name: PDF文件名 :param output_dir: 输出目录,默认为'output' :param image_subdir: 图片子目录名,默认为'images' :param simple_output: 是否使用简单输出模式,默认为False """ # 获取不带后缀的文件名 name_without_suff = os.path.splitext(os.path.basename(pdf_file_name))[0] # 创建输出子目录名 output_subdir = name_without_suff # 构建图片目录和markdown目录的路径 local_image_dir = os.path.join(output_dir, output_subdir, image_subdir) local_md_dir = os.path.join(output_dir, output_subdir) # 创建必要的目录 os.makedirs(local_image_dir, exist_ok=True) os.makedirs(local_md_dir, exist_ok=True) # 创建文件写入器 image_writer, md_writer = FileBasedDataWriter(local_image_dir), FileBasedDataWriter(local_md_dir) # 创建文件读取器并读取PDF文件 reader1 = FileBasedDataReader("") pdf_bytes = reader1.read(pdf_file_name) # 创建数据集对象 ds = PymuDocDataset(pdf_bytes) # 根据PDF类型选择处理方式 if ds.classify() == SupportedPdfParseMethod.OCR: # 使用OCR模式处理 infer_result = ds.apply(doc_analyze, ocr=True) pipe_result = infer_result.pipe_ocr_mode(image_writer) else: # 使用文本模式处理 infer_result = ds.apply(doc_analyze, ocr=False) pipe_result = infer_result.pipe_txt_mode(image_writer) # 构建markdown文件的完整路径 md_file_path = os.path.join(os.getcwd(), local_md_dir, f"{name_without_suff}.md") abs_md_file_path = os.path.abspath(md_file_path) if simple_output: # 简单输出模式:只输出markdown和内容列表 pipe_result.dump_md(md_writer, f"{name_without_suff}.md", os.path.basename(local_image_dir)) pipe_result.dump_content_list(md_writer, f"{name_without_suff}_content_list.json", os.path.basename(local_image_dir)) return abs_md_file_path else: # 完整输出模式:输出所有内容 pipe_result.dump_md(md_writer, f"{name_without_suff}.md", os.path.basename(local_image_dir)) pipe_result.dump_content_list(md_writer, f"{name_without_suff}_content_list.json", os.path.basename(local_image_dir)) # 生成可视化文件 infer_result.draw_model(os.path.join(local_md_dir, f"{name_without_suff}_model.pdf")) pipe_result.draw_layout(os.path.join(local_md_dir, f"{name_without_suff}_layout.pdf")) pipe_result.draw_span(os.path.join(local_md_dir, f"{name_without_suff}_spans.pdf")) return abs_md_file_path if __name__ == "__main__": # 指定要处理的PDF文件名 pdf_file_name = "/path/to/demo1.pdf" # 处理PDF文件并获取生成的markdown文件路径 md_file_path = process_pdf(pdf_file_name, output_dir="/path/to/output", simple_output=False) # 打印生成的markdown文件路径 print(md_file_path)
输出文件结构
复制output/ ├── [PDF文件名]/ │ ├── images/ # 存放提取的图片 │ ├── [PDF文件名].md # 生成的Markdown文件 │ ├── [PDF文件名]_content_list.json # 内容列表JSON文件 │ ├── [PDF文件名]_model.pdf # 模型可视化结果(完整模式) │ ├── [PDF文件名]_layout.pdf # 布局可视化结果(完整模式) │ └── [PDF文件名]_spans.pdf # 文本块可视化结果(完整模式)
4、API封装
API 端点
- URL:http://[host]:6601/process_pdf
- 方法: POST
- 内容类型: multipart/form-data
请求参数
参数:pdf_file
类型:文件
描述:要解析的PDF文件
响应
成功: 返回包含所有解析结果的ZIP文件
失败: 返回JSON格式的错误信息
代码
复制from flask import Flask, request, send_file, jsonify import os import shutil import zipfile from scripts.mineru_process_pdf import process_pdf app = Flask(__name__) def create_zip_from_directory(directory_path, zip_file_path): with zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_DEFLATED) as zipf: for root, dirs, files in os.walk(directory_path): for file in files: file_path = os.path.join(root, file) arcname = os.path.relpath(file_path, directory_path) zipf.write(file_path, arcname) @app.route('/process_pdf', methods=['POST']) def process_pdf_api(): if 'pdf_file' not in request.files: return jsonify({'error': 'No file part'}), 400 file = request.files['pdf_file'] if file.filename == '': return jsonify({'error': 'No selected file'}), 400 # Save the uploaded file to a temporary location input_pdf_path = os.path.join('temp', file.filename) os.makedirs('temp', exist_ok=True) file.save(input_pdf_path) try: # Process the PDF file output_dir = '/path/to/output' markdown_file_path = process_pdf(input_pdf_path, output_dir=output_dir, simple_output=False) # Create a zip file from the output directory temp_path = '/path/to/temp' os.makedirs(temp_path, exist_ok=True) zip_file_path = os.path.join(temp_path, f"{os.path.splitext(file.filename)[0]}.zip") create_zip_from_directory(os.path.join(output_dir, os.path.splitext(file.filename)[0]), zip_file_path) # Send the zip file as a response return send_file(zip_file_path, as_attachment=True) except Exception as e: return jsonify({'error': str(e)}), 500 finally: # Clean up temporary files if os.path.exists(input_pdf_path): os.remove(input_pdf_path) if os.path.exists(zip_file_path): os.remove(zip_file_path) if os.path.exists(output_dir): shutil.rmtree(output_dir) if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=6601)
5、调用示例
下面对该解析服务API提供了三种调用示例,可以根据需要选择使用:
代码
复制import requests import os import zipfile import io def parse_pdf_api_to_path(pdf_file_path, output_dir): url = "http://localhost:6601/process_pdf" # 确保输出目录存在 os.makedirs(output_dir, exist_ok=True) # 获取 PDF 文件的基础名称(不带扩展名) base_filename = os.path.splitext(os.path.basename(pdf_file_path))[0] with open(pdf_file_path, 'rb') as pdf_file: files = {'pdf_file': pdf_file} response = requests.post(url, files=files) if response.status_code == 200: # 保存返回的 zip 文件到指定目录,使用与 PDF 相同的基础文件名 output_zip_path = os.path.join(output_dir, f'{base_filename}.zip') with open(output_zip_path, 'wb') as f: f.write(response.content) print(f"Test passed: Received zip file and saved to {output_zip_path}.") else: print(f"Test failed: {response.status_code} - {response.json()}") def parse_pdf_api_to_content(pdf_file_path): url = "http://localhost:6601/process_pdf" # 获取 PDF 文件的基础名称(不带扩展名) base_filename = os.path.splitext(os.path.basename(pdf_file_path))[0] with open(pdf_file_path, 'rb') as pdf_file: files = {'pdf_file': pdf_file} response = requests.post(url, files=files) if response.status_code == 200: # 返回压缩包内容 print(f"Request successful: Received zip file for {base_filename}.") return response.content else: error_message = f"Request failed: {response.status_code} - {response.json()}" print(error_message) raise Exception(error_message) def save_zip_content_to_directory(zip_content, output_dir): # 确保输出目录存在 os.makedirs(output_dir, exist_ok=True) # 使用 zipfile 模块解压缩内容 with zipfile.ZipFile(io.BytesIO(zip_content)) as z: z.extractall(output_dir) print(f"Files extracted to {output_dir}") def save_zip_and_content_to_directory(zip_content, output_dir, zip_filename): # 确保输出目录存在 os.makedirs(output_dir, exist_ok=True) # 保存压缩包到指定目录 zip_path = os.path.join(output_dir, zip_filename) with open(zip_path, 'wb') as f: f.write(zip_content) print(f"Zip file saved to {zip_path}") # 使用 zipfile 模块解压缩内容 with zipfile.ZipFile(io.BytesIO(zip_content)) as z: z.extractall(output_dir) print(f"Files extracted to {output_dir}")
直接解压并保存到指定目录
复制pdf_file_path = "/path/to/your.pdf" output_unzip_dir = "/path/to/output/dir" # 获取压缩包内容 zip_content = parse_pdf_api_to_content(pdf_file_path) # 解压并保存到指定目录 save_zip_content_to_directory(zip_content, output_unzip_dir)
保存压缩包到指定目录并解压
复制pdf_file_path = "/path/to/your.pdf" output_unzip_dir = "/path/to/output/dir" # 获取压缩包内容 zip_content = parse_pdf_api_to_content(pdf_file_path) # 定义压缩包文件名 zip_filename = os.path.splitext(os.path.basename(pdf_file_path))[0] + ".zip" # 保存压缩包并解压 save_zip_and_content_to_directory(zip_content, output_unzip_dir, zip_filename)
将解析内容保存到本地
复制pdf_file_path = "/path/to/your.pdf" output_dir = "/path/to/output/dir" # 直接调用API并将结果保存到指定目录 parse_pdf_api_to_path(pdf_file_path, output_dir)