手把手教你搭建一个发票OCR识别系统,自动提取金额、日期、商户信息并分类归档。
项目背景
每个月报销对账,你是不是也经历过这样的场景:翻出几十张发票照片,手动录入日期、金额、商户名,花上两三个小时。
我试过手动输入,也用过微信自带的扫码功能,但后者只支持增值税电子发票二维码,纸质发票和手写收据完全没辙。
后来发现PaddleOCR对中文票据的识别效果远超Tesseract,而且开源免费。今天就来搭一个完整的发票OCR识别系统。
技术选型
| 组件 | 选择 | 理由 |
|---|---|---|
| OCR引擎 | PaddleOCR | 中文识别准确率最高,支持多语言 |
| 语言 | Python 3.10+ | 生态丰富,开发效率高 |
| 数据存储 | SQLite | 轻量级,零配置 |
| 图像处理 | Pillow | 裁剪、旋转、去噪 |
| 输出格式 | JSON + Excel | 方便导入财务系统 |
实现步骤
第一步:安装依赖
pip install paddlepaddle paddleocr pillow openpyxl
如果是CPU环境,装paddlepaddle就够了。有GPU的话装paddlepaddle-gpu,识别速度能快5倍。
第二步:编写核心识别脚本
创建 invoice_ocr.py:
import os
import json
import sqlite3
from pathlib import Path
from paddleocr import PaddleOCR
from PIL import Image, ImageFilter
# 初始化OCR引擎(首次运行会自动下载模型)
ocr = PaddleOCR(
use_angle_cls=True, # 开启文字方向分类
lang='ch', # 中文模式
show_log=False,
det_db_box_thresh=0.3 # 降低检测阈值,提高召回率
)
# 创建本地数据库
def init_db(db_path='invoices.db'):
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS invoices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
image_path TEXT,
raw_text TEXT,
amount REAL,
date TEXT,
merchant TEXT,
category TEXT,
confidence REAL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
return conn
# 图像预处理:旋转矫正 + 去噪
def preprocess_image(image_path):
img = Image.open(image_path)
# 如果图片是横向的,尝试旋转
if img.width > img.height * 1.5:
img = img.rotate(-90, expand=True)
# 转灰度图增强对比度
img = img.convert('L')
img = img.filter(ImageFilter.SHARPEN)
out_path = image_path.replace('.jpg', '_processed.jpg').replace('.png', '_processed.png')
img.save(out_path)
return out_path
# 从OCR结果中提取关键字段
def extract_fields(ocr_result):
if not ocr_result or ocr_result == []:
return {}
# 拼接所有识别到的文本
all_text = []
max_confidence = 0
count = 0
for line in ocr_result[0]:
text = line[1][0]
confidence = line[1][1]
all_text.append(text)
max_confidence += confidence
count += 1
full_text = '\n'.join(all_text)
avg_confidence = max_confidence / max(count, 1)
# 简单正则提取金额(支持多种格式)
import re
amount_match = re.search(r'([¥¥]?\s*\d{1,3}(?:,\d{3})*(?:\.\d{1,2})?)', full_text)
amount = float(amount_match.group(1).replace(',', '').replace('¥', '').replace('¥', '')) if amount_match else None
# 提取日期(YYYY-MM-DD或YYYY/MM/DD格式)
date_match = re.search(r'(\d{4}[-/.]\d{1,2}[-/.]\d{1,2})', full_text)
date = date_match.group(1) if date_match else None
# 简单分类逻辑
category = classify_invoice(full_text)
return {
'raw_text': full_text,
'amount': amount,
'date': date,
'merchant': extract_merchant(full_text),
'category': category,
'confidence': round(avg_confidence, 3)
}
# 根据关键词判断发票类别
def classify_invoice(text):
keywords = {
'餐饮': ['餐厅', '火锅', '烧烤', '咖啡', '奶茶', '快餐', '酒楼'],
'交通': ['出租车', '滴滴', '地铁', '公交', '高铁', '飞机', '打车'],
'住宿': ['酒店', '宾馆', '民宿', '客栈', '旅馆'],
'办公用品': ['文具', '打印', '复印', '纸张', '墨盒', '电脑', '键盘'],
'通讯': ['话费', '流量', '宽带', '电信', '移动', '联通'],
'其他': []
}
for cat, words in keywords.items():
for word in words:
if word in text:
return cat
return '其他'
# 尝试从文本中提取商户名(简化版)
def extract_merchant(text):
lines = text.split('\n')
# 通常第一行是商户名称
if lines:
return lines[0].strip()
return '未知'
# 处理单张图片
def process_image(image_path, conn):
print(f"正在处理: {image_path}")
# 预处理
processed_path = preprocess_image(image_path)
# OCR识别
result = ocr.ocr(processed_path, cls=True)
# 提取字段
fields = extract_fields(result)
fields['image_path'] = image_path
# 存入数据库
cursor = conn.cursor()
cursor.execute('''
INSERT INTO invoices (image_path, raw_text, amount, date, merchant, category, confidence)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (
image_path,
fields.get('raw_text', ''),
fields.get('amount'),
fields.get('date'),
fields.get('merchant', ''),
fields.get('category', '其他'),
fields.get('confidence', 0)
))
conn.commit()
print(f" 金额: ¥{fields.get('amount', 'N/A')}")
print(f" 日期: {fields.get('date', 'N/A')}")
print(f" 商户: {fields.get('merchant', 'N/A')}")
print(f" 类别: {fields.get('category', 'N/A')}")
print(f" 置信度: {fields.get('confidence', 0)}")
return fields
# 批量处理目录下的所有图片
def batch_process(image_dir, db_path='invoices.db'):
conn = init_db(db_path)
image_extensions = ('.jpg', '.jpeg', '.png', '.bmp', '.tiff')
images = [
f for f in os.listdir(image_dir)
if f.lower().endswith(image_extensions)
]
if not images:
print(f"在 {image_dir} 中未找到图片文件")
return
print(f"找到 {len(images)} 张图片,开始处理...\n")
results = []
for img in sorted(images):
img_path = os.path.join(image_dir, img)
result = process_image(img_path, conn)
results.append(result)
print()
# 导出Excel
export_to_excel(results, db_path)
conn.close()
print(f"\n处理完成!共识别 {len(results)} 张发票")
# 导出为Excel
def export_to_excel(results, db_path):
try:
import openpyxl
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "发票数据"
headers = ['图片路径', '原始文本', '金额', '日期', '商户', '类别', '置信度']
ws.append(headers)
for r in results:
ws.append([
r.get('image_path', ''),
r.get('raw_text', ''),
r.get('amount', ''),
r.get('date', ''),
r.get('merchant', ''),
r.get('category', ''),
r.get('confidence', '')
])
output_path = db_path.replace('.db', '_export.xlsx')
wb.save(output_path)
print(f"已导出Excel: {output_path}")
except ImportError:
print("openpyxl未安装,跳过Excel导出")
if __name__ == '__main__':
import sys
if len(sys.argv) > 1:
target = sys.argv[1]
if os.path.isdir(target):
batch_process(target)
elif os.path.isfile(target):
conn = init_db()
process_image(target, conn)
conn.close()
else:
print("用法:")
print(" python invoice_ocr.py <图片路径> # 处理单张图片")
print(" python invoice_ocr.py <图片目录> # 批量处理")
第三步:运行测试
准备几张发票照片,放在 test_invoices/ 目录下:
mkdir test_invoices
# 把发票照片复制进去
python invoice_ocr.py test_invoices/
首次运行会下载PaddleOCR模型(约200MB),之后都会走缓存。
第四步:查看结果
处理完成后会生成两个文件:
invoices.db— SQLite数据库,记录所有识别结果invoices_export.xlsx— Excel表格,可以直接导入财务系统
运行效果
识别一张普通增值税发票的典型输出:
正在处理: test_invoices/receipt_001.jpg
金额: ¥386.50
日期: 2026-06-15
商户: 星巴克咖啡有限公司
类别: 餐饮
置信度: 0.892
准确率方面,PaddleOCR在清晰拍摄的发票上金额识别准确率超过95%。日期和商户名的准确率稍低一些,大约在80%-90%之间,主要受拍摄角度和光线影响。
优化方向
这个基础版本已经能用了,但想更实用,还可以加几样东西:
二维码解析:增值税电子发票都有二维码,用
qrcode库解析后直接拿到结构化数据,准确率接近100%。人工校对界面:用Flask搭一个简单的Web页面,展示识别结果供人工确认和修改。
定期批量任务:配合cron定时扫描邮箱附件或网盘文件夹,自动入库。
月度报表:按类别汇总支出,生成饼图和趋势图,用matplotlib画出来。
移动端集成:做成微信小程序,拍照即识别,方便出差时随时录入。
发票对账这件事,机械重复但不可或缺。花半天时间搭个自动化工具,以后每个月能省下2小时以上。
你的公司用什么方式管理发票?有没有遇到过类似的痛点?欢迎在评论区聊聊。
这篇文章对你有帮助吗?