跳到正文
曾亮懿 ZengLiangYi Blog

百度智能云 PaddleOCR-VL 接入实践:一个文档解析校样工具的实现

用 Python 标准库接入百度智能云 PaddleOCR-VL 文档解析 API,串起异步任务、结果下载、原件预览与坐标联动,分享开源校样工具的实现与边界。

曾亮懿

曾亮懿

阅读约 8 分钟
目录 展开 收起

文档解析接口返回了 Markdown,接入工作就完成了吗?如果只是提取文字,或许已经够用。但面对双栏 PDF、跨页表格和混排图片,还需要回答几个问题:段落顺序对不对,表格有没有漏掉,结果中的这段文字究竟来自原件哪里?

我做了一个本地 Web 工具,把百度智能云 PaddleOCR-VL 文档解析 API 的调用和结果校样放在同一个页面里。左侧显示原件或重建版面,右侧显示阅读顺序、Markdown 和 JSON,点击元素可以定位到对应页面和区域。

工具源码已按 MIT 协议开源。文档识别由百度智能云完成,本地运行的是调用服务和展示界面,无需下载模型或准备 GPU。本文围绕这个工具的实现展开,不涉及模型训练,也不把界面展示当作识别准确率的证明。

从可运行的调用链开始

项目的主要代码只有两个文件:server.py 负责本地 HTTP 服务与云端调用,index.html 负责上传和校样。后端使用 Python 标准库中的 http.server、urllib.request 和 json,不需要安装第三方 Python 包。前端的 PDF.js 和 Markdown 渲染库通过 CDN 加载,因此整个工具仍有网络依赖。

调用过程可以拆成六步:

浏览器读取文档,发送文件名和 Base64
    ↓
本地服务读取密钥,获取 Access Token
    ↓
提交解析任务,取得 task_id
    ↓
轮询任务,等待成功或失败
    ↓
下载解析 JSON 和 Markdown
    ↓
浏览器展示结果,建立元素与页面之间的定位关系

浏览器只调用本地的 /api/parse。百度 API Key 和 Secret Key 由 Python 服务读取,不需要写进前端页面。

这里有两层不同的请求格式。浏览器向本地服务发送 JSON;Python 调用百度的任务提交和查询接口时,发送的是 POST + application/x-www-form-urlencoded。代码中的 _post_form() 统一完成表单编码、网络错误处理和 JSON 解析,不能把其中任意一层的请求格式直接套到另一层。

本文使用的任务接口路径是:

提交:/rest/2.0/brain/online/v2/paddle-vl-parser/task
查询:/rest/2.0/brain/online/v2/paddle-vl-parser/task/query

这些路径对应百度智能云的文档解析服务。接入时应使用对应 API 文档,确认凭据和请求参数,避免混用其他 PaddleOCR 部署方式的示例。

任务提交成功,还没有拿到解析结果

提交接口返回 task_id,表示任务已经建立。此时继续查询任务状态,直到 success 或 failed,成功后才有可下载的结果链接。

下面是项目轮询逻辑的简化写法,省略了响应结构检查;query_document() 是项目中已经封装好的查询函数:

deadline = time.monotonic() + MAX_WAIT_SECONDS

while time.monotonic() < deadline:
    payload = query_document(access_token, task_id)
    result = payload.get("result", {})
    status = result.get("status")

    if status == "success":
        return payload
    if status == "failed":
        raise BaiduAPIError(f"解析失败: {result.get('task_error')}")

    time.sleep(POLL_SECONDS)

raise BaiduAPIError("等待解析超时")

当前实现将轮询间隔设为 5 秒,等待阈值设为 600 秒。使用 time.monotonic() 计算经过时间,可以避免系统时钟调整影响截止时间判断。不过,这并不是整个请求严格限定在十分钟内:鉴权、提交、下载各有耗时,进行中的查询请求也有自己的超时。

另一个容易忽略的区别是:云端任务是异步的,但这个 Demo 的浏览器请求会一直等待本地服务完成轮询和下载。后端没有把任务保存到数据库,也没有给浏览器提供可恢复的任务查询接口。这个实现适合单人评估;如果要支持多人使用、断线恢复或长任务,需要进一步设计任务管理。

任务成功后,代码检查 parse_result_url 和 markdown_url 是否都存在,再分别下载 JSON 和 Markdown。这样,前端拿到的是可展示的内容,不必再自行请求这两个主结果地址。

下载主结果并不等于归档完成。Markdown 或 JSON 引用的图片仍可能指向云端临时地址;当前工具也没有持久化结果,需要保留的内容应及时导出。

阅读顺序来自接口,内容可能在别的字段里

结果展示以 pages 为入口。每页的 layouts 提供版面元素,而表格、图片的具体内容还可能分别保存在 tables 和 images 中。

如果界面只遍历 layouts 并显示 layout.text,就可能遇到这样的情况:版面上明明识别出了表格,右侧却显示为空。此时需要先看关联记录,不能直接认定接口没有返回内容。

工具通过同一页内的 layout_id 做关联,显示内容的优先级是:

  1. 找到对应表格时,取表格 Markdown 或 HTML。
  2. 找到对应图片时,取图片描述或图片地址。
  3. 没有对应记录时,再回退到 layout.text。

