☰
Python自动化:用pdfplumber批量提取PDF表格到Excel实战
2026/10/15 11:22:27 网站建设 项目流程

简介:这是一份面向Python自动化办公初学者的实战项目源码,聚焦于用Python一键提取PDF文档中的表格并转换为Excel文件,解决日常办公中批量处理PDF表格数据繁琐、易出错的问题。项目围绕PyPDF2与pandas两大核心库展开,涵盖PDF页面读取、文本提取、正则表达式分割、DataFrame构建、数据清洗与错误处理等关键环节,脚本pdf_to_excel.py以函数形式封装完整流程,并配有详细中文注解,逐步解释每步操作的目的与所用方法,新手也能一看就懂。压缩包为rar格式,大小约17.77MB,内含项目源码及相关文件,便于直接运行调试与二次修改。目前已有107人学习关注,适合希望掌握PDF表格自动化处理、提升办公效率的Python入门者,通过完整源码与注释快速理解文本处理、正则表达式与数据清洗思路,为后续自动化办公任务打下基础。

1. 用 Python 把 PDF 表格搬进 Excel:为什么你手敲 200 行数据不如 20 行代码

财务同事发来一份 80 页的 PDF 对账单,每页一张表格,要求当天整理成 Excel。手动复制粘贴的结局通常是:数字串行、合并单元格错位、小数点丢失,加班到深夜还挨骂。这个标题讲的就是用 Python 把 PDF 里的表格自动提取到 Excel,附带详细注解的源码,新手照着改路径就能跑。它解决的不是"PDF 怎么读"这种泛问题,而是"表格结构怎么还原、跨页怎么合并、数字格式怎么不翻车"这三个具体痛点。适合每天跟报表打交道的运营、财务、数据分析新手,也适合想找一个练手项目入门 Python 自动化的开发者。核心工具链是 pdfplumber 负责抽取、pandas 负责清洗、openpyxl 负责写盘,三件套装完就能开工。

2. 选型先定死:pdfplumber、camelot、tabula 到底该用哪个

2.1 三种主流方案的能力边界

PDF 里的表格分两类:一类是文本型 PDF,由 Word、Excel 直接导出,文字可选可搜;另一类是扫描型 PDF,本质是图片,必须先 OCR。这个标题的方案针对的是第一类,选错工具会直接卡死。

工具依赖适合场景明显短板
pdfplumber纯 Python,无外部依赖有线框的规则表格、需要精细控制无线框表格识别率一般
camelot依赖 Ghostscript、OpenCV复杂表格、批量处理环境配置容易翻车
tabula-py依赖 Java 运行时快速抽取、命令行友好参数调节粒度粗

我一般首选 pdfplumber,原因是它把每个字符的坐标都暴露出来,遇到合并单元格、跨页表格时能自己写逻辑兜底,而不是被工具的黑匣子卡住。camelot 精度在某些场景更高,但让新手先装 Ghostscript 就是劝退。tabula 依赖 Java,公司电脑没装 JDK 的话报错信息能看半天。

2.2 环境搭建与最小验证

先建虚拟环境,避免污染全局包。命令如下:

# 创建并激活虚拟环境(Windows 用 venv\Scripts\activate) python -m venv pdfenv source pdfenv/bin/activate # 安装三件套,pdfplumber 会自动带上 pdfminer.six pip install pdfplumber pandas openpyxl

装完先做一次最小验证,确认能打开目标 PDF 并读到页数:

import pdfplumber # 打开 PDF,with 语句保证文件句柄自动释放 with pdfplumber.open("sample.pdf") as pdf: print(f"总页数: {len(pdf.pages)}") # 取第一页,打印前 200 个字符,确认是文本型而非扫描件 first_page = pdf.pages[0] print(first_page.extract_text()[:200])

如果extract_text()返回空字符串或乱码,说明这份 PDF 是扫描件,pdfplumber 无能为力,得先走 OCR 路线,这不是本方案覆盖的范围。参数上pdfplumber.open()支持password参数处理加密 PDF,遇到需要密码的文件直接传进去即可。验证通过后再进入正式提取,别一上来就写完整脚本,先确认数据源可用,这是省时间的习惯。

