Python PDF自动化:文本提取、批量处理与扫描版OCR实战指南
2026/9/4 21:44:32 网站建设 项目流程

接到 50 个 PDF 需要提取文字做月度汇总,里面既有合同扫描档,也有系统导出的带目录 PDF。直接打开一个个复制粘贴,一晚上基本就耗在鼠标上;换用 Python 把这些文件按类型分开、批量读取文本、自动生成清单,半小时可以跑完初筛。这就是办公自动化里很典型的 PDF 场景。

PDF 自动化最值钱的地方,不是帮你写一个复杂解析器,而是帮你在不了解 PDF 内部结构的情况下,把重复劳动转换成可批量、可复现的脚本流程。适合正在学 Python、想往办公自动化方向落地的开发者和运营人员,也适合每天要整理大量电子文档的行政、科研助理。最值得先掌握的,不是某个库的高级 API,而是判断 PDF 能不能用代码直接处理,以及不同任务应该用什么库。

很多教程一上来就直接用 PyPDF2 读取页面文本,结果遇到扫描版时读出来全是空的;也有人只是想给 30 个 PDF 统一加水印,结果先去学了全文解析。PDF 自动化的第一步不是选库,而是把需求拆分清楚。

1. PDF 自动化能处理哪些办公需求

1.1 常见 PDF 任务和自动化价值

日常办公中,PDF 相关的自动化任务大致可以分成下面几类:

  • 基本读取:读取文件页数、标题、作者、创建时间等信息。
  • 页面操作:合并、拆分、旋转、调整顺序、删除指定页。
  • 内容提取:提取页面文字、图片、表格。
  • 格式转换:PDF 转 Word、PDF 转图片、图片导出、网页生成 PDF。
  • 安全处理:给自己生成的 PDF 加打开密码,或处理自己有权处理文件的权限问题。
  • 批量归档:把散落各处的 PDF 按规则命名、移动到统一目录,生成汇总清单。

不同任务对工具的要求完全不一样。合并拆分用轻量库就好,未必需要完整解析页面文字;表格抽取需要更细的坐标信息;图片提取要能访问 PDF 内部对象;而 PDF 转 Word 对版式还原要求最高。

我建议先在纸上把需求写清楚:输入是什么、输出是什么、文件量有多大、是单文件还是批量。只要输入文件数量和频率足够高,脚本的收益就很大。比如一个月只转 3 个 PDF,手动处理完全够用;但如果每周都来几十个文件,自动化才值得做。

1.2 先学会区分文本型 PDF 与扫描版 PDF

PDF 自动化实操中最容易踩的坑,是把“能打开看”等同于“能用代码提取”。实际上 PDF 内部有非常大的形态差异。

文本型 PDF 里面有真正的文字字符,用阅读器选中文字后能复制,脚本可以直接把内容抽出来。扫描版 PDF 本质是图片,每一页都是页面照片,代码直接提取文字时大概率拿到空内容或乱码。还有一类 PDF 用“字体路径”渲染文字,阅读器显示正常,但内部不包含标准 Unicode 映射,抽取结果也会乱。

判断方法很简单:用 PDF 阅读器打开文件,试着选中某段中文。能选中且复制出来不乱码,说明有文字层;选不中或者选中的是一整块图片,基本就是扫描版。如果需要写代码判断,可以在脚本里抽取第一页文本,如果文本长度接近 0,再看页面里有没有大尺寸图片对象,两者结合基本能确定这是一份扫描 PDF。

区分文本型和扫描版意义很大。如果目录中 80% 都是扫描版,那处理思路要往 OCR 方向走;如果都是系统导出的文本型 PDF,那直接做文字抽取、合并拆分和格式转换就够用。盲目对所有文件套同一个解析脚本,大概率会得到一堆空结果。

2. 环境准备和库选型:PDF 自动化第一关

2.1 Python 环境和依赖安装建议

PDF 自动化对 Python 版本没有特别苛刻的要求,常用的 3.10、3.11、3.12 都可以。如果你的电脑还没有 Python,建议从官方渠道下载安装包,安装时勾选“Add Python to PATH”,避免后面在命令行里敲python提示找不到命令。

装完以后先确认环境:

python --version pip --version

两个命令都有输出,环境基本就绪。如果pip下载依赖很慢,可以临时使用国内镜像源,但不建议把镜像参数写死到全局配置,避免以后需要私有源时产生干扰。

