从纸质票到结构化数据:火车票识别API在差旅报销场景中的落地实践

适用场景:从纸质票到数字化票据的最后一公里

在企业财务报销、行程管理、票据核验等业务中,火车票(含高铁、动车、普通车票)是最常见的纸质凭证之一。传统人工录入方式存在效率低、易出错、难追溯等问题。借助OCR技术,将火车票图片直接转化为结构化数据,可大幅提升自动化水平。

典型业务场景包括:

  • 差旅费报销自动填报:员工上传火车票照片,系统自动提取乘车人、车次、日期、金额等信息,免去手动输入。
  • 行程单电子化归档:对接ERP或费控系统,将识别结果存入数据库,便于后续统计与审计。
  • 票务信息校验:对比用户填写的行程与票面信息是否一致,减少虚假报销风险。

接口能力边界

火车票识别接口(slug: ocr-train-ticket)支持国内全类型火车票,包括红色软纸票、蓝色磁票、电子客票报销凭证等。单次请求返回13个字段,覆盖票面所有关键信息。

接口限制:

  • 鉴权方式:仅限已登录用户调用,匿名访问不开放。请求需携带Bearer Token形式的API Key。
  • QPS:每秒最多2次请求,超出限制会返回频率限制错误。
  • 图片输入:支持URL和Base64两种方式,单张图片大小建议不超过10MB,分辨率不低于300x300像素。

接口不承诺识别成功率(与图片质量直接相关),但实测对清晰、无遮挡、正角度拍摄的票面可达较高准确率。

请求参数与鉴权

接口地址

POST https://v1.apizero.cn/api/ocr-train-ticket

Header参数

参数名必须类型说明
Authorizationstring格式:Bearer <你的API Key>
Content-Typestring建议设为application/json

请求体(JSON)

请求体是一个单元素数组,内含一个对象,包含两个字段:

[ { "input_type": "url", "input_data": "https://example.com/train-ticket.jpg" } ]
字段必须类型说明
input_typestring图片传输方式,可选url(公网可访问的图片链接)或base64(图片的Base64编码,可包含data:image/xxx;base64,前缀)
input_datastring图片内容:若input_type=url则填http/https链接;若input_type=base64则填Base64字符串

注意:请求体需包裹在数组内(API设计为支持批量,但目前仅建议单张传入)。

代码接入示例

1. cURL请求示例

以下命令使用公网图片URL进行识别,需将$APIZERO_API_KEY替换为实际密钥:

curl -sS \ -X POST \ -H "Authorization: Bearer $APIZERO_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"

若图片为本地文件,可先转为Base64并内嵌:

# 将图片转换为Base64字符串(去掉换行) IMAGE_BASE64=$(base64 -w0 /path/to/ticket.jpg) # 发送请求 curl -sS -X POST \ -H "Authorization: Bearer $APIZERO_API_KEY" \ -H "Content-Type: application/json" \ -d "[{\"input_type\":\"base64\",\"input_data\":\"$IMAGE_BASE64\"}]" \ "https://v1.apizero.cn/api/ocr-train-ticket"

2. Python 接入示例

使用requests库,代码简洁且易于集成到现有工程:

import requests import base64 API_URL = "https://v1.apizero.cn/api/ocr-train-ticket" API_KEY = "你的API Key" # 从环境变量或配置文件读取 def ocr_train_ticket(image_path_or_url, is_url=True): headers = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json" } if is_url: body = [{ "input_type": "url", "input_data": image_path_or_url }] else: with open(image_path_or_url, "rb") as f: encoded = base64.b64encode(f.read()).decode("utf-8") body = [{ "input_type": "base64", "input_data": encoded }] resp = requests.post(API_URL, headers=headers, json=body) resp.raise_for_status() return resp.json() # 使用示例 result = ocr_train_ticket("https://example.com/train-ticket.jpg", is_url=True) print(result)

注意:生产环境应将API Key从环境变量读取(如os.getenv("APIZERO_API_KEY")),避免硬编码。

