PDF 是办公场景里绕不开的文件格式但很多 Python 初学者做到 PDF 相关需求时第一反应还是去网上找在线转换工具。实际上PDF 的读取、拆分、合并、提取文字和图片用 Python 就能直接搞定。这一篇先解决“了解 PDF”这部分基础问题PDF 到底是个什么结构、Python 处理 PDF 有哪些主流路线、不同库分别擅长什么以及如何用代码快速验证一个 PDF 文件能不能正常解析。后续章节再继续深入批量拆分、合并和提取实战。1. 核心能力速览能力项说明章节定位Python 办公自动化 PDF 处理入门第 1 讲核心内容PDF 格式认知、Python PDF 处理库选型、基础读取验证基础要求Python 环境已安装、能运行 pip 命令主要依赖库pypdf、PyMuPDFfitz、pdfplumber需要联网首次安装依赖库时需要适用读者准备做 PDF 批量处理的办公人员、Python 自动化初学者后续方向PDF 批量合并拆分、文字提取、图片提取、表格抽取备注文中不涉及读取加密 PDF 或绕过权限保护的方法从课程编号也能看出来这是“Python 100 例”系列中 Level 2 的第 87 个实战点。到了这个阶段默认你已经具备 Python 基础语法、文件读写、异常处理的基本能力。PDF 不是一个新的编程概念而是一个需要被 Python 程序理解的文件格式。2. 适用场景与使用边界PDF 在办公自动化里的价值主要体现在三个方向信息提取从大量 PDF 文件中获取文本、表格、图片数据。典型场景包括合同关键字段提取、论文参考文献整理、产品说明书信息归档。格式重组把多个 PDF 合并成一个把一个 PDF 按页拆分或者把 PDF 页面插入到另一个文档中。典型场景包括标书整合、电子发票归档、扫描件分类整理。格式转换PDF 转 Word、PDF 转 Excel、PDF 转图片。严格来说这属于 PDF 处理的上层应用。本讲先不做太复杂的处理重点是把 PDF 在 Python 里的“可解析性”搞清楚。因为 PDF 有一个特点不是所有 PDF 都能直接提取文字很多扫描件本质是图片只是被封装在了 PDF 容器里。如果没搞清楚输入文件的情况就写提取程序很容易出现“代码没报错但结果为空”的问题。使用边界同样重要。PDF 文件可能包含版权保护内容处理他人 PDF 文件前需要确认是否有相应权限。另外带有打开密码或权限密码的 PDF在未取得授权的情况下不应尝试绕过保护。企业内部资料处理也应遵守保密规定。文中的方案只面向自己有权限操作的 PDF 文件。3. PDF 格式认知先搞清楚你面对的是什么很多教程一上来就教写代码但遇到实际问题时完全不知道选哪个库。原因就在于不了解 PDF 的多层结构。PDF 文件最外层是物理结构由对象Object组成对象的类型包括布尔值、数字、字符串、数组、字典、流Stream等。页面内容、嵌入字体、图片资源通常以流的形式存放。对办公自动化来说大多数情况下不需要直接操作底层对象而是通过解析库去读取。但有一个概念必须建立PDF 里的文字提取取决于文字是以文本对象方式写入还是以图像方式写入。用最直观的方式理解电子生成的 PDF比如 Word、WPS 导出的 PDF、系统自动生成的报表 PDF文字是可选中、可搜索的。Python 可以直接提取文本内容。扫描件 PDF比如扫描仪输出的合同、手机拍的纸质文件后转的 PDF每一页本质是一张图片。Python 无法直接提取文字必须先做 OCR光学字符识别才能得到文本内容。这个差别直接决定后续技术选型。如果输入材料都是扫描件却选择纯文本提取库那结果必然不理想。反过来如果输入材料本来就是电子 PDF还要强行套 OCR反而浪费时间。另外还有一层结构差异PDF 中的文字可能是整段连续存储的也可能是按视觉位置分散存储的。前者用简单 API 提取效果不错后者常常出现提取顺序混乱、文字断裂的现象。这时候需要更细粒度的解析方式比如 pdfplumber 的字符级定位或者 PyMuPDF 的文本块读取。本讲先知道这个结论后面实战遇到乱序再针对性处理。4. Python PDF 处理库选型Python 处理 PDF 的库很多但真正高频使用的是下面四个方向4.1 pypdf / PyPDF2 系PyPDF2 是老牌库但维护节奏不太稳定。pypdf 是 PyPDF2 的继任项目API 基本兼容功能聚焦在 PDF 的拆分、合并、旋转、裁剪、加密解密仅限自己有权限的文件以及基础文本提取。适合做的场景多 PDF 合并成一个 PDF。把一个 PDF 按页码切片。读取页面数量、文件元信息。不适合做的场景精确到每个字符的位置分析。复杂排版 PDF 的文本还原。从扫描版 PDF 中提取文字。4.2 pdfplumberpdfplumber 是在 PDF 文本提取领域口碑很好的库。它基于 pdfminer.six 构建能拿到页面级、行级、字符级的详细信息也能提取表格。适合做的场景文本内容提取尤其是对文字顺序有要求的场景。表格数据抽取。需要分析字符坐标的精细化任务。不适合做的场景高并发大规模 PDF 处理性能相对吃力。需要处理扫描件仍需配合 OCR 引擎。4.3 PyMuPDFfitzPyMuPDF 的性能比较突出内置 MuPDF 渲染引擎既能提取文本也能把 PDF 页面渲染成图片还能反向把图片生成 PDF。对办公自动化来说它是“瑞士军刀”型选手。适合做的场景快速提取文本、图片。PDF 页面转 PNG、JPEG。对 PDF 页面做区域截图。需要较高处理速度的批量任务。不适合做的场景复杂表格结构还原精度不如 pdfplumber 的表格抽取机制。4.4 OCR 扩展当 PDF 是扫描件时需要 OCR 环节。Python 处理 PDF 扫描件常用组合是pip install pytesseract pdf2image配合本机安装的 Tesseract OCR 引擎使用。不过这一块单独展开内容非常多本讲先给出结论先判断 PDF 是文本型还是图片型再决定是否进入 OCR 流程。不要一开始就盲目套 OCR。5. 环境准备与依赖安装Windows、macOS、Linux 均可操作。需要准备的是 Python 3.9 及以上版本。建议在虚拟环境中安装依赖避免和系统 Python 环境里的包互相干扰。5.1 创建虚拟环境# Windows python -m venv venv_pdf venv_pdf\Scripts\activate # macOS / Linux python3 -m venv venv_pdf source venv_pdf/bin/activate5.2 安装依赖本讲先安装两个库pypdf 和 PyMuPDF。pdfplumber 放到后续做表格提取时再装防止初学者一次引入太多依赖产生困惑。pip install pypdf PyMuPDF安装完成后验证版本python -c import pypdf; print(pypdf, pypdf.__version__) python -c import fitz; print(PyMuPDF, fitz.__doc__)输出类似下面内容说明环境就绪pypdf 4.2.0 PyMuPDF 1.24.9如果安装 PyMuPDF 时出现网络问题可以换国内镜像源pip install pypdf PyMuPDF -i https://pypi.tuna.tsinghua.edu.cn/simple6. 准备工作准备 PDF 测试样本写识别和提取代码之前先准备测试文件。推荐准备两类样本样本 A从 Word 或 WPS 里导出一份带文字的 PDF。用于验证文本提取功能。样本 B用扫描仪或手机扫描一页纸质文件输出成 PDF。用于理解“图片型 PDF”的表现差异。把测试文件放到一个独立的pdf_samples目录里pdf_samples/ ├── sample_text.pdf # 电子导出的文字型 PDF ├── sample_scan.pdf # 扫描版 PDF图片型 └── output/ # 输出结果目录输出目录提前建好避免后续代码写入时因为目录不存在而报错。这是办公自动化脚本里非常常见的坑逻辑没问题但忘记创建输出目录。7. 基础实操读取 PDF 元数据和状态检查入门第一个功能不是提取文字而是先“看”一个 PDF 的基本信息。这里用 pypdf 来做。7.1 读取页数和基本属性from pypdf import PdfReader pdf_path pdf_samples/sample_text.pdf reader PdfReader(pdf_path) print(页面数量:, len(reader.pages)) print(PDF 版本:, reader.pdf_header) print(是否加密:, reader.is_encrypted) metadata reader.metadata if metadata: print(标题:, metadata.title) print(作者:, metadata.author) print(创建工具:, metadata.creator)输出的信息能帮你快速判断文件的基本状态。页面数量是 0 或者读取报错说明文件可能不是合法 PDF或者路径有问题。is_encrypted字段为True时后面做处理前需要特殊处理这个放到后续进阶章节单独讲。7.2 提取第一页文本from pypdf import PdfReader pdf_path pdf_samples/sample_text.pdf reader PdfReader(pdf_path) page reader.pages[0] text page.extract_text() print( 第一页文本 ) print(text[:500] if text else 未提取到文本)如果输出正常文字说明是文字型 PDF。如果展示未提取到文本先不用急着怀疑代码很可能这个 PDF 是扫描图片型。可以用下面的 PyMuPDF 方案进一步判断。8. 进阶验证用 PyMuPDF 快速解析 PDF 内容pypdf 适合做结构操作但遇到文本提取稳定性、图片提取这类需求时PyMuPDF 更直接。这一节用 PyMuPDF 验证 PDF 的可提取内容。8.1 获取页面信息和文本块import fitz pdf_path pdf_samples/sample_text.pdf doc fitz.open(pdf_path) for page_index in range(min(3, len(doc))): page doc[page_index] text page.get_text(text) print(f 第 {page_index 1} 页 ) print(text[:300] if text.strip() else 该页无可提取文本)8.2 检查页面内嵌图片数量这个操作用来判断页面到底是纯文字、图文混排还是扫描图片。import fitz pdf_path pdf_samples/sample_text.pdf doc fitz.open(pdf_path) for page_index in range(len(doc)): page doc[page_index] image_list page.get_images(fullTrue) print(f第 {page_index 1} 页内嵌图片数量: {len(image_list)})配合文本提取结果做判断有文本、无图片典型的电子文本 PDF。有文本、有图片图文混排 PDF比如带产品图片的说明文档。无文本、每页一张大图大概率是扫描件 PDF需要 OCR。import fitz pdf_scan pdf_samples/sample_scan.pdf doc_scan fitz.open(pdf_scan) page_scan doc_scan[0] text_scan page_scan.get_text(text) image_count len(page_scan.get_images(fullTrue)) print(扫描件提取文本结果:, repr(text_scan[:50])) print(扫描件内嵌图片数量:, image_count)如果text_scan内容为空但image_count等于 1 甚至更大基本可以判定为扫描图片型 PDF。后续要提取文字时就得接入 OCR 管线。8.3 把 PDF 页面按页分成独立文件理解 PDF 的页结构之后可以先做一个实用性很高的操作按页拆分。这里用 pypdf 实现一个简单的按范围拆分功能。from pypdf import PdfReader, PdfWriter def split_pdf_by_page(input_path, output_dir, start_page, end_page): reader PdfReader(input_path) total_pages len(reader.pages) if start_page 1 or end_page total_pages or start_page end_page: raise ValueError(页范围不正确) writer PdfWriter() for index in range(start_page - 1, end_page): writer.add_page(reader.pages[index]) output_path f{output_dir}/split_{start_page}_{end_page}.pdf with open(output_path, wb) as output_file: writer.write(output_file) print(f已生成: {output_path}共 {end_page - start_page 1} 页) split_pdf_by_page(pdf_samples/sample_text.pdf, pdf_samples/output, 1, 2)这个拆分函数的特点是参数使用页码从 1 开始符合人的直觉内部转换时再减去 1 对应 Python 索引。调用后能在output目录看到新生成的 PDF 文件。合并功能的实现思路和拆分对称from pypdf import PdfReader, PdfWriter def merge_pdfs(pdf_list, output_path): writer PdfWriter() for pdf_path in pdf_list: reader PdfReader(pdf_path) for page in reader.pages: writer.add_page(page) with open(output_path, wb) as output_file: writer.write(output_file) print(f合并完成: {output_path}共 {len(writer.pages)} 页) merge_pdfs( [pdf_samples/sample_text.pdf, pdf_samples/sample_scan.pdf], pdf_samples/output/merged.pdf )这里把文字型 PDF 和扫描型 PDF 合并到同一个文件里并不会导致报错。因为 PDF 本身是容器每一页放什么内容由各自的页面对象决定。合并操作跟页面内容是文字还是图片没有关系。9. PDF 内容提取的稳定性问题初学 PDF 处理时最容易产生困惑的一点是同一个 PDF用 pypdf 提取文字顺序乱了但用 PyMuPDF 提取效果正常。这不是代码写错了而是 PDF 内部没有统一的文字存储顺序。PDF 的文本对象会按照页面上的画布指令被依次写入。一个典型场景是双栏排版左栏文字先写入页面中间插入一个图片对象然后又写右栏文字。如果按存储顺序直接提取输出结果就会左栏一半、右栏一半地交错。这是 PDF 格式本身造成的不是某个库可以彻底解决的。遇到这种情况有几个处理策略优先用 PyMuPDF 的“块”模式读取先把页面里面的文本块按从上到下、从左到右的顺序排序。对复杂版式提取后人工校验关键内容。对连续出现的乱序问题考虑按区域坐标裁剪后再提取。这里先跑一个排序示例方便后续扩展import fitz def extract_text_in_reading_order(pdf_path, page_index0): doc fitz.open(pdf_path) page doc[page_index] blocks page.get_text(blocks) # blocks 排序先按 y 坐标再按 x 坐标 blocks.sort(keylambda b: (b[1], b[0])) for block in blocks: x0, y0, x1, y1, text, block_no, block_type block if block_type 0: # 文本块 print(text.strip()) extract_text_in_reading_order(pdf_samples/sample_text.pdf)这个逻辑虽然简单但在办公自动化里很实用。当批量处理几百个 PDF 时统一排序规则能让后面的结果更稳定。10. 资源占用与性能观察PDF 文件纯逻辑处理时CPU 占用是主要观察对象内存占用取决于页面对象数量和页面内嵌资源的复杂度。几个大方向上的经验读取大尺寸扫描 PDF 时内存占用会明显上升因为每张图片页都包含完整的位图数据。PyMuPDF 对大型 PDF 的解析速度通常快于 pypdf办公场景中如果单文件超过 100 页建议优先 PyMuPDF。pypdf 的合并拆分操作并不重新编码页面对象所以速度尚可但如果对内容做流解析或写入修改耗时会有明显增加。当批量处理几百个文件时不要一次性把所有文件都打开后写入到内存再统一保存建议逐文件名处理打开、处理、关闭、释放。如下代码演示逐文件释放的思路import os from pypdf import PdfReader, PdfWriter input_dir pdf_samples/single_files output_dir pdf_samples/output for filename in os.listdir(input_dir): if not filename.lower().endswith(.pdf): continue input_path os.path.join(input_dir, filename) reader PdfReader(input_path) writer PdfWriter() for page in reader.pages: writer.add_page(page) output_path os.path.join(output_dir, new_ filename) with open(output_path, wb) as f: writer.write(f) print(处理完成:, output_path)写批量任务时最忌讳的是在一个循环体里面不断追加列表导致所有 PDF 的对象都残留在内存里。处理完一个文件立刻把 writer 对象重置再进入下一个文件。11. 接口 API 与批量任务到这一步你可能会想不是办公自动化吗怎么还没看到批量要解释清楚这一讲是 PDF 了解篇的第 1 讲核心目的是建立正确的 PDF 处理认知。批量任务需要先确认基础 API 可用、方向正确。但这里可以先给出一个批量任务的脚手架设计让后续把拆分、合并、提取逻辑填充进去。11.1 批量任务的基本结构import os from pypdf import PdfReader, PdfWriter def process_single_pdf(input_path, output_dir, pages_per_split10): 单文件处理逻辑 reader PdfReader(input_path) total_pages len(reader.pages) base_name os.path.splitext(os.path.basename(input_path))[0] split_count 0 for start in range(0, total_pages, pages_per_split): end min(start pages_per_split, total_pages) writer PdfWriter() for idx in range(start, end): writer.add_page(reader.pages[idx]) output_filename f{base_name}_part{split_count 1}.pdf output_path os.path.join(output_dir, output_filename) with open(output_path, wb) as f: writer.write(f) split_count 1 print(f{input_path}: 共 {total_pages} 页拆分为 {split_count} 个文件) def batch_split_pdfs(input_dir, output_dir, pages_per_split10): 批量遍历目录 os.makedirs(output_dir, exist_okTrue) pdf_files [ filename for filename in os.listdir(input_dir) if filename.lower().endswith(.pdf) ] pdf_files.sort() for filename in pdf_files: input_path os.path.join(input_dir, filename) process_single_pdf(input_path, output_dir, pages_per_split) if __name__ __main__: batch_split_pdfs(pdf_samples/single_files, pdf_samples/output, pages_per_split5)11.2 失败重试与日志记录批量任务跑着跑着中断是比较常见的情况。加入简单日志输出能让你快速定位问题import os import logging from datetime import datetime logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, handlers[ logging.FileHandler(fpdf_batch_{datetime.now():%Y%m%d}.log, encodingutf-8), logging.StreamHandler() ] ) def batch_split_pdfs_with_log(input_dir, output_dir, pages_per_split10): os.makedirs(output_dir, exist_okTrue) pdf_files [ filename for filename in os.listdir(input_dir) if filename.lower().endswith(.pdf) ] pdf_files.sort() success_count 0 fail_count 0 for filename in pdf_files: input_path os.path.join(input_dir, filename) try: process_single_pdf(input_path, output_dir, pages_per_split) success_count 1 logging.info(f成功: {filename}) except Exception as exc: fail_count 1 logging.error(f失败: {filename}, 原因: {exc}) logging.info(f批量任务完成成功 {success_count} 个失败 {fail_count} 个)整个批量任务没有引入复杂的队列系统和异步框架对大多数办公自动化场景来说单线程顺序处理已经够用。只有当文件量超过几千个且单文件处理耗时时长明显时才需要考虑concurrent.futures做线程池或进程池。12. 常见问题与排查方法问题现象可能原因排查方式解决方案安装 pypdf 报错网络问题或 Python 版本过低检查 pip 版本和 Python 版本升级 pip 或切换国内镜像源安装PdfReader读取文件时提示 No such file路径错误打印当前工作目录确认路径使用绝对路径或os.path.join拼接提取文本结果为空PDF 是扫描图片型用 PyMuPDF 检查页面图片数量接入 OCR 引擎处理提取文字顺序混乱PDF 内部对象顺序和阅读顺序不一致查看页面文本块坐标按坐标排序文本块合并后的 PDF 文件过大页面中内嵌大量图片资源查看页面图片尺寸考虑压缩图片后重新生成或拆分处理程序运行到一半内存飙升循环中持续持有 reader 对象观察内存曲线变化逐文件释放对象引用pip install卡住长时间无反应网络不稳定CtrlC 后观察报错信息加-i指定国内源PyMuPDF 导入报错库安装不完整或版本冲突打印import fitz报错堆栈卸载重装检查是否已有同名文件13. PDF 文件验证工作台一个综合示例到这里把本讲内容串起来写一个简单但完整的 PDF 验证脚本。它可以一次读取多页 PDF 的基本信息、状态并判断文件属于哪种类型。这个脚本后续做任何 PDF 项目都可以先跑一遍。import os import sys import fitz from pypdf import PdfReader def analyze_pdf(pdf_path): 输出一个 PDF 文件的核心画像信息 if not os.path.exists(pdf_path): print(f文件不存在: {pdf_path}) return print( * 40) print(文件:, pdf_path) print(大小: %.2f KB % (os.path.getsize(pdf_path) / 1024)) reader PdfReader(pdf_path) doc fitz.open(pdf_path) print(页面数:, len(reader.pages)) print(是否加密:, reader.is_encrypted) for index in range(min(3, len(doc))): page doc[index] text page.get_text(text).strip() image_count len(page.get_images(fullTrue)) if text: content_type 文本型 elif image_count 0: content_type 扫描图片型需 OCR else: content_type 空白页或异常页 print(f第 {index 1} 页: 文本长度 {len(text)}, 图片数量 {image_count}, 判定为 {content_type}) doc.close() if __name__ __main__: analyze_pdf(pdf_samples/sample_text.pdf) analyze_pdf(pdf_samples/sample_scan.pdf)这个脚本在批量处理项目里很有用。需要把几百个 PDF 做解析归档时先用这种迷你分析工作台过一遍就能知道这批文件用哪些策略去处理哪些直接提取文本哪些走 OCR哪些是加密文件需要单独跟进。14. 最佳实践与使用建议14.1 目录结构不要省PDF 办公自动化项目无论规模大小都建议固定以下目录划分pdf_project/ ├── input/ # 原始 PDF ├── output/ # 处理结果 ├── logs/ # 运行日志 └── scripts/ # Python 脚本不要把输入、输出、脚本全塞在一个目录里后面找文件会非常浪费时间。14.2 每一步都留输出处理 PDF 时尽量把中间结果保留下来不要追求一步到位。例如拆分之前先输出一份页数清单提取文本之前先做 PDF 类型判定。中间结果能帮你快速定位到底是解析问题还是原始文件问题。14.3 文件命名规则批量处理 PDF 时输出的文件名不要直接沿用原始文件名。建议加处理标识例如原文件名_拆分_第1部分.pdf。防止多次处理后无法区分版本。14.4 权限和版权意识PDF 文件里如果包含个人信息、公司内部资料或他人版权内容处理前需要明确是否取得文件处理授权。输出结果是否能保存到本地。结果是否能对外展示或用于商业用途。办公自动化追求效率但不能以牺牲合规性为代价。15. 这讲之后你还需要什么这一讲结束后你应该能完成这些事情创建虚拟环境并安装 PDF 处理依赖。用 pypdf 读取 PDF 的页数、元数据、加密状态。用 PyMuPDF 提取页面文本并观察页面内嵌图片数量。判断 PDF 是文字型还是扫描图片型。写出一个简单的 PDF 拆分、合并功能。搭建批量任务的基本模板包含日志记录和错误处理。接下来如果要继续深入可以把下面这些内容作为后续学习方向PDF 批量加水印。PDF 转 Word 文本还原。PDF 表格抽取成 Excel。PDF 扫描件 OCR。PDF 页面尺寸和版式分析。在 Web 服务中提供 PDF 处理接口。需要注意的是本讲的批量脚本是单线程顺序执行。后续要处理超大文件集时可以关注concurrent.futures的ThreadPoolExecutor或ProcessPoolExecutor。PyMuPDF 在释放全局解释器锁方面表现不错但 pypdf 的纯 Python 实现并发提升有限。具体采用哪种方式取决于你的运行环境和任务类型。准备好测试 PDF 文件后把本讲的第 7 节代码跑一遍如果能正确输出页面数量并提取出文本说明你的 Python PDF 处理环境已经通了。下一步就可以进入批量实操。