然后安装 PDF 处理常用库:

pip install pypdf pdfplumber PyMuPDF pdf2docx

这里说明一下,PyMuPDF安装成功后在 Python 里导入的名称是fitz,很多新手会以为装错了包。

2.2 常用 PDF 库的选型对比

先列一张选型表,之后按场景选择就行:

主要场景建议库说明
合并、拆分、旋转、加密码pypdfAPI 简单,适合页面级文件操作
文本抽取、表格抽取pdfplumber能拿到更多版面坐标信息
全能批处理、图片提取、页面渲染PyMuPDF速度快,功能综合,导入名是 fitz
PDF 转 Wordpdf2docx适合文本型和简单版式文件
生成新 PDF、模板填充reportlab从零创建 PDF 时使用

如果任务以“页面合并拆分”和“读取基本信息”为主,pypdf 就够,没必要引入重库。如果任务需要处理大量文本和表格,pdfplumber 比 pypdf 的基础提取更细致,速度和内存占用会大一些,换来的定位信息更丰富。如果任务涉及把 PDF 页面渲染成图片、提取内部原始图片,PyMuPDF 是首选。

提醒一点:PyMuPDF 的常见许可证需要根据项目情况评估,公司内部项目使用前建议先确认合规性。学习阶段安装使用没问题,但如果是后端服务或商业产品,不能只看功能方便。

2.3 多个库同时安装会不会冲突

这几个库可以安装在同一套 Python 环境里,它们之间没有强依赖冲突。实际安装报错如果出现,多数原因是当前 Python 版本太老,或者 pip 版本过旧,与某个库的发布版本不兼容。

处理方式不是去网上下载“整天候绿色版 Python 安装包”,而是先升级 pip:

pip install --upgrade pip

再重新安装对应库,并查看报错信息里提示的 Python 版本范围。办公自动化脚本通常跑在本地或公司内部服务器,使用虚拟环境可以避免污染系统 Python。学习阶段用一个环境先跑通,项目复杂之后再按虚拟环境拆分也来得及。

3. 第一个实用脚本:读取页数、元数据和批量文件清单

3.1 从单文件元数据开始验证链路

第一个脚本不需要做复杂解析,先把一个 PDF 的基本信息读出来。这样能验证安装、路径、文件是否能正常打开,为后面更深度的处理铺路。

from pathlib import Path from pypdf import PdfReader file = Path("sample.pdf") reader = PdfReader(file) print("文件名称:", file.name) print("页数:", len(reader.pages)) if reader.metadata: print("标题:", reader.metadata.title) print("作者:", reader.metadata.author)

这里有几个小提醒。PDF 的元数据不一定会完整填写,很多文件metadata.title是空值,打印前做判断更稳妥。另外,页面索引从 0 开始,后面做拆分和定位要注意。

跑这个脚本的真正目的不是只看文件名,而是确认“读取 PDF -> 拿到对象 -> 打印结果”这条路是通的。如果这一步都报错,大概率不是代码问题,而是 Python 环境、依赖版本或文件路径的问题。

3.2 批量扫描目录并统计异常文件

实际工作中很少只看单个 PDF,通常是一个目录下几十个文件。这时可以把清单脚本做成一个小工具,顺便把处理不了的文件标出来。

import json from pathlib import Path from pypdf import PdfReader result = [] for pdf_file in Path("pdf_folder").glob("*.pdf"): try: reader = PdfReader(pdf_file) item = { "文件": pdf_file.name, "页数": len(reader.pages), "加密": reader.is_encrypted, "路径": str(pdf_file) } result.append(item) except Exception as exc: result.append({ "文件": pdf_file.name, "错误": f"{type(exc).__name__}: {exc}" }) with open("pdf_manifest.json", "w", encoding="utf-8") as f: json.dump(result, f, ensure_ascii=False, indent=2) print("处理完成:", len(result))

这段代码会扫描目录下所有.pdf文件,并把页数、加密状态、能否正常读取写入 JSON 文件。加密文件没有密码时不必强行打开,先标记为加密,后面单独确认权限。

跑完整理后再决定怎么做,会比“直接对全部文件跑文本抽取”稳妥得多。因为用一个文件清单你就能发现:有几份文件其实没权限打开、有几份页数可能异常、有几份根本不是 PDF 而是改后缀名的文件。

3.3 路径和文件名是很多坑的来源