返回值解读

成功响应(HTTP 200)JSON结构如下:

{ "code": 0, "msg": "成功", "request_id": "req_abc123", "data": { "start_station": "北京南", "end_station": "上海虹桥", "train_num": "G101", "name": "张三", "id_num": "110101199001011234", "seat_cls": "二等座", "seat_num": "05车12A号", "ticket_num": "E123456789", "sale_num": "G123456", "time": "2024-01-15 09:00", "price": "553.00", "total_amount": "¥553.00", "sale_station": "北京南" } }

data字段详解

字段类型说明
start_stationstring出发站名称
end_stationstring到达站名称
train_numstring车次号(如G101)
namestring乘车人姓名
id_numstring身份证号(脱敏处理视业务需求)
seat_clsstring座位等级(二等座/一等座/硬卧等)
seat_numstring座位编号(如05车12A号)
ticket_numstring票号(识别码)
sale_numstring售票编码
timestring出发时间(格式:YYYY-MM-DD HH:mm)
pricestring票价金额(数字字符串,不含货币符号)
total_amountstring含货币符号的总金额(如¥553.00)
sale_stationstring售票站名称

注意:id_num字段包含敏感个人信息,在日志存储及前端展示时需进行脱敏处理(如110101********1234)。

错误码说明

codemsg处理建议
0成功正常解析
1001参数错误检查请求体格式,确保 input_type 和 input_data 不为空
1002图片不存在或无法下载若使用URL模式,确认图片链接可公网访问;若使用Base64,检查编码是否完整
1003识别失败图片模糊、非火车票、或票面覆盖严重,建议重新拍照上传
1004频率限制当前QPS为2/s,请控制并发或添加重试退避
1005鉴权失败检查 Authorization Header 格式是否正确,API Key是否有效
2001系统内部错误联系服务提供商排查

常见错误排查

  1. 401 Unauthorized:确认Header中Authorization前缀是否为Bearer(注意大小写),且API Key未过期。
  2. 400 参数错误:检查请求体是否包裹在数组内([{...}]),而非直接传对象。部分开发者容易遗漏最外层方括号。
  3. 图片无法识别:首选URL方式调试,确认图片链接未失效且为火车票正面照;若使用Base64,建议去掉换行符。
  4. 返回字段缺失:部分旧版票面可能缺少某些字段(如sale_station),这些字段可能为空字符串,需在业务逻辑中做空值判断。

工程化注意事项

1. 图片质量优化

  • 拍照时确保票面平整、无反光、无折叠,字符清晰可辨。
  • 建议图片分辨率不低于800x600,文件大小不超过5MB。
  • 对于批量上传场景,可增加预检步骤:检测图片尺寸和清晰度,对不合格图片提前提示用户。

2. 并发与重试策略

接口QPS限制为2次/秒,若业务需要高吞吐(例如财务月末集中报销),可引入队列和限流:

import time import threading class RateLimiter: def __init__(self, max_per_second): self.min_interval = 1.0 / max_per_second self.last_call = time.monotonic() self.lock = threading.Lock() def acquire(self): with self.lock: now = time.monotonic() sleep_time = self.min_interval - (now - self.last_call) if sleep_time > 0: time.sleep(sleep_time) self.last_call = time.monotonic()

同时建议对非200状态码(如429或503)实现指数退避重试(最多3次)。

3. 数据安全与脱敏

接口返回的id_num(身份证号)和name(姓名)属于高度敏感信息。在存储和传输过程中应遵循最小权限原则:

  • 数据库表中对身份证号进行AES加密存储,仅展示脱敏格式。
  • 日志中禁用完整身份证号,可统一替换为***
  • 前端展示时使用*隐藏中间8位。

4. 异常情况处理

  • 对于返回code != 0的情况,记录request_id以便后续排查。
  • 考虑识别置信度(接口暂未提供,可结合业务规则校验:如日期格式、金额合理性、车站名称是否在已知列表中)。

参考文档

  • 火车票识别接口文档
  • 原始Markdown文档