乐于分享
好东西不私藏

PDF 发票批量识别与归档——财务人的电子发票管理神器

PDF 发票批量识别与归档——财务人的电子发票管理神器

PDF 发票批量识别与归档——财务人的电子发票管理神器

随着全面数字化电子发票的普及,财务人面临一个新挑战:

每月收到几百张 PDF 格式的电子发票,需要逐张打开查看金额、核对信息、分类归档……

如果手动操作,这是一项极其耗时且容易出错的工作。而用 Python 实现自动化后:

  • • ✅ 批量读取 PDF 发票中的关键信息
  • • ✅ 自动提取 发票号码、金额、日期、开票方
  • • ✅ 智能分类 按供应商/费用类型归档
  • • ✅ 汇总统计 自动生成发票台账
  • • ✅ 重复检测 防止同一张发票报销两次

一、 工具准备

安装必要的库

# PDF 文本提取
pip install PyPDF2 pdfplumber

# PDF 表格提取(更精确)
pip install tabula-py

# OCR(如果 PDF 是扫描件图片)
# pip install pytesseract pdf2image
# 还需要安装 Tesseract OCR 引擎

库的选择指南

适用场景
特点
PyPDF2
纯文本 PDF
轻量快速,但中文支持一般
pdfplumber
含表格的 PDF
中文支持好,表格提取强
tabula-py
表格型 PDF
基于 Java Tabula,精度高
pdf2image + pytesseract
扫描件/图片 PDF
需要 OCR 引擎

二、 基础操作:读取 PDF 内容

import pdfplumber
import pandas as pd
import os
import re
from datetime import datetime


defextract_text_from_pdf(pdf_path):
"""
    从 PDF 文件中提取所有文本内容
    """

    text_content = []

try:
with pdfplumber.open(pdf_path) as pdf:
print(f"📄 文件: {os.path.basename(pdf_path)}")
print(f"   页数: {len(pdf.pages)}")

for i, page inenumerate(pdf.pages):
                text = page.extract_text()
if text:
                    text_content.append({
"页码": i + 1,
"内容": text
                    })
# 打印前200字符预览
if i == 0:
print(f"   第1页预览: {text[:200]}...")

except Exception as e:
print(f"   ❌ 读取失败: {e}")
returnNone

return text_content


# 测试
# extract_text_from_pdf("示例发票.pdf")

三、 核心功能:发票信息智能提取

电子发票的关键字段识别

中国电子发票(增值税普通发票/专用发票)通常包含以下关键字段:

classInvoiceExtractor:
"""电子发票信息提取器"""

# 关键字正则模式
    PATTERNS = {
"发票号码"r"发票号码[::]\s*(\d{8,12})",
"发票代码"r"发票代码[::]\s*(\d{10,12})",
"开票日期"r"开票日期[::]\s*(\d{4}年\d{1,2}月\d{1,2}日|\d{4}-\d{1,2}-\d{1,2})",
"校验码"r"校验码[::]\s*(\w+)",
"购买方名称"r"购买方[名称]*[::]\s*(.+?)(?:\n|纳税人)",
"销售方名称"r"销售方[名称]*[::]\s*(.+?)(?:\n|纳税人)",
"价税合计(大写)"r"价税合计.*?大写[):].*?(\S+圆整)",
"价税合计(小写)"r"价税合计.*?小写[):]\s*[¥¥]?\s*(\d+\.?\d*)",
"金额合计"r"合计.*?金额[):]\s*[¥¥]?\s*(\d+\.?\d*)",
"税额合计"r"合计.*?税额[):]\s*[¥¥]?\s*(\d+\.?\d*)",
    }

def__init__(self):
self.extracted_data = []

defextract_invoice_info(self, pdf_path):
"""
        从单个 PDF 发票中提取结构化信息
        """

        result = {
"文件名": os.path.basename(pdf_path),
"文件路径": pdf_path,
"提取时间": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        }

try:
with pdfplumber.open(pdf_path) as pdf:
# 合并所有页面文本
                full_text = ""
for page in pdf.pages:
                    text = page.extract_text()
if text:
                        full_text += text + "\n"

                result["原始文本"] = full_text[:500]  # 保存前500字符用于调试

# 使用正则表达式提取各字段
for field_name, pattern inself.PATTERNS.items():
match = re.search(pattern, full_text)
ifmatch:
                        result[field_name] = match.group(1).strip()