这个关联保留了两种信息:layouts 决定元素的顺序和位置,表格或图片记录补足元素的内容。

右侧的阅读顺序列表沿用接口返回的元素顺序,不再按坐标重新排序。双栏文档中的“从上到下、从左到右”并不一定等于正确的阅读顺序;在评估阶段自行排序,也会改变正在检查的模型输出。界面同样保留接口返回的版面类型,未知类型也显示出来。

把解析坐标映射回页面

有了内容,还需要建立“这条结果来自哪里”的联系。

工具先将不同形式的位置数据归一化为 [x, y, width, height]。对于多边形坐标,当前实现取各顶点的最小、最大横纵坐标,得到轴对齐的外接矩形。这个矩形用于定位,不代表精确描绘了倾斜文字的轮廓。

页面显示尺寸会随窗口变化,覆盖框因此采用相对坐标。假设解析页面的宽高为 pageWidth、pageHeight,核心计算是:

const [x, y, width, height] = box;

const leftPercent = (x / pageWidth) * 100;
const topPercent = (y / pageHeight) * 100;
const widthPercent = (width / pageWidth) * 100;
const heightPercent = (height / pageHeight) * 100;

源码还对位置和最小显示尺寸做了限制,以便很小的区域仍能看见。PDF 的画布渲染尺寸则另外处理设备像素比,覆盖框跟随页面的显示比例。

但坐标换算成立有一个前提:接口的页面坐标与当前显示的原件必须对应。

坐标不匹配时回退到重建版面

coordinateSpaceMatches() 会检查解析页面的宽高、旋转角度,以及它与原件的宽高比。当前代码在角度为零、宽高有效,且宽高比相对差异小于 0.035 时,才尝试把坐标叠加在原件上。

这是一道启发式检查,并不是精确的图像配准。相同宽高比也无法证明页面没有发生裁切或偏移,因此最终仍需观察覆盖框是否对齐。

检查不通过时,工具使用解析结果的页面尺寸、元素位置和内容重建版面。PDF.js 加载失败,或者浏览器无法直接预览某种文档时,也可以尝试这条路径。

重建版面的用途是检查元素之间的位置关系,不能拿它代替原件来证明识别正确。界面会区分“原件 + 版面”和“PaddleOCR-VL 版面”,让使用者知道自己在看什么。

用页码和元素标识连接两侧界面

点击右侧元素时,界面保存目标的页面索引、layout_id 和坐标。目标不在当前页,就先切换页面,再更新覆盖框和元素卡片的选中状态。

匹配卡片时同时使用页面索引和 layout_id,避免只凭元素标识定位到另一页。行级定位则读取 span_boxes,让检查可以从整个段落细化到具体文字行。

这使校样形成一个短路径:发现右侧内容可疑,点击定位,再回到左侧检查原件。使用者不用在一长段 Markdown 和多页 PDF 之间反复查找。

保留四种视图,让问题有处可查

阅读顺序、Markdown、MD 源码和 JSON 各自回答不同的问题:

视图 适合检查的问题
阅读顺序 元素顺序是否合理,内容对应原件的哪个区域
Markdown 文档作为连续内容阅读时,标题、段落和表格怎样呈现
MD 源码 渲染异常是否来自 Markdown 本身,链接和标记是什么
JSON 接口实际返回了哪些字段、坐标和关联关系

例如,表格在 Markdown 视图中显示异常时,可以先看源码,再检查 JSON 的表格记录。问题可能来自解析结果,也可能来自展示方式;保留原始输出,才能继续区分。

工具没有增加业务字段提取、正则修正或章节截断。对于接入评估来说,先看清云端返回了什么,才方便决定后续应用需要补哪些处理。

运行这个开源示例

准备 Python 3.9 或更高版本,以及有权调用对应文档解析服务的百度智能云 OCR 应用凭据。以下命令用于 macOS 或 Linux:

git clone https://github.com/ZengLiangYi/paddleocr-vl-document-parser-demo.git
cd paddleocr-vl-document-parser-demo
cp .env.example .env

在 .env 中填写:

BAIDU_API_KEY=你的_API_Key
BAIDU_SECRET_KEY=你的_Secret_Key

然后运行:

python3 server.py

访问 http://127.0.0.1:8765,选择文档并开始解析。不要直接双击 index.html,因为页面需要调用本地服务。Windows 的配置与启动命令见仓库 README。

当前 Demo 使用 Base64 上传:图片大小检查上限为 10 MiB,其他支持的文档为 50 MiB,没有实现 file_url 上传路径。这是工具当前实现的范围;服务支持的格式、额度和计费应以百度官方文档及账户配置为准。

运行时,文档会发送到百度智能云,调用消耗自己的账户额度。服务默认只监听本机,没有应用账号、访问鉴权或生产级安全设计,不应直接作为公开上传服务使用。

如果要复用到业务系统,可以先从 server.py 的调用链和 index.html 的结果关联逻辑入手。任务持久化、并发限制、上传校验,以及返回内容的安全渲染,需要按实际使用场景补齐。

源码与参考

本文依据项目提交 e633377 中的实现整理,未进行识别准确率或性能评测。

分享:

从文章到代码

到我的 GitHub 看看 ChatCrystal、开发者工具和日常学习记录,欢迎交流实现细节。