ARTICLE · 984038
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_encryptedexcept: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_textexcept Exception as e:return Nonedef is_scanned(text):"""判断是否为扫描件(提取到的文本极少)"""if text is None:return Truereturn 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] = valueelse: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. 处理所有PDFprint(f"\n开始处理...")results, log_entries = process_all_pdfs(pdf_files)# 3. 导出Excelexport_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*)",}
正则编写技巧:
用 [::]同时匹配中文冒号和英文冒号(PDF 里两种都可能出现)。金额用 [\d,]+\.?\d*匹配带逗号和小数的数字。日期用 \d{4}[-/年]\d{1,2}[-/月]\d{1,2}日?兼容多种日期格式。客户名称用 (.+?)(?:\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 解放双手"。