使用 Python + pdftotext 实现发票PDF批量处理,自动提取开票日期、小写金额等关键信息,按模板生成费用报销Excel报表。
背景
在企业财务报销场景中,员工常需提交大量PDF格式的中文电子发票。手动提取每张发票的开票日期、小写金额、销售方等信息并填入Excel模板,耗时且易出错。本文介绍一种自动化解决方案,实现从PDF发票到Excel报表的全流程自动化。
技术栈
- pdftotext: Poppler 工具集,用于从PDF提取文本
- Python 3: 数据处理和Excel生成
- openpyxl: Excel文件读写库
- 正则表达式: 解析中文发票文本结构
核心思路
- 文本提取 : 使用
pdftotext将PDF发票转换为纯文本 - 模式匹配: 通过正则表达式定位关键字段(开票日期、金额、销售方等)
- 模板映射: 将提取数据映射到预定义的Excel模板列
- 批量输出: 生成结构化Excel报表
前置条件
bash
# 安装 Poppler (macOS)
brew install poppler
# 安装 Python 依赖
pip install openpyxl
实现步骤
步骤1:列出发票文件
python
import os
pdf_dir = "/path/to/invoices"
pdf_files = sorted([f for f in os.listdir(pdf_dir) if f.endswith('.pdf')])
步骤2:提取PDF文本
python
import subprocess
def extract_pdf_text(pdf_path: str) -> str:
"""使用 pdftotext 从 PDF 提取文本"""
result = subprocess.run(
['pdftotext', pdf_path, '-'],
capture_output=True, text=True, timeout=30
)
return result.stdout
步骤3:解析关键字段
中文电子发票具有固定文本结构,可通过正则表达式定位:
python
import re
def parse_invoice(text: str, filename: str) -> dict:
"""解析发票文本,提取关键字段"""
invoice = {
'file': os.path.basename(filename),
'date': '',
'amount': 0.0,
'seller': '',
'item': '',
'number': ''
}
# 提取开票日期
date_match = re.search(r'开票日期[::]\s*(\d{4})年(\d{2})月(\d{2})日', text)
if date_match:
invoice['date'] = f"{date_match.group(1)}年{date_match.group(2)}月{date_match.group(3)}日"
# 提取小写金额
amount_match = re.search(r'(小写)\s*¥\s*([\d.]+)', text)
if amount_match:
invoice['amount'] = float(amount_match.group(1))
# 提取销售方名称
seller_match = re.search(r'销售方\s*名称[::]\s*([^\n]+)', text)
if seller_match:
invoice['seller'] = seller_match.group(1).strip()
# 提取项目名称
item_match = re.search(r'项目名称\s*\n\s*\*[^*]+\*([^\n]+)', text)
if item_match:
invoice['item'] = item_match.group(1).strip()
# 提取发票号码
number_match = re.search(r'发票号码[::]\s*([\d]+)', text)
if number_match:
invoice['number'] = number_match.group(1)
return invoice
步骤4:生成Excel报表
python
import openpyxl
def generate_excel(invoices: list, template_path: str, output_path: str):
"""从提取的发票数据生成 Excel 文件"""
wb = openpyxl.load_workbook(template_path)
ws = wb.active
# 从模板第2行获取固定值
fixed_values = {}
for col_num in range(1, ws.max_column + 1):
cell = ws.cell(row=2, column=col_num)
fixed_values[col_num] = cell.value
# 从第7行开始写入新数据(假设模板已有6行数据)
start_row = 7
for i, inv in enumerate(invoices):
row = start_row + i
ws.cell(row=row, column=1).value = i + 1 # 序号
ws.cell(row=row, column=2).value = fixed_values.get(2) # 审批编号
ws.cell(row=row, column=3).value = fixed_values.get(3) # 公司名称
ws.cell(row=row, column=4).value = fixed_values.get(4) # 报销人
ws.cell(row=row, column=5).value = f'业务招待费-{inv["item"]}' # 费用明细
ws.cell(row=row, column=6).value = inv['amount'] # 报销金额
ws.cell(row=row, column=9).value = inv['seller'] # 招待对象
ws.cell(row=row, column=13).value = inv['date'] # 备注 (开票日期)
wb.save(output_path)
print(f"已生成: {output_path}")
print(f"记录数: {len(invoices)}")
完整脚本
python
#!/usr/bin/env python3
"""批量提取发票信息并生成Excel报表"""
import os
import sys
import re
import subprocess
import openpyxl
from pathlib import Path
def extract_pdf_text(pdf_path: str) -> str:
"""从 PDF 提取文本"""
try:
result = subprocess.run(
['pdftotext', pdf_path, '-'],
capture_output=True, text=True, timeout=30
)
return result.stdout
except Exception as e:
print(f"提取失败 {pdf_path}: {e}")
return ""
def parse_invoice(text: str, filename: str) -> dict:
"""解析发票文本,提取关键字段"""
invoice = {
'file': os.path.basename(filename),
'date': '',
'amount': 0.0,
'seller': '',
'item': '',
'number': ''
}
# 提取开票日期
date_match = re.search(r'开票日期[::]\s*(\d{4})年(\d{2})月(\d{2})日', text)
if date_match:
invoice['date'] = f"{date_match.group(1)}年{date_match.group(2)}月{date_match.group(3)}日"
# 提取小写金额
amount_match = re.search(r'(小写)\s*¥\s*([\d.]+)', text)
if amount_match:
invoice['amount'] = float(amount_match.group(1))
# 提取销售方名称
seller_match = re.search(r'销售方\s*名称[::]\s*([^\n]+)', text)
if seller_match:
invoice['seller'] = seller_match.group(1).strip()
# 提取项目名称
item_match = re.search(r'项目名称\s*\n\s*\*[^*]+\*([^\n]+)', text)
if item_match:
invoice['item'] = item_match.group(1).strip()
# 提取发票号码
number_match = re.search(r'发票号码[::]\s*([\d]+)', text)
if number_match:
invoice['number'] = number_match.group(1)
return invoice
def generate_excel(invoices: list, template_path: str, output_path: str):
"""生成 Excel 报表"""
wb = openpyxl.load_workbook(template_path)
ws = wb.active
# 获取固定值
fixed_values = {}
for col_num in range(1, ws.max_column + 1):
cell = ws.cell(row=2, column=col_num)
fixed_values[col_num] = cell.value
start_row = 7
for i, inv in enumerate(invoices):
row = start_row + i
ws.cell(row=row, column=1).value = i + 1
ws.cell(row=row, column=2).value = fixed_values.get(2)
ws.cell(row=row, column=3).value = fixed_values.get(3)
ws.cell(row=row, column=4).value = fixed_values.get(4)
ws.cell(row=row, column=5).value = f'业务招待费-{inv["item"]}'
ws.cell(row=row, column=6).value = inv['amount']
ws.cell(row=row, column=9).value = inv['seller']
ws.cell(row=row, column=13).value = inv['date']
wb.save(output_path)
print(f"已生成: {output_path}")
print(f"记录数: {len(invoices)}")
def main():
if len(sys.argv) < 3:
print("用法: python extract_invoices.py <pdf目录> <模板.xlsx> [输出.xlsx]")
sys.exit(1)
pdf_dir = sys.argv[1]
template_path = sys.argv[2]
output_path = sys.argv[3] if len(sys.argv) > 3 else 'output.xlsx'
# 查找所有 PDF 文件
pdf_files = sorted([f for f in os.listdir(pdf_dir) if f.endswith('.pdf')])
if not pdf_files:
print("未找到 PDF 文件。")
sys.exit(1)
# 提取每张发票数据
invoices = []
for pdf_file in pdf_files:
pdf_path = os.path.join(pdf_dir, pdf_file)
print(f"处理: {pdf_file}")
text = extract_pdf_text(pdf_path)
if text:
invoice = parse_invoice(text, pdf_path)
if invoice['date'] or invoice['amount']:
invoices.append(invoice)
else:
print(f" 警告: 无法从 {pdf_file} 提取日期/金额")
if invoices:
generate_excel(invoices, template_path, output_path)
else:
print("未找到有效发票。")
sys.exit(1)
if __name__ == '__main__':
main()
使用方法
bash
# 基本用法
python extract_invoices.py /path/to/invoices /path/to/template.xlsx output.xlsx
# 示例
python extract_invoices.py ./invoices 模板.xlsx 5月份报销.xlsx
输出示例
| 序号 | 审批编号 | 公司名称 | 报销人 | 费用明细 | 报销金额 |
|---|---|---|---|---|---|
| 1 | 202605200002 | XX技术有限公司 | 王XX | 业务招待费-餐费 | 356.00 |
| 2 | 202605200002 | XX技术有限公司 | 王XX | 业务招待费-餐费 | 177.00 |
| 3 | 202605200002 | XX技术有限公司 | 王XX | 业务招待费-餐饮服务 | 316.00 |
注意事项
- PDF 格式: 仅适用于标准中文电子发票PDF,手写或扫描件需OCR预处理
- 文本提取 :
pdftotext对某些加密PDF可能失效,需先解密 - 模板匹配: 需确保Excel模板列结构与代码映射一致
- 错误处理: 建议添加日志记录,便于排查提取失败的文件
扩展方向
- 支持增值税专用/普通发票自动分类
- 添加发票验真接口(对接税务局API)
- 支持图片格式发票(OCR预处理)
- Web界面上传和批量处理
- 与财务系统API对接自动提交
总结
本文介绍了使用 Python 批量提取中文电子发票信息的完整方案。通过 pdftotext + 正则表达式解析 + openpyxl 生成Excel,实现了从PDF发票到结构化报表的全流程自动化,大幅提升了财务报销工作效率。
以上过程可以直接使用codex-app帮你生成一个发票整理技能。