3. 提取表格的核心代码:从 extract_tables 到 DataFrame

3.1 用 extract_tables 拿到原始二维列表

pdfplumber 的extract_tables()返回的是嵌套列表,结构是[页][表][行][列]。先看清楚原始数据长什么样,再决定怎么清洗:

import pdfplumber with pdfplumber.open("sample.pdf") as pdf: for i, page in enumerate(pdf.pages): # 提取当前页所有表格,返回列表,每张表是二维列表 tables = page.extract_tables() for j, table in enumerate(tables): print(f"第{i+1}页 第{j+1}张表,共{len(table)}行") # 打印表头,确认列对齐情况 print(table[0])

extract_tables()的关键参数有三个:table_settings用于自定义线框识别策略,vertical_strategy和horizontal_strategy控制按线还是按文本对齐来切分。默认值是lines,即依赖表格线框。如果 PDF 表格没有线框,改成text会按字符间距推断列边界,但准确率下降,需要配合snap_tolerance微调。

3.2 清洗空值与合并单元格

原始数据里最常见的两个问题:None值(空单元格)和跨行合并导致的错位。清洗逻辑如下:

import pandas as pd def clean_table(raw_table): # 把 None 替换成空字符串,避免后续写入 Excel 报错 cleaned = [["" if cell is None else str(cell).strip() for cell in row] for row in raw_table] # 用第一行做表头,其余做数据 df = pd.DataFrame(cleaned[1:], columns=cleaned[0]) # 去掉整行全空的行(常见于跨页表格的分隔行) df = df[~df.apply(lambda r: r.str.strip().eq("").all(), axis=1)] return df

这里str(cell).strip()顺手去掉了单元格里的换行符和首尾空格,PDF 提取出来的文本经常带\n,不处理的话写进 Excel 会变成多行显示。df.apply(...)那行判断整行是否全空,跨页表格中间常有一行空白,留着会污染数据。参数上axis=1表示按行判断,别写成 0。

3.3 多页表格合并与去重表头

80 页的 PDF 每页都有表头,直接拼接会出现重复表头行。处理方式是:第一页保留表头,后续页跳过第一行:

all_dfs = [] with pdfplumber.open("sample.pdf") as pdf: for i, page in enumerate(pdf.pages): tables = page.extract_tables() for table in tables: df = clean_table(table) if i > 0: # 后续页丢弃表头行,只保留数据 df = df.iloc[1:] all_dfs.append(df) # 合并所有页,ignore_index 重置行号 final_df = pd.concat(all_dfs, ignore_index=True) final_df.to_excel("output.xlsx", index=False)

pd.concat的ignore_index=True很重要,不加的话每页行号会重复。to_excel的index=False去掉 pandas 默认的行索引列,否则 Excel 里会多一列无意义的数字。到这一步,一个能跑通的最小闭环就完成了。

4. 避坑指南:五个让新手当场翻车的细节

4.1 提取出来全是 None 或空列表

现象:extract_tables()返回[],或者每行都是[None, None, None]。原因:PDF 表格没有线框,默认的lines策略识别不到边界。解决:改用文本对齐策略,并适当放宽容差:

