简介:这份资源面向需要批量处理PDF表格的Python学习者与办公自动化开发者,提供从PDF中一键提取表格并导出Excel的完整实例。包内共14个文件,以png截图、xlsx结果表、pdf样本、ipynb笔记本和py脚本为主,压缩包约18.34MB,涵盖年报类PDF与对应Excel输出,便于对照验证提取效果。已有117人学习下载。读者可参考脚本与Notebook理解pdfplumber、pandas、openpyxl等库的配合方式,掌握读取PDF、识别表格、构建DataFrame、写入Excel的完整链路,并借助截图与结果文件排查格式不规范、数据缺失等常见问题,提升数据处理效率。
1. 批量提取 PDF 表格到 Excel:一份能直接跑的 Python 实例拆解
财务对账单、供应商报价单、检测报告,这些 PDF 里的表格要一条条复制到 Excel,手动干一下午眼睛都花了。这份 Python 实例解决的就是这个场景:用 pdfplumber 定位表格线,用 pandas 清洗数据,最后批量写入 Excel。它适合经常处理报表的运营、财务、数据分析师,也适合刚学完 Python 基础想找个真实项目练手的开发者。整个脚本不依赖付费接口,本地跑,数据不出机器。下面按「环境怎么搭、表格怎么抽、坑怎么绕」的顺序拆开讲,每一步都能直接抄。
2. 环境搭建与依赖选型:为什么是 pdfplumber 而不是 PyPDF2
2.1 三个库的分工与安装
处理 PDF 表格这件事,Python 生态里能选的库不少,但真正能打的就三个:pdfplumber 负责抽取表格结构,pandas 负责数据清洗和格式转换,openpyxl 负责写 Excel。PyPDF2 只能读文本流,遇到有线框的表格直接抓瞎;camelot 依赖 Ghostscript,装起来容易翻车。pdfplumber 基于 pdfminer.six,能识别表格的横竖线,对规则表格的还原度最高。
安装命令一行搞定:
pip install pdfplumber pandas openpyxl这里有个版本兼容的坑:pdfplumber 0.10.x 和 pdfminer.six 20221105 之后的版本配合最稳。如果装完 import 报cannot import name 'PDFObjRef',大概率是 pdfminer.six 版本太老,升级一下就行:
pip install --upgrade pdfminer.six参数说明:--upgrade会拉取最新版,但生产环境建议锁版本,比如pdfminer.six==20221105,避免某天自动升级后接口变了。
2.2 验证环境是否可用
装完别急着写业务代码,先跑一个最小验证,确认库能正常加载:
import pdfplumber import pandas as pd import openpyxl print("pdfplumber:", pdfplumber.__version__) print("pandas:", pd.__version__) print("openpyxl:", openpyxl.__version__)逻辑说明:这段代码只做一件事——确认三个库都能 import 且打印版本号。如果某一行报ModuleNotFoundError,说明对应的库没装上,回到 2.1 重新装。版本号打印出来是为了后面排查问题时有个依据,比如 pdfplumber 0.5.x 和 0.10.x 的extract_tables返回结构有差异,知道版本能少走弯路。
常见做法是建一个requirements.txt,把版本钉死:
pdfplumber==0.10.3 pandas==2.1.4 openpyxl==3.1.2这样换机器或者给同事复现时,pip install -r requirements.txt一把过,不会出现「我这能跑你那报错」的玄学问题。
3. 表格抽取核心逻辑:extract_tables 的参数怎么调
3.1 单页表格抽取与 DataFrame 转换
pdfplumber 的核心方法是page.extract_tables(),它返回一个嵌套列表,每个元素是一张表,表里每一行是一个列表。直接转 DataFrame 就能用:
import pdfplumber import pandas as pd def extract_tables_from_pdf(pdf_path): all_tables = [] with pdfplumber.open(pdf_path) as pdf: for page_num, page in enumerate(pdf.pages, start=1): # 抽取当前页所有表格 tables = page.extract_tables() for table in tables: if not table or len(table) < 2: continue # 跳过空表或只有表头的表 # 第一行作为列名,其余作为数据 df = pd.DataFrame(table[1:], columns=table[0]) df["来源页"] = page_num # 标记数据来自哪一页 all_tables.append(df) return all_tables tables = extract_tables_from_pdf("report.pdf") for i, df in enumerate(tables): print(f"第 {i+1} 张表,{df.shape[0]} 行 x {df.shape[1]} 列") print(df.head())逻辑说明:pdfplumber.open以上下文管理器打开文件,确保用完自动释放。enumerate(pdf.pages, start=1)给每页编号,方便追溯数据来源。extract_tables()不带参数时用默认的线框检测策略,适合大多数有线表格。table[1:]跳过表头行,columns=table[0]把第一行当列名。加一列「来源页」是个实用习惯,后面核对数据时能快速定位。
参数说明:extract_tables()支持table_settings参数,常用配置如下:
| 参数 | 作用 | 适用场景 |
|---|---|---|
| vertical_strategy | 竖线检测策略 | lines(默认,有线框)、text(按文字间距) |
| horizontal_strategy | 横线检测策略 | lines(默认)、text |
| snap_tolerance | 线条吸附容差 | 表格线模糊时调大,默认 3 |
| join_tolerance | 线段合并容差 | 虚线表格调大,默认 3 |
| min_words_vertical | 竖策略为 text 时最少词数 | 默认 3 |
如果表格没有线框,只有文字对齐,就得改成:
table_settings = { "vertical_strategy": "text", "horizontal_strategy": "text", "min_words_vertical": 2, "min_words_horizontal": 1, } tables = page.extract_tables(table_settings)3.2 多页批量处理与 Excel 写入
单张表抽出来只是第一步,实际场景往往是几十个 PDF 每个几十页。批量处理要解决两个问题:文件遍历和 Excel 分 sheet 写入。
import os import glob import pdfplumber import pandas as pd def batch_extract_to_excel(input_dir, output_path): pdf_files = glob.glob(os.path.join(input_dir, "*.pdf")) if not pdf_files: print("没找到 PDF 文件,检查路径") return with pd.ExcelWriter(output_path, engine="openpyxl") as writer: for pdf_file in pdf_files: base_name = os.path.splitext(os.path.basename(pdf_file))[0] with pdfplumber.open(pdf_file) as pdf: for page_num, page in enumerate(pdf.pages, start=1): tables = page.extract_tables() for t_idx, table in enumerate(tables): if not table or len(table) < 2: continue df = pd.DataFrame(table[1:], columns=table[0]) # sheet 名不能超过 31 字符,且不能含特殊字符 sheet_name = f"{base_name}_p{page_num}_t{t_idx}"[:31] df.to_excel(writer, sheet_name=sheet_name, index=False) print(f"完成,输出到 {output_path}") batch_extract_to_excel("./pdfs", "./output.xlsx")逻辑说明:glob.glob匹配目录下所有 PDF,os.path.splitext去掉扩展名拿文件名。pd.ExcelWriter用 openpyxl 引擎,支持在一个文件里写多个 sheet。sheet 名拼接了文件名、页码、表序号,方便追溯。[:31]是硬性截断,Excel 的 sheet 名上限就是 31 个字符,超了直接报错。
参数说明:to_excel的index=False表示不写行索引,否则 Excel 里会多一列 0、1、2。如果数据量大,ExcelWriter可以加mode="a"追加模式,但要注意文件必须已存在。
4. 避坑与排查:表格抽取翻车的五个真实场景
4.1 现象:抽出来的表格全是空行或乱码
原因:PDF 是扫描件,本质是图片,没有文字层。pdfplumber 只能处理有文字层的 PDF,扫描件需要先做 OCR。
解决:用 pdfplumber 的page.images检查是否有图片对象,如果有且extract_text()返回空,基本可以判定是扫描件。常见做法是先过一遍 OCR 工具转成文字层 PDF,再走抽取流程。判断代码:
with pdfplumber.open("scan.pdf") as pdf: page = pdf.pages[0] text = page.extract_text() if not text or len(text.strip()) < 10: print("疑似扫描件,需要 OCR 预处理")4.2 现象:表格列错位,数字跑到别的列里
原因:PDF 里表格线不完整,或者单元格内有换行导致 pdfplumber 把一行拆成两行。
解决:先调snap_tolerance和join_tolerance,从默认 3 调到 5 或 8,让断开的线能接上。如果还不行,改用text策略按文字位置切分。另外可以在extract_tables之后加一步清洗,把空字符串替换成None,再用dropna去掉全空行:
df = df.replace("", None).dropna(how="all")4.3 现象:中文列名变成乱码或问号
原因:PDF 内嵌字体编码问题,pdfplumber 解码时拿不到正确的 Unicode 映射。
解决:这种情况比较棘手,先确认 PDF 是不是标准字体。可以试page.extract_text(layout=True)看文本层是否正常。如果文本层正常但表格抽取乱码,尝试升级 pdfplumber 到最新版,新版本对 CJK 字体的处理有改进。实在不行,用pdfplumber抽文本再自己按位置解析,放弃extract_tables。
4.4 现象:Excel 写入报ValueError: Sheetname must be a string
原因:sheet 名拼接时用了非字符串类型,或者包含 Excel 不允许的字符:\ / * ? [ ] :。
解决:写一个清洗函数,把非法字符替换掉:
import re def clean_sheet_name(name): name = re.sub(r'[\\/*?\[\]:]', '_', str(name)) return name[:31] sheet_name = clean_sheet_name(f"{base_name}_p{page_num}")4.5 现象:大文件处理到一半内存爆了
原因:pdfplumber会把页面对象缓存在内存里,几百页的 PDF 加上多个 DataFrame 同时存在,内存直接拉满。
解决:逐页处理,处理完一页就把page对象释放,DataFrame 及时写入 Excel 而不是全部攒在列表里。上面的批量代码已经是流式写入,如果还爆,可以加gc.collect()手动触发回收,或者把 PDF 拆成小文件分批跑。
注意:pdfplumber 打开文件后如果不关闭,Windows 下会锁文件,后续想移动或删除会报「文件被占用」。务必用
with语句。
5. 进阶技巧:合并多表与格式保留
5.1 跨页表格合并
很多报表的表格是跨页的,第一页有表头,第二页直接续数据。处理思路是:检测后续页的第一行是否与首页表头一致,如果一致就跳过表头直接拼数据。
def merge_cross_page_tables(pdf_path): merged_rows = [] header = None with pdfplumber.open(pdf_path) as pdf: for page in pdf.pages: tables = page.extract_tables() for table in tables: if not table: continue if header is None: header = table[0] merged_rows.extend(table[1:]) else: # 如果当前表头和已存表头一致,跳过表头行 if table[0] == header: merged_rows.extend(table[1:]) else: merged_rows.extend(table) return pd.DataFrame(merged_rows, columns=header)逻辑说明:用一个header变量记住第一张表的表头,后续表如果表头相同就只取数据行,不同则视为新表全部保留。这个逻辑适合结构一致的连续报表,如果 PDF 里混了多种表格,需要加判断条件区分。
5.2 写入 Excel 时保留数字格式
pdfplumber 抽出来的全是字符串,写入 Excel 后数字左对齐、无法求和。可以在to_excel之前做类型转换:
def convert_types(df): for col in df.columns: # 尝试转数字,失败就保留字符串 df[col] = pd.to_numeric(df[col], errors="ignore") return df df = convert_types(df) df.to_excel(writer, sheet_name=sheet_name, index=False)参数说明:errors="ignore"表示转换失败时保留原值,不会报错。如果确定某列是数字,可以用errors="coerce"把失败值变成 NaN,再统一填充。
5.3 验证抽取结果是否完整
抽完之后怎么确认没漏数据?我一般用两个办法:一是对比 PDF 页数和 Excel 里有数据的 sheet 数,二是随机抽几页人工核对行数。还可以写个校验脚本,统计每张表的行数和列数,跟 PDF 里肉眼看到的做个粗略对比:
def validate_extraction(pdf_path, excel_path): with pdfplumber.open(pdf_path) as pdf: pdf_table_count = sum(len(p.extract_tables()) for p in pdf.pages) xl = pd.ExcelFile(excel_path) print(f"PDF 中检测到 {pdf_table_count} 张表,Excel 中有 {len(xl.sheet_names)} 个 sheet") for sheet in xl.sheet_names: df = xl.parse(sheet) print(f" {sheet}: {df.shape[0]} 行 x {df.shape[1]} 列")从那以后我每次跑完批量抽取,都强制走一遍这个校验,确认表数量和行数量级对得上,再交付。希望帮到你。
本文还有配套的精品资源,点击获取