05 - 百度 OCR 字段映射¶
接口:百度智能云「增值税发票识别」(vat_invoice),支持普票/专票/全电发票/卷票/区块链发票全字段结构化识别。本文档定义 OCR JSON → invoice 表 → StandardInvoice 的完整映射。
官方文档:增值税发票识别。多票种混拍场景可后续升级「智能财务票据识别」(
vat_invoice为其子类型type,字段结构一致)。
1. 字段映射总表¶
OCR 结果位于 words_result 对象内(金额/日期均为字符串,需清洗)。
1.1 票面标识¶
| OCR 字段 | invoice 字段 | 类型 | 说明 |
|---|---|---|---|
InvoiceNum |
invoice_num |
TEXT NOT NULL | 发票号码(业务防重键之一) |
InvoiceCode |
invoice_code |
TEXT | 发票代码,全电票为空 |
InvoiceType |
invoice_type |
TEXT | 普通发票/专用发票/电子普通发票/电子专用发票/通行费电子普票/区块链发票/通用机打电子发票/电子发票(专用发票)/电子发票(普通发票) |
InvoiceTypeOrg |
invoice_type_org |
TEXT | 发票名称,如"浙江增值税专用发票" |
ServiceType |
service_type |
TEXT | 百度现成的消费类型:餐饮/电器设备/通讯/服务/日用品食品/医疗/交通/其他 → 分类器链兜底环(直映 + 别名归一到科目,规则未命中时生效) |
InvoiceTag |
invoice_tag |
TEXT | 左上角标志:通行费/销项负数/代开/收购/成品油/其他 |
InvoiceDate |
invoice_date |
TEXT | "2016年06月02日" → 清洗为 YYYY-MM-DD |
CheckCode |
check_code |
TEXT | 校验码(专票无) |
InvoiceNumDigit |
invoice_num_digit |
TEXT | 数电票号码(密码区,纸质数电票才有) |
忽略:InvoiceCodeConfirm/InvoiceNumConfirm(辅助校验码)、Password(密码区)、SheetNum(联次)、Agent、OnlinePay。
1.2 购销双方¶
| OCR 字段 | invoice 字段 | 说明 |
|---|---|---|
SellerName |
seller_name |
销售方名称(重命名用"滴滴出行"式短名可再截断) |
SellerRegisterNum |
seller_register_num |
销售方纳税人识别号 |
SellerAddress |
seller_address |
销售方地址电话 |
SellerBank |
seller_bank |
销售方开户行及账号 |
PurchaserName |
purchaser_name |
购买方名称(可校验"抬头是否本公司") |
PurchaserRegisterNum |
purchaser_register_num |
购买方识别号 |
忽略:PurchaserAddress/PurchaserBank/Province/City(报销场景无用,原始 JSON 里都有)。
1.3 金额(全部 ×100 转分存储)¶
| OCR 字段 | invoice 字段 | 说明 |
|---|---|---|
AmountInFiguers |
amount_fen |
价税合计(小写),如 "100000.00" → 10000000 |
TotalAmount |
total_amount_fen |
合计金额(不含税)——公司模板要求价税分离时用 |
TotalTax |
total_tax_fen |
合计税额 |
AmountInWords |
amount_in_words |
价税合计(大写),对账校验用(大写与小写应一致,不一致标记疑似识别错误) |
1.4 明细行(array[{row, word}],按 row 对齐)¶
| OCR 字段 | 去向 | 说明 |
|---|---|---|
CommodityName |
items_raw(按 row 排序拼接,; 分隔)+ commodity_count |
规则引擎主要输入,如 *信息技术服务*软件服务费 |
CommodityAmount/CommodityTax/CommodityTaxRate/CommodityNum/CommodityUnit/CommodityPrice |
仅存 ocr_raw_json |
需要行结构时经 core/goods.py::parse_goods_lines 重建(见下),不另建表 |
物品行重建(core/goods.py::parse_goods_lines(ocr_raw_json) -> list[GoodsLine]):UI 明细展开与后续 Excel 明细 sheet 消费的展示层函数——以 CommodityName 的 row 为主序,从各 Commodity 平行数组按 row 对齐拼成 GoodsLine(row, name, num, unit, price, amount, tax_rate);金额字段保留 OCR 原始字符串透传(仅展示,不参与运算,红线 1)。raw json 是唯一数据源,落库后任何字段调整免重放重付费。
2. 清洗规则¶
from decimal import Decimal, InvalidOperation
from datetime import date
def to_fen(s: str | None) -> int:
"""'100000.00' → 10000000;空/脏值 → 0 并打低置信标记"""
if not s:
return 0
try:
return int(Decimal(s.strip().replace("¥", "").replace(",", "")) * 100)
except InvalidOperation:
return 0
def to_date(s: str | None) -> str | None:
"""'2016年06月02日' → '2016-06-02'(ISO)"""
if not s:
return None
for fmt in ("%Y年%m月%d日", "%Y-%m-%d", "%Y/%m/%d"):
try:
return datetime.strptime(s, fmt).date().isoformat()
except ValueError:
continue
return None
def join_items(arr: list[dict]) -> str:
"""CommodityName 数组按 row 排序拼接:'*餐饮*餐费;*服务*会议费'"""
return ";".join(x["word"] for x in sorted(arr or [], key=lambda x: int(x.get("row", 0))))
一致性校验:入库前核对 amount_fen == total_amount_fen + total_tax_fen、大写金额(中文数字解析)与小写一致;不一致时正常入库但置 is_manual_edited 待复核标记,UI 高亮提示。
3. StandardInvoice 领域模型¶
Provider 产出、规则引擎消费、落库前的统一中间表示:
from dataclasses import dataclass, field
@dataclass
class StandardInvoice:
# 防重与溯源
file_hash: str = ""
ocr_raw_json: str = "" # 完整原始返回
source_path: str = ""
# 票面标识
invoice_num: str = ""
invoice_code: str = "" # 全电票为空
invoice_type: str = ""
service_type: str = "" # 百度消费类型,分类器链兜底环(直映 + 别名归一)
invoice_date: str | None = None # ISO YYYY-MM-DD
# 购销双方
seller_name: str = ""
seller_register_num: str = ""
purchaser_name: str = ""
# 金额(分)
amount_fen: int = 0 # 价税合计
total_amount_fen: int = 0 # 不含税
total_tax_fen: int = 0 # 税额
# 明细
items_raw: str = ""
commodity_count: int = 0
# 规则引擎回填
category: str = "未分类"
# 一致性校验结果
confidence_flags: list[str] = field(default_factory=list) # 如 ['amount_mismatch']
4. Provider 接口¶
class InvoiceProvider(ABC):
"""发票解析统一接口——百度实现为 BaiduProvider,未来接其他厂商实现同类"""
@abstractmethod
def parse(self, file_path: str) -> StandardInvoice: ...
BaiduProvider 职责:读文件 → access_token 鉴权(缓存至过期)→ POST vat_invoice → 限流退避(429/QPS 超限)→ 按本文档 §1~§2 映射清洗 → 产出 StandardInvoice。字段新增/调整只改 Provider 映射与 ocr_raw_json 重放,不动业务层、不重新付费。
TS 实现落在 packages/providers(StandardInvoice 24 字段与上文一一对应;本文 Python 片段为设计原型)。错误契约:Provider 以异常类型表达失败语义——额度耗尽(百度 17/19)→ QuotaExceededError、QPS 限流(18/429)→ RateLimitedError、票据不匹配(282103)→ NotAnInvoiceError、key 无效 → ProviderAuthError;worker 消费侧按类型映射错误码与退避策略(见 03-architecture.md §5.2)。
4.1 百度 key 号池¶
单个免费 key 的日/月额度与 QPS(2) 都可能成为瓶颈,BaiduProvider 内置多 key 号池做负载均衡(InvoiceProvider 接口与 worker 消费侧零改动):
- 配置:
BAIDU_OCR_API_KEYS/BAIDU_OCR_SECRET_KEYS逗号分隔、按位置配对;只配单数BAIDU_OCR_API_KEY/SECRET_KEY时退化为单 key 池(向后兼容)。两列表长度不一致视为配置错误(ProviderAuthError终态),不静默丢弃。 - 调度:round-robin 轮询;每个凭据独立 token 缓存。池大小 N 时理论 QPS = 2N。
- 熔断(错误码驱动,进程内存态,重启无害):17 日额度 → 冷却至次日 00:01(本地时区);19 月额度 → 冷却至下月 1 日 00:01;18/HTTP 429 → 60s;key 无效 → 10min(防每请求空试坏 key)。
- 换 key 重试:凭据侧错误(额度/限流/鉴权)自动换池内下一个未冷却 key,单次 parse 至多试完整池;内容性问题(282103)不换 key——换 key 结果相同,白烧额度。
- 全池耗尽聚合:含限流 →
RateLimitedError(可重试优先,稍后有 key 冷却结束恢复);否则含额度耗尽 →QuotaExceededError(终态);全为鉴权失败 →ProviderAuthError(终态)。 - 安全(红线 4):日志与错误消息只出现
baidu-key#N序号,key 值不进任何错误文本。 - 用户自备 key(docs/03 §5.7):用户在设置页填写的百度 Key 经信封加密落库(
ocr_credential),worker 以服务角色解密后叠加到环境号池之后,标号baidu-key#user:<userId>。号池 = 环境 key + 全部启用的用户 key,round-robin 且对所有用户共享(填写者的额度也供他人使用)。
5. Excel 导出取数(价税分离模板)¶
| 报销单列 | 取数 |
|---|---|
| 日期 | invoice.invoice_date |
| 金额(不含税) | total_amount_fen / 100 |
| 税额 | total_tax_fen / 100 |
| 价税合计 | amount_fen / 100 |
| 发票号码 | invoice_num(+ invoice_code 非空时) |
| 附件 | attachment 表按 invoice_id 汇总文件名清单 |
6. 附件子管道:通用文字识别(general_basic)¶
订单截图、行程单等非发票文件由 vat_invoice 拒识(282103 模板不匹配)后转此接口(管道见 03-architecture.md §3.6)。与 vat_invoice 免费额度独立(各 1000 次/月、QPS=2),鉴权 token 缓存与 QPS 退避复用 BaiduProvider。个人免费层:每个用户每月 10 次(按上传文件计),耗尽且未自备 key 时上传被拒(QUOTA_EXCEEDED_OCR)。
- 接口:
POST /rest/2.0/ocr/v1/general_basic,参数image(图片 base64)或pdf_file(PDF base64,仅识别首页即够匹配信号)。 - 返回:
{words_result: [{words: "…"}], words_result_num, log_id},按行拼接为attachment.ocr_raw_text全文留存(重匹配不再付费)。
6.1 匹配信号提取(全文 → 结构化信号)¶
| 信号 | 提取规则 | 用途 |
|---|---|---|
| 金额 | 正则 (实付|实付款|合计|总计|订单金额)[^0-9]{0,6}([0-9,]+\.?[0-9]{0,2});多候选优先"实付",否则优先取带小数点形态的金额(规避 OCR 吃掉小数点造成的 ×100 假值),全为整数时取最大值;带 ¥/¥ 前缀的金额一并收集。小数形态"X.XX元"后缀值一律并入锚点池:OCR 吃掉小数点时关键词锚点会 ×100(真机样例:高德行程单"合计1863元"实为 18.63,真值"18.63元"在表格行中兜回锚点);要求带小数点以避开"1个行程"类整数噪声。转分存储。提取前先剔除折扣段:(共减|优惠|立减|红包|折扣)[¥¥]?金额(真机样例:淘宝订单"实付款共减¥2.86"是优惠额,实付额在下一行"¥19.14") |
必要锚点:与 invoice.amount_fen 精确相等才进候选集 |
| 金额(文件名) | 原文件名单独一遍宽松金额提取(只对文件名生效,不改全文严格规则):被非数字边界包住的两位小数,兼容裸小数 20251207_94.81_行程单.pdf 与 X.XX元 【妥妥E行-76.91元-1个行程】…。文件名金额并入锚点池,并优先作为主锚点(平台导出名比 OCR 可靠)。真机样例:OCR 把"合计76.91元"识成整数 7691(假锚点 769100),真值只有文件名保留 |
必要锚点(补 OCR 丢小数时的真值) |
| 商户 | 全文与候选发票 seller_name 双向子串匹配(剥离"有限公司/个体工商户"等后缀干扰) |
消歧 +3 |
| 商户(容错) | 卖方 3-gram 与全文 3-gram 编辑距离 ≤1 命中 ≥2 个 → 弱命中(真机样例:高德行程单"鞍马出行"被 OCR 成"酸马出行");双 gram 门槛避开"电子商务"类通用词误报 | 消歧 +2 |
| 日期 | \d{4}[年/-]\d{1,2}[月/-]\d{1,2},与 invoice_date 差 ≤3 天 |
消歧 +2 |
| 日期(容错) | OCR 年份识别错(2026→2028)但月日相同且年差 ≤4 | 消歧 +1 |
| 商品词 | 候选发票 items_raw 品名(去 *分类* 前缀)在全文中的命中数 ≥1 |
消歧 +2 |
| 文件名(消歧) | 附件原名与候选发票源文件名去扩展名及平台模板词(发票/行程单/订单/截图/高德打车/AMAP 等)后,核心实体片段(含商户与金额)互相包含,且两侧长度均 ≥5 | 消歧 +3 |
金额提取失败 → 直接 unmatched(无锚点不猜)。正则集中在 core/matcher.ts 信号提取区,与 providers/ 的日期/金额清洗同一红线:解析失败不抛错、只降级为信号缺失。
行程单等票据还可经结构化财务票据端点(如
online_taxi_itinerary)直接得到城市/起终点/每段金额,作为上述全文 + 正则的补充与增强(金额一并进锚点池,城市/起终点用于命名与规则)。统一文档模型、端点注册表与票种归类见 12-receipt-ocr.md。
附件类型识别(classify_attachment,落 attachment.attachment_type):关键词命中行程单(行程单/ITINERARY/行程时间/行程人/网约车/打车)→ itinerary;订单(订单号/订单详情/下单时间/订单金额/物流详情)→ order_screenshot;票面(增值税/发票代码/发票号码/机器编号/开票日期)→ proof;全不命中落 other 不猜。优先级行程单 > 订单 > 票面(高德行程单含"开6%增值税专票"推广语,须先判行程单)。