夜雨聆风学习资料网

ARTICLE · 984038

Python PDF 自动化保姆级⑦:实战!批量处理上千份 PDF,自动提取数据汇总 Excel

Python PDF 自动化保姆级⑦:实战!批量处理上千份 PDF,自动提取数据汇总 Excel

前面六篇学了 PDF 的各种操作技能,今天来一个综合实战项目,把前面学的东西全部串起来。

场景是这样的:你是公司的运营 / 财务 / 行政,每个月会收到几百上千份业务 PDF(合同、发票、报表、申请单等),领导让你把这些 PDF 里的关键数据提取出来,汇总成一张 Excel 表。手动处理?几百份文件,每份打开复制关键信息,一周都搞不完。

01 项目需求分析

先明确需求,这是做任何自动化项目的第一步:

输入: 一个文件夹,里面有上千份 PDF,格式不完全统一(有合同、有发票、有报表)。

输出: 一个 Excel 文件,包含每份 PDF 的关键信息(文件名、日期、金额、客户名称、类型等)。

额外要求:

  • 处理加密的 PDF(跳过或记录)
  • 处理扫描件(标记为需人工处理)
  • 提取失败的文件要记录原因,不能中断整个流程
  • 最后生成统计摘要(共处理多少份、成功多少、失败多少、各类型数量)

02 技术方案设计

整个流程分为五个模块:

模块 1:文件遍历 — 用 os.walk 遍历文件夹,收集所有 PDF 文件。

模块 2:PDF 预处理 — 检查是否加密、是否扫描件,提取文本。

模块 3:信息提取 — 用正则表达式从文本中提取关键字段(日期、金额、客户名等)。

模块 4:数据汇总 — 用 pandas 把所有数据汇总成 DataFrame,导出 Excel。

模块 5:统计报告 — 生成处理统计,输出摘要。

用到的库:pdfplumber(文本提取)、PyPDF2(加密检测)、pandas(数据处理)、re(正则匹配)、os(文件操作)。

03 完整代码实现