else:
                        result[field_name] = None

# 尝试提取表格数据(明细行)
                tables = []
for page in pdf.pages:
                    tables.extend(page.extract_tables() or [])

if tables:
# 通常第一个表格是发票明细
                    result["表格数据"] = tables[0]
                    result["表格行数"] = len(tables[0])

except Exception as e:
            result["错误"] = str(e)

self.extracted_data.append(result)
return result

defformat_result(self, result):
"""格式化输出提取结果"""
print("\n" + "=" * 50)
print(f"📋 发票信息: {result['文件名']}")
print("=" * 50)

        display_fields = [
"发票代码""发票号码""开票日期",
"购买方名称""销售方名称",
"价税合计(小写)""金额合计""税额合计"
        ]

for field in display_fields:
            value = result.get(field)
if value:
print(f"   {field}{value}")
else:
print(f"   {field}: (未识别)")

if"错误"in result:
print(f"   ⚠️ 错误: {result['错误']}")

return result

四、 批量处理:整个文件夹的发票

defbatch_process_invoices(folder_path, output_file=None):
"""
    批量处理文件夹中的所有 PDF 发票
    """

print("=" * 60)
print("📦 PDF 发票批量处理系统")
print("=" * 60)

# 1. 扫描文件夹中的 PDF 文件
    pdf_files = []
for root, dirs, files in os.walk(folder_path):
for file in files:
if file.lower().endswith(".pdf"):
                pdf_files.append(os.path.join(root, file))

ifnot pdf_files:
print(f"❌ 在 '{folder_path}' 中未找到 PDF 文件")
returnNone

print(f"\n📂 找到 {len(pdf_files)} 个 PDF 文件")

# 2. 逐个提取
    extractor = InvoiceExtractor()
    results = []

for i, pdf_path inenumerate(pdf_files, 1):
print(f"\n[{i}/{len(pdf_files)}] 处理中...")
        result = extractor.extract_invoice_info(pdf_path)
        extractor.format_result(result)
        results.append(result)

# 3. 汇总为 DataFrame
    summary_data = []
