首页 / 资讯中心 / 文章详情

Python批量提取PDF表格到Excel:pdfplumber与openpyxl实战

Python批量提取PDF表格到Excel:pdfplumber与openpyxl实战 ★ FEATURED ARTICLE
简介这份资源面向需要批量处理文档数据的Python学习者与办公自动化从业者聚焦于将PDF中的表格数据一键提取并导出为Excel这一常见痛点。包内共14个文件以png截图、xlsx结果文件、pdf原始文档为主另含1个ipynb交互式笔记与1个py脚本压缩包约18.34MB覆盖从原始年报PDF到提取后Excel的完整对照样例。内容围绕pdfplumber、pandas、openpyxl等库的实际调用展开并附带东旭蓝天、华特气体、保利地产等真实年报表格作为练习素材便于读者直接运行脚本、观察提取效果并对照截图排查问题。目前已有117人学习下载适合希望掌握PDF表格抽取与数据清洗技巧、提升办公数据处理效率的初中级Python使用者参考。1. 批量表格提取从几十份 PDF 里把数据捞进 Excel上个月帮一个做财务的朋友处理报销凭证对方甩过来一个压缩包里面塞了四十多份 PDF 对账单每份都有三四个表格。他原本的计划是复制粘贴干到第三份就崩溃了。我看了眼文件结构表格边框规整、没有合并单元格这种场景用 Python 做批量提取再合适不过。这个项目就是干这件事的把 PDF 里的表格识别出来按页或按文件写入 Excel支持批量遍历目录。它适合经常跟报表、发票、对账单打交道的运营和财务人员也适合想找一个 pdfplumber 实战入口的 Python 开发者。整个脚本不依赖付费接口本地跑数据不出机器。2. 技术选型与目录结构为什么是 pdfplumber 加 openpyxl2.1 三个候选库的取舍处理 PDF 表格Python 圈子里常见的选择有三个pdfplumber、camelot、tabula。tabula 依赖 Java 运行时部署时多一层环境负担camelot 对表格线敏感遇到无边框表格容易翻车而且底层也依赖 opencv 和 ghostscript。pdfplumber 纯 Python 实现基于 pdfminer.six安装即用对有线表格的识别率相当稳还能拿到每个字符的坐标方便做二次判断。这个项目选的就是 pdfplumber 负责解析openpyxl 负责写 Excel。openpyxl 的优势是能精确控制单元格样式、合并、列宽而且不依赖 Excel 软件本身。两者组合一个管读一个管写职责清晰。提示如果你的 PDF 是扫描件pdfplumber 提取不到文字层需要先走 OCR。这个项目不覆盖扫描件场景。2.2 目录结构长什么样一个能直接跑的工程目录不用复杂但得让人一眼看懂pdf_table_extractor/ ├── main.py # 入口命令行参数解析 ├── extractor.py # PDF 表格提取核心逻辑 ├── writer.py # Excel 写入封装 ├── config.py # 可调参数集中管理 ├── requirements.txt # 依赖清单 ├── input/ # 待处理的 PDF 放这里 └── output/ # 生成的 Excel 落这里把提取逻辑和写入逻辑拆开是为了后面改需求时不动一处牵全身。比如你想把结果写成 CSV只改 writer.py 就行想换解析库只动 extractor.py。config.py 里放表格识别参数、输出文件名规则、日志级别这些避免硬编码散落各处。2.3 依赖安装与环境确认requirements.txt 里就三行pdfplumber0.10.3 openpyxl3.1.2 pandas2.1.4pandas 在这里不是必须的但做数据清洗和拼接时非常顺手建议留着。安装命令pip install -r requirements.txt装完先验证一下 pdfplumber 能不能正常导入有些环境里 pdfminer.six 的版本冲突会导致 import 报错import pdfplumber print(pdfplumber.__version__)如果这一步报ModuleNotFoundError大概率是虚拟环境没激活或者 pip 装到了系统 Python 而不是当前环境。用which python和which pip确认两者指向同一路径。3. 核心提取逻辑从页面对象到二维列表3.1 打开 PDF 与逐页遍历pdfplumber 的用法很直观open之后得到一个 PDF 对象pages属性是页面列表。核心提取函数大概长这样import pdfplumber def extract_tables_from_pdf(pdf_path, table_settingsNone): 从单个 PDF 中提取所有表格 :param pdf_path: PDF 文件路径 :param table_settings: 传给 extract_tables 的参数字典 :return: 列表每个元素是 (页码, 表格二维列表) results [] with pdfplumber.open(pdf_path) as pdf: for page_num, page in enumerate(pdf.pages, start1): # extract_tables 返回该页所有表格每个表格是二维列表 tables page.extract_tables(table_settings or {}) for table in tables: if table and len(table) 1: # 至少两行才算有效表格 results.append((page_num, table)) return results这里有几个点值得说。extract_tables返回的是一个三维结构页面上可能有多个表格每个表格是一个二维列表行是列表元素单元格是字符串。空单元格返回None而不是空字符串后面写入 Excel 时要处理。len(table) 1这个判断是为了过滤掉那些被误识别成表格的单行文本块实际跑的时候你会发现这种误判不少。3.2 表格识别参数怎么调table_settings是 pdfplumber 提供的调节入口默认值对规整表格够用但遇到特殊情况需要改。常用参数参数默认值作用vertical_strategylines垂直线识别策略可选 lines/texthorizontal_strategylines水平线识别策略snap_tolerance3线条吸附容差值越大越容易把邻近线合并join_tolerance3线段连接容差edge_min_length3最小边缘长度过滤噪点如果 PDF 表格没有边框线把两个 strategy 都改成textpdfplumber 会根据文字对齐来推断列边界。代价是准确率下降列多的时候容易串列。我一般会先跑一遍默认参数看输出结果再决定要不要调。# 无边框表格的配置示例 no_border_settings { vertical_strategy: text, horizontal_strategy: text, snap_tolerance: 5, join_tolerance: 5, }snap_tolerance调大到 5 是为了容忍扫描或排版造成的轻微偏移。这个值不是越大越好超过 8 之后相邻两列可能被合并成一列反而更糟。3.3 数据清洗None 值和换行符提取出来的原始数据不能直接写 Excel得先过一遍清洗。两个高频问题单元格是None以及单元格内有多余换行。def clean_table(table): 清洗表格数据None 转空串去除换行和首尾空格 cleaned [] for row in table: new_row [] for cell in row: if cell is None: new_row.append() else: # PDF 里单元格内换行很常见统一替换成空格 text str(cell).replace(\n, ).strip() new_row.append(text) cleaned.append(new_row) return cleaned换行符处理是个血泪经验。PDF 表格里一个单元格的文字经常被硬换行拆成多行直接写进 Excel 会变成单元格内换行看起来乱且影响后续筛选。替换成空格之后数据在 Excel 里是一整行做透视表方便得多。当然如果你需要保留原始换行把replace那步去掉即可。4. 写入 Excel 与批量调度让脚本能处理整个目录4.1 openpyxl 写入与样式控制写入逻辑封装在 writer.py 里核心是创建一个 Workbook逐行写入顺便设置列宽和表头加粗from openpyxl import Workbook from openpyxl.styles import Font, Alignment def write_tables_to_excel(tables_data, output_path): 将提取的表格写入 Excel :param tables_data: [(页码, 表格二维列表), ...] :param output_path: 输出 xlsx 路径 wb Workbook() wb.remove(wb.active) # 删掉默认空 sheet for idx, (page_num, table) in enumerate(tables_data, start1): ws wb.create_sheet(titlefP{page_num}_T{idx}) for row_idx, row in enumerate(table, start1): for col_idx, value in enumerate(row, start1): cell ws.cell(rowrow_idx, columncol_idx, valuevalue) if row_idx 1: cell.font Font(boldTrue) cell.alignment Alignment(horizontalcenter) # 根据内容长度粗略设置列宽 for col in ws.columns: max_len max(len(str(c.value or )) for c in col) ws.column_dimensions[col[0].column_letter].width min(max_len 4, 40) wb.save(output_path)每个表格单独一个 sheet命名带上页码和序号方便回溯。表头加粗居中列宽按内容自适应但封顶 40避免某一列文字特别长把表格撑得没法看。wb.remove(wb.active)这步别漏否则会多一个空白 sheet。4.2 批量遍历目录与异常隔离单个文件跑通之后批量处理就是套一层目录遍历。关键是异常隔离某个 PDF 损坏或加密不能让整个批次挂掉。import os from pathlib import Path def batch_process(input_dir, output_dir): 批量处理目录下所有 PDF input_path Path(input_dir) output_path Path(output_dir) output_path.mkdir(parentsTrue, exist_okTrue) pdf_files list(input_path.glob(*.pdf)) print(f共发现 {len(pdf_files)} 个 PDF 文件) for pdf_file in pdf_files: try: tables extract_tables_from_pdf(str(pdf_file)) if not tables: print(f[跳过] {pdf_file.name} 未识别到表格) continue out_file output_path / f{pdf_file.stem}_tables.xlsx write_tables_to_excel(tables, str(out_file)) print(f[完成] {pdf_file.name} - {out_file.name}) except Exception as e: # 单个文件失败不影响后续 print(f[失败] {pdf_file.name}: {e})glob(*.pdf)只匹配当前目录不递归子目录。如果你的 PDF 分散在多级目录里换成rglob(*.pdf)。异常捕获这里故意捕获宽泛的Exception因为 PDF 解析可能抛出各种底层异常逐个列举不现实。打印出文件名和错误信息事后可以针对性排查。4.3 命令行入口与参数设计main.py 用 argparse 接收参数让脚本不用改代码就能指定输入输出目录import argparse from extractor import batch_process def main(): parser argparse.ArgumentParser(description批量提取 PDF 表格到 Excel) parser.add_argument(-i, --input, defaultinput, helpPDF 所在目录) parser.add_argument(-o, --output, defaultoutput, helpExcel 输出目录) args parser.parse_args() batch_process(args.input, args.output) if __name__ __main__: main()运行方式python main.py -i ./input -o ./output不传参数就用默认目录。这种设计对非开发者友好把 PDF 丢进 input 文件夹双击运行或者命令行敲一行就行。5. 避坑与常见问题那些跑起来才会遇到的事5.1 提取结果全是空值现象脚本正常跑完Excel 里每个单元格都是空的。原因通常是 PDF 是扫描件没有文字层pdfplumber 只能拿到图像。解决用page.extract_text()测试一下如果返回空字符串说明确实是扫描件需要先做 OCR 再提取。另一个可能是表格线颜色太浅pdfplumber 默认阈值识别不到把snap_tolerance调大试试。5.2 列错位或串列现象提取出来的表格列数对不上某一列的数据跑到了隔壁。原因多半是表格没有完整边框或者存在跨页表格。解决先确认表格是否有线无线表格改用text策略跨页表格需要手动拼接pdfplumber 不会自动合并。我一般会在提取后打印每张表的列数列数突变的地方就是问题所在。5.3 中文乱码或方框现象Excel 里中文显示为乱码。原因PDF 内嵌字体缺少 Unicode 映射pdfplumber 提取出来的是乱码字符。解决这种情况比较棘手可以尝试用pdfplumber的chars属性拿到字符对象检查text字段是否正常。如果源头就是乱码换库也救不了只能走 OCR。写入 Excel 时确保用 openpyxl 默认的 UTF-8 处理不要手动指定编码。5.4 内存占用过高现象处理大 PDF 时内存飙升甚至卡死。原因pdfplumber 会把页面对象缓存在内存里几百页的 PDF 加上复杂表格占用很可观。解决不要一次性把所有页面结果存列表再写改成逐页提取、逐页写入或者分批处理。另外with pdfplumber.open()的上下文管理器一定要用确保文件句柄及时释放。5.5 输出文件被占用现象第二次运行时保存 Excel 报PermissionError。原因上一次生成的 Excel 还开着没关。解决这是 Windows 下的经典问题脚本层面可以在保存前检查文件是否存在并尝试删除或者输出文件名带上时间戳避免覆盖。我习惯在输出文件名里加日期既避免占用冲突也方便追溯版本。6. 二次开发把提取结果接进你的数据管道跑通之后这个脚本最值钱的地方不是提取本身而是它作为一个数据入口能接进后续流程。我一般会从三个方向改。第一个方向是输出结构化。现在每个表格一个 sheet适合人工看但不适合程序消费。如果你要把数据灌进数据库或者做分析把 writer.py 改成输出 CSV 或者直接写 SQLite。pandas 在这里很好用import pandas as pd def tables_to_dataframe(tables_data): 把提取结果拼成一个 DataFrame方便后续处理 frames [] for page_num, table in tables_data: df pd.DataFrame(table[1:], columnstable[0]) # 首行做表头 df[_source_page] page_num frames.append(df) return pd.concat(frames, ignore_indexTrue)加上_source_page列是为了追溯数据来源出问题时能定位到具体页码。表头处理这里假设每个表格首行都是列名如果实际数据不是这样需要按业务调整。第二个方向是加校验规则。财务对账单这类场景金额列必须能转成数字日期列必须符合格式。在写入前加一层校验把不合格的行单独记到日志里def validate_row(row, numeric_cols): 校验指定列是否为数字返回错误信息列表 errors [] for col in numeric_cols: try: float(str(row[col]).replace(,, )) except (ValueError, KeyError): errors.append(f列 {col} 值异常: {row.get(col)}) return errorsreplace(,, )是为了处理千分位分隔符这个细节不注意的话1,234.56会直接抛异常。校验不过的行不要静默丢弃记下来人工复核数据处理的后悔药就是日志。第三个方向是加进度和重试。批量处理几百个文件时你需要知道跑到哪了。用 tqdm 加个进度条失败的文件记到一个 failed.txt 里下次只跑失败的from tqdm import tqdm for pdf_file in tqdm(pdf_files, desc提取进度): ...重试逻辑针对的是那些因为文件被占用或临时读取失败的情况隔几秒重试一次往往就好了。但如果是 PDF 本身损坏重试多少次都没用所以重试次数设 2 到 3 次就够别死循环。改造的时候有个原则提取逻辑和业务逻辑分开。extractor.py 只管从 PDF 拿数据校验、转换、入库这些放到单独的模块。这样换业务场景时提取部分不用动。我见过太多项目把校验规则硬塞进提取函数里后来想复用到另一个报表就完全没法拆。从那以后我每次做这类批量提取都强制先拿三个样本文件跑通全流程确认输出格式和边界情况再放开跑全量。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站