统信UOS容器下安装pdf转md模型MinerU实操

统信UOS容器下安装pdf转md模型MinerU实操



前言

MinerU是一款将PDF转化为机器可读格式的工具(如markdown、json),可以很方便地抽取为任意格式。

本文记录在统信UOS 1050e容器中集成MinerU的详细过程,所有操作均在root下完成。

关于如何在内网环境安装docker,可参考统信UOS下安装快速安装Docker实操


一、创建容器环境

以统信UOS 1050e操作系统为基础镜像生成容器,将宿主机的/mnt/tmp作为挂载点,5020作为对外服务的端口。

docker run --v /mnt/tmp:/tmp -p 5020:5020 -it uniontechos-server-20-1050e:latest /bin/bash

说明:

  • tmp挂载点主要是为了方便宿主机和容器间进行文件上传下载,避免频繁的文件拷贝,如果嫌烦也可以不设置。
  • 统信UOS 1050e容器有个设置,如果5分钟内无交互会自动停止容器,如要取消此限制,进入容器后修改/etc/bashrc中的TMOUT参数为0即可。

二、安装MinerU

1. 安装python

参考之前的文章《统信UOS下源码编译安装Python实操》安装Python3.10。
anaconda安装过程中会把python3.7作为python的默认版本,为确保python3.10作为默认版本,需将python的路径加入环境变量。

echo 'export PATH=$PATH:/usr/local/python3.10/bin' >> /etc/bashrc
source /etc/bashrc

2. 安装依赖

yum install mesa-libGL
rm -rf /usr/bin/python3
ln -s /usr/bin/python3.7 /usr/bin/python3
yum install -y wget
rm -rf /usr/bin/python3
ln -s /usr/local/python3.10/bin/python3.10 /usr/bin/python3
python --version  // 确保输出为Python3.10.x
python3 --version  // 确保输出为Python3.10.x

说明:如果按照《统信UOS下源码编译安装Python实操》安装Python3.10,可能会出现yum无法使用的情况,此时需要将python3替换回操作系统默认的python版本,然后再安装wget,安装完成后再将python3的默认版本修改回python3.10。

3. 安装anaconda

下载地址:https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/
从中选择合适的版本进行下载,本文下载的是Anaconda3-5.3.1-Linux-x86_64.sh。

将下载的安装包放在/mnt/tmp目录下,自动映射至容器的/tmp目录中。

cd /tmp
./Anaconda3-5.3.1-Linux-x86_64.sh
ln -s /root/anaconda3/bin/conda /usr/bin/conda

说明:

  • 安装过程中遇到yes或no的选择,一律选择yes,其他一律默认敲回车或空格。

4. 安装magic-pdf

conda create -n mineru 'python>=3.10' -y
echo 'conda activate mineru' >> /root/.bashrc
source /root/.bashrc
pip install -U "magic-pdf[full]" -i https://mirrors.aliyun.com/pypi/simple

说明:因为将激活虚拟环境的命令放入了.bashrc,所以每次进入容器都会自动激活虚拟环境。

5. 下载模型权重文件

pip install modelscope -i https://pypi.tuna.tsinghua.edu.cn/simple
wget https://gcore.jsdelivr.net/gh/opendatalab/MinerU@master/scripts/download_models.py -O download_models.py
python download_models.py

三、运行与验证

1. 服务器本地调用

magic-pdf -p {some_pdf} -o {some_output_dir} -m auto

2. 通过http提供服务

(1)接口代码

使用DeepSeek编写了以下程序,实现通过http接口调用。

import os
from typing import Union, BinaryIO
from fastapi import FastAPI, UploadFile, File, HTTPException, Query
from fastapi.responses import StreamingResponse, FileResponse
from pydantic import BaseModel
import io

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

app = FastAPI(title="PDF to Markdown Converter API",
              description="API for converting PDF files to markdown format using mineru")

class FilePathRequest(BaseModel):
    file_path: str

class PathConversionResponse(BaseModel):
    status: str
    markdown_path: str
    message: Union[str, None] = None