for r in results:
        row = {
"文件名": r.get("文件名"),
"发票代码": r.get("发票代码"),
"发票号码": r.get("发票号码"),
"开票日期": r.get("开票日期"),
"购买方": r.get("购买方名称"),
"销售方": r.get("销售方名称"),
"金额(元)": r.get("金额合计"),
"税额(元)": r.get("税额合计"),
"价税合计(元)": r.get("价税合计(小写)"),
"提取状态""✅ 成功"if r.get("发票号码"else"⚠️ 不完整",
"提取时间": r.get("提取时间")
        }
        summary_data.append(row)

    df = pd.DataFrame(summary_data)

# 4. 统计摘要
print(f"\n{'='*60}")
print(f"📊 处理结果统计:")
print(f"   总文件数: {len(df)}")
print(f"   成功提取: {len(df[df['提取状态']=='✅ 成功'])}")
print(f"   提取不完整: {len(df[df['提取状态']!='✅ 成功'])}")

# 金额汇总(如果能提取到的话)
    amount_col = "价税合计(元)"
    valid_amounts = pd.to_numeric(df[amount_col], errors="coerce").dropna()
iflen(valid_amounts) > 0:
print(f"   金额合计: {valid_amounts.sum():,.2f} 元")
print(f"   平均金额: {valid_amounts.mean():,.2f} 元")

# 5. 保存结果
if output_file isNone:
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        output_file = f"发票台账_{timestamp}.xlsx"

    df.to_excel(output_file, index=False)
print(f"\n💾 台账已保存: {output_file}")

# 6. 重复检测
    duplicate_numbers = df[df["发票号码"].notna() & df["发票号码"].duplicated(keep=False)]
iflen(duplicate_numbers) > 0:
print(f"\n⚠️ 发现 {len(duplicate_numbers)} 张疑似重复发票:")
print(duplicate_numbers[["文件名""发票号码"]].to_string(index=False))

return df


# ===== 使用示例 =====
# batch_process_invoices(
#     folder_path="./待处理发票/",
#     output_file="发票台账_2024Q1.xlsx"
# )

五、 高级功能:自动分类归档

defauto_classify_and_archive(invoices_df, source_folder, archive_folder):
"""
    根据发票信息自动分类并归档文件
    :param invoices_df: 发票台账 DataFrame
    :param source_folder: 发票源文件夹
    :param archive_folder: 归档目标根目录
    """

import shutil

    os.makedirs(archive_folder, exist_ok=True)

# 定义分类规则
    classification_rules = [
        {"关键词": ["差旅""火车""飞机""酒店"], "类别""01-差旅费"},
        {"关键词": ["餐饮""食品""餐饮服务"], "类别""02-业务招待费"},
        {"关键词": ["办公""耗材""文具""打印"], "类别""03-办公费"},
        {"关键词": ["运输""快递""物流""运费"], "类别""04-运杂费"},
        {"关键词": ["广告""宣传""推广""传媒"], "类别""05-广告费"},
        {"关键词": ["咨询""服务""技术服务"], "类别""06-咨询服务费"},
        {"关键词": ["租赁""房租""物业"], "类别""07-租赁费"},
    ]

    archive_log = []

for _, invoice in invoices_df.iterrows():
        filename = invoice["文件名"]
        source_path = os.path.join(source_folder, filename)

ifnot os.path.exists(source_path):
continue

# 匹配分类规则
        target_category = "99-其他"
        raw_text = invoice.get("原始文本"""or""

for rule in classification_rules:
ifany(kw in raw_text for kw in rule["关键词"]):
                target_category = rule["类别"]
break

# 也可以按销售方名称分类
        seller = invoice.get("销售方"or""
if"京东"in seller or"淘宝"in seller:
            target_category = "08-电商采购"

# 创建目标目录并复制文件
        target_dir = os.path.join(archive_folder, target_category)
        os.makedirs(target_dir, exist_ok=True)

        target_path = os.path.join(target_dir, filename)
        shutil.copy2(source_path, target_path)

        archive_log.append({
"原文件": filename,
"目标分类": target_category,
"归档路径": target_path,
"归档时间": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        })

# 保存归档日志
    log_df = pd.DataFrame(archive_log)
    log_file = os.path.join(archive_folder, f"归档日志_{datetime.now().strftime('%Y%m%d')}.xlsx")
    log_df.to_excel(log_file, index=False)

# 统计各类别数量
print("\n📁 归档统计:")
    category_counts = log_df["目标分类"].value_counts()
for cat, count in category_counts.items():
print(f"   {cat}{count} 张")

print(f"\n✅ 归档完成! 共归档 {len(archive_log)} 张发票")
print(f"📁 归档目录: {archive_folder}")

return log_df

六、 进阶:OCR 识别扫描件发票

如果你的发票是扫描件或图片格式的 PDF(无法直接提取文字),需要使用 OCR 技术:

defocr_extract_invoice(image_pdf_path):
"""
    使用 OCR 从图片型 PDF 中提取文字
    注意:需要安装 Tesseract OCR 引擎
    """

try:
import pdf2image
from PIL import Image
import pytesseract

# 将 PDF 转换为图片
        images = pdf2image.convert_from_path(image_pdf_path)

        all_text = ""
for i, image inenumerate(images):
# OCR 识别(设置中文语言包)
            text = pytesseract.image_to_string(
                image,
                lang="chi_sim+eng",  # 中文简体 + 英文
                config="--psm 6"# 页面分割模式
            )
            all_text += f"=== 第{i+1}页 ===\n{text}\n"

print("📝 OCR 识别结果(前500字):")
print(all_text[:500])

return all_text

except ImportError:
print("❌ 缺少依赖库,请安装:")
print("   pip install pdf2image pytesseract pillow")
print("   并安装 Tesseract OCR 引擎")
returnNone
except Exception as e:
print(f"❌ OCR 识别失败: {e}")
returnNone

OCR 环境配置提示:

  1. 1. 下载安装 Tesseract OCR
  2. 2. 下载中文语言包 chi_sim.traineddata
  3. 3. 设置环境变量 TESSDATA_PREFIX 指向语言包目录

七、 完整工作流:从接收到入账

deffull_invoice_workflow(incoming_folder, archive_folder):
"""
    发票完整处理工作流:
    接收 → 识别 → 分类 → 归档 → 生成台账 → 异常报告
    """

print("=" * 70)
print("🔄 电子发票全自动处理流程")
print("=" * 70)

    start_time = datetime.now()

# Step 1: 扫描新发票
print("\n[Step 1/5] 📥 扫描待处理发票...")
    new_invoices = batch_process_invoices(incoming_folder)

if new_invoices isNoneorlen(new_invoices) == 0:
print("没有需要处理的发票")
return

# Step 2: 数据清洗和标准化
print("\n[Step 2/5] 🔧 数据标准化...")
# 统一日期格式
defnormalize_date(date_str):
if pd.isna(date_str):
returnNone
        date_str = str(date_str).replace("年""-").replace("月""-").replace("日""")
try:
return pd.to_datetime(date_str).strftime("%Y-%m-%d")
except:
return date_str

    new_invoices["开票日期_标准"] = new_invoices["开票日期"].apply(normalize_date)

# 金额数值化
for col in ["金额(元)""税额(元)""价税合计(元)"]:
        new_invoices[col] = pd.to_numeric(new_invoices[col], errors="coerce")

# Step 3: 自动分类归档
print("\n[Step 3/5] 📁 自动分类归档...")
    archive_log = auto_classify_and_archive(
        new_invoices, incoming_folder, archive_folder
    )

# Step 4: 生成分析报表
print("\n[Step 4/5] 📊 生成分析报表...")

    report_file = os.path.join(archive_folder,
f"发票分析报告_{datetime.now().strftime('%Y%m%d')}.xlsx")

with pd.ExcelWriter(report_file, engine="openpyxl"as writer:
# Sheet 1: 完整台账
        new_invoices.to_excel(writer, sheet_name="发票台账", index=False)

# Sheet 2: 按销售方汇总
if"销售方"in new_invoices.columns:
            by_seller = new_invoices[new_invoices["价税合计(元)"].notna()] \
                          .groupby("销售方")["价税合计(元)"].agg(["sum""count"]) \
                          .round(2)
            by_seller.columns = ["金额合计""发票数量"]
            by_seller = by_seller.sort_values("金额合计", ascending=False)
            by_seller.to_excel(writer, sheet_name="按供应商汇总")

# Sheet 3: 按月份汇总
if"开票日期_标准"in new_invoices.columns:
            new_invoices["月份"] = pd.to_datetime(
                new_invoices["开票日期_standard"], errors="coerce"
            ).dt.to_period("M")
            by_month = new_invoices[new_invoices["价税合计(元)"].notna()] \
                         .groupby("月份")["价税合计(元)"].agg(["sum""count"])
            by_month.columns = ["金额合计""发票数量"]
            by_month.to_excel(writer, sheet_name="按月度汇总")

# Sheet 4: 异常发票
        abnormal = new_invoices[new_invoices["提取状态"] != "✅ 成功"]
iflen(abnormal) > 0:
            abnormal.to_excel(writer, sheet_name="需人工复核", index=False)

# Step 5: 输出总结
    elapsed = (datetime.now() - start_time).total_seconds()

print(f"\n[Step 5/5] ✅ 全部完成!")
print(f"   ⏱️ 总耗时: {elapsed:.1f} 秒")
print(f"   📊 处理发票: {len(new_invoices)} 张")
print(f"   💰 总金额: {new_invoices['价税合计(元)'].sum():,.2f} 元")
print(f"   📁 报表文件: {report_file}")
print(f"   📁 归档目录: {archive_folder}")


# 使用示例
# full_invoice_workflow(
#     incoming_folder="./新发票接收区/",
#     archive_folder="./发票归档库/"
# )

八、 避坑指南

问题
原因
解决方法
提取出的中文是乱码
PDF 编码问题
尝试 pdfplumber 或指定编码
表格数据错位
PDF 表格线不清晰
用 tabula-py 替代
扫描件提取不出文字
需要使用 OCR
安装 Tesseract + pytesseract
大量文件处理慢
逐个读取效率低
用多线程/多进程并行处理
同一张发票被多次处理
缺少去重机制
以发票号码建立唯一索引

九、 总结

功能
传统方式
Python 自动化
发票信息录入
手动打开每张 PDF 输入
批量自动提取
发票台账维护
Excel 手动登记
自动生成 Excel
分类归档
手动拖拽到不同文件夹
按规则自动归类
重复检测
人工比对
自动发现重复
月末汇总
多小时手工统计
几秒钟出结果

电子发票管理的核心原则:让机器做机器擅长的事(批量处理、精确匹配),让人做人擅长的事(异常审核、业务判断)。

下一篇文章是这个系列的收官之作——我们将把前面学的所有知识串联起来,搭建一个完整的财务自动化工作流