MinerU部署實踐:從零開始搭建你的專屬PDF解析服務
在多模態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)