class PDFToMarkdownConverter:
    def __init__(self, output_dir: str = "/tmp/output"):
        """
        初始化转换器
        
        Args:
            output_dir: 输出目录,默认为"output"
        """
        self.output_dir = output_dir
        self.local_image_dir = os.path.join(output_dir, "images")
        os.makedirs(self.local_image_dir, exist_ok=True)
        
    def _process_pdf_bytes(self, pdf_bytes: bytes, original_filename: str = "document.pdf") -> tuple[str, str]:
        """
        内部方法,处理PDF字节流并转换为markdown
        
        Args:
            pdf_bytes: PDF文件的字节流
            original_filename: 原始文件名,用于生成输出文件名
            
        Returns:
            tuple: (markdown文件内容, markdown文件路径)
        """
        # 准备写入器
        image_writer = FileBasedDataWriter(self.local_image_dir)
        md_writer = FileBasedDataWriter(self.output_dir)
        
        # 获取不带扩展名的文件名
        name_without_suff = os.path.splitext(os.path.basename(original_filename))[0]
        image_dir = os.path.basename(self.local_image_dir)
        
        # 处理PDF
        ds = PymuDocDataset(pdf_bytes)
        
        if ds.classify() == SupportedPdfParseMethod.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)
        
        # 生成各种输出文件(可选)
        infer_result.draw_model(os.path.join(self.output_dir, f"{name_without_suff}_model.pdf"))
        pipe_result.draw_layout(os.path.join(self.output_dir, f"{name_without_suff}_layout.pdf"))
        pipe_result.draw_span(os.path.join(self.output_dir, f"{name_without_suff}_spans.pdf"))
        
        # 获取markdown内容
        md_content = pipe_result.get_markdown(image_dir)
        md_filename = f"{name_without_suff}.md"
        md_path = os.path.join(self.output_dir, md_filename)
        
        # 保存markdown文件
        pipe_result.dump_md(md_writer, md_filename, image_dir)
        
        return md_content, md_path
    
    def convert_from_file(self, file_path: str) -> tuple[str, str]:
        """
        从文件路径转换PDF为markdown
        
        Args:
            file_path: PDF文件的路径
            
        Returns:
            tuple: (markdown文件内容, markdown文件路径)
        """
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"文件不存在: {file_path}")
            
        # 读取文件内容
        reader = FileBasedDataReader("")
        pdf_bytes = reader.read(file_path)
        
        return self._process_pdf_bytes(pdf_bytes, os.path.basename(file_path))
    
    def convert_from_stream(self, file_stream: Union[BinaryIO, bytes], filename: str = "document.pdf") -> tuple[str, str]:
        """
        从文件流转换PDF为markdown
        
        Args:
            file_stream: PDF文件流或字节
            filename: 文件名(用于生成输出文件名)
            
        Returns:
            tuple: (markdown文件内容, markdown文件路径)
        """
        if isinstance(file_stream, bytes):
            pdf_bytes = file_stream
        else:
            pdf_bytes = file_stream.read()
            
        return self._process_pdf_bytes(pdf_bytes, filename)

# 全局转换器实例
converter = PDFToMarkdownConverter()

@app.post("/convert_from_path/", response_model=PathConversionResponse)
async def convert_from_path(request: FilePathRequest):
    """
    接口1:通过文件路径转换PDF为markdown
    
    - **file_path**: PDF文件的本地路径
    - 返回: JSON响应,包含markdown文件路径
    """
    try:
        md_content, md_path = converter.convert_from_file(request.file_path)
        return {
            "status": "success",
            "markdown_path": md_path,
            "message": "Conversion completed successfully"
        }
    except FileNotFoundError as e:
        raise HTTPException(status_code=404, detail=str(e))
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Conversion failed: {str(e)}")

@app.post("/convert_from_upload/")
async def convert_from_upload(file: UploadFile = File(...)):
    """
    接口2:通过上传文件转换PDF为markdown
    
    - **file**: 上传的PDF文件
    - 返回: 直接返回markdown文件流
    """
    try:
        # 检查文件类型
        if not file.filename.lower().endswith('.pdf'):
            raise HTTPException(status_code=400, detail="Only PDF files are accepted")
            
        md_content, md_path = converter.convert_from_stream(file.file, file.filename)
        
        # 创建文件流响应
        file_stream = io.BytesIO(md_content.encode('utf-8'))
        response = StreamingResponse(
            file_stream,
            media_type="text/markdown",
            headers={
                "Content-Disposition": f"attachment; filename={os.path.basename(md_path)}",
                "X-Markdown-Path": md_path  # 可选:在header中返回文件路径
            }
        )
        
        return response
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Conversion failed: {str(e)}")


# 在原有代码基础上添加以下内容

