OCR 不是终点:从图片和 PDF 到可核验结构化 JSON 的 Python 实践

OCR 不是终点:从图片和 PDF 到可核验结构化 JSON 的 Python 实践

很多文档处理项目的第一版都停在“把字识别出来”。图片交给 OCR,PDF 转成纯文本,然后把结果直接写进数据库或向量库。这个流程能跑,却很难回答三个真正影响生产质量的问题:字段来自哪里、缺失时如何发现、后续规则变化时怎样重放。

更稳妥的做法,是把文档处理拆成两个边界清楚的阶段:先恢复文本,再按明确的 JSON Schema 抽取字段。前者解决“读出来”,后者解决“按业务结构交付”。

本文用 OCR 文字识别 APIPDF 转文本 API 统一图片与 PDF 的输入,再把规范化文本交给 文档字段抽取 API。示例使用 Python 标准库,不依赖第三方 HTTP 包。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

先把管线拆对

一条可维护的文档数据管线可以表示为:

图片 URL ──> OCR ─────────────┐
                              ├─> 规范化文本 ─> Schema 字段抽取 ─> 校验 ─> 人工复核/入库
PDF 文件 ──> PDF 转文本 ──────┘

这样拆分有四个直接好处:

  1. 输入层和业务字段解耦。OCR 或 PDF 解析方式变化,不需要重写入库模型。
  2. Schema 可以版本化。新增字段时,可以用保留的原始文本重放,不必重新上传文档。
  3. 失败位置明确。是文本恢复失败、字段抽取失败,还是业务校验失败,可以分别处理。
  4. 结果可核验。字段值之外还能保留请求 ID、失败字段、告警和证据,方便复查。

三个接口的职责不同

阶段输入输出适合做什么
OCR图片 URL 或图片 Base64文本行数组扫描件、截图、照片
PDF 转文本PDF 文件连续文本可解析 PDF、报告、说明书
字段抽取文本或文件 + JSON Schema结构化字段与状态合同、采购文件、表单入库

不要把 OCR 的文本行数组直接当成最终业务对象。识别文本里通常仍有页眉、换行、顺序和噪声;而“项目名称”“预算金额”“截止时间”这类字段需要明确类型、必填规则和失败处理。

用 JSON Schema 定义交付格式

下面以采购文档为例,只定义四个字段:

{
  "type": "object",
  "properties": {
    "projectName": {"type": "string", "description": "项目名称"},
    "projectCode": {"type": "string", "description": "项目编号"},
    "budgetAmount": {"type": "number", "description": "预算金额"},
    "bidDeadline": {"type": "string", "description": "投标截止时间"}
  },
  "required": ["projectName", "projectCode"]
}

Schema 的价值不只是告诉模型“抽哪些字段”。它还是调用方与下游系统之间的契约:类型不符、必填字段缺失、字段名变化,都可以在入库前被发现。

Python 调用要点

先把 AppKey 放进环境变量,不要把真实密钥写进代码:

export GUGUDATA_APPKEY='your-app-key'

OCR 请求使用表单编码。下面的代码同时检查 HTTP 响应和业务状态:

import json
import os
from urllib.parse import urlencode
from urllib.request import Request, urlopen


def recognize_image(image_url: str) -> list[str]:
    app_key = os.environ["GUGUDATA_APPKEY"]
    query = urlencode({"appkey": app_key})
    body = urlencode({"imageurl": image_url}).encode("utf-8")
    request = Request(
        f"https://api.gugudata.com/imagerecognition/ocr?{query}",
        data=body,
        headers={"Content-Type": "application/x-www-form-urlencoded"},
        method="POST"
    )
    with urlopen(request, timeout=30) as response:
        payload = json.load(response)

    status = payload.get("DataStatus", {})
    if int(status.get("StatusCode", 0)) != 100:
        raise RuntimeError(status.get("StatusDescription", "OCR failed"))
    return payload.get("Data", {}).get("ResultText", [])

PDF 转文本和字段抽取使用 multipart 请求。无论使用哪种输入,都不要把 AppKey、原始敏感文档或完整响应写入公共日志。

不能只检查 HTTP 200

HTTP 200 只表示网关完成了响应,不等于业务处理成功。调用方还应检查返回对象中的 DataStatus.StatusCode,只有状态值为 100 时才进入下一步。

lines = recognize_image(image_url)
if not lines:
    raise ValueError("OCR returned no text lines")

推荐把失败分成三类:

  • 传输失败:网络超时、非 2xx HTTP 状态,按退避策略重试。
  • 业务失败:业务状态不为 100,记录状态信息后进入失败队列。
  • 内容失败:请求成功但文本为空、字段缺失或类型不符,进入人工复核。

重试应由调用方设置上限,并为同一文档保存稳定的业务 ID。不要在未知原因下无限重试,也不要把 AppKey、原始敏感文档或完整响应写入公共日志。

抽取结果还要过一道业务校验

字段抽取成功,不代表可以直接入库。至少应做以下检查:

def validate_values(values: dict) -> list[str]:
    errors = []
    if not values.get("projectName"):
        errors.append("projectName is required")
    if "budgetAmount" in values and values["budgetAmount"] < 0:
        errors.append("budgetAmount must be non-negative")
    return errors

对于金额、日期、证件号等字段,建议在 Schema 类型校验之外再做领域规则校验。低置信、无证据、必填字段失败或警告不为空的记录,应进入人工复核,而不是静默写入正式表。
在这里插入图片描述

保存哪些追踪信息

为了支持排错和重放,建议为每次处理保存:

  • 内部文档 ID 和内容哈希;
  • API 返回的请求 ID 与处理模式;
  • Schema 名称、版本和哈希;
  • 文本恢复阶段与字段抽取阶段的状态;
  • 成功字段、失败字段、告警和人工复核结果;
  • 创建时间、完成时间和有限次重试记录。

原文件与全文是否长期保存,应由数据分级和合规要求决定。能只保存哈希和必要字段时,不要额外复制敏感文档。

一次公开 Demo 能证明什么

在本文核验时,三个公开 Demo 都返回了 HTTP 200 和业务状态 100;字段抽取 Demo 返回 7 个成功字段、0 个失败字段。这能证明示例返回结构与管线设计相符,但不能代表你的文档准确率、账号吞吐量或生产 SLA。

上线前仍应使用脱敏、具有代表性的自有样本建立测试集,分别统计文本恢复成功率、关键字段完整率、人工复核率和单文档成本。只有这些指标稳定,OCR 才真正从“能识别文字”变成一条可运营的结构化数据管线。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DevOpenClub

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值