Windows 命令行下经常遇到中文路径或者文件名里带空格、括号的情况。Python 的pathlib已经能比较好地处理这些问题,但某些库在内部调用时仍可能因为编码和字符转义报错。

我一般会建议:批量处理前,先把待处理的 PDF 放到一个纯英文目录中,文件名尽量用字母、数字、下划线。这样做不是“Windows 不支持中文”,而是为了减少排查成本。如果你愿意处理编码问题,中文路径通常也能跑通,但办公自动化追求的是稳定,不是挑战环境上限。

不推荐用str(Path)拼接路径,更不建议在 Windows 上写死C:\xxx\pdf这种含反斜杠的路径,跨平台脚本会受影响。统一用pathlib接管目录和文件路径,是成本最低的规避方式。

4. 文本抽取:中文乱码、空格错乱和扫描版误判

4.1 用 pdfplumber 抽取整页文本

当清单确认文件是文本型 PDF 后,可以进入文字抽取。推荐先用 pdfplumber,因为它对版面信息的还原比 pypdf 的基础方法更细。

import pdfplumber with pdfplumber.open("sample.pdf") as pdf: for page_index, page in enumerate(pdf.pages, start=1): text = page.extract_text() if text: print(f"=== 第 {page_index} 页 ===") print(text[:500]) else: print(f"=== 第 {page_index} 页:未提取到文本 ===")

extract_text()返回的可能不是空字符串,而是None,所以不能直接用if not text判断,要用if text is None或上面的方式。如果连续多页都返回空,优先检查是不是扫描版 PDF,而不是反复调参数。

pdfplumber 的优点是能拿到每个单词的坐标、字体、大小等细节,适合做“找到某一页某个区域的文字”这种精细化任务。缺点是处理大文件时比 PyMuPDF 慢。文件多、页数多、内容以全文为主时,我更倾向于用 PyMuPDF 快速拿结果。

import fitz doc = fitz.open("sample.pdf") for page in doc: text = page.get_text("text") if text.strip(): print(text[:500]) doc.close()

批量脚本里会优先考虑这套逻辑:先用 PyMuPDF 快速抽取全文;遇到整页文本为空的再交给 pdfplumber 进一步看对象信息;确认是图片页后标记成“待 OCR”。

4.2 中文乱码和空格错乱的真实原因

中文 PDF 提取乱码不一定是你代码写错。PDF 为了保证字体显示,可能把字体子集嵌入,但没有写入完整的文本映射表。阅读器在显示时用字形轮廓绘图,所以你看不出异常,但脚本提取时拿不到正常 Unicode,就会输出乱码。

另一种常见情况是:内容能提取,但汉字之间突然出现大量空格。这通常是因为 PDF 内部把每个文字当成独立文本块,版式信息里记录了坐标,却丢失了连续的文本游程。pdfplumber 抽取时可以试试带版面参数的读取方式:

text = page.extract_text(layout=True)

layout=True会尝试根据坐标还原原始排版关系,多栏结构下效果更明显。如果依然不满意,可以改用 PyMuPDF 的get_text("blocks"),拿每一块的坐标和文字后自行重排:

for block in page.get_text("blocks"): x0, y0, x1, y1, content, block_no, block_type = block if block_type == 0: print(round(x0, 1), round(y0, 1), content)

这里block_type == 0表示文本块,图片块是 1。拿到坐标后,就能按阅读顺序人工排序,比只显示乱序内容更容易定位。

遇到排版文件先不要认定“库不支持中文”。优先确认三件事:文件能否选中文字、选中后复制是否正常、字体是否完整嵌入。“复制正常但脚本乱码”通常和字体映射有关,不是换一个库就能彻底解决的。

4.3 需要 OCR 时怎么快速判断

如果一份 PDF 每一页都像扫描图片,那文本抽取这条路走不通。继续调库参数、换提取模式只是在浪费时间。

判断方法可以固化到脚本里:

  1. 提取当前页文本。
  2. 如果文本为空或字数极少。
  3. 再获取当前页的图片对象数量。
  4. 如果图片对象数量很多,标记为扫描页。

用 PyMuPDF 获取页面图片对象非常方便:

page_images = page.get_images(full=True)

如果一页文字长度为 0,同时有图片对象,就可以判断这一页需要 OCR,而不是文本抽取。