import osimport reimport pdfplumberfrom PyPDF2 import PdfReaderimport pandas as pdfrom datetime import datetime# ==================== 配置区 ====================INPUT_DIR = "./business_pdfs"       # PDF文件夹OUTPUT_EXCEL = "summary_report.xlsx"  # 输出ExcelLOG_FILE = "process_log.txt"          # 处理日志# 正则表达式配置(根据实际PDF格式调整)PATTERNS = {    "date": r"(?:日期|时间|开票日期|签订日期)[::]\s*(\d{4}[-/年]\d{1,2}[-/月]\d{1,2}日?)",    "amount": r"(?:金额|总价|合计|价税合计|总金额)[::]\s*[¥¥]?\s*([\d,]+\.?\d*)",    "customer": r"(?:客户名称|购买方|甲方|客户|公司名称)[::]\s*(.+?)(?:\n|$)",    "contract_no": r"(?:合同编号|发票号码|单号|编号)[::]\s*([A-Za-z0-9\-]+)",    "type": r"(?:合同类型|业务类型|类别)[::]\s*(.+?)(?:\n|$)",}# ==================== 模块1:文件遍历 ====================def scan_pdf_files(input_dir):    """遍历文件夹,收集所有PDF文件"""    pdf_files = []    for root, dirs, files in os.walk(input_dir):        for f in files:            if f.lower().endswith(".pdf"):                pdf_files.append(os.path.join(root, f))    print(f"扫描到 {len(pdf_files)} 个PDF文件")    return pdf_files# ==================== 模块2:PDF预处理 ====================def check_encrypted(pdf_path):    """检查PDF是否加密"""    try:        reader = PdfReader(pdf_path)        return reader.is_encrypted    except:        return Falsedef extract_text(pdf_path):    """提取PDF全文文本"""    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"            return full_text    except Exception as e:        return Nonedef is_scanned(text):    """判断是否为扫描件(提取到的文本极少)"""    if text is None:        return True    return len(text.strip()) < 50  # 少于50个字符认为是扫描件# ==================== 模块3:信息提取 ====================def extract_info(text, patterns):    """用正则表达式从文本中提取关键字段"""    result = {}    for field, pattern in patterns.items():        match = re.search(pattern, text)        if match:            value = match.group(1).strip()            # 清理金额中的逗号            if field == "amount":                value = value.replace(",", "")            result[field] = value        else:            result[field] = ""    return resultdef detect_pdf_type(text):    """根据文本内容自动识别PDF类型"""    text_lower = text.lower()    if "发票" in text or "invoice" in text_lower:        return "发票"    elif "合同" in text or "contract" in text_lower:        return "合同"    elif "报表" in text or "报告" in text:        return "报表"    elif "申请" in text:        return "申请单"    else:        return "其他"# ==================== 模块4:主处理流程 ====================def process_all_pdfs(pdf_files):    """处理所有PDF文件"""    results = []    log_entries = []    for i, pdf_path in enumerate(pdf_files, 1):        filename = os.path.basename(pdf_path)        print(f"[{i}/{len(pdf_files)}] 处理: {filename}")        record = {            "文件名": filename,            "文件路径": pdf_path,            "处理状态": "",            "PDF类型": "",            "日期": "",            "金额": "",            "客户名称": "",            "合同编号": "",            "备注": "",        }        # 检查加密        if check_encrypted(pdf_path):            record["处理状态"] = "加密-跳过"            record["备注"] = "PDF有密码保护,无法自动提取"            results.append(record)            log_entries.append(f"[跳过] {filename}: 加密文件")            continue        # 提取文本        text = extract_text(pdf_path)        # 检查扫描件        if is_scanned(text):            record["处理状态"] = "扫描件-需人工"            record["备注"] = "疑似扫描件,文本提取不足,需人工处理"            results.append(record)            log_entries.append(f"[人工] {filename}: 疑似扫描件")            continue        # 识别类型        pdf_type = detect_pdf_type(text)        record["PDF类型"] = pdf_type        # 提取信息        info = extract_info(text, PATTERNS)        record["日期"] = info.get("date", "")        record["金额"] = info.get("amount", "")        record["客户名称"] = info.get("customer", "")        record["合同编号"] = info.get("contract_no", "")        # 判断提取质量        if info.get("amount") or info.get("date"):            record["处理状态"] = "成功"            log_entries.append(f"[成功] {filename}: 类型={pdf_type}, 金额={info.get('amount', 'N/A')}")        else:            record["处理状态"] = "部分成功"            record["备注"] = "未提取到关键金额或日期,建议人工核对"            log_entries.append(f"[部分] {filename}: 关键字段缺失")        results.append(record)    return results, log_entries# ==================== 模块5:导出Excel和统计 ====================def export_to_excel(results, output_path):    """导出结果到Excel,包含数据和统计摘要"""    df = pd.DataFrame(results)    # 金额转为数值类型(用于统计)    df["金额数值"] = pd.to_numeric(df["金额"], errors="coerce")    # 生成统计摘要    total = len(df)    success = len(df[df["处理状态"] == "成功"])    partial = len(df[df["处理状态"] == "部分成功"])    encrypted = len(df[df["处理状态"] == "加密-跳过"])    scanned = len(df[df["处理状态"] == "扫描件-需人工"])    total_amount = df["金额数值"].sum()    summary = pd.DataFrame({        "统计项": ["总文件数", "成功", "部分成功", "加密跳过", "扫描件需人工", "提取总金额"],        "数量/金额": [total, success, partial, encrypted, scanned, f"{total_amount:,.2f}"],    })    # 按类型统计    type_stats = df[df["处理状态"] == "成功"]["PDF类型"].value_counts().reset_index()    type_stats.columns = ["PDF类型", "数量"]    # 写入Excel(多个Sheet)    with pd.ExcelWriter(output_path, engine="openpyxl") as writer:        df.drop(columns=["金额数值"]).to_excel(writer, sheet_name="详细数据", index=False)        summary.to_excel(writer, sheet_name="统计摘要", index=False)        type_stats.to_excel(writer, sheet_name="类型统计", index=False)    print(f"\n数据已导出到: {output_path}")    print(f"总计: {total} 份 | 成功: {success} | 部分: {partial} | 加密: {encrypted} | 扫描件: {scanned}")    print(f"提取总金额: {total_amount:,.2f}")def save_log(log_entries, log_file):    """保存处理日志"""    with open(log_file, "w", encoding="utf-8") as f:        f.write(f"PDF批量处理日志 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")        f.write("=" * 60 + "\n\n")        for entry in log_entries:            f.write(entry + "\n")    print(f"日志已保存到: {log_file}")# ==================== 主函数 ====================def main():    print("=" * 60)    print("PDF批量数据提取系统")    print("=" * 60)    # 1. 扫描文件    pdf_files = scan_pdf_files(INPUT_DIR)    if not pdf_files:        print("未找到PDF文件,请检查输入目录")        return    # 2. 处理所有PDF    print(f"\n开始处理...")    results, log_entries = process_all_pdfs(pdf_files)    # 3. 导出Excel    export_to_excel(results, OUTPUT_EXCEL)    # 4. 保存日志    save_log(log_entries, LOG_FILE)    print("\n全部处理完成!")if __name__ == "__main__":    main()

04 代码解读:关键设计思路

1. 配置区分离

把文件夹路径、正则表达式都放在配置区,换一种 PDF 格式只需要改正则,不需要动主逻辑。这是工程化的基本素养。

2. 异常隔离

每个文件的处理都有独立的状态记录。一个文件失败不会影响其他文件,最后还能看到哪些失败了、为什么失败。批量处理中这一点至关重要。

3. 分级处理

