想在现有ERP/财务系统里快速集成一个OCR功能的兄弟们看的。
别去折腾什么 Tesseract 了,对于中文发票这种复杂排版,PaddleOCR 的预训练模型(尤其是 V4 版本)已经是目前开源界的“版本答案”。
不多废话,咱们的目标是:10分钟内,在一个 Python 环境里把发票图片变成结构化的 JSON 数据。
1. 环境准备:别在依赖上浪费时间
假设你机器上已经有了 Python 3.8+。 如果要在生产环境跑,强烈建议用 Linux + Docker,但本地调试的话,Windows/Mac 也行。
首先装 PaddlePaddle 核心库。
- 如果你有 N 卡(GPU),一定要装 GPU 版,推理速度快几十倍:Bash
# 这里的 cuda11.7 根据你服务器的 CUDA 版本改 python -m pip install paddlepaddle-gpu==2.6.1.post117 -f https://www.paddlepaddle.org.cn/whl/linux/mkl/avx/stable.html - 如果是 CPU 环境(办公电脑测试):Bash
python -m pip install paddlepaddle==2.6.1
接着装 PaddleOCR 的 whl 包:
Bash
pip install "paddleocr>=2.7.0"
pip install opencv-python-headless # 加上这个防止cv2依赖报错
2. 核心思路:通用模型 + 正则暴力提取
做过发票识别的都知道,发票虽然有标准(专票、普票、卷票),但版式其实挺乱的。 “10分钟方案”的策略是:不重新训练模型(KIE 关键信息提取模型训练周期太长),直接用 PaddleOCR 的通用模型把所有文字“扫”出来,然后通过关键词定位和**正则(Regex)**来提取我们需要的信息(如发票代码、金额、日期)。
这种方式虽然看起来“土”,但在 90% 的标准增值税发票场景下,鲁棒性极高,而且不需要标注数据。
3. 代码实现
新建一个 invoice_ocr.py,直接把下面代码粘进去。
这段代码做了三件事:
- 加载模型:开启
use_angle_cls,因为用户上传的发票经常是歪的。 - 全图识别:拿到所有的文本块。
- 后处理(Post-processing):这是重点,用正则把非结构化文本变成 Key-Value。
Python
import re
from paddleocr import PaddleOCR
import cv2
import numpy as np
class InvoiceParser:
def __init__(self, use_gpu=False):
# 初始化 PaddleOCR
# lang='ch' 会自动下载并加载 ch_PP-OCRv4_xx 模型
# use_angle_cls=True 非常重要,防止发票倒置或倾斜
print("正在加载 PaddleOCR 模型,初次运行需要下载模型权重...")
self.ocr = PaddleOCR(use_angle_cls=True, lang="ch", use_gpu=use_gpu, show_log=False)
def parse_invoice(self, img_path):
# 1. 执行 OCR 识别
# result 结构: [[[[x1,y1],[x2,y2]...], (text, confidence)], ...]
result = self.ocr.ocr(img_path, cls=True)
if not result or result[0] is None:
return {"error": "未检测到文字"}
# 2. 将所有识别出的文本拼接成列表,方便后续处理
# 过滤掉置信度低于 0.6 的噪声
txt_list = [line[1][0] for line in result[0] if line[1][1] > 0.6]
print(f"--- 原始OCR文本 ({len(txt_list)} 行) ---")
# 实际调试时可以打印出来看看
# print(txt_list)
# 3. 关键信息提取 (基于规则和正则)
extract_data = {
"invoice_code": None, # 发票代码
"invoice_number": None, # 发票号码
"date": None, # 开票日期
"amount": None, # 价税合计
"seller": None # 销售方
}
# 简单的状态机或正则匹配
# 注意:实际生产中,这里的正则库需要不断完善
for i, txt in enumerate(txt_list):
txt = txt.replace(" ", "").replace(":", ":") # 预处理符号
# --- 提取发票号码 (通常是 8位 或 20位数字) ---
if "发票号码" in txt:
# 尝试在当前行找
num = re.findall(r'\d{8,20}', txt)
if num:
extract_data["invoice_number"] = num[0]
# 经常出现 "发票号码" 和 具体数字 分在两行的情况,往后找一行
elif i + 1 < len(txt_list):
next_txt = txt_list[i+1]
num_next = re.findall(r'\d{8,20}', next_txt)
if num_next:
extract_data["invoice_number"] = num_next[0]
# --- 提取发票代码 (通常 10-12位) ---
if "发票代码" in txt and not extract_data["invoice_code"]:
code = re.findall(r'\d{10,12}', txt)
if code:
extract_data["invoice_code"] = code[0]
elif i + 1 < len(txt_list):
code_next = re.findall(r'\d{10,12}', txt_list[i+1])
if code_next:
extract_data["invoice_code"] = code_next[0]
# --- 提取金额 (价税合计) ---
# 匹配逻辑:找 "小写" 或者 "¥" 或者 "¥" 附近的数字
if ("小写" in txt or "¥" in txt or "¥" in txt) and "合计" in txt:
# 匹配金额格式,如 123.00, 1,234.56
amount = re.findall(r'\d{1,3}(?:,\d{3})*(?:\.\d+)?', txt)
if amount:
extract_data["amount"] = amount[-1] # 通常取最后一个匹配到的数字
# --- 提取日期 ---
# 格式多样:2023年01月01日 或 2023-01-01
if "年" in txt and "月" in txt and "日" in txt:
extract_data["date"] = txt # 简单处理,直接存文本
return extract_data
# --- 测试入口 ---
if __name__ == "__main__":
# 替换成你本地的一张发票图片路径
img_file = "test_invoice.jpg"
parser = InvoiceParser(use_gpu=False) # 如果有显卡改成 True
try:
data = parser.parse_invoice(img_file)
print("\n=== 识别结果 ===")
print(data)
except Exception as e:
print(f"处理失败: {e}")
4. 这里的几个“坑”和“填坑技巧”
代码跑通不难,难的是在业务里跑稳。以下是几个技术人员必须注意的点:
- 图片分辨率(Input Shape): 发票通常是高频细节很多的图。如果上传的是压缩得很厉害的缩略图,PaddleOCR 默认的 resize 策略可能会导致数字(特别是小数点)丢失。
- 解决:在初始化
PaddleOCR时,可以调整det_limit_side_len参数。默认是 960,如果你的图片是 2K/4K 的高清扫描件,建议调大到 1280 甚至 1600,否则大图会被强行缩小,导致小字看不清。 self.ocr = PaddleOCR(..., det_limit_side_len=1600)
- 解决:在初始化
- 红章干扰: 发票上那个鲜红的公章经常会盖在“价税合计”或者“开票人”上面,导致 OCR 识别乱码。
- 解决:如果发现红章遮挡严重,可以在送入 OCR 之前用 OpenCV 做一个颜色过滤(HSV 空间),把红色通道淡化或者直接二值化处理掉。这比换更强的模型要经济实惠得多。
- 正则的局限性: 上面的代码用的是基于位置和关键词的“软匹配”。如果是处理海量的、版式完全固定的发票(比如只处理某一家供应商的),建议用坐标截图的方式:直接在该区域切片(Crop),然后只对这个切片做 OCR,速度快且准。
5. 下一步优化
如果你要上线这套东西,这个脚本只能算 Demo。下一步你需要做的是:
- 服务化:用 FastAPI 包一层,把
ocr对象的初始化放在startup事件里(千万别每来一个请求 load 一次模型,内存会爆,耗时也伤不起)。 - KIE (Key Information Extraction):如果你发现正则写得想吐,这时候再去研究 PaddleOCR 文档里的
PP-Structure或者LayoutXLM。那是专门用来解决“不知道哪个数字是金额”的高级玩法,但也意味着你需要标注数据来微调了