用 OpenDataLoader PDF 搭一个兼容 MinerU 的解析服务

最近整理了一下 PDF 解析的调用链。上层应用已经按照 MinerU 的 /file_parse 接口接入,如果直接替换解析引擎,上传参数、页码规则和返回数据都要跟着修改。于是有了 opendataloader-pdf-service:在 OpenDataLoader PDF 外面加一层轻量的 FastAPI 适配器,尽量保持原有调用方式不变。

这个项目不是重新实现 PDF 解析算法。真正的版面分析、文本提取和 Markdown 生成仍由 opendataloader-pdf 完成,服务本身负责处理 HTTP 上传、参数转换、结果适配和文件打包。

一条请求是怎样完成的

项目只有一个主要接口:POST /file_parse。一次请求大致经过下面几个阶段:

text 复制代码
multipart 上传
    ↓
分块写入任务目录
    ↓
pdfinfo 读取页数和每页尺寸
    ↓
opendataloader-pdf 生成 JSON、Markdown 和图片
    ↓
转换为兼容的 content_list
    ↓
返回 JSON,或打包为 ZIP

服务会为每次请求生成一个 8 位任务 ID,并把文件写入 output/<task_id>/。上传文件按 1 MiB 分块落盘,因此不会先把整份 PDF 全部读进内存。解析工作是同步且偏 CPU/IO 密集的,接口通过 FastAPI 的 run_in_threadpool 执行,避免直接阻塞异步事件循环。

OpenDataLoader 一次可以接收多份输入,所以批量上传的文件会在一次转换调用中完成。每个文件仍有独立的 successfailed 状态,最终响应还会汇总总数、成功数、失败数和耗时。

兼容层主要适配了什么

表面上看,这个服务只是把 Python 函数放到 HTTP 接口后面,实际最有价值的部分是两种数据格式之间的转换。

页码规则

接口沿用 start_page_idend_page_id:从 0 开始,并且包含结束页;OpenDataLoader 的 pages 参数从 1 开始。比如:

text 复制代码
start_page_id=0, end_page_id=2  -> pages="1-3"
start_page_id=3, end_page_id=3  -> pages="4"

如果只给起始页,服务会先通过 pdfinfo 获取总页数,再生成从起始页到末页的范围。

坐标系统

这是最容易出现"文字对了,框却飘了"的地方。OpenDataLoader 返回 PDF 坐标,原点位于左下角;MinerU 风格的 bbox 使用左上角原点,并把每一页分别归一化到 0-1000

服务先用 pdfinfo -box 读取每页真实宽高,然后进行坐标翻转和缩放:

text 复制代码
x' = x / page_width  * 1000
y' = (page_height - y) / page_height * 1000

最终的 bbox 固定为 [x0, y0, x1, y1] 整数数组,并限制在 0-1000 范围内。这里按页读取尺寸很重要,因为同一份 PDF 里可能同时存在横向页和纵向页,不能拿第一页的尺寸套用整份文档。

内容列表

OpenDataLoader 的文档 JSON 是树形结构,而兼容接口需要较扁平的 content_list。适配器会按元素类型转换:

  • 普通段落、标题和列表项转换为 text
  • 页眉、页脚保留为 headerfooter
  • 表格转成包含 rowspancolspan 的 HTML;
  • 图片转换为 image,并保留图片路径、页码和坐标。

有些图片会嵌套在列表、表格单元格或页眉中。代码会递归遍历整棵元素树,将这些图片额外提升为独立条目,避免扁平化时悄悄丢失。

return_images=true 时,解析器输出的图片会转换成 Data URI 放在响应的 images 字典中。既支持 OpenDataLoader JSON 已经内嵌的图片,也会扫描外部图片目录并进行 Base64 编码。

本地启动

项目使用 Python 3.11 或 3.12,并通过 uv 管理依赖。OpenDataLoader PDF 的本地解析器依赖 Java,读取 PDF 元数据还需要 Poppler 提供的 pdfinfo

准备好 uv、Java 11+ 和 Poppler 后运行:

