PaddleOCR PP-StructureV3 响应结构与基础 OCR 不同,复用老解析器静默返回空文本
接 PaddleOCR HTTP 接口做图片文字识别,调通了、HTTP 200、errorCode: 0,图片里明明印着一堆字,解析结果却是一个空数组。没有错误可以追,日志里什么异常都没有。
这种"成功但没有内容"的 silent failure 是最难排查的一类 bug——它把你的时间全耗在排查网络、图片格式、Base64 编码上,真正的根因藏在你从未打开过的响应结构里。
现象
调用自建 PaddleOCR HTTP 端点(如 AI Studio 共享实例),使用标准请求格式:
POST /ocr
Headers: Authorization: token <T>, Content-Type: application/json
Body: { file: "<url|base64>", fileType: 1, visualize: false }服务端返回 200 OK,errorCode: 0,errorMsg: ""。按照网上或旧项目里的解析模式处理:
const pages = json.result.ocrResults || []
for (const p of pages) {
const texts = p.prunedResult.rec_texts || []
out.push(texts.join('\n'))
}
// => out 永远是 []输出始终为空。图片里有清晰印刷文字,dataInfo 和 preprocessedImages 字段存在(说明图片确实被读取了),但识别文本就是出不来。
根因
PaddleOCR 至少有两种 HTTP 端点,请求体完全相同,但响应 schema 完全不同:
基础 OCR(text_recognition):
{
"result": {
"ocrResults": [
{ "prunedResult": { "rec_texts": ["识别到的文字"] } }
]
}
}PP-StructureV3 版面解析(pipeline.layout_parsing):
{
"result": {
"layoutParsingResults": [
{
"prunedResult": {
"parsing_res_list": [...],
"page_count": 1
},
"markdown": { "text": "## 识别到的标题\n正文内容..." }
}
],
"dataInfo": "...",
"preprocessedImages": "..."
}
}AI Studio 默认分配的"共享实例"绝大多数是 PP-StructureV3 版面解析——这个更强大的变体。但几乎所有文档示例和旧代码都是为基础 OCR 写的。请求参数一模一样,只有响应的顶层 key(layoutParsingResults vs ocrResults)能区分你打到了哪个端点。
从请求侧完全无法判断当前实例是哪个版本。dataInfo 和 preprocessedImages 的存在会误导你认为"调用成功了"——它们只说明图片被预处理了,不包含任何识别文本。老的 result.ocrResults 解析路径在 PP-StructureV3 上始终拿到 undefined,|| [] 兜底后静默返回空,没有任何报错。
正解
第一步:同时检测两种 schema
const pages =
json?.result?.ocrResults || // 基础 OCR
json?.result?.layoutParsingResults || // PP-StructureV3
[]第二步:按优先级走 PP-StructureV3 的三层文本路径
for (const p of pages) {
// 优先:顶层 markdown(已格式化,质量最好)
const md = p?.markdown
const mdText = typeof md === 'string' ? md : (md?.text || md?.markdown_texts || md?.content)
if (mdText?.trim()) {
out.push(mdText.trim())
continue
}
// 次选:版面块列表
const blocks = p?.prunedResult?.parsing_res_list || []
const parts = blocks
.map((b: any) => b?.block_content || b?.content || b?.text)
.filter(Boolean)
if (parts.length) {
out.push(parts.join('\n'))
continue
}
// 兜底:rec_texts(两种端点都可能有)
const recTexts =
p?.prunedResult?.rec_texts ||
p?.prunedResult?.overall_ocr_res?.rec_texts ||
p?.rec_texts ||
[]
if (recTexts.length) out.push(recTexts.join('\n'))
}注意:p.markdown 是 p.prunedResult 的兄弟节点,不是嵌套在它里面的——这是读 schema 时最常见的误判。
第三步:首次接入必加调试日志
if (process.env.PADDLEOCR_DEBUG) {
console.error('result keys:', Object.keys(json?.result || {}))
if (pages[0]) console.error('page[0] keys:', Object.keys(pages[0]))
}一行环境变量控制的 key dump,能在 30 秒内确认你打的是哪个端点变体,省去一小时的盲目排查。
合并检测两种顶层 key,再按优先级走三层文本路径(markdown.text → parsing_res_list → rec_texts),覆盖所有已知变体。首次接入新实例时打开 PADDLEOCR_DEBUG=1,一眼看清响应结构,而不是靠猜。
如果图片来自 OSS/S3 预签名 URL,PaddleOCR 会因为 URL 中没有识别的文件扩展名报 errorCode: 10002,errorMsg: "文件 URL 无法识别"。私有存储链路建议直接将图片字节转 Base64 传入 file 字段,绕开 URL 解析问题。
一句话外卖
PaddleOCR 的两种端点共享同一个请求路径和请求体,但响应 schema 完全不同——只有响应里的顶层 key 才能区分;复用任何基于
ocrResults的老解析器,在 PP-StructureV3 实例上都会静默返回空。