OCR 不是终点:从图片和 PDF 到可核验结构化 JSON 的 Python 实践
很多文档处理项目的第一版都停在“把字识别出来”。图片交给 OCR,PDF 转成纯文本,然后把结果直接写进数据库或向量库。这个流程能跑,却很难回答三个真正影响生产质量的问题:字段来自哪里、缺失时如何发现、后续规则变化时怎样重放。
更稳妥的做法,是把文档处理拆成两个边界清楚的阶段:先恢复文本,再按明确的 JSON Schema 抽取字段。前者解决“读出来”,后者解决“按业务结构交付”。
本文用 OCR 文字识别 API 和 PDF 转文本 API 统一图片与 PDF 的输入,再把规范化文本交给 文档字段抽取 API。示例使用 Python 标准库,不依赖第三方 HTTP 包。

先把管线拆对
一条可维护的文档数据管线可以表示为:
图片 URL ──> OCR ─────────────┐
├─> 规范化文本 ─> Schema 字段抽取 ─> 校验 ─> 人工复核/入库
PDF 文件 ──> PDF 转文本 ──────┘
这样拆分有四个直接好处:
- 输入层和业务字段解耦。OCR 或 PDF 解析方式变化,不需要重写入库模型。
- Schema 可以版本化。新增字段时,可以用保留的原始文本重放,不必重新上传文档。
- 失败位置明确。是文本恢复失败、字段抽取失败,还是业务校验失败,可以分别处理。
- 结果可核验。字段值之外还能保留请求 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 才真正从“能识别文字”变成一条可运营的结构化数据管线。

188

被折叠的 条评论
为什么被折叠?



