返回博客

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 OKerrorCode: 0errorMsg: ""。按照网上或旧项目里的解析模式处理:

const pages = json.result.ocrResults || []
for (const p of pages) {
  const texts = p.prunedResult.rec_texts || []
  out.push(texts.join('\n'))
}
// => out 永远是 []

输出始终为空。图片里有清晰印刷文字,dataInfopreprocessedImages 字段存在(说明图片确实被读取了),但识别文本就是出不来。

根因

PaddleOCR 至少有两种 HTTP 端点,请求体完全相同,但响应 schema 完全不同

基础 OCRtext_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)能区分你打到了哪个端点。

坑:两个端点共享请求路径,响应 schema 却截然不同

从请求侧完全无法判断当前实例是哪个版本。dataInfopreprocessedImages 的存在会误导你认为"调用成功了"——它们只说明图片被预处理了,不包含任何识别文本。老的 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.markdownp.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 秒内确认你打的是哪个端点变体,省去一小时的盲目排查。

正解:用 layoutParsingResults vs ocrResults 做运行时检测

合并检测两种顶层 key,再按优先级走三层文本路径(markdown.textparsing_res_listrec_texts),覆盖所有已知变体。首次接入新实例时打开 PADDLEOCR_DEBUG=1,一眼看清响应结构,而不是靠猜。

附:私有 URL 识别失败时改用 Base64

如果图片来自 OSS/S3 预签名 URL,PaddleOCR 会因为 URL 中没有识别的文件扩展名报 errorCode: 10002errorMsg: "文件 URL 无法识别"。私有存储链路建议直接将图片字节转 Base64 传入 file 字段,绕开 URL 解析问题。

一句话外卖

PaddleOCR 的两种端点共享同一个请求路径和请求体,但响应 schema 完全不同——只有响应里的顶层 key 才能区分;复用任何基于 ocrResults 的老解析器,在 PP-StructureV3 实例上都会静默返回空。