table_settings = { "vertical_strategy": "text", "horizontal_strategy": "text", "snap_tolerance": 5, # 字符对齐容差,默认 3,调大更宽容 "join_tolerance": 5, # 线段连接容差 } tables = page.extract_tables(table_settings)

如果还是不行,说明表格结构太不规则,考虑换 camelot 的flavor="stream"模式。

4.2 数字变成科学计数法或丢失前导零

现象:订单号0012345写进 Excel 变成12345,长数字变成1.23E+10。原因:Excel 自动把纯数字字符串当数值处理。解决:写入时强制指定列格式为文本。用 openpyxl 直接控制:

from openpyxl import Workbook from openpyxl.utils.dataframe import dataframe_to_rows wb = Workbook() ws = wb.active for row in dataframe_to_rows(final_df, index=False, header=True): ws.append(row) # 把 A 列(订单号)整列设为文本格式 for cell in ws["A"]: cell.number_format = "@" wb.save("output.xlsx")

"@"是 Excel 的文本格式代码,加上后前导零和长数字都能保住。

4.3 跨页表格被拆成两张,数据错位

现象:第 3 页末尾和第 4 页开头的行拼不上,列数对不齐。原因:每页独立提取,列宽不一致时 pandas 按位置对齐会错位。解决:先统一列数,缺列补空。在clean_table里加一步:

def align_columns(df, expected_cols): # 缺列补空字符串,多列截断 for col in expected_cols: if col not in df.columns: df[col] = "" return df[expected_cols]

expected_cols从第一页的表头取,作为全局标准。

4.4 中文乱码或提取出方框

现象:中文显示为□□□或乱码。原因:PDF 内嵌字体缺少 Unicode 映射表。解决:pdfplumber 对此基本无解,属于 PDF 生成时的缺陷。可以尝试用pdfminer.six的laparams参数调整,但成功率不高。实务中遇到这种文件,直接找原始数据源比死磕提取更划算。

4.5 大文件内存爆掉

现象:处理 500 页以上 PDF 时程序卡死或 MemoryError。原因:pdfplumber.open()会把页面对象缓存在内存里。解决:逐页处理,处理完立即释放:

with pdfplumber.open("big.pdf") as pdf: for page in pdf.pages: tables = page.extract_tables() # 处理完当前页后手动清理 page.flush_cache()

flush_cache()是 pdfplumber 提供的显式释放方法,处理大文件时能显著降低内存占用。

5. 进阶技巧:让提取结果直接可用而不是"能跑就行"

5.1 用 table_settings 精细控制列边界

默认策略在遇到"表格里嵌套小表格"时会切错。这时候需要手动指定列的位置。pdfplumber 允许传入显式的分割线坐标:

# 假设已知表格从 x=50 到 x=550,分三列 custom_settings = { "vertical_strategy": "explicit", "explicit_vertical_lines": [50, 200, 380, 550], "horizontal_strategy": "lines", } tables = page.extract_tables(custom_settings)

坐标怎么来?先用page.debug_tablefinder()可视化当前识别结果,它会返回一个带边框信息的对象,打印出来就能看到每条线的位置。这个调试方法比盲猜参数快得多。

5.2 批量处理文件夹里的所有 PDF

单个文件跑通后,扩展成批量只需加一层循环:

import os import glob pdf_files = glob.glob("pdfs/*.pdf") # 匹配 pdfs 目录下所有 PDF for pdf_path in pdf_files: all_dfs = [] with pdfplumber.open(pdf_path) as pdf: for i, page in enumerate(pdf.pages): for table in page.extract_tables(): df = clean_table(table) if i > 0: df = df.iloc[1:] all_dfs.append(df) if all_dfs: result = pd.concat(all_dfs, ignore_index=True) # 输出文件名与源文件同名,后缀改 xlsx out_name = os.path.splitext(os.path.basename(pdf_path))[0] + ".xlsx" result.to_excel(os.path.join("output", out_name), index=False)

glob.glob支持通配符,os.path.splitext用来换后缀。注意output目录要提前建好,否则to_excel会报路径不存在。

5.3 验证提取质量的三个检查点

写完脚本别急着交付,跑三个检查:第一,行数对不对,用len(final_df)跟 PDF 里肉眼数的行数比对;第二,随机抽三行核对数值,尤其是带小数点和负号的;第三,用final_df.dtypes看列类型,如果本该是数字的列显示object,说明混进了非数字字符,需要进一步清洗。

我自己的习惯是:任何自动化脚本交付前,先拿一份"已知答案"的小样本跑一遍,确认输出跟手工整理的结果一致,再上大批量。这个习惯帮我省过好几次返工——有次 PDF 里藏了一个全角空格,肉眼看不出来,导致金额列全部变成字符串,Excel 里没法求和,幸亏小样本测试时发现了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询