OCR 本身是另一个主题,涉及识别引擎、语言包、图片预处理和置信度判断,适合作为 PDF 自动化系列的第二篇单独展开。本篇建议先把“扫描版需要走 OCR”这个判断埋进脚本,把需要 OCR 的文件单独导出一份清单,不要混在纯文本型 PDF 里继续处理。

5. 页面级操作:合并、拆分、旋转和加密码

5.1 合并多个 PDF

合并文件最常用的是 pypdf,思路是创建一个合并器,然后不断把源文件页面加入:

from pypdf import PdfReader, PdfWriter writer = PdfWriter() file_list = ["file1.pdf", "file2.pdf", "file3.pdf"] for file_name in file_list: reader = PdfReader(file_name) for page in reader.pages: writer.add_page(page) with open("merged.pdf", "wb") as f: writer.write(f)

这里最容易被忽略的是文件顺序。如果目录里有很多按序号命名的 PDF,用glob("*.pdf")拿到的顺序不一定是 1、2、3、4…… 而是 1、10、11、2、3 这种字典序。

正确的做法是显式构造文件列表,或者用自然排序:

file_list.sort(key=lambda p: int(p.stem))

如果文件名不是纯数字,则按实际业务规则排序。合并之前建议先打印每个文件的页数,避免合完以后才发现某一章节顺序放错。

5.2 拆分提取指定页

拆分相当于反向操作,把源文件的若干页复制到新文件:

from pypdf import PdfReader, PdfWriter reader = PdfReader("large.pdf") writer = PdfWriter() start_page = 2 end_page = 5 for page_index in range(start_page - 1, end_page): writer.add_page(reader.pages[page_index]) with open("extract_pages.pdf", "wb") as f: writer.write(f)

这里容易错一页。代码里的range(2, 5)实际取的是索引 2、3、4,对应第 3、4、5 页。如果你想把“从第 2 页到第 5 页”的内容抽出来,要写range(1, 5)。建议统一用页码减一的方式写,避免肉眼误判。

如果要按照章节拆分一份几十页的 PDF,可以事先从清单脚本里记录每个章节的起始页,再循环生成多个输出文件。输出文件名不要用part.pdf这种固定写法,新一批任务会直接覆盖旧结果,最好带上原文名称或章节号。

5.3 旋转页面和删除空白页

扫描设备出来的 PDF,偶尔会有个别页面方向不对。pypdf 可以按页码旋转:

for page_index in [1, 3]: page = reader.pages[page_index] page.rotate(90)

旋转前最好先打印每个页面的开头几个字符,定位到真正需要旋转的页码。不要凭感觉猜测,也不要在循环里把所有页面都旋转。大部分 PDF 阅读器自带的“自动旋转”功能是基于页面内容方向判断的,脚本里没有这个自动判断能力,需要自己根据实际方向写条件。

删除空白页看起来简单,其实有风险。有些书会保留空白页,删除后页码和目录都会乱;有些页面看着空白,实际包含整页背景图片。如果一定要做,建议先把所有“文本为空、图片数量为 0”的页面列出来,人工确认后执行,而不是写死“空页全部删掉”。

5.4 给 PDF 加打开密码的合规操作

办公流程中经常需要把自己生成的 PDF 加上打开密码再发出去。pypdf 支持设置用户密码:

from pypdf import PdfReader, PdfWriter reader = PdfReader("source.pdf") writer = PdfWriter() for page in reader.pages: writer.add_page(page) writer.encrypt(user_password="your-password") with open("protected.pdf", "wb") as f: writer.write(f)

这里要强调一个边界:本文只讨论自己拥有合法处理权限的文件加密场景。如果收到一个打开密码未知的 PDF,在未经授权情况下想办法跳过密码访问,不在办公自动化的正常范围内。实际工作中有权限问题,应该先通过与文件提供方确认来解决,而不是写绕过脚本。

另外要注意encrypt的签名在不同 pypdf 版本里略有差异,写代码前可以用help(writer.encrypt)看一眼当前版本的参数。处理过程中准备一份“加密文件清单”,标记哪些已处理、哪些跳过,比手动记忆可靠。

6. 图片提取、PDF 转 Word 和页面导出图的边界

6.1 批量提取 PDF 内部的原始图片

很多用户搜索“python 提取 pdf 中的图片”,其实要两类不同的操作。一是把 PDF 页面转成图片,这是截图渲染;二是从 PDF 内部对象里取出原始嵌入图片,这才更贴近“提取”。

