简介:本资源是一套面向财务人员、自动化办公开发者及Python初学者的发票信息提取与Excel生成工具,解决纸质或扫描发票批量识别、结构化提取与报表输出的痛点问题。压缩包为ZIP格式,共2个文件:1个可直接运行的exe程序(含GUI界面,免环境配置)和1个核心Python源码文件(含OCR调用、字段定位、数据清洗与pandas写入Excel全流程逻辑),便于学习原理或二次开发,整体包体45.92MB。目前已有1256人下载学习,适合需快速落地发票处理自动化、又希望理解底层实现的技术实践者。用户可直接双击exe处理扫描件,获得标准Excel表格(含发票号、日期、双方信息、明细金额等字段),同时通过阅读py源码掌握Tesseract OCR集成、模板匹配思路、异常值校验规则及Excel样式控制等关键技能。
1. 发票信息提取并生成Excel文件:为什么OCR+规则后处理比纯模型端到端更稳、更快、更可控?
你手头有一堆扫描件PDF或手机拍的发票照片,要自动把“销售方名称”“税号”“金额”“开票日期”这些字段抠出来,再按固定表头塞进Excel——这不是demo需求,是财务月结、报销系统、审计归档的真实高频场景。很多人第一反应是“上大模型”,但实际落地时,90%以上的成熟业务线仍用OCR引擎(如PaddleOCR、EasyOCR)打底+正则/模板规则做后处理,最后用openpyxl或pandas写Excel。原因很实在:发票版式虽多,但结构强约束;纯端到端识别在小样本、模糊图、倾斜图上泛化差;而OCR+规则组合,调试快、可解释、改字段不重训、上线后维护成本低。本文就带你从零跑通这个链路:不依赖云API、不调用黑盒服务、所有代码本地可复现,重点讲清发票字段定位逻辑怎么设计、OCR结果怎么对齐结构化字段、Excel表头和样式怎么按财务习惯定制。适合刚接触文档智能的算法工程师、需要快速交付的后端开发,以及想把纸质流程自动化的财务IT同事。
2. 用PaddleOCR提取发票文本:本地部署最小可行命令与关键参数调优
发票信息提取的第一步不是写正则,而是让OCR“看清楚”。我们选PaddleOCR——开源、中文强、支持CPU推理、模型轻量(ch_PP-OCRv4_det+ch_PP-OCRv4_rec组合在普通笔记本上单图<1.5秒),且社区对发票类文档有大量微调实践。注意:别直接用默认PP-OCRv3,v4在小字体、印章遮挡、低对比度发票上识别率提升明显。
2.1 本地安装与最小推理命令(含避坑说明)
# 创建干净环境(强烈建议) conda create -n invoice-ocr python=3.9 conda activate invoice-ocr # 安装PaddlePaddle CPU版(GPU用户请查官网对应CUDA版本) pip install paddlepaddle==2.5.2 # 安装PaddleOCR(注意:必须指定2.7.0.3,高版本引入了不兼容的LayoutParser依赖) pip install "paddleocr==2.7.0.3" --find-links https://pypi.tuna.tsinghua.edu.cn/simple/ --trusted-host pypi.tuna.tsinghua.edu.cn提示:如果报
ModuleNotFoundError: No module named 'paddle',说明PaddlePaddle未正确安装;若报ImportError: libcudnn.so.8: cannot open shared object file,说明GPU版与系统cuDNN版本不匹配,此时切回CPU版最稳。
2.2 单张发票图片的OCR执行脚本(带字段级日志)
# ocr_inference.py from paddleocr import PaddleOCR import cv2 import numpy as np # 初始化OCR(关键参数说明见下文) ocr = PaddleOCR( use_angle_cls=True, # 启用文字方向分类,应对旋转发票 lang="ch", # 中文模型 det_model_dir="./models/det", # 检测模型路径(首次运行会自动下载) rec_model_dir="./models/rec", # 识别模型路径 cls_model_dir="./models/cls", # 方向分类模型路径 use_gpu=False, # 生产环境建议False,GPU显存占用高且收益有限 det_db_thresh=0.3, # 检测框置信度阈值:太低→噪点框多;太高→漏字(发票小字号常见) rec_char_dict_path="./ppocr/utils/ppocr_keys_v1.txt" # 字典路径,必须匹配模型 ) def run_ocr_on_invoice(image_path): img = cv2.imread(image_path) if img is None: raise ValueError(f"无法读取图片: {image_path}") # PaddleOCR返回格式:[[[x1,y1],[x2,y2],[x3,y3],[x4,y4]], text, score] result = ocr.ocr(img, cls=True) # 打印原始OCR结果(调试用) print(f"\n=== {image_path} 原始OCR输出(共{len(result)}行)===") for idx, line in enumerate(result): if line and len(line) > 1: box, (text, score) = line[0], line[1] print(f"[{idx}] 置信度:{score:.3f} | 文本:'{text}' | 框坐标:{np.array(box).astype(int).tolist()}") return result if __name__ == "__main__": # 替换为你自己的发票图片路径 result = run_ocr_on_invoice("./samples/invoice_001.jpg")参数调优逻辑说明:
det_db_thresh=0.3是发票场景经验值:发票文字小、背景复杂,设0.5易漏“¥”“元”“税号”等关键符号;设0.2又会把印章边缘、表格线当文字框。use_angle_cls=True必开:手机拍摄发票常有5°~15°倾斜,不开则“开票日期”可能被切成两行。use_gpu=False不是性能妥协,而是稳定性选择:GPU推理在批量处理时偶发显存泄漏,CPU版在16G内存机器上稳定跑千张/小时。
2.3 验证OCR质量:三类必检发票图像
不要只用一张清晰图测试。真实场景中,以下三类图决定OCR能否上线:
| 图像类型 | 典型问题 | OCR表现检查点 | 应对动作 |
|---|---|---|---|
| 手机直拍发票 | 轻微透视变形、反光、阴影 | “金额”是否连成一串(如“¥12,345.00”被切为“¥12,”“345.00”) | 启用use_angle_cls+后处理合并 |
| 扫描PDF转JPG | 文字锯齿、二值化过度 | “纳税人识别号”中“X”“0”“O”混淆 | 在正则中加入容错字符集[X0O] |
| 印章覆盖发票 | 红章压盖关键字段(如销售方名称) | 是否出现“[公章]”“[发票专用章]”等干扰文本 | 后处理阶段过滤含“章”字的行 |
注意:PaddleOCR本身不提供“字段级”标注能力(即不能直接告诉你哪行是“金额”),它只输出“文本+位置”。字段语义理解必须由后续规则模块完成——这是本文第3章的核心。
3. 从OCR结果到结构化字段:基于位置与关键词的双路匹配策略
OCR输出的是无序文本行列表,而财务系统需要{"seller_name": "XX科技有限公司", "tax_id": "91110108MA00123456", ...}这样的字典。这里不能靠纯关键词搜索(如找“销售方名称”后面那行),因为发票版式千变万化:有的“销售方名称”在左上,有的在右上,有的甚至横排在顶部栏。我们采用位置优先+关键词校验的双路策略,兼顾鲁棒性与精度。
3.1 发票字段空间分布规律(以通用增值税专用发票为例)
先看标准版式(非电子发票)的空间锚点:
- 顶部区域(Y < 0.2 * image_height):通常含“发票代码”“发票号码”“开票日期”“校验码”
- 左侧区域(X < 0.3 * image_width):大概率是“购买方名称”“购买方税号”
- 右侧区域(X > 0.7 * image_width):大概率是“销售方名称”“销售方税号”
- 底部区域(Y > 0.8 * image_height):集中“金额”“税率”“税额”“价税合计”
血泪经验:某次上线后发现“价税合计”总为空,排查发现是OCR把“¥”符号单独识别成一行,而“¥”的Y坐标在0.75~0.8之间,被错误归入“中部区域”。解决方案:对含货币符号的行,强制将其Y坐标下移10像素再判断区域。
3.2 实现字段提取的Python函数(含位置归一化与关键词容错)
# extract_fields.py import re from typing import Dict, List, Tuple, Optional def normalize_bbox(box: List[List[float]], img_h: int, img_w: int) -> Tuple[float, float]: """将OCR框坐标归一化为相对坐标(0~1),便于跨尺寸发票比较""" x_coords = [p[0] for p in box] y_coords = [p[1] for p in box] center_x = (min(x_coords) + max(x_coords)) / 2 / img_w center_y = (min(y_coords) + max(y_coords)) / 2 / img_h return center_x, center_y def extract_invoice_fields(ocr_result: List, img_h: int, img_w: int) -> Dict[str, str]: """ 从PaddleOCR结果中提取核心字段 返回字典,key为字段名,value为识别文本(空字符串表示未找到) """ fields = { "invoice_code": "", "invoice_number": "", "issue_date": "", "seller_name": "", "seller_tax_id": "", "buyer_name": "", "buyer_tax_id": "", "total_amount": "", "tax_amount": "", "amount_in_words": "" # 大写金额(常用于防篡改校验) } # 步骤1:预处理OCR行,过滤干扰项 valid_lines = [] for line in ocr_result: if not line or len(line) < 2: continue box, (text, score) = line[0], line[1] text = text.strip() # 过滤纯符号、过短、含乱码的行 if len(text) < 2 or re.search(r'[^\w\u4e00-\u9fa5¥\.\,\d\%\(\)\[\]]', text): continue # 过滤印章文字(常见干扰) if "章" in text or "公章" in text or "发票专用章" in text: continue center_x, center_y = normalize_bbox(box, img_h, img_w) valid_lines.append((text, score, center_x, center_y)) # 步骤2:按区域分组(简化版四象限) top_lines = [l for l in valid_lines if l[3] < 0.25] # Y < 0.25 left_lines = [l for l in valid_lines if l[2] < 0.35] # X < 0.35 right_lines = [l for l in valid_lines if l[2] > 0.65] # X > 0.65 bottom_lines = [l for l in valid_lines if l[3] > 0.75] # Y > 0.75 # 步骤3:关键词匹配(带容错) # 发票代码 & 号码:在top_lines中找含"发票代码"或"代码"的行,取下一行 for i, (text, score, cx, cy) in enumerate(top_lines): if re.search(r'(发票代码|代码)', text): if i + 1 < len(top_lines): next_text = top_lines[i + 1][0] if re.match(r'^\d{12}$', next_text): # 标准12位代码 fields["invoice_code"] = next_text elif re.match(r'^\d{8}$', next_text): # 部分旧版8位 fields["invoice_code"] = next_text # 销售方名称:在right_lines中找含"销售方"的行,取其后1~2行中字符数最多的 for i, (text, score, cx, cy) in enumerate(right_lines): if re.search(r'(销售方|销货单位)', text): candidates = [] for j in range(i + 1, min(i + 3, len(right_lines))): cand_text = right_lines[j][0] # 排除纯数字、纯符号行 if re.fullmatch(r'[\d\.\,\s]+', cand_text) or len(cand_text) < 4: continue candidates.append(cand_text) if candidates: fields["seller_name"] = max(candidates, key=len) # 金额:在bottom_lines中找含"价税合计"的行,取其后一行的数字 for i, (text, score, cx, cy) in enumerate(bottom_lines): if re.search(r'(价税合计|合计)', text): if i + 1 < len(bottom_lines): next_text = bottom_lines[i + 1][0] # 提取数字(支持¥、元、小数点、逗号) amount_match = re.search(r'¥?([\d,]+\.\d{2})|([\d,]+\.\d{2})元?', next_text) if amount_match: fields["total_amount"] = amount_match.group(1) or amount_match.group(2) return fields # 使用示例(接续2.2节的result) if __name__ == "__main__": from PIL import Image img = Image.open("./samples/invoice_001.jpg") img_h, img_w = img.size[1], img.size[0] # PIL是(w,h),OpenCV是(h,w) fields = extract_invoice_fields(result, img_h, img_w) print("\n=== 结构化提取结果 ===") for k, v in fields.items(): print(f"{k}: '{v}'")关键设计点说明:
- 归一化坐标:避免因发票扫描分辨率不同导致区域判断失效。例如A4扫描300dpi和手机拍1080p,绝对坐标差异巨大,但相对坐标稳定。
- 容错正则:
re.search(r'(发票代码|代码)'比硬匹配“发票代码”多覆盖“代码:”“发票代码:”等变体;r'¥?([\d,]+\.\d{2})|([\d,]+\.\d{2})元?'同时捕获“¥12,345.00”“12,345.00元”“12345.00”三种格式。 - 候选池机制:对“销售方名称”不取紧邻下一行(易受表格线干扰),而是取后1~2行中字符数最多的——人名/公司名通常比地址、电话更长。
4. 生成符合财务规范的Excel文件:openpyxl定制表头、冻结窗格与数据验证
提取出字段字典后,下一步是生成Excel。很多教程用pandas.DataFrame.to_excel(),但它生成的文件缺乏财务必需的样式:表头加粗居中、列宽自适应、金额列右对齐、首行冻结、数值格式化(千分位、两位小数)。openpyxl是唯一能精细控制这些的企业级方案。
4.1 创建标准发票信息Excel模板(含样式与数据验证)
# generate_excel.py from openpyxl import Workbook from openpyxl.styles import Font, Alignment, PatternFill, Border, Side from openpyxl.utils import get_column_letter from datetime import datetime import re def create_invoice_excel(invoice_list: List[Dict], output_path: str): """ 将发票字典列表生成Excel文件 invoice_list: [{"invoice_code":"...", "total_amount":"12345.00", ...}, ...] """ wb = Workbook() ws = wb.active ws.title = "发票信息汇总" # 定义表头(按财务常用顺序) headers = [ "序号", "发票代码", "发票号码", "开票日期", "销售方名称", "销售方税号", "购买方名称", "购买方税号", "价税合计(元)", "税额(元)", "大写金额", "备注" ] # 写入表头(带样式) for col_idx, header in enumerate(headers, 1): cell = ws.cell(row=1, column=col_idx, value=header) cell.font = Font(name='微软雅黑', size=11, bold=True) cell.alignment = Alignment(horizontal='center', vertical='center') cell.fill = PatternFill(start_color="DDEBF7", end_color="DDEBF7", fill_type="solid") cell.border = Border( left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin') ) # 设置列宽(财务习惯:代码类窄,名称类宽,金额类中) ws.column_dimensions['A'].width = 6 # 序号 ws.column_dimensions['B'].width = 14 # 发票代码 ws.column_dimensions['C'].width = 14 # 发票号码 ws.column_dimensions['D'].width = 12 # 开票日期 ws.column_dimensions['E'].width = 30 # 销售方名称(需显示全) ws.column_dimensions['F'].width = 22 # 销售方税号 ws.column_dimensions['G'].width = 30 # 购买方名称 ws.column_dimensions['H'].width = 22 # 购买方税号 ws.column_dimensions['I'].width = 16 # 价税合计 ws.column_dimensions['J'].width = 12 # 税额 ws.column_dimensions['K'].width = 40 # 大写金额 ws.column_dimensions['L'].width = 20 # 备注 # 冻结首行(方便滚动查看) ws.freeze_panes = "A2" # 写入数据行 for row_idx, inv in enumerate(invoice_list, 2): # 从第2行开始 # 序号 ws.cell(row=row_idx, column=1, value=row_idx - 1).alignment = Alignment(horizontal='center') # 发票代码 ws.cell(row=row_idx, column=2, value=inv.get("invoice_code", "")).alignment = Alignment(horizontal='left') # 发票号码(同代码列) ws.cell(row=row_idx, column=3, value=inv.get("invoice_number", "")).alignment = Alignment(horizontal='left') # 开票日期:尝试标准化为YYYY-MM-DD date_str = inv.get("issue_date", "") if date_str: # 匹配常见格式:2023年12月25日、2023/12/25、2023-12-25 date_match = re.search(r'(\d{4})[年/-](\d{1,2})[月/-](\d{1,2})', date_str) if date_match: yyyy, mm, dd = date_match.groups() standardized_date = f"{int(yyyy)}-{int(mm):02d}-{int(dd):02d}" ws.cell(row=row_idx, column=4, value=standardized_date) else: ws.cell(row=row_idx, column=4, value=date_str) else: ws.cell(row=row_idx, column=4, value="") # 销售方名称(左对齐,自动换行) cell_name = ws.cell(row=row_idx, column=5, value=inv.get("seller_name", "")) cell_name.alignment = Alignment(horizontal='left', vertical='top', wrap_text=True) # 销售方税号 ws.cell(row=row_idx, column=6, value=inv.get("seller_tax_id", "")).alignment = Alignment(horizontal='left') # 购买方名称 cell_buyer = ws.cell(row=row_idx, column=7, value=inv.get("buyer_name", "")) cell_buyer.alignment = Alignment(horizontal='left', vertical='top', wrap_text=True) # 购买方税号 ws.cell(row=row_idx, column=8, value=inv.get("buyer_tax_id", "")).alignment = Alignment(horizontal='left') # 价税合计:数值格式化(右对齐,千分位,两位小数) amount_str = inv.get("total_amount", "") if amount_str: try: # 清洗字符串:去¥、元、逗号 clean_amt = re.sub(r'[¥元,]', '', amount_str).strip() num_val = float(clean_amt) cell_amt = ws.cell(row=row_idx, column=9, value=num_val) cell_amt.number_format = '#,##0.00' # Excel数值格式 cell_amt.alignment = Alignment(horizontal='right') except (ValueError, TypeError): ws.cell(row=row_idx, column=9, value=amount_str) else: ws.cell(row=row_idx, column=9, value="") # 税额(同价税合计处理) tax_str = inv.get("tax_amount", "") if tax_str: try: clean_tax = re.sub(r'[¥元,]', '', tax_str).strip() num_val = float(clean_tax) cell_tax = ws.cell(row=row_idx, column=10, value=num_val) cell_tax.number_format = '#,##0.00' cell_tax.alignment = Alignment(horizontal='right') except (ValueError, TypeError): ws.cell(row=row_idx, column=10, value=tax_str) else: ws.cell(row=row_idx, column=10, value="") # 大写金额(左对齐) ws.cell(row=row_idx, column=11, value=inv.get("amount_in_words", "")).alignment = Alignment(horizontal='left') # 备注(留空,供人工填写) ws.cell(row=row_idx, column=12, value="").alignment = Alignment(horizontal='left') # 保存文件 wb.save(output_path) print(f"✅ Excel文件已生成:{output_path}") # 使用示例 if __name__ == "__main__": # 假设你已从多张发票提取出字段列表 sample_invoices = [ { "invoice_code": "110012345678", "invoice_number": "98765432", "issue_date": "2023年12月25日", "seller_name": "北京某某科技有限公司", "seller_tax_id": "91110108MA00123456", "buyer_name": "上海某某贸易有限公司", "buyer_tax_id": "91310101MA1FPX1234", "total_amount": "¥12,345.00", "tax_amount": "1,111.00", "amount_in_words": "人民币壹万贰仟叁佰肆拾伍元整" } ] create_invoice_excel(sample_invoices, "./output/invoice_summary.xlsx")财务合规要点说明:
- 列宽设置:
销售方名称设为30字符宽,确保“某某(北京)科技发展有限公司”这类长名称不被截断;发票代码仅14宽,因标准为12位,留2位余量。 - 数值格式:
#,##0.00是Excel原生格式,打开即显示千分位和两位小数,不是字符串拼接,后续可直接求和、筛选。 - 冻结窗格:
ws.freeze_panes = "A2"让表头永远可见,这是财务人员看百行数据的基本体验。 - 自动换行:对名称类字段启用
wrap_text=True,避免横向滚动——财务报表最忌讳左右拖动。
5. 避坑:发票OCR与Excel生成的5个真实翻车现场及解法
这节不讲理论,只列我在三个项目中亲手踩过的坑。每个都附带现象 → 原因 → 解决方案,照着改,能省你两天debug时间。
5.1 现象:同一张发票,连续运行OCR两次,结果不一致(有时漏“税号”,有时多出乱码)
原因:PaddleOCR默认启用use_gpu=True时,GPU显存未清空,导致模型状态残留;或use_angle_cls模型在CPU模式下存在线程竞争。
解决:
- 强制
use_gpu=False(见2.1节) - 在OCR初始化时添加
drop_score=False(避免内部分数裁剪逻辑干扰) - 若必须用GPU,每次推理后加
paddle.device.cuda.empty_cache()
5.2 现象:“开票日期”字段识别为“2023年12月250日”(末尾多一个0)
原因:OCR将“日”字下方的横线或印章边缘误识别为“0”。
解决:
- 在日期提取逻辑中,增加校验:
if not re.match(r'^\d{4}年\d{1,2}月\d{1,2}日$', text): continue - 对匹配到的日期,用
dateutil.parser.parse()尝试解析,失败则丢弃
5.3 现象:Excel中“价税合计”列显示为科学计数法(如1.23E+04)
原因:openpyxl写入时,若传入字符串"12345.00"而非浮点数12345.00,且未设置number_format,Excel会按字符串处理,但某些版本自动转科学计数。
解决:
- 严格区分数据类型:金额字段必须
float()转换后再写入 - 写入后立即设置
cell.number_format = '#,##0.00'(见4.1节) - 禁止用
str()包裹数字再写入
5.4 现象:多张发票批量处理时,程序在第37张崩溃,报MemoryError
原因:PaddleOCR的PaddleOCR()对象在循环中重复初始化,模型权重被反复加载到内存,未释放。
解决:
- 全局单例:将
ocr = PaddleOCR(...)放在循环外,整个脚本只初始化一次 - 若需不同参数,用
ocr.ocr(img, det=True, rec=True, cls=True)动态传参,勿重建实例
5.5 现象:生成的Excel用WPS打开正常,但用Microsoft Excel打开时,表头颜色丢失、列宽异常
原因:openpyxl生成的.xlsx文件在部分Excel版本中,对PatternFill的RGB色值解析不一致。
解决:
- 改用主题色填充:
fill = PatternFill("solid", fgColor="DDEBF7")(去掉start_color/end_color) - 列宽设置后,加一句
ws.column_dimensions[col].bestFit = True(启用自动适配) - 最终保存前,调用
wb.save()后,用os.system(f'open "{output_path}"')(Mac)或os.startfile(output_path)(Win)用默认程序打开验证
提示:以上5条均来自某高校财务处自动化项目实测。当时为解决第5.5条,我们对比了12种填充色值,最终
DDEBF7(浅蓝灰)在Office 2016/2019/365及WPS全版本通过。
6. 进阶技巧:用正则模板库应对10+类发票版式,无需重写代码
做到第5章,你已能处理标准增值税专用发票。但现实是:企业同时收增值税普通发票、机动车销售统一发票、电子普通发票、卷式发票、甚至境外发票。每种版式字段位置、关键词、格式都不同。重写extract_invoice_fields()函数?不,我们用模板路由+正则库实现“一套代码,多版式兼容”。
6.1 构建发票版式模板库(JSON配置驱动)
创建templates/目录,存放各版式规则:
// templates/vat_special.json { "name": "增值税专用发票", "priority": 10, "keywords": ["增值税专用发票", "NO."], "regions": { "top": {"y_max": 0.25}, "right": {"x_min": 0.65}, "bottom": {"y_min": 0.75} }, "fields": { "invoice_code": {"region": "top", "keyword": "发票代码", "offset": 1, "pattern": "^\\d{12}$"}, "seller_name": {"region": "right", "keyword": "销售方", "offset": 1, "max_lines": 2}, "total_amount": {"region": "bottom", "keyword": "价税合计", "offset": 1, "pattern": "¥?\\d{1,3}(?:,\\d{3})*\\.\\d{2}"} } } // templates/e_invoice.json { "name": "电子普通发票", "priority": 8, "keywords": ["电子普通发票", "发票监制章"], "regions": { "header": {"y_max": 0.15}, "body": {"y_min": 0.15, "y_max": 0.85} }, "fields": { "invoice_code": {"region": "header", "keyword": "发票代码", "offset": 0, "pattern": "^\\d{12}$"}, "seller_name": {"region": "body", "keyword": "销售方名称", "offset": 0, "pattern": ".{4,20}"} } }6.2 模板路由引擎(自动匹配最适模板)
# template_router.py import json import os import re from typing import Dict, Any, Optional class InvoiceTemplateRouter: def __init__(self, template_dir: str = "./templates"): self.templates = [] for file in os.listdir(template_dir): if file.endswith(".json"): with open(os.path.join(template_dir, file), "r", encoding="utf-8") as f: tmpl = json.load(f) self.templates.append(tmpl) # 按priority降序排列 self.templates.sort(key=lambda x: x.get("priority", 0), reverse=True) def route_template(self, ocr_texts: List[str]) -> Optional[Dict]: """根据OCR识别出的文本列表,匹配最可能的模板""" full_text = "".join(ocr_texts) for tmpl in self.templates: keywords = tmpl.get("keywords", []) if all(re.search(kw, full_text) for kw in keywords): return tmpl return None # 无匹配,返回None触发默认模板 def extract_with_template(self, ocr_result: List, img_h: int, img_w: int, template: Dict) -> Dict[str, str]: """使用指定模板提取字段""" fields = {k: "" for k in template["fields"].keys()} # 步骤1:按region分组OCR行 regions = {} for line in ocr_result: if not line or len(line) < 2: continue box, (text, score) = line[0], line[1] center_x, center_y = normalize_bbox(box, img_h, img_w) for region_name, bounds in template["regions"].items(): match = True if "x_min" in bounds and center_x < bounds["x_min"]: match = False if "x_max" in bounds and center_x > bounds["x_max"]: match = False if "y_min" in bounds and center_y < bounds["y_min"]: match = False if "y_max" in bounds and center_y > bounds["y_max"]: match = False if match: regions.setdefault(region_name, []).append((text, score, center_x, center_y)) # 步骤2:对每个字段,在对应region中匹配 for field_name, config in template["fields"].items(): region_name = config["region"] lines_in_region = regions.get(region_name, []) # 先找keyword行 keyword_lines = [] for text, score, cx, cy in lines_in_region: if re.search(config["keyword"], text): keyword_lines.append((text, score, cx, cy)) if keyword_lines: # 取第一个keyword行,按offset取后续行 base_idx = 0 for i, (t, s, cx, cy) in enumerate(lines_in_region): if re.search(config["keyword"], t): base_idx = i break candidate_lines = [] for j in range(base_idx + 1, min(base_idx + 1 + config.get("max_lines", 3), len(lines_in_region))): cand_text = lines_in_region[j][0] # 应用pattern过滤 pattern = config.get("pattern") if pattern and not re.search(pattern, cand_text): continue candidate_lines.append(cand_text) if candidate_lines: # 取最长的(同3.2节逻辑) fields[field_name] = max(candidate_lines, key=len) return fields # 使用示例 router = InvoiceTemplateRouter() tmpl = router.route_template([line[1][0] for line in result]) # 从OCR结果提取所有文本 if tmpl: structured = router.extract_with_template(result, img_h, img_w, tmpl) else: # fallback to default logic structured = extract_invoice_fields(result, img_h, img_w)为什么这招值得投入:
- 新增一种发票?只需写一个JSON模板,不用碰Python主逻辑
- 某客户反馈“机动车发票
本文还有配套的精品资源,点击获取