MinerU 2.0部署教程!
1、MinerU簡介
MinerU 2帶來了諸多重要更新,主要涵蓋架構、性能、體驗、模型、兼容性等方面。
在架構上,深度重構代碼組織與交互方式,去除對pymupdf
的依賴,無需手動編輯JSON配置文件,新增模型自動下載與更新機制。性能優化顯著,大幅提升特定分辨率文檔的預處理速度、pipeline
后端批量處理少量頁數文檔時的后處理速度以及layout分析速度,在滿足一定配置的設備上整體解析速度提升超50%。
體驗上,內置fastapi service
和gradio webui
,適配sglang
0.4.8版本降低顯存要求,支持參數透傳和基于配置文件的功能擴展。集成了小參數量、高性能多模態文檔解析模型,解析精度超越傳統72B級別的VLM,單卡NVIDIA 4090上推理速度峰值吞吐量超10,000 token/s。
此外,還解決了一些兼容性問題,支持Python 3.13等,同時優化了多方面的解析效果和在線demo功能。
https://gitee.com/myhloli/MinerU
2、MinerU私有化部署
軟硬件環境準備
在開始安裝之前,需要確保你的系統滿足以下軟硬件要求:
解析后端 | pipeline | vlm-transformers | vlm-sglang |
操作系統 | Linux/Windows/macOS | Linux/ Windows | Linux/ Windows (via WSL2) |
CPU 推理支持 | ? | ? | ? |
GPU 要求 | Turing 及以后架構,6G 顯存以上或 Apple Silicon | Turing 及以后架構,8G 顯存以上 | Turing 及以后架構,8G 顯存以上 |
內存要求 | 最低 16G 以上,推薦 32G 以上 | 最低 16G 以上,推薦 32G 以上 | 最低 16G 以上,推薦 32G 以上 |
磁盤空間要求 | 20G 以上,推薦使用 SSD | 20G 以上,推薦使用 SSD | 20G 以上,推薦使用 SSD |
Python 版本 | 3.10 - 3.13 | 3.10 - 3.13 | 3.10 - 3.13 |
安裝依賴包
conda create -n mineru pythnotallow=3.10
conda activate mineru
pip install uv -i https://mirrors.aliyun.com/pypi/simple
uv pip install -U "mineru[core]" -i https://mirrors.aliyun.com/pypi/simple
提示:mineru[core]
包含除 sglang
加速外的所有核心功能,兼容 Windows / Linux / macOS 系統,適合絕大多數用戶。如果你有使用 sglang
加速 VLM 模型推理,或是在邊緣設備安裝輕量版 client 端等需求,可以參考文檔擴展模塊安裝指南。
使用本地模型
模型權重下載
方法一:從 ModelScope下載模型
將MinerU代碼clone到本地,使用python腳本 從ModelScope下載模型文件
python mineru/cli/models_download.py
方法二:使用交互式命令行工具選擇模型下載:
mineru-models-download
詳細參考如何下載模型文件2.配置文件
詳細參考如何下載模型文件
2.配置文件
windows的用戶目錄為 "C:\Users\用戶名", linux用戶目錄為 "/home/用戶名", macOS用戶目錄為 "/Users/用戶名"
- 下載完成后,模型路徑會在當前終端窗口輸出,并自動寫入用戶目錄下的
mineru.json
。 - 您也可以通過將配置模板文件復制到用戶目錄下并重命名為
mineru.json
來創建配置文件。 - 模型下載到本地后,您可以自由移動模型文件夾到其他位置,同時需要在
mineru.json
中更新模型路徑。 - 如您將模型文件夾部署到其他服務器上,請確保將
mineru.json
文件一同移動到新設備的用戶目錄中并正確配置模型路徑。 - 如您需要更新模型文件,可以再次運行
mineru-models-download
命令,模型更新暫不支持自定義路徑,如您沒有移動本地模型文件夾,模型文件會增量更新;如您移動了模型文件夾,模型文件會重新下載到默認位置并更新mineru.json
。
3、解析代碼
process_pdf
是核心解析函數,主要功能包括:
- 自動識別 PDF 類型(普通文本 PDF 或掃描版 PDF),支持多種文件格式,包括
.pdf
、.png
、.jpeg
、.jpg
等,可自動將圖片文件轉換為 PDF 進行處理。 - 提取文本內容和圖片資源,能夠根據設置的起始和結束頁碼進行精準解析,支持多語言識別,可提高 OCR 識別的準確性。
- 生成 Markdown 格式的輸出,支持 LaTeX 公式和表格的解析與輸出,能夠根據不同的后端和模式進行靈活處理。
- 可選生成可視化分析結果,提供布局和文本塊的可視化分析結果,方便用戶進行調試和檢查。
參數 | 類型 | 默認值 | 描述 |
pdf_file_name | str | 無 | 要解析的 PDF 文件路徑,支持 |
output_dir | str | "output" | 輸出文件的主目錄,程序會在該目錄下為每個處理的文件創建單獨的子目錄 |
image_subdir | str | "images" | 存放圖片的子目錄名稱,位于每個文件的子目錄下 |
simple_output | bool | True | 是否使用簡單輸出模式(True 時只輸出 Markdown 和內容列表),False 時輸出所有解析結果,包括可視化分析結果、中間 JSON 文件和模型輸出文件 |
backend | str | "pipeline" | 解析 PDF 的后端,可選值包括 |
method | str | "auto" | 解析 PDF 的方法,可選值包括 |
lang | str | "ch" | 輸入 PDF 中的語言,可選值包括 |
server_url | str | None | 當后端為 |
start_page_id | int | 0 | 解析的起始頁碼,從 0 開始計數 |
end_page_id | int | None | 解析的結束頁碼,從 0 開始計數,默認為 None,表示解析到文檔末尾 |
輸出文件結構:
output/
├── [PDF文件名]/
│ ├── images/ # 存放提取的圖片
│ ├── [PDF文件名].md # 生成的Markdown文件
│ ├── [PDF文件名]_origin.pdf # 原始 PDF 文件(完整模式)
│ ├── [PDF文件名]_content_list.json # 內容列表JSON文件
│ ├── [PDF文件名]_model.pdf # 模型可視化結果(完整模式)
│ ├── [PDF文件名]_middle.pdf # 模型中間處理結果(完整模式)
│ ├── [PDF文件名]_layout.pdf # 布局可視化結果(完整模式)
│ └── [PDF文件名]_spans.pdf # 文本塊可視化結果(完整模式)
import os
os.environ['MINERU_MODEL_SOURCE'] = "local"
from pathlib import Path
from loguru import logger
from mineru.cli.common import convert_pdf_bytes_to_bytes_by_pypdfium2, prepare_env, read_fn
from mineru.data.data_reader_writer import FileBasedDataWriter
from mineru.utils.draw_bbox import draw_layout_bbox, draw_span_bbox
from mineru.utils.enum_class import MakeMode
from mineru.backend.vlm.vlm_analyze import doc_analyze as vlm_doc_analyze
from mineru.backend.pipeline.pipeline_analyze import doc_analyze as pipeline_doc_analyze
from mineru.backend.pipeline.pipeline_middle_json_mkcontent import union_make as pipeline_union_make
from mineru.backend.pipeline.model_json_to_middle_json import result_to_middle_json as pipeline_result_to_middle_json
from mineru.backend.vlm.vlm_middle_json_mkcontent import union_make as vlm_union_make
def process_pdf(pdf_file_name, output_dir="output", image_subdir="images", simple_output=True, backend="pipeline", method="auto", lang="ch", server_url=None, start_page_id=0, end_page_id=None):
"""
處理PDF文件,將其轉換為Markdown格式并保存相關資源
:param pdf_file_name: PDF文件名
:param output_dir: 輸出目錄,默認為'output'
:param image_subdir: 圖片子目錄名,默認為'images'
:param simple_output: 是否使用簡單輸出模式,默認為False
:param backend: 解析PDF的后端,默認為'pipeline'
:param method: 解析PDF的方法,默認為'auto'
:param lang: 輸入PDF中的語言,默認為'ch'
:param server_url: 當后端為`vlm-sglang-client`時需要指定的服務器URL
:param start_page_id: 解析的起始頁碼,默認為0
:param end_page_id: 解析的結束頁碼,默認為None
"""
# 獲取不帶后綴的文件名
name_without_suff = os.path.splitext(os.path.basename(pdf_file_name))[0]
# 構建圖片目錄和markdown目錄的路徑
local_image_dir = os.path.join(output_dir, output_subdir, image_subdir)
local_md_dir = output_dir
# 創建必要的目錄
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文件
pdf_bytes = read_fn(pdf_file_name)
pdf_file_names = [name_without_suff]
pdf_bytes_list = [pdf_bytes]
p_lang_list = [lang]
if backend == "pipeline":
new_pdf_bytes = convert_pdf_bytes_to_bytes_by_pypdfium2(pdf_bytes, start_page_id, end_page_id)
pdf_bytes_list[0] = new_pdf_bytes
infer_results, all_image_lists, all_pdf_docs, lang_list, ocr_enabled_list = pipeline_doc_analyze(pdf_bytes_list, p_lang_list, parse_method=method, formula_enable=True, table_enable=True)
model_list = infer_results[0]
model_json = model_list.copy()
images_list = all_image_lists[0]
pdf_doc = all_pdf_docs[0]
_lang = lang_list[0]
_ocr_enable = ocr_enabled_list[0]
middle_json = pipeline_result_to_middle_json(model_list, images_list, pdf_doc, image_writer, _lang, _ocr_enable, True)
pdf_info = middle_json["pdf_info"]
pdf_bytes = pdf_bytes_list[0]
# 生成Markdown文件和內容列表(無論哪種模式都需要)
md_content_str = pipeline_union_make(pdf_info, MakeMode.MM_MD, image_dir)
md_writer.write_string(
f"{name_without_suff}.md",
md_content_str,
)
content_list = pipeline_union_make(pdf_info, MakeMode.CONTENT_LIST, image_dir)
md_writer.write_string(
f"{name_without_suff}_content_list.json",
str(content_list),
)
# 僅在完整輸出模式下生成額外文件
if not simple_output:
# 生成布局可視化文件
draw_layout_bbox(pdf_info, pdf_bytes, local_md_dir, f"{name_without_suff}_layout.pdf")
# 生成文本塊可視化文件
draw_span_bbox(pdf_info, pdf_bytes, local_md_dir, f"{name_without_suff}_span.pdf")
# 保存原始PDF文件
md_writer.write(
f"{name_without_suff}_origin.pdf",
pdf_bytes,
)
# 保存中間JSON和模型輸出
md_writer.write_string(
f"{name_without_suff}_middle.json",
str(middle_json),
)
md_writer.write_string(
f"{name_without_suff}_model.json",
str(model_json),
)
logger.info(f"local output dir is {local_md_dir}")
else:
if backend.startswith("vlm-"):
backend = backend[4:]
f_draw_span_bbox = False
parse_method = "vlm"
pdf_bytes = convert_pdf_bytes_to_bytes_by_pypdfium2(pdf_bytes, start_page_id, end_page_id)
middle_json, infer_result = vlm_doc_analyze(pdf_bytes, image_writer=image_writer, backend=backend, server_url=server_url)
pdf_info = middle_json["pdf_info"]
if not simple_output:
if True:
draw_layout_bbox(pdf_info, pdf_bytes, local_md_dir, f"{name_without_suff}_layout.pdf")
if f_draw_span_bbox:
draw_span_bbox(pdf_info, pdf_bytes, local_md_dir, f"{name_without_suff}_span.pdf")
if True:
md_writer.write(
f"{name_without_suff}_origin.pdf",
pdf_bytes,
)
image_dir = str(os.path.basename(local_image_dir))
if simple_output:
# 簡單輸出模式:只輸出markdown和內容列表
md_content_str = vlm_union_make(pdf_info, MakeMode.MM_MD, image_dir)
md_writer.write_string(
f"{name_without_suff}.md",
md_content_str,
)
content_list = vlm_union_make(pdf_info, MakeMode.CONTENT_LIST, image_dir)
md_writer.write_string(
f"{name_without_suff}_content_list.json",
str(content_list),
)
else:
# 完整輸出模式:輸出所有內容
md_content_str = vlm_union_make(pdf_info, MakeMode.MM_MD, image_dir)
md_writer.write_string(
f"{name_without_suff}.md",
md_content_str,
)
content_list = vlm_union_make(pdf_info, MakeMode.CONTENT_LIST, image_dir)
md_writer.write_string(
f"{name_without_suff}_content_list.json",
str(content_list),
)
md_writer.write_string(
f"{name_without_suff}_middle.json",
str(middle_json),
)
model_output = ("\n" + "-" * 50 + "\n").join(infer_result)
md_writer.write_string(
f"{name_without_suff}_model_output.txt",
model_output,
)
logger.info(f"local output dir is {local_md_dir}")
# 構建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)
return abs_md_file_path
if __name__ == "__main__":
# 指定要處理的PDF文件名
pdf_file_name = "/home/MinerU/demo/pdfs/demo1.pdf"
# 處理PDF文件并獲取生成的markdown文件路徑
md_file_path = process_pdf(pdf_file_name, output_dir="/home/MinerU/demo/output", simple_output=False, backend="pipeline")
# 打印生成的markdown文件路徑
print(md_file_path)
4、API封裝
from flask import Flask, request, send_file, jsonify
import os
os.environ['MINERU_MODEL_SOURCE'] = "local"
import shutil
import zipfile
from scripts.process_pdf import process_pdf
from pathlib import Path
from mineru.cli.common import read_fn, pdf_suffixes, image_suffixes
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_file', methods=['POST'])
def process_file_api():
# 檢查請求中是否包含文件
if 'file' not in request.files:
return jsonify({'error': 'No file part'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': 'No selected file'}), 400
file_path = Path(file.filename)
# 檢查文件類型是否支持
if file_path.suffix.lower() not in pdf_suffixes + image_suffixes:
return jsonify({'error': f"Unsupported file type: {file_path.suffix}"}), 400
# 獲取文件名(不含擴展名)用于創建子文件夾
file_stem = file_path.stem
# 創建以文件名命名的臨時子文件夾
temp_subdir = os.path.join('temp', file_stem)
os.makedirs(temp_subdir, exist_ok=True)
# 保存上傳的文件到臨時子文件夾
input_file_path = os.path.join(temp_subdir, file.filename)
file.save(input_file_path)
try:
# 讀取文件內容
pdf_bytes = read_fn(input_file_path)
# 僅當輸入不是PDF時,保存轉換后的PDF
temp_pdf_path = None
if file_path.suffix.lower() not in pdf_suffixes:
temp_pdf_path = os.path.join(temp_subdir, f"{file_stem}.pdf")
with open(temp_pdf_path, 'wb') as f:
f.write(pdf_bytes)
else:
# 輸入是PDF,直接使用原文件路徑
temp_pdf_path = input_file_path
# 處理文件
markdown_file_path = process_pdf(
temp_pdf_path,
output_dir=temp_subdir,
)
# 直接在temp_subdir下創建ZIP文件
name_without_suff = os.path.splitext(os.path.basename(input_file_path))[0]
zip_file_path = os.path.join(temp_subdir, f"{name_without_suff}.zip")
create_zip_from_directory(temp_subdir, zip_file_path)
# 發送ZIP文件作為響應
return send_file(zip_file_path, as_attachment=True)
except Exception as e:
return jsonify({'error': str(e)}), 500
finally:
# 僅清理ZIP文件,保留其他臨時文件
if os.path.exists(zip_file_path):
os.remove(zip_file_path)
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=6601)
API文檔
1)API 端點
- URL:
http://localhost:6601/process_file
- 方法: POST
- 內容類型: multipart/form-data
2)請求參數
參數 | 類型 | 描述 |
file | 文件 | 要解析的文件,支持 |
output_dir | 字符串 | 輸出目錄,默認為 "output" |
image_subdir | 字符串 | 圖片子目錄,默認為 "images" |
simple_output | 布爾值 | 是否使用簡單輸出,默認為 |
backend | 字符串 | 解析 PDF 的后端,默認為 "pipeline" |
method | 字符串 | 解析 PDF 的方法,默認為 "auto" |
lang | 字符串 | 文檔語言,默認為 "ch" |
server_url | 字符串 | 服務器 URL,默認為 |
start_page_id | 整數 | 開始解析的頁碼,默認為 0 |
end_page_id | 整數 | 結束解析的頁碼,默認為 |
3)響應
- 成功: 返回包含所有解析結果的 ZIP 文件
- 失敗: 返回 JSON 格式的錯誤信息
4)狀態碼
狀態碼 | 描述 |
200 | 成功處理并返回 ZIP 文件 |
400 | 請求參數錯誤,如未提供文件、文件類型不支持等 |
500 | 服務器內部錯誤 |
5、調用示例代碼
最后提供了三種調用示例,可以根據需要選擇使用:
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}")
if __name__ == "__main__":
pdf_file_path = "/path/to/your.pdf"
output_unzip_dir = "/path/to/output/dir"
try:
# 獲取壓縮包內容
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_content_to_directory(zip_content, output_unzip_dir)
# 保存壓縮包到指定目錄并解壓
save_zip_and_content_to_directory(zip_content, output_unzip_dir, zip_filename)
except Exception as e:
print(f"An error occurred: {e}")
用例1: 直接解壓并保存到指定目錄
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)
用例2: 保存壓縮包到指定目錄并解壓
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)
用例3: 將解析內容保存到本地
pdf_file_path = "/path/to/your.pdf"
output_dir = "/path/to/output/dir"
# 直接調用API并將結果保存到指定目錄
parse_pdf_api_to_path(pdf_file_path, output_dir)