用 PyMuPDF 提取原始图片,核心是拿到图片对象的交叉引用编号xref,再转换成 Pixmap 保存:

import fitz doc = fitz.open("sample.pdf") for page_index in range(len(doc)): page = doc[page_index] images = page.get_images(full=True) for img_index, img in enumerate(images): xref = img[0] pix = fitz.Pixmap(doc, xref) if pix.n - pix.alpha >= 4: pix = fitz.Pixmap(fitz.csRGB, pix) output_name = f"page{page_index + 1}_img{img_index + 1}.png" pix.save(output_name) pix = None doc.close()

代码里pix.n - pix.alpha >= 4用来判断是否有透明通道或 CMYK 模式,必要时转换成 RGB,避免保存出来的图片颜色发黑或者格式异常。

提取图片后要检查输出数量和图片质量。不是 PDF 里每个图片对象都有实际价值,比如底纹、背景、logo 都会作为独立图片被提出,大批量提取后通常需要再按分辨率或尺寸过滤一遍。

6.2 PDF 转 Word 的效果取决于源文件

“pdf 转 word”这个关键词搜索热度一直很高,但很多效果问题不是工具能解决的。PDF 作为固定版式格式,本身不保存 Word 的段落层级、样式和可编辑结构。PDF 转 Word 更像是“根据坐标反向推测版面结构”,而不是“打开一份 Word 原稿”。

pdf2docx 可以处理比较规整的文本型 PDF:

from pdf2docx import Converter converter = Converter("sample.pdf") converter.convert("sample.docx") converter.close()

这条链路对单栏、文字型、无复杂背景的 PDF 效果不错。但遇到这些情况会明显变差:

  • 多栏报纸式排版。
  • 大量文本框悬浮、旋转。
  • 跨页表格。
  • 带水印或背景图。
  • 扫描版 PDF。

扫描版 PDF 直接转 Word,往往得到的是一份空白或几乎无文字的文档,因为 pdf2docx 只会处理文本对象,不会自动 OCR。正确顺序是:扫描版 PDF 先做 OCR 或人工重新录入,再进入 Word 整理。

如果只是提取部分内容做初筛,没必要追求完整还原 Word 版式。把文字和表格数据抽出来直接进 Excel 或数据库,往往比转 Word 更实用。

6.3 用 PyMuPDF 做页面导出和截图预览

很多办公场景不是要转 Word,而是要把 PDF 页面转成图片,用于系统预览、网页展示、内部审核。PyMuPDF 在这个任务上几乎是首选:

import fitz doc = fitz.open("sample.pdf") page = doc[0] pix = page.get_pixmap(dpi=150) pix.save("preview.png") doc.close()

dpi参数直接影响图片清晰度和文件大小。屏幕预览用 150 足够,需要打印或存档可以提高到 300。如果把dpi调到 500,输出会很清晰,但处理时间和磁盘占用也会明显上涨,批量跑之前一定要先拿一页做测试。

浏览器里的“网页页面 PDF 打印”如果要做自动化,一般不是直接用 PDF 处理库,而是通过无头浏览器把网页渲染成 PDF,这属于网页自动化方向。当前这篇偏 PDF 文件本身操作,后续可以结合自动化测试工具单独展开。

7. 把脚本放进工作流:重点处理失败情况

7.1 单个文件失败不能拖垮整批任务

办公自动化脚本和课程 Demo 最大的区别,是可以接受“中途有不少文件出错”。真实文件来自不同部门,命名习惯、生成软件、版本参数完全不同,很难保证每个文件都规范。

批量处理时,我建议把每个文件包在独立的异常处理里,不要让异常中断整个循环:

success = [] failed = [] for pdf_file in file_list: try: process_pdf(pdf_file) success.append(pdf_file.name) except Exception as exc: failed.append({ "file": pdf_file.name, "error": f"{type(exc).__name__}: {exc}" }) print("成功数量:", len(success)) print("失败数量:", len(failed)) for item in failed: print(item["file"], item["error"])

这样跑一轮后你能看到有多少文件失败、失败原因是什么。修复一批问题后重新跑,已经成功的文件可以跳过。

7.2 常见错误分类和应对顺序

真实项目中报错类型大概可以按下面这张表排查:

| 报错或现象 | 常见原因 |

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

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

立即咨询