引言:当报销流程遇见rpa
在企业日常运营中,发票报销是一个典型的高频、低效、易出错场景。财务人员每天要面对大量纸质或电子发票:人工录入金额、日期、供应商到excel表格,再逐一发送给审核人。这个过程不仅耗时,而且容易因为手误导致数据错误。
那么,能否让一个“机器人”自动完成这些工作?答案是肯定的。本文将带领读者从零开始,打造一个智能票据处理机器人。它能够:
- 自动扫描指定文件夹中的发票图片(jpg/png/pdf);
- 使用ocr技术提取发票的关键信息(金额、日期、供应商名称);
- 将提取的数据自动填写到excel报销单模板中;
- 最后通过邮件将填写好的excel文件发送给指定的审核人。
本文不仅会给出完整的代码实现,还会深入讲解ocr选型与优化、excel自动化操作、邮件发送以及异常处理与日志等rpa核心能力,帮助读者构建一套健壮、可维护的自动化解决方案。
一、项目概述与技术选型
1.1 业务流程设计
智能票据处理机器人的工作流如下:
[监控文件夹] → [发现新发票图片] → [ocr提取关键字段] → [写入excel报销单] → [保存excel文件] → [发送邮件给审核人] → [归档或移动原文件]
这是一个典型的事件驱动型rpa,可以通过定时任务(如每5分钟扫描一次)或文件系统监控(watchdog)来触发。
1.2 技术栈选择
| 功能模块 | 技术方案 | 理由 |
|---|---|---|
| ocr识别 | paddleocr(轻量级中文模型) | 中文发票识别准确率高,支持印刷体,无需复杂预处理 |
| excel操作 | openpyxl | 支持.xlsx格式,功能强大,可读写单元格、样式、公式 |
| 邮件发送 | smtplib + email | python标准库,稳定可靠,支持附件 |
| 文件监控 | watchdog + 定时轮询(简化版) | 生产环境可用watchdog,本文采用轮询降低复杂度 |
| 配置管理 | dotenv + 环境变量 | 敏感信息不硬编码 |
| 日志 | logging | 标准库,便于调试和审计 |
| 图像预处理 | opencv (cv2) + pil | 辅助处理倾斜、噪点等(如需) |
1.3 环境准备
# 安装依赖 pip install paddlepaddle paddleocr openpyxl pillow opencv-python python-dotenv # 邮件发送使用标准库,无需额外安装
二、ocr识别:从发票图片中提取关键信息
2.1 ocr引擎对比与选择
在rpa项目中,ocr的选型直接影响识别准确率和开发效率。常见方案对比:
| ocr引擎 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| tesseract | 开源免费,支持多语言 | 中文准确率一般,预处理复杂 | 英文文档、简单验证码 |
| paddleocr | 中文准确率高,轻量模型仅8.6mb | 依赖paddlepaddle框架 | 中文发票、身份证、表格 |
| easyocr | 支持80+语言,gpu加速 | 模型较大,速度较慢 | 多语言混合场景 |
| 百度/阿里ocr api | 准确率最高(>95%),无需预处理 | 收费、依赖网络、数据隐私 | 生产级高要求场景 |
考虑到发票多为中文印刷体,且希望免费离线运行,本文选择paddleocr。它提供了轻量级的中文检测和识别模型,在普通cpu上也能达到实时识别速度。
2.2 使用paddleocr识别发票
初始化ocr引擎
from paddleocr import paddleocr
# 初始化ocr引擎(首次运行会自动下载模型)
ocr = paddleocr(
use_angle_cls=true, # 使用方向分类器,处理旋转文字
lang='ch', # 中文模型
show_log=false # 关闭冗余日志
)
识别并提取关键字段
发票上的关键信息通常以“键值对”形式出现,如“金额:¥100.00”、“开票日期:2025年03月15日”、“购买方名称:xx公司”。我们可以通过正则表达式从ocr识别的文本中提取。
import re
def extract_invoice_info(ocr_result):
"""
从paddleocr识别结果中提取发票关键信息
ocr_result格式: list of [box, (text, confidence)]
"""
full_text = ""
for line in ocr_result:
text = line[1][0]
confidence = line[1][1]
if confidence > 0.7: # 只保留高置信度结果
full_text += text + " "
# 提取金额(支持多种写法)
amount_pattern = r'(?:金额|合计|总计)[::\s]*([¥¥]?(\d+(?:\.\d{1,2})?))'
amount_match = re.search(amount_pattern, full_text)
amount = amount_match.group(1) if amount_match else none
# 提取开票日期(常见格式:yyyy年mm月dd日 或 yyyy-mm-dd)
date_pattern = r'(?:开票日期|发票日期)[::\s]*(\d{4}[年-]\d{1,2}[月-]\d{1,2}日?)'
date_match = re.search(date_pattern, full_text)
invoice_date = date_match.group(1) if date_match else none
# 提取供应商(销售方名称)
seller_pattern = r'(?:销售方|出售方|供应商)[::\s]*([\u4e00-\u9fa5a-za-z0-9()()]+公司[\u4e00-\u9fa5]*)'
seller_match = re.search(seller_pattern, full_text)
seller = seller_match.group(1) if seller_match else none
return {
"amount": amount,
"date": invoice_date,
"supplier": seller,
"raw_text": full_text
}
完整的ocr调用函数
def ocr_invoice_image(image_path):
"""对单张发票图片执行ocr并返回结构化信息"""
try:
result = ocr.ocr(image_path, cls=true)
if not result or not result[0]:
raise valueerror("ocr未识别到任何文本")
# result[0] 是图片中所有文本行
info = extract_invoice_info(result[0])
return info
except exception as e:
print(f"ocr识别失败: {image_path}, 错误: {e}")
return none
2.3 处理pdf格式的发票
实际场景中,供应商可能发送pdf格式的电子发票。我们可以借助pdf2image将pdf转换为图片,再调用ocr。
pip install pdf2image # 还需要安装poppler(windows需下载,linux apt install poppler-utils)
from pdf2image import convert_from_path
def ocr_invoice_pdf(pdf_path):
images = convert_from_path(pdf_path, dpi=200, first_page=1, last_page=1)
if not images:
return none
# 将pil image转换为临时文件或直接处理
import tempfile
with tempfile.namedtemporaryfile(suffix=".png", delete=false) as tmp:
images[0].save(tmp.name, "png")
result = ocr.ocr(tmp.name, cls=true)
info = extract_invoice_info(result[0]) if result and result[0] else none
return info
三、excel自动填写:使用openpyxl操作报销单
3.1 设计excel报销单模板
假设我们有一个名为报销单模板.xlsx的文件,结构如下:
| a列(字段) | b列(值) |
|---|---|
| 日期 | |
| 供应商 | |
| 金额(元) | |
| 备注 |
或者更常见的列表式报销明细表(每行一条记录)。为简化,我们采用逐行追加的方式:每次处理一张发票,就在“报销明细”工作表中新增一行,填写日期、供应商、金额。
3.2 使用openpyxl读写excel
from openpyxl import load_workbook
import os
def append_to_excel(excel_path, invoice_info):
"""
将发票信息追加到excel文件的报销明细表中
假设工作表名为"报销明细",表头为:日期, 供应商, 金额, 备注
"""
if not os.path.exists(excel_path):
# 如果文件不存在,创建新文件并写入表头
from openpyxl import workbook
wb = workbook()
ws = wb.active
ws.title = "报销明细"
ws.append(["日期", "供应商", "金额", "备注"])
else:
wb = load_workbook(excel_path)
ws = wb["报销明细"]
# 追加一行数据
row = [
invoice_info.get("date", ""),
invoice_info.get("supplier", ""),
invoice_info.get("amount", ""),
f"ocr自动识别 {invoice_info.get('raw_text', '')[:20]}..."
]
ws.append(row)
# 保存文件
wb.save(excel_path)
print(f"已追加记录到 {excel_path}")
进阶技巧:
- 使用
openpyxl.styles设置单元格格式(如金额保留两位小数); - 使用公式自动计算合计金额;
- 保护工作表防止误修改。
四、邮件发送:将报销单发给审核人
4.1 使用smtplib发送带附件的邮件
import smtplib
from email.mime.multipart import mimemultipart
from email.mime.text import mimetext
from email.mime.base import mimebase
from email import encoders
import os
def send_excel_by_email(receiver_email, excel_path, subject="报销单待审核", body="请查收本周报销明细"):
"""
发送excel文件作为附件到指定邮箱
"""
# 邮箱配置(从环境变量读取)
smtp_server = os.getenv("smtp_server", "smtp.qq.com")
smtp_port = int(os.getenv("smtp_port", "465"))
sender_email = os.getenv("sender_email")
sender_password = os.getenv("sender_password") # 授权码
if not sender_email or not sender_password:
raise valueerror("请在.env文件中配置发件邮箱和密码")
# 构建邮件
msg = mimemultipart()
msg["from"] = sender_email
msg["to"] = receiver_email
msg["subject"] = subject
msg.attach(mimetext(body, "plain", "utf-8"))
# 添加附件
with open(excel_path, "rb") as f:
part = mimebase("application", "octet-stream")
part.set_payload(f.read())
encoders.encode_base64(part)
filename = os.path.basename(excel_path)
part.add_header("content-disposition", f"attachment; filename={filename}")
msg.attach(part)
# 发送
with smtplib.smtp_ssl(smtp_server, smtp_port) as server:
server.login(sender_email, sender_password)
server.sendmail(sender_email, receiver_email, msg.as_string())
print(f"邮件已发送至 {receiver_email}")
4.2 邮件配置管理(.env文件)
创建.env文件:
smtp_server=smtp.qq.com smtp_port=465 sender_email=rpa@company.com sender_password=your_authorization_code auditor_email=auditor@company.com
在代码中加载:
from dotenv import load_dotenv load_dotenv()
五、整合实战:完整的票据处理机器人
5.1 项目结构
invoice_robot/
├── main.py # 主程序入口
├── ocr_engine.py # ocr识别模块
├── excel_handler.py # excel操作模块
├── mail_sender.py # 邮件发送模块
├── config.py # 配置加载
├── invoices/ # 待处理的发票图片文件夹
├── processed/ # 已处理的备份文件夹
├── output/ # 生成的excel文件存放路径
├── .env # 环境变量
└── requirements.txt
5.2 主程序实现(main.py)
import os
import time
import shutil
from pathlib import path
import logging
from dotenv import load_dotenv
from ocr_engine import ocr_invoice_image, ocr_invoice_pdf
from excel_handler import append_to_excel
from mail_sender import send_excel_by_email
# 加载配置
load_dotenv()
input_dir = "invoices"
processed_dir = "processed"
output_excel = "output/报销明细.xlsx"
auditor_email = os.getenv("auditor_email")
scan_interval = 60 # 扫描间隔(秒)
# 配置日志
logging.basicconfig(
level=logging.info,
format="%(asctime)s - %(levelname)s - %(message)s",
handlers=[
logging.filehandler("invoice_robot.log"),
logging.streamhandler()
]
)
logger = logging.getlogger(__name__)
def process_single_file(file_path):
"""处理单个发票文件"""
logger.info(f"开始处理文件: {file_path}")
ext = file_path.suffix.lower()
# 1. ocr识别
if ext in ['.jpg', '.jpeg', '.png']:
info = ocr_invoice_image(str(file_path))
elif ext == '.pdf':
info = ocr_invoice_pdf(str(file_path))
else:
logger.warning(f"不支持的文件类型: {ext}")
return false
if not info:
logger.error(f"ocr识别失败: {file_path}")
return false
logger.info(f"识别结果: 金额={info['amount']}, 日期={info['date']}, 供应商={info['supplier']}")
# 2. 写入excel
try:
append_to_excel(output_excel, info)
except exception as e:
logger.error(f"excel写入失败: {e}")
return false
# 3. 移动文件到已处理文件夹
processed_path = path(processed_dir) / file_path.name
shutil.move(str(file_path), str(processed_path))
logger.info(f"文件已归档: {processed_path}")
return true
def scan_and_process():
"""扫描文件夹,处理所有待处理文件"""
input_path = path(input_dir)
if not input_path.exists():
input_path.mkdir()
files = list(input_path.glob("*.*"))
if not files:
logger.info("暂无待处理文件")
return
for file_path in files:
if file_path.suffix.lower() in ['.jpg', '.jpeg', '.png', '.pdf']:
process_single_file(file_path)
else:
logger.info(f"跳过非图片/pdf文件: {file_path.name}")
def main():
logger.info("智能票据处理机器人启动")
logger.info(f"监控文件夹: {input_dir}, 扫描间隔: {scan_interval}秒")
# 确保输出目录存在
path("output").mkdir(exist_ok=true)
path(processed_dir).mkdir(exist_ok=true)
# 持续运行模式(也可改为单次执行后退出)
try:
while true:
scan_and_process()
time.sleep(scan_interval)
except keyboardinterrupt:
logger.info("机器人已停止")
if __name__ == "__main__":
main()
5.3 运行与测试
- 将若干张发票图片(或pdf)放入
invoices文件夹。 - 运行
python main.py。 - 观察控制台日志,机器人会自动识别、填写excel并发送邮件。
- 检查
output/报销明细.xlsx文件,确认数据已追加。 - 审核人邮箱会收到包含附件的邮件。
六、健壮性增强与最佳实践
6.1 异常处理与重试机制
在实际生产中,ocr识别可能因图片质量差而失败,网络波动可能导致邮件发送失败。我们需要增加重试和降级逻辑。
ocr重试:对于识别结果置信度低的字段,可以尝试对图片进行预处理(灰度、二值化、降噪)后再次识别。
import cv2
def preprocess_image(image_path):
img = cv2.imread(image_path, cv2.imread_grayscale)
# 自适应阈值二值化
img = cv2.adaptivethreshold(img, 255, cv2.adaptive_thresh_gaussian_c, cv2.thresh_binary, 11, 2)
# 去噪
img = cv2.medianblur(img, 3)
return img
邮件发送重试:使用tenacity库。
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10))
def send_excel_with_retry(*args, **kwargs):
send_excel_by_email(*args, **kwargs)
6.2 日志与监控
- 记录每张发票的处理时间、识别结果、成功/失败状态。
- 可将日志输出到elk或splunk进行集中监控。
- 当连续失败超过阈值时,发送告警邮件给管理员。
6.3 部署方式
- 定时任务:使用cron(linux)或任务计划程序(windows)每隔10分钟运行一次
python main.py(单次扫描后退出,而非无限循环)。 - docker容器化:方便部署在任何环境。
dockerfile示例:
from python:3.9-slim run apt-get update && apt-get install -y poppler-utils workdir /app copy requirements.txt . run pip install -r requirements.txt copy . . cmd ["python", "main.py"]
6.4 扩展思路
- 支持更多发票类型:火车票、出租车票、增值税专用发票等,只需调整正则表达式或使用更智能的字段定位(如基于坐标模板)。
- 接入api:对于识别难度高的发票,可调用百度ocr api作为备选。
- web界面:使用flask或fastapi提供上传界面,用户可手动上传发票并实时查看识别结果。
- 多审核人轮询:根据报销金额或部门自动选择不同的审核人邮箱。
七、总结与展望
本文从零开始,完整实现了一个智能票据处理机器人,涵盖了rpa项目中的三大核心增强能力:
- ocr识别:使用paddleocr高效提取发票中的金额、日期、供应商等关键信息;
- excel自动化:通过openpyxl动态填写报销明细表;
- 邮件通知:利用smtplib自动发送带附件的邮件给审核人。
这个机器人能够显著提升财务报销流程的效率,将人工处理一张发票的3-5分钟缩短到10秒以内,且准确率可达90%以上(通过持续优化ocr和正则规则可进一步提升)。
以上就是使用python从零打造智能票据处理机器人的详细内容,更多关于python智能票据处理的资料请关注代码网其它相关文章!
发表评论