@app.post("/convert_from_url/")
async def convert_from_url(
    file_url: str = Query(..., description="可直接在浏览器中打开的PDF文件URL"),
    return_type: str = Query("stream", description="返回类型,可选 'stream' 或 'path'")
):
    """
    接口3:通过PDF文件URL转换PDF为markdown
    
    - **file_url**: 可直接在浏览器中打开的PDF文件URL
    - **return_type**: 返回类型,'stream'返回文件流,'path'返回文件路径
    
    - 返回: 根据return_type返回markdown文件流或路径信息
    """
    try:
        # 验证URL
        if not file_url.lower().startswith(('http://', 'https://')):
            raise HTTPException(status_code=400, detail="Invalid URL format")
        
        # 下载PDF文件到临时目录
        with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp_file:
            try:
                response = requests.get(file_url, stream=True, timeout=30)
                response.raise_for_status()
                
                for chunk in response.iter_content(chunk_size=8192):
                    tmp_file.write(chunk)
                tmp_file_path = tmp_file.name
            except requests.RequestException as e:
                raise HTTPException(status_code=400, detail=f"Failed to download PDF: {str(e)}")
        
        try:
            # 获取文件名(优先从Content-Disposition获取,否则从URL提取)
            content_disposition = response.headers.get('content-disposition', '')
            filename = ""
            if 'filename=' in content_disposition:
                filename = content_disposition.split('filename=')[1].strip('"\'')
            if not filename:
                filename = os.path.basename(file_url.split('?')[0])  # 去除URL参数
            
            # 转换PDF
            md_content, md_path = converter.convert_from_file(tmp_file_path)
            
            # 根据return_type返回不同格式
            if return_type.lower() == "path":
                return {
                    "status": "success",
                    "markdown_path": md_path,
                    "message": "Conversion completed successfully"
                }
            else:
                # 默认返回文件流
                file_stream = io.BytesIO(md_content.encode('utf-8'))
                return StreamingResponse(
                    file_stream,
                    media_type="text/markdown",
                    headers={
                        "Content-Disposition": f"attachment; filename={os.path.basename(md_path)}",
                        "X-Markdown-Path": md_path
                    }
                )
                
        finally:
            # 清理临时文件
            try:
                os.unlink(tmp_file_path)
            except:
                pass
                
    except HTTPException:
        raise
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Conversion failed: {str(e)}")


@app.get("/files/{file_path:path}")
async def serve_file(file_path: str):
    """
    提供文件访问服务
    - file_path: 相对于服务器指定根目录的文件路径
    - 示例: /files/reports/document.pdf
    """
    # 设置安全的文件根目录(防止目录遍历攻击)
    BASE_DIR = "/tmp/output"  # 替换为你的实际文件存储目录
    absolute_path = os.path.abspath(os.path.join(BASE_DIR, file_path))
    
    # 安全检查
    if not absolute_path.startswith(BASE_DIR):
        raise HTTPException(status_code=403, detail="Access denied")
    if not os.path.exists(absolute_path):
        raise HTTPException(status_code=404, detail="File not found")
    
    # 自动识别MIME类型,浏览器会尝试直接打开支持的文件类型
    return FileResponse(
        absolute_path,
        filename=os.path.basename(file_path)  # 下载时显示的文件名
    )        
        
@app.get("/health/")
async def health_check():
    """
    健康检查端点
    """
    return {"status": "healthy"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=5020)

以上代码提供了三个接口:

  • 后台批量处理:通过 /convert_from_path 处理服务器上的PDF文档。
  • 用户交互:通过网页上传PDF(/convert_from_upload)并立即下载Markdown。
  • 集成第三方服务:通过URL(/convert_from_url)直接转换网络PDF文档。

(2)运行接口

将以上文件上传至/home目录下,假设命名为app.py。

pip install fastapi -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install python-multipart -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install uvicorn -i https://pypi.tuna.tsinghua.edu.cn/simple
cd /home
python app.py

(3)验证

假设在容器/tmp目录下有pdf文件CDC7D522975CE50FFB9F6C7C2C80DCF8.pdf,调用convert_from_path接口进行验证。

curl -X POST "http://localhost:5020/convert_from_path/" -H "Content-Type: application/js
on" -d '{"file_path":"/tmp/CDC7D522975CE50FFB9F6C7C2C80DCF8.pdf"}'
// 执行结果如下
{"status":"success","markdown_path":"/tmp/output/CDC7D522975CE50FFB9F6C7C2C80DCF8.md","message":"Conversion completed successfully"}

四、制作Docker镜像

anaconda3和模型权重文件体积较大,直接制作镜像可能导致镜像文件体积超过20GB,建议将anaconda3和模型权重文件目录挂载出来。

总结

1. 需要注意的地方:

  • python版本与MinerU模型的兼容性,建议使用Python3.10。
  • 安装magic-pdf时,要将’conda activate mineru’写入/root/.bashrc,否则可能无法激活虚拟环境。
  • 通过http提供服务时,当前配置只能处理很小的文件,文件内容稍多可能会引起http请求超时,因此建议根据自己的需要继续修改app.py。

2. 非Docker环境部署

如果不需要封装docker镜像,跳过“一、创建容器环境”和“四、制作docker镜像”的环节即可。

如果不想费这个功夫,也可以联系我获取封装好的docker镜像。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值