办公场景里,PDF 表格和文本批量转 Excel 的需求一直不少。月初对账、年报汇总、财务报表归档、产品参数整理,都会遇到“数据明明清晰可见,却没法制成表格”的尴尬。手动录入效率低,复制粘贴又容易把排版弄乱。很多学习 Python 的同学在这一步会卡住:到底该用什么库?为什么extract_text()拿不到内容?怎么把读取到的结果稳定写入 Excel?
这一篇就来完整梳理pdfplumber的基础用法,并用带表格、带文本的 PDF 文件做两个实战案例,最终把数据写入 Excel。文章按“入门概念 → 环境准备 → API 拆解 → 实战案例 → 常见问题 → 工程建议”的顺序展开,既有可复制的代码,也有能直接对照排查的避坑清单。
如果你曾用正则表达式硬抠 PDF 文本,或者手动复制过多张 PDF 表格,这篇文章很值得收藏。
1. pdfplumber 能解决什么问题
1.1 为什么不是所有 PDF 都能直接读取
很多初学者刚接触 PDF 时会有一个疑问:PDF 里的文字明明能被鼠标选中,为什么用代码读取这么费劲?
这和 PDF 的文件结构有关。PDF 更像一份“打印排版稿”,文件内部保存的是文字、图形的坐标位置信息,而不是像 Word、Markdown 那样天然带有段落层级。页码、页眉、页脚、表格线、文字框,都是基于坐标绘制的。pdfplumber做的事,就是把这种“基于坐标的绘制内容”重新抽取为 Python 可以处理的文本、表格、图片对象。
如果你直接把 PDF 文件重命名成.txt再读取,看到的内容往往是乱码,原因就在这里。
1.2 pdfplumber 能提取哪些内容
pdfplumber是 Python 生态中比较成熟的开源 PDF 解析库,底层依赖pdfminer.six。它既可以提取文本,也可以识别表格线、图片、矩形,还能拿到每个字符在页面上的精确坐标。
常用能力包括:
| 功能 | 说明 | 常用方法 |
|---|---|---|
| 提取文本 | 获取整页或指定区域的纯文本 | page.extract_text() |
| 提取表格 | 根据页面表格线重组出行列数据 | page.extract_table() |
| 提取单词 | 获取单词文本及坐标位置 | page.extract_words() |
| 提取图片 | 获取页面中的图片对象 | page.images |
| 裁剪页面 | 按矩形区域局部解析 | page.crop() |
对比PyPDF2这类偏“PDF 文件操作”的库,pdfplumber更专注于“内容解析”,所以它在读取表格、带上坐标信息时优势明显。
1.3 它不擅长处理什么
要注意,pdfplumber不是万能的。它读取的是 PDF 内部的文字和矢量线条,如果 PDF 本身是扫描件,也就是“每页都是一张图片”的那种,它拿不到可编辑文本,extract_text()可能返回空内容或者乱码。这种情况需要先接入 OCR 识别组件,例如Tesseract,属于另一套技术路线。
另外,pdfplumber也不是 PDF 编辑器。它不会自动矫正乱排版,更不会把一张图片里的表格变成 Excel 表格。
2. 环境准备与安装
2.1 安装 pdfplumber 和 openpyxl
本文涉及两个 Python 第三方库:
pdfplumber:负责 PDF 内容解析。openpyxl:负责将数据写入.xlsx文件。
如果你电脑上还没有安装 Python,建议先安装 Python 3 版本。安装完成后打开命令行工具执行:
pip install pdfplumber openpyxl如果机器上同时存在多个 Python 版本,可能需要用pip3代替pip:
pip3 install pdfplumber openpyxl安装完成后,可以运行下面的代码验证:
import pdfplumber import openpyxl print("pdfplumber 版本:", pdfplumber.__version__) print("openpyxl 版本:", openpyxl.__version__)如果能看到版本号,说明环境已经就绪。
我建议你顺手新建一个项目目录,例如pdf_to_excel_demo,后续代码都放在这个目录里。示例结构如下:
pdf_to_excel_demo/ ├── input/ # 存放测试 PDF ├── output/ # 存放生成的 Excel └── demo.py # Python 脚本2.2 准备一份测试 PDF
代码实战前需要准备一份测试文件,推荐使用带清晰表格线的 PDF,例如月度销售汇总表、成绩单、库存清单。本文演示中使用的文件名为月度报表.pdf,放在input目录下。后面写代码时,你可以把它替换成自己的真实文件路径。
假设月度报表.pdf第一页内容大致如下:
| 商品名称 | 数量 | 单价 | 金额 |
|---|---|---|---|
| 苹果 | 10 | 5.00 | 50.00 |
| 香蕉 | 5 | 3.50 | 17.50 |
| 橙子 | 8 | 6.00 | 48.00 |
这里的数据并不来自任何真实企业,只是为了演示代码输出。
3. pdfplumber 核心概念与用法拆解
3.1 打开 PDF:用with管理文件
在pdfplumber中,推荐使用上下文管理器打开 PDF 文件:
import pdfplumber # 文件路径改成自己的 PDF 路径 pdf_path = "input/月度报表.pdf" with pdfplumber.open(pdf_path) as pdf: print("PDF 总页数:", len(pdf.pages)) first_page = pdf.pages[0] text = first_page.extract_text() print(text)运行后,第一页的文字会按阅读顺序输出。
通过pdf.pages可以拿到页面列表,pdf.pages[0]表示第一页。页面编号和 PDF 一样从 0 开始,所以第一页对应索引 0。
这里使用with好处很明显:解析完成后文件会自动关闭,避免句柄占用,尤其在批量处理大量 PDF 时很关键。
3.2extract_text()提取文本
extract_text()是最常用的方法。它会把页面中的文字按顺序组合成字符串,并尽量保留换行。
示例:
import pdfplumber with pdfplumber.open("input/月度报表.pdf") as pdf: page = pdf.pages[0] text = page.extract_text() # 输出前 500 个字符,避免内容过长 print(text[:500])这里有两点需要说明。
第一,extract_text()对中英文混排的文本支持度尚可,但 PDF 内部没有“段落”概念,所以读到什么换行位置、文字顺序,取决于 PDF 内容本身的绘制顺序。某些 PDF 分栏排版、文本框悬浮,文本顺序可能和视觉顺序不完全一致。
第二,如果 PDF 页面是空白扫描图片,这个方法返回的值可能是None或者空字符串。
3.3extract_table()提取表格
表格提取是pdfplumber的拿手好戏。它会根据页面中的垂直线和水平线,把表格区域切分成行列结构,并返回一个二维列表。
import pdfplumber with pdfplumber.open("input/月度报表.pdf") as pdf: page = pdf.pages[0] table = page.extract_table() # 输出表格 for row in table: print(row)输出结果类似:
['商品名称', '数量', '单价', '金额'] ['苹果', '10', '5.00', '50.00'] ['香蕉', '5', '3.50', '17.50'] ['橙子', '8', '6.00', '48.00']table是一个列表,列表中的每个元素又是列表,对应表格中的一行。这种结构非常接近 Python 中的二维数组,也很方便后续写入 Excel。
如果表格跨页,extract_table()一次只提取当前页面内的表格。跨页表格需要逐页提取后再合并,后面会专门演示。
3.4extract_words()提取单词与坐标
有些场景下,你不只需要文字,还需要文字的位置信息。例如只需要某一列、判断某个单元格属于表头还是正文,都可以用到extract_words()。
import pdfplumber with pdfplumber.open("input/月度报表.pdf") as pdf: page = pdf.pages[0] words = page.extract_words() for word in words[:5]: print(word)每个单词对象的字段通常包括:
| 字段 | 含义 |
|---|---|
text | 单词文本 |
x0 | 单词左边界距离 |
top | 单词顶部距离 |
x1 | 单词右边界距离 |
bottom | 单词底部距离 |
page_number | 所在页码 |
这些坐标通常以点为单位。页面左上角是坐标原点,向右是 x 轴正方向,向下是 y 轴正方向。如果以后要实现“只提取某个区域内的文本”,这些坐标会非常有用。
3.5chars字符对象
pdfplumber在页面中提供了更细粒度的page.chars,每个字符都可以提取出来,包括字体名、字号、颜色等信息。
import pdfplumber with pdfplumber.open("input/月度报表.pdf") as pdf: page = pdf.pages[0] first_char = page.chars[0] print(first_char)当你想判断某个标题字体是否加粗、字号是否大于正文,可以通过chars实现。在很多“批量整理 PDF 目录”的任务中,先根据字体大小定位标题,再提取标题下正文,就是比较常见的做法。
3.6 常见误区:表格线和文字的关系
使用extract_table()时,很多新手会误以为只要 PDF 长得像表格,就一定能提取成功。实际上,pdfplumber主要依赖“可见的表格线”来切分表格。如果 PDF 中的表格没有边框线,或者表格线是扫描图片里的图像,而不是矢量线,提取效果就不理想。
此时可尝试调整table_settings,例如设置vertical_strategy="text",让程序尝试通过文字 x 坐标对齐来推测表格列边界:
table_settings = { "vertical_strategy": "text", "horizontal_strategy": "text" } table = page.extract_table(table_settings)如果仍然失败,通常只能考虑先对扫描图片做 OCR。
4. 实战一:读取 PDF 文本并写入 Excel
4.1 需求说明
假设有一份纯文本型 PDF,每行是一条报告记录,内容如下:
工号 部门 姓名 出勤天数 缺勤次数 1001 销售部 张三 22 0 1002 市场部 李四 21 1 1003 技术部 王五 23 0需求是:读取 PDF 全部页面的文本,拆分成行后写入 Excel。
这种场景常见于把“文字报告”转成结构化工作簿,方便二次筛选。
4.2 完整示例代码
创建一个新文件text_to_excel.py:
# -*- coding: utf-8 -*- import pdfplumber from openpyxl import Workbook pdf_path = "input/考勤报告.pdf" excel_path = "output/考勤报告.xlsx" wb = Workbook() ws = wb.active ws.title = "考勤记录" with pdfplumber.open(pdf_path) as pdf: target_page = pdf.pages[0] text = target_page.extract_text() # 按换行拆分成列表 lines = text.splitlines() # 逐行写入 Excel for row_idx, line in enumerate(lines, start=1): # 如果原始内容用空格分隔,先拆分 columns = line.split(" ") # 去掉空字符串,避免因为多个空格产生空列 columns = [col for col in columns if col != ""] for col_idx, value in enumerate(columns, start=1): ws.cell(row=row_idx, column=col_idx, value=value) wb.save(excel_path) print("已保存:", excel_path)4.3 代码解释
先通过text.splitlines()把整页文本切成行,但 PDF 里的空格不一定是单个字符,有可能会出现连续多个空格或全角空格,因此拆列前先按空格拆分,再过滤空字符串,能有效减少“空列错位”现象。
不过这种处理方式仍然依赖 PDF 本身的行列格式。如果实际内容中的字段是用 Tab 分隔的,可以改用line.split("\t");如果字段间距是通过多个空格实现的,过滤空格后效果会更好。真正规范的数据报告,建议先输出拆分结果检查一遍。
4.4 运行验证
命令行执行:
python text_to_excel.py如果输出目录下出现考勤报告.xlsx,打开后表格内容与原 PDF 对应,说明第一个实战完成了。
5. 实战二:提取 PDF 表格并写入 Excel
5.1 需求说明
这是 PDF 转 Excel 的高频场景。业务部门发来一份带边框线的 PDF 表格,需要把它中的数据导入 Excel 进行透视分析或数据清洗。
这里用input/月度报表.pdf作为输入文件,内容是最常见的三行商品数据。
5.2 技术方案
步骤拆解如下:
- 用
pdfplumber读取 PDF 页面。 - 调用
extract_table()提取二维表格数据。 - 遍历二维列表并写入 openpyxl 工作表。
- 保存为
.xlsx文件。
5.3 表格提取并写入 Excel
新建table_to_excel.py:
# -*- coding: utf-8 -*- import pdfplumber from openpyxl import Workbook from openpyxl.styles import Font, Alignment pdf_path = "input/月度报表.pdf" excel_path = "output/月度报表.xlsx" # 创建 Excel 工作簿 wb = Workbook() ws = wb.active ws.title = "月度报表" with pdfplumber.open(pdf_path) as pdf: # 这里先处理第一页,多页场景见下一节 page = pdf.pages[0] # 提取表格,返回二维列表 table = page.extract_table() if table: # 第 1 行通常是表头 for row_idx, row in enumerate(table, start=1): for col_idx, cell_value in enumerate(row, start=1): ws.cell(row=row_idx, column=col_idx, value=cell_value) else: print("当前页面未提取到表格,请检查 PDF 是否存在清晰表格线") # 简单美化表头 for col_cell in ws[1]: col_cell.font = Font(bold=True) col_cell.alignment = Alignment(horizontal="center", vertical="center") # 保存文件 wb.save(excel_path) print("表格写入完成:", excel_path)5.4 预期结果
运行后,Excel 内容如下:
| 商品名称 | 数量 | 单价 | 金额 |
|---|---|---|---|
| 苹果 | 10 | 5.00 | 50.00 |
| 香蕉 | 5 | 3.50 | 17.50 |
| 橙子 | 8 | 6.00 | 48.00 |
代码中ws[1]表示第一行,也就是表头所在行。这里将字体加粗,表头居中显示,方便识别。
有一点要注意:extract_table()返回的单元格中,某些cell_value可能是None,通常是因为该单元格没有文字内容或合并单元格导致。openpyxl 写入None时会生成空单元格,不会导致程序崩溃,因此可以先写入,后续再统一清洗。
5.5 单元格数字被 Excel 认为是文本怎么办
pdfplumber提取出的单元格内容默认全部是字符串。比如"10"在 Python 中是字符串"10",写入 Excel 后显示为文本。如果后续需要做数值求和,可能需要在 Excel 里手动转格式。
如果希望写入时自动转换成数字,可以在代码里做一个安全转换函数:
def to_number_if_possible(value): if value is None: return value text_value = str(value).strip() # 去掉千分位符号 text_value = text_value.replace(",", "") try: # 试用整数转换 return int(text_value) except ValueError: pass try: # 再试用浮点数转换 return float(text_value) except ValueError: return text_value在写入单元格前调用这个函数:
ws.cell(row=row_idx, column=col_idx, value=to_number_if_possible(cell_value))这样可以尽量避免“看起来像数字,实际却无法求和”的问题。
6. 进阶:多页 PDF 转换多个 Sheet
实际业务中,一份 PDF 往往不止一页,每页都有一张结构相同的表格。手工一页页复制效率很低。此时可以让 Python 逐页提取表格,每个页面写入 Excel 的一个 Sheet,也可以用 Sheet 名区分页码。
6.1 完整代码:多页转多 Sheet
新建multi_page_to_excel.py:
# -*- coding: utf-8 -*- import pdfplumber from openpyxl import Workbook from openpyxl.utils import get_column_letter pdf_path = "input/多页库存报表.pdf" excel_path = "output/多页库存报表.xlsx" # 创建新的工作簿 wb = Workbook() # 删除默认的空白 Sheet,稍后按页新建 wb.remove(wb.active) with pdfplumber.open(pdf_path) as pdf: for page_idx, page in enumerate(pdf.pages, start=1): # 提取当前页表格 table = page.extract_table() # 如果页面没有表格,跳过该页 if not table: continue # 以页码作为 Sheet 名称,注意 Excel Sheet 名不能重复 sheet_name = f"第{page_idx}页" ws = wb.create_sheet(title=sheet_name) # 写入表格数据 for row_idx, row in enumerate(table, start=1): for col_idx, cell_value in enumerate(row, start=1): ws.cell(row=row_idx, column=col_idx, value=cell_value) # 简单自适应列宽 if table and table[0]: for col_idx in range(1, len(table[0]) + 1): column_letter = get_column_letter(col_idx) ws.column_dimensions[column_letter].width = 16 wb.save(excel_path) print("多页转换完成:", excel_path)6.2 代码中的关键点
第一个关键点是wb.remove(wb.active)。Workbook()默认会生成一个名为Sheet的空的表,如果不删除它,后面又多创建了 Sheet,Excel 文件里就会出现一个无用空白表。当然你也可以选择把第一个页面写到默认 Sheet 里,这是一个习惯问题。
第二个关键点是get_column_letter(col_idx)。它可以把数字列号转成 Excel 字母列号,例如1变成A,2变成B。手动设置列宽时会用到。
第三个关键点是if not table: continue,它能自动跳过没有表格的页面。如果 PDF 中有封面页、目录页,后面没有表格的页面不会生成空 Sheet。
6.3 跨页表格合并问题
如果一张表格跨越多页,像“表头在第一页,第二页继续显示数据”,上述写法会把它保存为两个 Sheet。如果需要合并成一张连续表,做法是:
- 遍历所有页面,使用
extract_table()。 - 第一页的返回值直接作为表格开头。
- 后续页的返回值去掉表头后追加到前一个列表。
思路可以用伪代码表达为:
all_rows = [] for page in pdf.pages: table = page.extract_table() if not table: continue if all_rows: # 去掉后续页的重复表头 table = table[1:] all_rows.extend(table)随后再一次性写入 Excel,就能得到一个跨页合并后的完整表格。
7. 常见问题与排查思路
PDF 解析过程中问题很多,下面把最高频的错误场景整理成表格,方便查阅。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
extract_text()返回空或None | PDF 是扫描图片,无法直接提取文本 | 先做 OCR,或确认源 PDF 是否有文字层 |
| 中文提取成乱码 | PDF 字体编码特殊,或缺少字体资源 | 尝试extract_text()后另存为 txt 查看;部分场景需自行做字符映射 |
表格提取结果为None | 页面没有可见表格线 | 调整table_settings,改用 text 策略 |
| 表格提取后列错位 | 有合并单元格、斜线表头、跨行单元格 | 人工清洗后处理;根据实际情况设计规则 |
| 单元格数据是小数,却显示成一长串 | 浮点数格式化问题 | 使用round()或字符串格式控制 |
写入 Excel 时提示PermissionError | Excel 文件正在打开,或目标目录无权限 | 关闭 Excel 文件后重试,或换输出路径 |
| 提取到的数字无法求和 | 全部被当作字符串写入 | 转换数值后写入,或 Excel 中批量转数值 |
| 提取 PDF 时速度很慢 | 页面数量大、页面包含复杂图形 | 按页切片处理,减少单次解析范围 |
| 页码越界 | pdf.pages[1]但 PDF 只有 1 页 | 先判断len(pdf.pages)再访问 |
报错KeyError | 因 PDF 内部结构异常导致对象缺失 | 升级库或先尝试解析失败页;复杂文件可用Uncertain容忍模式 |
7.1 典型报错补充:报错页面被跳过
批量处理大量 PDF 时,最忌讳“一份文件报错,整个程序中断”。可以在循环里加入try / except,把解析失败的页面或文件记录下来,继续处理后面的内容。
import pdfplumber pdf_path = "input/问题文件.pdf" try: with pdfplumber.open(pdf_path) as pdf: for page in pdf.pages: text = page.extract_text() # 这里继续处理业务逻辑 except Exception as e: print(f"解析文件失败:{pdf_path}") print("错误信息:", e)被except捕获后,程序不会因一个页面失败而停止。不过要注意,异常捕获范围不宜过大,否则会把“代码逻辑 bug”也一并掩盖掉。对真实项目来说,失败文件需要写入日志,方便后续人工排查。
8. 最佳实践与工程建议
当你把pdfplumber真正用到生产环境或日常自动化脚本时,下面这些经验非常值得注意。
8.1 使用上下文管理器,避免文件句柄泄漏
操作 PDF 时,建议始终使用with pdfplumber.open(pdf_path) as pdf:。如果不使用上下文管理器,退出时没有主动调用pdf.close(),在 Windows 下很容易造成文件占用,导致后续删除、移动文件失败。
8.2 优先按页解析,不要一次性读全部内容
如果 PDF 有几百页,一次性把所有页面文本读入内存,速度慢且占用高。建议只解析需要的页面:
# 只解析第 3 页到第 5 页 for page_idx in range(2, 5): page = pdf.pages[page_idx] text = page.extract_text() # 处理 text范围下标的具体写法需要根据业务需求调整。遇到非常大的 PDF,可以考虑把整份文件拆分成多个任务处理。
8.3 批量处理时保留失败日志
批量目录转换时,一个健壮的程序应当输出“成功的文件列表”和“失败的文件列表”。不要在控制台只打印一堆异常,时间久了根本找不到问题源头。建议记录文件名、页码、异常信息。
8.4 备份原始 Excel,避免覆盖
如果脚本每天定时跑,而且输出 Excel 会被其他人打开,千万不要直接覆盖原文件。更稳妥的做法是生成新文件,文件名加入日期后缀:
from datetime import datetime today = datetime.now().strftime("%Y%m%d") excel_path = f"output/月度报表_{today}.xlsx"这样可以保留历史文件,避免数据覆盖后无法找回。
8.5 正则表达式后处理必不可少
pdfplumber提取出的文本很难百分之百干净,尤其遇到“商品名称:苹果”这种带标签的数据,正则表达式可以帮你统一清洗。例如提取金额字段:
import re text = "订单金额:1,234.56元" match = re.search(r"金额[::]?\s*([\d,]+\.?\d*)", text) if match: number_str = match.group(1).replace(",", "") amount = float(number_str) print(amount)需要注意:正则表达式用于 PDF 结构化提取时,要先看清 PDF 内部文本规律,避免误提取其它相似字段。
8.6 路径中的中文与特殊字符
在 Windows 环境中,如果路径包含中文,只要 Python 源码文件保存为 UTF-8 编码,通常没问题。但要注意不要让脚本依赖于 cmd 的 GBK 编码打印中文。如果控制台出现编码报错,可以临时设置环境变量或在代码开头声明编码。
8.7 对 PDF 质量做预检
批量处理之前,可以先对 PDF 做一个“健康检查”:总页数是否合理、某个页面文字是否为空、表格是否存在。把预检结果输出成一个汇总文件,能极大提升自动化任务成功率。
9. 总结与下一步学习建议
通过这篇教程,你应该已经掌握了pdfplumber的几个核心动作:用extract_text()读文本,用extract_table()提取表格,用extract_words()获取坐标信息,再结合openpyxl把二维数据写成 Excel 文件。
如果你只需要处理少量带表格的 PDF,直接使用上面的extract_table()+openpyxl方案,基本能覆盖大多数日常工作。但真实项目往往没有这么理想,PDF 里可能出现合并单元格、只读加密、扫描图片、复杂表头等情况。这时候我建议按“检查是否有文字层 → 逐页尝试 → 清洗数据 → 人工兜底复核”的顺序逐步推进,不要指望一个函数解决所有文件。
下一步可以尝试这些方向:
- 把上面几段代码封装成一个
pdf_table_to_excel(pdf_path, excel_path)函数,方便复用。 - 学习用
page.crop()裁剪页面特定区域,只提取左上角或右下角的数据。 - 研究
chars中的字体大小,实现自动识别标题和正文。 - 对扫描版 PDF 接入 OCR 工具,把图片中的文字识别出来后再做表格清洗。
技术可以解决重复劳动,但 PDF 格式本身并没有统一标准。脚本处理完的结果,建议在关键场景下增加一轮人工校验。如果这篇文章解决了你的一两个实际问题,或者让你对pdfplumber的边界更清楚,别忘了收藏备用。后续遇到批量 PDF 处理需求,按本文的目录顺序翻一翻就能快速上手。