接到一个 OCR 识别需求时,很多开发者第一反应是"拿图片换 JSON"。但当接口真的返回字段后,才发现字段名与业务字段对不上、请求频率撑不住批量任务、含个人信息的字段在合规上需要额外处理。这些问题的根源,往往不是接口调用失败,而是对接口的能力边界缺少系统梳理。本文以火车票识别 API 为例,从适用场景、输入约束、返回结构和工程化限制四个角度拆解,帮助你在接入前就把账算清楚。
适用场景:哪些业务形态适合用它
火车票识别 API 的目标是从一张火车票图片中抽出结构化字段。基于这个能力,适合的业务场景有以下几类。
差旅报销单据自动录入
员工在移动端拍摄火车票上传,后端调用接口输出出发站、到达站、车次、票价等字段,再写入报销表单。这个场景的关键收益是减少人工录入,而不是替代财务审核。接口返回的票价、行程信息可以作为预填值,最终仍需要人工确认。
行程管理 / 差旅平台归档
企业差旅平台需要把散落的票据信息汇总到后台。此时接口重点是结构化输出。识别结果可以按车次、日期、乘车人维度聚合,形成行程归档,便于后续按项目或部门检索。
票据核对与验真前置
报销系统里已经存在订单数据,需要对比实际票面信息与系统记录是否一致。接口返回的车次、发车时间、票价等字段可与订单记录比对,不一致时标记出来进入人工流程。需要说明的是,识别接口只负责"把票面文字变成字段",不承担真伪验证职责。
接口基础信息与能力边界
先给出接口的基本信息,便于后续讨论有共同上下文。
| 项目 | 值 |
|---|---|
| 接口名称 | 火车票识别 |
| slug | ocr-train-ticket |
| 请求方法 | POST |
| 请求地址 | https://v1.apizero.cn/api/ocr-train-ticket |
| 分类 | 文档识别 |
| QPS | 2 / s |
识别对象边界
接口面向国内全类型火车票,包括高铁、动车和普通车票。输出固定为 13 个字段,覆盖出发站、到达站、车次、乘车人姓名、座位号、票价、出发时间、身份证号、售卖站等信息。
需要注意:"全类型"指的是国内票种覆盖,不包含国际车票。如果业务中有境外票据识别需求,这个接口并不对口。
能输出的字段边界
接口返回的是票面字段的结构化文本,不包含票面版式还原、印章识别或票据真伪判定。它回答的是"票上写了什么",而不是"这张票是不是真的"。
访问约束边界
由于返回内容包含姓名和身份证号,接口仅限已登录用户调用,匿名访问不开放。这意味着调用方需要持有有效的 API Key,并且需要在请求中携带鉴权头。对内部系统集成来说,还需要确保 Key 的存放与传递不落入前端代码。
频率约束边界
接口配额为2 QPS。它不是无限制的高并发接口,批量处理场景需要自行做任务队列、限速和退避重试。如果你的业务流程是员工即时上传单张票据,2 QPS 通常够用;如果是定时批量补录历史票据,则要拉长执行窗口。
能力边界小结:能识别国内火车票的 13 个票面字段,输入为单张图片,输出为 JSON 文本;有登录鉴权和 QPS 限制;不含真伪校验与境外车票识别。接口的更多边界细节以官方文档为准。
请求参数与鉴权说明
Header 参数
| 参数 | 是否必填 | 类型 | 说明 |
|---|---|---|---|
Authorization | 是 | string | Bearer <你的 API Key> |
Content-Type | 否 | string | 请求体格式,一般传application/json |
请求体字段
请求体是一个 JSON 对象,字段如下。
| 参数 | 是否必填 | 类型 | 说明 |
|---|---|---|---|
input_type | 是 | string | 图片传输方式,支持url(公网图片地址)或base64(图片的 base64 编码) |
input_data | 是 | string | 图片内容。input_type=url时填 http/https 图片链接;input_type=base64时填 base64 字符串,可含data:image/xxx;base64,前缀 |
这里有两个容易踩坑的点。
第一,url 方式下的图片链接必须公网可访问。内网地址、带鉴权的临时链接都会导致拉取失败。
第二,base64 字符串体积较大。一张车票照片的 base64 可能达到数百 KB,请求体过大会带来不必要的网络耗时。建议先对图片做压缩裁剪,再编码传输。
另外需要留意一个细节:部分版本的接入文档使用X-API-Key作为鉴权头。两种写法在不同版本文档中都出现过,接入前请以官方文档页的最新说明为准,避免按旧示例写死导致鉴权失败。
可运行的请求示例
curl 示例
下面的示例使用环境变量TRAIN_OCR_API_KEY保存密钥,避免在命令中硬编码。
export TRAIN_OCR_API_KEY="your_api_key_here" curl -sS \ -X POST \ -H "Authorization: Bearer $TRAIN_OCR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "input_type": "url", "input_data": "https://example.com/train-ticket.jpg" }' \ "https://v1.apizero.cn/api/ocr-train-ticket"执行成功后,响应的 JSON 会被输出到终端。如果当前文档要求使用X-API-Key头,把Authorization一行替换为-H "X-API-Key: $TRAIN_OCR_API_KEY"即可。
Python requests 示例
在日常脚本中,用 Python 接入更直观。下面的代码把请求封装成一个函数,方便后续在批量任务中复用。
import base64 import os import requests API_URL = "https://v1.apizero.cn/api/ocr-train-ticket" API_KEY = os.environ["TRAIN_OCR_API_KEY"] def parse_train_ticket(input_data: str, input_type: str = "url") -> dict: payload = { "input_type": input_type, "input_data": input_data, } headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", } resp = requests.post(API_URL, json=payload, headers=headers, timeout=15) resp.raise_for_status() return resp.json() if __name__ == "__main__": # 用图片 URL 调用 result = parse_train_ticket("https://example.com/train-ticket.jpg") print(result) # 用 base64 调用 with open("ticket.jpg", "rb") as f: b64 = base64.b64encode(f.read()).decode("utf-8") result_b64 = parse_train_ticket(b64, input_type="base64") print(result_b64)这个函数只做了最基本的事情:拼参数、发请求、解析 JSON。实际工程中还需要处理超时重试、异常捕获和日志记录,这些内容会在后文展开。
返回结果字段解读
响应外层结构
成功响应是一个 JSON 对象,顶层包含code、data、msg和request_id。
{ "code": 0, "data": { "end_station": "上海虹桥", "id_num": "110101199001011234", "name": "张三", "price": "553.00", "sale_num": "G123456", "sale_station": "北京南", "seat_cls": "二等座", "seat_num": "05车12A号", "start_station": "北京南", "ticket_num": "E123456789", "time": "2024-01-15 09:00", "total_amount": "¥553.00", "train_num": "G101" }, "msg": "成功", "request_id": "req_abc123" }code为 0 表示成功,request_id用于在排查问题时定位具体请求。
13 个字段的业务含义
下面对data内的字段做逐个说明。
| 字段 | 示例值 | 含义 |
|---|---|---|
start_station | 北京南 | 出发站 |
end_station | 上海虹桥 | 到达站 |
train_num | G101 | 车次 |
name | 张三 | 乘车人姓名 |
seat_cls | 二等座 | 座席类别 |
seat_num | 05车12A号 | 座位号 |
time | 2024-01-15 09:00 | 出发时间 |
price | 553.00 | 票价(文本,不含货币符号) |
total_amount | ¥553.00 | 含货币符号的票价 |
ticket_num | E123456789 | 电子客票号或票号 |
sale_num | G123456 | 售票编号或报销凭证号 |
sale_station | 北京南 | 售卖站 |
id_num | 110101199001011234 | 乘车人身份证号 |
字段类型需要特别留意:所有字段都是字符串。price是"553.00"而不是数字 553.0,time是"2024-01-15 09:00"而不是时间戳。这种设计在 OCR 场景中很常见,因为票面印刷格式可能变化,保留原始文本最稳妥。
在业务侧对接时,建议按以下方式处理:
- 金额字段在入库时统一转为 Decimal 类型,避免在字符串和浮点数之间反复转换;
- 时间字段使用
datetime.strptime(value, "%Y-%m-%d %H:%M")解析到本地时间; - 身份证号和姓名字段属于个人信息,系统落地时要做加密存储和脱敏展示。
常见错误与排查思路
接口调用失败时的报错信息不一定总是语义清晰。下面从实际排查角度梳理几类典型问题。
鉴权类问题
- 表现:返回 401,提示未认证或无效凭证。
- 排查:确认请求头中的
Authorization是否为Bearer加空格再加 Key;检查环境变量是否正确注入;确认 Key 没有过期或被服务端重置。 - 补充:如果文档使用
X-API-Key,需要按照文档调整请求头名称。
参数校验类问题
- 表现:返回 400,提示请求参数错误。
- 排查:核对
input_type是否传了url或base64之外的值;检查input_data是否为空字符串;如果是 base64,确认编码没有换行符混入。
图片拉取与解析问题
- 表现:请求成功但
data中部分字段为空,或提示图片无法识别。 - 排查:使用 curl 单独拉取图片地址,确认 URL 可公网访问;检查图片是否模糊、倾斜、反光;车票占画面比例过小时,先裁剪再上传。
- 需要强调:空字段不等于接口故障,要区分"图片里没有"和"识别遗漏"两种情况。如果多次识别同一张票的关键字段都不稳定,优先从图片质量入手。
限流类问题
- 表现:短时间连续请求后收到限流类错误。
- 排查:接口 QPS 为 2 / s,批量场景需要人为控制请求间隔。建议使用信号量或队列,限制并发不超过配额。
服务端异常
- 表现:5xx 错误返回。
- 排查:记录
request_id,携带该 ID 查阅文档或联系支持时能加快定位速度。可做指数退避重试,例如重试 3 次,间隔分别为 1s、2s、4s。
工程化注意事项
个人信息合规处理
接口返回的id_num和name属于敏感个人信息。在生产系统中,以下几点需要落实:
- 传输链路使用 HTTPS;
- 数据库中对身份证号加密存储,展示时只保留前 6 后 4;
- 日志打印时对姓名和证件号做脱敏;
- 不要把这些字段原样写入前端日志或埋点数据。
QPS 配额下的批量任务设计
2 QPS 意味着 1 小时内最多约 7200 次请求。如果补录 10 万张历史票据,按满速跑也需要数小时。更合理的做法是:
- 用任务表存储待识别图片,状态分为待处理、处理中、成功、失败;
- 定时任务按固定节奏(如每 500ms 一张)拉取任务;
- 识别失败的任务进入重试队列,记录失败原因;
- 整体速度以 2 QPS 为上限做节流,不依赖单次调用速度。
图片预处理策略
图片质量直接决定 OCR 空字段率。接入前可以对图片做以下通用处理:
- 将图片缩放到合适宽度(如 1500px 以内),避免超大原图上传;
- 用 OpenCV 做旋转矫正,保持票面水平;
- 去除多余边框,让车票主体占满画面;
- 灰度和对比度增强可以提高热敏纸票面的识别效果。
这些预处理不是接口的要求,但在批量场景中能显著降低人工补录比例。
请求超时与重试
OCR 接口响应时间通常比普通业务接口长,客户端超时时间建议设置为 15 秒以上。重试时注意:
- 仅对网络层错误和 5xx 错误重试;
- 对参数错误(4xx)不做重试,直接标记失败;
- 重试次数控制在 2 到 3 次,避免对接口造成额外压力。
结果入库的字段映射
接口字段名与业务表字段名不一定一致。建议在接入层做一层明确的映射,而不是把data原样丢给前端。例如:
def to_business_model(ocr_data: dict) -> dict: return { "departure_station": ocr_data.get("start_station", ""), "arrival_station": ocr_data.get("end_station", ""), "train_no": ocr_data.get("train_num", ""), "passenger_name": ocr_data.get("name", ""), "seat_class": ocr_data.get("seat_cls", ""), "seat_no": ocr_data.get("seat_num", ""), "departure_time": ocr_data.get("time", ""), "ticket_price": ocr_data.get("price", ""), "passenger_id": ocr_data.get("id_num", ""), }映射层的好处是:即使接口字段名后续调整,业务侧改动也只在映射函数内部完成。
参考文档
- 接口文档页:https://apizero.cn/aidocs/ocr-train-ticket
- 原始文档:https://apizero.cn/aidocs/ocr-train-ticket/raw.md