bash 复制代码
git clone https://github.com/42tr/opendataloader-pdf-service.git
cd opendataloader-pdf-service
uv sync
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000

启动后可以访问 http://localhost:8000/docs 查看 Swagger 文档,或通过健康检查确认服务状态:

bash 复制代码
curl http://localhost:8000/health

返回结果如下:

json 复制代码
{"status":"ok","parser":"opendataloader-pdf"}

解析一份 PDF:

bash 复制代码
curl -X POST 'http://localhost:8000/file_parse' \
  -F 'files=@./example.pdf' \
  -F 'return_md=true' \
  -F 'return_content_list=true' \
  -F 'return_images=true' \
  -o output.json

批量上传只需重复 files 字段:

bash 复制代码
curl -X POST 'http://localhost:8000/file_parse' \
  -F 'files=@./a.pdf' \
  -F 'files=@./b.pdf' \
  -F 'start_page_id=0' \
  -F 'end_page_id=4'

如果设置 response_format_zip=true,接口会返回 ZIP 文件,其中包含响应 JSON 和本次上传的 PDF。

用 Docker 部署

仓库里的 Dockerfile 使用 python:3.11-slim-bookworm,并预装 OpenJDK 17 和 poppler-utils,省去了宿主机准备解析环境的步骤:

bash 复制代码
docker build -t opendataloader-pdf-api .
docker run --rm \
  -p 8000:8000 \
  -v "$PWD/output:/app/output" \
  opendataloader-pdf-api

挂载 output 目录后,任务文件在容器退出后仍然保留。镜像通过 uv sync --frozen 按锁文件安装依赖,部署结果也更容易复现。

后端选择与兼容边界

默认的 backend=pipeline 使用 OpenDataLoader 本地 Java 解析器。传入 doclingdocling-fasthybrid 时会统一映射为 docling-fast,这要求另行部署 opendataloader-pdf-hybrid 服务;hancom-ai 也会原样映射到对应混合后端。其他值会回落到本地解析。

兼容不等于功能完全相同,当前有几个边界需要注意:

  • 服务目前面向 PDF,OpenDataLoader 不原生支持 Office 文件;
  • formula_enabletable_enable 等字段为了兼容现有客户端会被接收,但不会改变 OpenDataLoader 的解析行为;
  • output_dir、语言相关字段和部分中间结果开关同样只保留接口形状;
  • 任务目录不会自动清理,长期运行时需要额外配置定时清理或生命周期策略;
  • 接口没有内置鉴权、限流和上传大小限制,暴露到公网前应放在网关之后。

这种适配方式的好处是把变化控制在服务端:已有客户端仍然上传同样的 multipart 表单、读取相近的结果结构,而底层解析器可以独立替换和演进。对于依赖 Markdown、版面坐标、表格和图片的 RAG 或文档入库流程,这层小小的边界转换往往比"再写一个解析器"更实用。

相关推荐
aircrushin1 小时前
Claude 5 之后,上下文工程该做减法了
前端·人工智能·后端
Csvn1 小时前
📊 SQL 入门 Day 16:数据插入技巧
后端·sql
鸽芷咕1 小时前
从MongoDB迁移到KES:一次文档库国产化替换的实战手记
后端
用户1861558008601 小时前
MinIO 数据迁移实战:导出压缩包、下载到本地并绑定目标服务器
后端
程序边界1 小时前
SQL Server数据迁移这件事,远比你想的复杂——但也远比你想的简单(上)
后端
circuitsosk1 小时前
长文本与高并发下的Token“瘦身”策略:Prompt压缩与上下文窗口优化
java·前端·python·prompt·上下文窗口·token优化
JakeJiang1 小时前
抓到接口还不够:用 AIProxy 改返回、Mock 数据、切测试环境
前端·后端
YuePeng1 小时前
Java 开发者的 Django Admin,终于来了
后端·架构·github
XuCoder1 小时前
Redis 分片集群:它到底是怎么把数据分片又路由的?
后端