不是所有 PDF 都能自动处理。代码分了四个状态:成功、部分成功、加密跳过、扫描件需人工。这样你知道哪些可以直接用、哪些需要核对、哪些必须人工处理。

4. 自动类型识别

根据文本关键词自动判断 PDF 是发票、合同还是报表。这样汇总后可以按类型筛选和统计。

5. 多 Sheet 输出

Excel 包含三个 Sheet:详细数据(每份 PDF 的提取结果)、统计摘要(整体处理情况)、类型统计(各类型数量)。领导看摘要,你看详细数据,各取所需。

05 正则表达式调优指南

这个项目最关键的部分是正则表达式。不同公司的 PDF 格式不一样,你需要根据实际情况调整。

调试正则的方法:

先拿一份典型 PDF,提取文本后打印出来,看看关键字段周围的文字是什么样的:

import pdfplumberwith pdfplumber.open("sample.pdf") as pdf:    text = pdf.pages[0].extract_text()    print(text)

然后根据实际文本写正则。比如你看到:

合同编号:HT-2024-0089签订日期:2024年08月15日甲方:某某科技有限公司总金额:¥125,000.00

对应的正则就是:

PATTERNS = {    "contract_no": r"合同编号[::]\s*([A-Za-z0-9\-]+)",    "date": r"签订日期[::]\s*(\d{4}年\d{1,2}月\d{1,2}日)",    "customer": r"甲方[::]\s*(.+)",    "amount": r"总金额[::]\s*[¥¥]?([\d,]+\.?\d*)",}

正则编写技巧:

  1. 用 [::] 同时匹配中文冒号和英文冒号(PDF 里两种都可能出现)。
  2. 金额用 [\d,]+\.?\d* 匹配带逗号和小数的数字。
  3. 日期用 \d{4}[-/年]\d{1,2}[-/月]\d{1,2}日? 兼容多种日期格式。
  4. 客户名称用 (.+?)(?:\n|$) 非贪婪匹配,取到行尾。

06 性能优化:处理上千份文件

处理上千份 PDF 时,速度很重要。几个优化建议:

1. 多进程并行处理

PDF 提取是 CPU 密集型任务,用多进程可以充分利用多核 CPU:

from multiprocessing import Pool, cpu_countdef process_single_pdf(pdf_path):    """处理单个PDF(独立函数,用于多进程)"""    # ... 把上面的单文件处理逻辑放这里 ...    return recorddef process_all_pdfs_parallel(pdf_files):    """多进程并行处理"""    num_workers = min(cpu_count(), 4)  # 最多4个进程    print(f"使用 {num_workers} 个进程并行处理")    with Pool(num_workers) as pool:        results = pool.map(process_single_pdf, pdf_files)    return results

2. 只提取前几页

很多 PDF 的关键信息(日期、金额、客户)都在前 1-2 页。如果确认是这样,可以只提取前 2 页,速度提升数倍:

def extract_text_first_pages(pdf_path, max_pages=2):    with pdfplumber.open(pdf_path) as pdf:        full_text = ""        for page in pdf.pages[:max_pages]:            text = page.extract_text()            if text:                full_text += text + "\n"        return full_text

3. 增量处理

第一次处理完后,后续只处理新增的文件。可以记录已处理的文件名,跳过已处理的:

import jsondef load_processed(log_file):    """加载已处理文件列表"""    if os.path.exists(log_file):        with open(log_file, "r", encoding="utf-8") as f:            return set(json.load(f))    return set()

07 常见坑与解决方案

坑 1:正则匹配不到

90% 的原因是 PDF 提取的文本和你看到的不一样。比如 "金额:100" 在提取后可能变成 "金额: 100"(英文冒号)或 "金额 100"(没有冒号)。一定要先打印提取的文本,根据实际文本写正则。

坑 2:金额提取错误

PDF 里可能有多个金额(小计、合计、税额),正则可能匹配到第一个而不是你要的。解决方法:

1)用更精确的关键词(如 "价税合计" 而不是 "金额");

2)提取所有金额后取最大值;

3)根据位置信息判断。

坑 3:客户名称提取不全

公司名称可能很长,跨行显示。正则 (.+?)(?:\n|$) 只能取一行。解决方法:提取后判断是否以 "公司"、"有限公司" 结尾,如果不是,拼接下一行。

坑 4:处理速度慢

参考第 06 节的优化方案:多进程、只提取前几页、增量处理。上千份文件用单进程可能要 30 分钟,优化后 5-10 分钟搞定。

写在最后

这个实战项目把前面六篇的技能全部串起来了:文件遍历、文本提取、加密检测、正则匹配、数据汇总、Excel 导出。它不是一个完美的通用方案,但它是一个可以直接改改用在自己工作中的框架。

自动化的价值不在于代码多牛逼,而在于解决真实问题、省下真实时间。当你用这个脚本把一周的工作量压缩到 10 分钟,你就真正理解了什么叫 "用 Python 解放双手"。

相关学习资料