文档解析接口返回了 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 做关联,显示内容的优先级是:
- 找到对应表格时,取表格 Markdown 或 HTML。
- 找到对应图片时,取图片描述或图片地址。
- 没有对应记录时,再回退到
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 中的实现整理,未进行识别准确率或性能评测。