引言:当报销流程遇见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 环境准备
bash
# 安装依赖
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识别发票
2.2.1 初始化OCR引擎
python
from paddleocr import PaddleOCR
# 初始化OCR引擎(首次运行会自动下载模型)
ocr = PaddleOCR(
use_angle_cls=True, # 使用方向分类器,处理旋转文字
lang='ch', # 中文模型
show_log=False # 关闭冗余日志
)
2.2.2 识别并提取关键字段
发票上的关键信息通常以"键值对"形式出现,如"金额:¥100.00"、"开票日期:2025年03月15日"、"购买方名称:XX公司"。我们可以通过正则表达式从OCR识别的文本中提取。
python
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
}
2.2.3 完整的OCR调用函数
python
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。
bash
pip install pdf2image
# 还需要安装poppler(Windows需下载,Linux apt install poppler-utils)
python
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
python
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发送带附件的邮件
python
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文件:
ini
SMTP_SERVER=smtp.qq.com
SMTP_PORT=465
SENDER_EMAIL=rpa@company.com
SENDER_PASSWORD=your_authorization_code
AUDITOR_EMAIL=auditor@company.com
在代码中加载:
python
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)
python
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重试:对于识别结果置信度低的字段,可以尝试对图片进行预处理(灰度、二值化、降噪)后再次识别。
python
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库。
python
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示例:
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和正则规则可进一步提升)。