尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python PDF处理实战:从文本提取到拆分合并的办公自动化指南

Python PDF处理实战:从文本提取到拆分合并的办公自动化指南 身为一个每天和 Word、Excel、PPT 打交道的办公人员或是经常需要处理合同、报表、论文的技术开发者你迟早会遇到一个绕不开的问题PDF。PDF 是一种极其常见的文档格式但它和 Word 不一样它更像一张“电子纸”上面的内容被固定住了。这就意味着你没法像编辑 Word 那样随意修改连复制里面的文字都经常会乱码或排版错乱。于是很多人为了转换一个 PDF 文件到处找在线工具结果不是要付费就是担心文件上传到第三方服务器有泄露风险。如果处理的是合同、标书、身份证扫描件这类敏感文件用在线工具其实非常不安全。用 Python 来处理 PDF并不是什么黑科技它其实是在“读”和“写”之间寻找一种结构化的平衡。Python 可以把 PDF 从“死文档”变成“活数据”让你能够批量提取、拆分、合并甚至自动填充。你不需要掌握多复杂的计算机知识只要装上几个库照着代码往下写就能自己搭建一个本地、离线、免费的 PDF 工具箱。这篇文章呢我就以“办公自动化”为场景带你从零开始认识 PDF 在 Python 眼中的样子然后带你动手写出几个最常用的 PDF 处理脚本。不管你是刚入门 Python 的小白还是有一定基础的开发同学这篇文章都会给你一个清晰、能直接上手的思路。1. 为什么办公自动化绕不开 PDF在正式写代码之前我们先要回答一个问题你可能已经会处理 Excel、Word 了为什么偏偏 PDF 是办公自动化里最让人头疼却又必须攻克的一环核心原因在于交互方式的差异。Word、Excel 的本质是“内容容器”它们的设计目标就是方便你随时修改和编辑。PDF 的全称是 Portable Document Format它的目标是“跨平台输出不变样”。它把字体、图片、排版全部“冻结”进一个文件里让所有设备打开都能看到同一幅画面。因此PDF 并不像 Word 那样友好地“允许”程序直接修改内部结构。很多财务人员、行政人员每天都要在几十个 PDF 里做同样的事情提取几行文字、把多份合同拆出来、给 PDF 加水印或是把扫描件转成可搜索的文档。这些事情靠人工点击一次两次还勉强能接受如果次数多呢枯燥、易错、还伤眼睛。从办公自动化的角度看PDF 处理可以分为三个层次。第一层是“看懂它”把 PDF 里面的文字、表格、图片识别出来。这个层次要解决的是数据录入和信息检索问题。第二层是“重排它”按页码拆分、合并、旋转、加解密。这类操作不是在内容内部进行重排而是对整个文档的结构进行操作。第三层是“改造它”或“生成它”把一批 HTML、图片或数据动态填写进去变成一份格式统一的 PDF 报告。这通常是自动化的最后一个输出环节。我见过很多初级自动化项目往往在开始时低估了 PDF 文本提取的复杂度。你从 PDF 中提取出的文字顺序可能不是阅读顺序而是绘制顺序也可能是乱码。假如没有深入理解“为什么 PDF 里的文字不是天生整齐的”这个道理你的代码测试一百遍都可能通过不了。所以在 Python 办公自动化实践中PDF 处理的代码逻辑往往不复杂复杂的恰恰是你对这个格式本身的理解以及你对第三方库边界能力的判断。这也是我写这一篇文章的初衷先把 PDF 这件事想清楚再去写代码这样你会少走很多弯路。2. PDF 在 Python 眼中的基础概念如果你曾经尝试过用记事本打开 PDF 文件你会看到一堆乱码。这很正常因为 PDF 本质上是一个复杂的、带压缩和对象索引的二进制文档格式。我建议你先把 PDF 理解成一个容器而不是一个纯文本文件。它的内部包含多个页面对象每个页面对象都有一份资源字典记录了页面上使用了什么字体、什么图片、什么图形指令。它并不是像 HTML 那样按我们阅读的顺序“从上到下”存储内容的。举个很简单的例子如果你在 PDF 里看到一个“你好”单词它在这个文件中可能是以四个独立字符的形式存在的每个字符都被精确地指定了坐标。这也就是为什么直接用文本方式复制 PDF 时很可能变成一行行乱序的片段。在 Python 生态中要想处理好 PDF最核心的一条原则是按需选择工具。如果你只是想读取 PDF 的元数据、合并拆分 PDF、写入门级的文本提取可以优先考虑 PyPDF2 或者它的维护升级版本 pypdf。这类库在结构操作上很在行但在复杂版式的文本提取上表现一般。如果你需要从带有复杂排版的 PDF 里提取干净的文本或者需要提取表格数据推荐 pdfplumber。它在底层封装了 pdfminer.six对坐标和划线检测做了很多优化非常适合处理含表格的版式。如果你需要把 PDF 转成图片或者在图像层面做 OCR那么 PyMuPDF也就是 fitz是性能最好的选择之一。它的渲染速度非常快能一页一页地把 PDF 画成高分辨率的 PNG。如果只是单纯的处理扫描版 PDF比如要把图片上的文字识别出来并做成可搜索文本则需要结合 OCR 库比如 PaddleOCR 或 Tesseract。很多同学一上来就学了一堆 PDF 库遇到问题也不知道该用哪个代码写着写着就越写越乱。真正的做法是用一个小小决策来统一逻辑你要做文档结构处理拆分、合并、加密、旋转首选 PyPDF2/pypdf你要做文本和表格数据抽取首选 pdfplumber你要做页面渲染和矢量绘图首选 PyMuPDF。这篇文章中我尽量围绕最常见的操作讲读取 PDF 信息、提取文本、拆页、合并、加密。这几个操作用 PyPDF2 系列就可以完成。而在表格提取的部分我会引入 pdfplumber 演示。3. 环境准备与前置条件在正式开始编码之前你需要确保你的 Python 环境是正常的。我推荐你使用 Python 3.8 以上版本因为本文中使用的库在新版本 Python 上表现更稳定。如果你的电脑还没有安装 Python建议去 Python 官网下载稳定版安装时记得勾选“Add Python to PATH”。然后我们需要安装以下几个库。你可以在命令行Windows 下是 CMD 或 PowerShellmacOS/Linux 下是终端中输入以下命令pip install pypdf pdfplumber这里要说明一点早期大家常看到的 PyPDF2 在 2022 年前后已经停止了积极维护社区更推荐使用 pypdf 作为替代它的 API 与 PyPDF2 基本兼容而且修复了很多 bug。我们这里使用pypdf作为示例。当然你机器上如果之前已经安装了 PyPDF2很多代码同样适用只是类名的导入路径可能会有差异建议你在动手前先用下面的命令确认一下版本。pip show pypdf如果你看到类似Version: 4.x.x的输出说明安装没有问题。除此之外我们或许还会用到文件路径的操作所以一起安装标准库里的 pathlib 吗不需要pathlib 是 Python 3 标准自带的直接用就行。在开始写代码前我们最好准备一个测试用的 PDF 文件。你可以随意创建一个 Word 文档另存为 PDF也可以从你手头的资料中找一个 PDF。文章里的示例文件我统一用/tmp/sample.pdf和/tmp/output作为示意大家在实际操作时记得换成自己的真实路径。如果你是 Windows 环境可以把这些路径改成D:\\pdf_demo\\sample.pdf这类路径。4. 用 Python 读取 PDF 基础信息我们先用最常遇到的需求开始拿到一个 PDF 文件后自动读取它的文件名、页数、作者、创建时间等基础信息。在没有使用 Python 前你多半是打开文档然后去属性里翻页数。现在这个操作可以变成代码里的一个函数。下面是最小化的示例。我这里用pypdf.PdfReader来加载文件# 文件路径read_pdf_info.py from pypdf import PdfReader def show_pdf_info(pdf_path): reader PdfReader(pdf_path) pages len(reader.pages) meta reader.metadata print(文件路径:, pdf_path) print(PDF 页数:, pages) if meta is not None: print(标题:, meta.title) print(作者:, meta.author) print(创建者:, meta.creator) print(创建时间:, meta.creation_date) else: print(该 PDF 不包含元数据信息) if __name__ __main__: show_pdf_info(/tmp/sample.pdf)这段代码背后的逻辑很简单先用PdfReader读取文件路径生成一个可操作的 reader 对象然后通过len(reader.pages)获取页面总数随后从reader.metadata中读取可选的元数据字段。需要注意不是所有 PDF 都有 metadata 字段比如一些打印店生成的 PDF 可能没有作者和标题。因此代码里用if meta is not None做了个保护避免程序报错。运行这个脚本预期会输出类似如下内容文件路径: /tmp/sample.pdf PDF 页数: 12 标题: 2024年度项目报告 作者: 创建者: Microsoft Word 创建时间: 2024-01-15 10:23:45如果出现FileNotFoundError这类报错大概率是路径不对。如果出现PyCryptodome is required for AES encryption说明你打开的是一个加密的 PDF需要额外安装或输入密码我们后文会提到。5. 使用 Python 提取 PDF 文本与表格接下来是 PDF 办公自动化中最核心的需求之一提取文本。在做数据录入、报销审核、合同关键信息抓取时我们通常希望从 PDF 中直接把文字弄出来。如果你用 PyPDF2/pypdf 去提取文本会发现它对纯文字型 PDF 支持尚可但对复杂版式或表格效果不理想。这里我们要明白一个概念PDF 中文字的抽取更像是一次“还原”而不是简单的“复制”。还原的规则依赖库对内容流的解析和坐标排序。所以如果你的材料是扫描件本质是图片那么任何提取文本的库都无法直接产生文字必须借助 OCR。下面用pdfplumber提取文本。这是一个能保留坐标关系并做重组的好工具。# 文件路径extract_text.py import pdfplumber def extract_text_from_pdf(pdf_path, start_page0, end_pageNone): all_text [] with pdfplumber.open(pdf_path) as pdf: total_pages len(pdf.pages) if end_page is None: end_page total_pages - 1 for page_index in range(start_page, min(end_page 1, total_pages)): page pdf.pages[page_index] text page.extract_text() if text: all_text.append(f----- 第 {page_index 1} 页 -----) all_text.append(text) return \n.join(all_text) if __name__ __main__: content extract_text_from_pdf(/tmp/sample.pdf) print(content) # 可以把内容保存为 txt 文件 with open(/tmp/sample_text.txt, w, encodingutf-8) as f: f.write(content)调用page.extract_text()时如果返回None大概率这一页是扫描图片或没有可提取的文本层。这种情况下如果你真的想拿到文字需要走 OCR 路线。再讲一个常见的办公场景提取表格。很多 PDF 里的表格看似是表格其实只是用横线和竖线画出来的图形上面散布着文字。pdfplumber 提供了extract_tables()方法专门处理这种带划线表格。# 文件路径extract_table.py import pdfplumber def extract_table_from_pdf(pdf_path, page_number0): with pdfplumber.open(pdf_path) as pdf: page pdf.pages[page_number] tables page.extract_tables() for table_idx, table in enumerate(tables): print(f发现第 {table_idx 1} 个表格) for row in table: print(row) if __name__ __main__: extract_table_from_pdf(/tmp/sample.pdf, page_number2)这里需要注意extract_tables()对完整有线框的表格效果最好。如果表格没有线框只是用空白间距排版出来的视觉表格那很难直接抽取需要你先用extract_text()拿到整块文本再用正则或关键词做二次整理。所以我们在实际项目里不要指望一个库能解决所有版式。判断版式然后选择合适的策略这才是工程化的思路。6. 拆分与合并 PDF最实用的办公脚本讲完“读”我们再看“重排”。拆分与合并 PDF可能是日常办公中最高频、最节省时间的操作了。试想一下你手头有一份 100 页的 PDF领导只需要其中的 7 到 18 页你要怎么发给他如果手动操作要用 Adobe 或 WPS 的“提取页面”再另存为新文档。但如果每天都要拆十几份材料呢手点太慢了。Python 可以轻松实现按页拆分。# 文件路径split_pdf.py from pypdf import PdfWriter, PdfReader def split_pdf_pages(pdf_path, output_dir, page_ranges): :param pdf_path: 原始 PDF 路径 :param output_dir: 输出目录 :param page_ranges: 由元组组成的列表如 [(0, 2), (3, 5)] 表示保留第 1-3 页和 4-6 页页码索引从 0 开始 reader PdfReader(pdf_path) total_pages len(reader.pages) for idx, (start, end) in enumerate(page_ranges): writer PdfWriter() start max(start, 0) end min(end, total_pages - 1) if start end: continue for page_no in range(start, end 1): writer.add_page(reader.pages[page_no]) output_path f{output_dir}/split_part_{idx 1}.pdf with open(output_path, wb) as f: writer.write(f) print(f已生成: {output_path}包含 {end - start 1} 页) if __name__ __main__: split_pdf_pages(/tmp/sample.pdf, /tmp/output, [(0, 2), (3, 5), (6, 9)])在上面的代码中page_ranges里的数字是从 0 开始的比如(0, 2)表示提取第 1 页到第 3 页。很多初学者容易在这里数错页码我建议你在实际应用前先读懂len(reader.pages)的页数含义。在实际办公中为了更友好你可以把 page_ranges 设计成用户直观看到的 1 到 N然后在代码内部减 1。我上面没有做这个转换是为了保持示例简单真实项目里请务必做一层转换防止用户把“第 1 页”理解成索引 1 导致定位错误。合并 PDF 也同样简单。它的逻辑是创建统一的 PdfWriter然后遍历多个 PdfReader 对象把页面添加到同一个 writer 里。# 文件路径merge_pdf.py from pypdf import PdfReader, PdfWriter def merge_pdfs(pdf_paths, output_path): writer PdfWriter() for pdf_path in pdf_paths: try: reader PdfReader(pdf_path) print(f添加文件: {pdf_path}页数: {len(reader.pages)}) for page in reader.pages: writer.add_page(page) except Exception as e: print(f处理 {pdf_path} 失败: {e}) with open(output_path, wb) as f: writer.write(f) print(f合并完成: {output_path}) if __name__ __main__: pdf_list [/tmp/part1.pdf, /tmp/part2.pdf, /tmp/part3.pdf] merge_pdfs(pdf_list, /tmp/merged.pdf)关于这个代码有一点要提醒PdfReader在打开加密 PDF 时可能需要PdfReader(pdf_path, password密码)这样传入密码否则会抛异常。所以在拆分合并批量文件之前建议先把来源文件的加密情况摸清否则脚本会在运行中段崩溃。7. 给 PDF 加密与解密在办公场景里加密 PDF 也很常见。你可以给一份客户合同添加打开密码避免无关人员看到内容。用 Python 可以做到这一点代码非常好懂。# 文件路径protect_pdf.py from pypdf import PdfReader, PdfWriter def encrypt_pdf(pdf_path, password, output_path): reader PdfReader(pdf_path) writer PdfWriter() for page in reader.pages: writer.add_page(page) # 设置打开密码并限制打印和复制权限 writer.encrypt(user_passwordpassword, owner_passwordNone, use_128bitTrue, permissions_flag0xFFFF) with open(output_path, wb) as f: writer.write(f) print(f加密完成: {output_path}) if __name__ __main__: encrypt_pdf(/tmp/sample.pdf, your-password-123, /tmp/encrypted.pdf)这里permissions_flag0xFFFF表示授予所有权限。如果你希望禁止打印需要把 permissions_flag 设置为某个特定的位运算值。但在实际办公软件中不同阅读器对权限位的理解未必完全一致所以这里我建议先保持默认的授权模式。为什么因为权限控制不解决真正的安全问题它只是对阅读器的一个提醒。如果要真正保密还是要用打开密码。解密 PDF 在 pypdf 中也有对应方法但要注意解密操作只能针对用户密码为空的文件如果源文件设置了打开密码而你不知道那从原理上就不该尝试绕过因为你没有授权。# 文件路径decrypt_pdf.py from pypdf import PdfReader, PdfWriter def decrypt_pdf(pdf_path, password, output_path): reader PdfReader(pdf_path) if reader.is_encrypted: result reader.decrypt(password) if result 0: print(密码错误无法解密) return writer PdfWriter() for page in reader.pages: writer.add_page(page) with open(output_path, wb) as f: writer.write(f) print(f解密完成: {output_path}) if __name__ __main__: decrypt_pdf(/tmp/encrypted.pdf, your-password-123, /tmp/decrypted.pdf)再强调一次安全边界这里讨论的加密解密对象必须是本人持有、或已获得合法授权的 PDF 文档。你可以在办公自动化时用它管理自己的文件但不要打算去破解别人的加密文件。逆向处理未授权文件既无必要也不符合正当技术规范。8. 运行验证与效果检查我们写好了这么多脚本问题是怎么验证它是否正常工作。很多初学者喜欢把代码运行一把看到没有报错就说好了这远远不够。对于办公自动化脚本结果文件的正确性比运行不报错重要得多。我的建议是采用“三层验证法”。第一层是命令行运行成功。比如你运行python merge_pdf.py能打印出“合并完成”且没有抛异常。第二层是文件级验证。代码执行后除了看输出日志还要检查生成的文件大小是否非零。如果原来合并的文件总共 10 MB合并后的文件却只有 1 KB那大概率是有问题的即使脚本没有报错。第三层是内容级验证。你可以写一个校验函数读取生成后的 PDF 页数并打开某一页用extract_text()判断某个关键词是否存在。# 文件路径verify_pdf.py from pypdf import PdfReader import pdfplumber def verify_pdf(pdf_path, expect_pagesNone, expect_keywordsNone): print(开始验证:, pdf_path) reader PdfReader(pdf_path) actual_pages len(reader.pages) print(页数:, actual_pages) if expect_pages is not None: if actual_pages expect_pages: print(页数校验通过) else: print(f页数校验失败预期 {expect_pages}实际 {actual_pages}) if expect_keywords: with pdfplumber.open(pdf_path) as pdf: text for page in pdf.pages: page_text page.extract_text() if page_text: text page_text for keyword in expect_keywords: if keyword in text: print(f关键词 [{keyword}] 存在) else: print(f关键词 [{keyword}] 不存在) if __name__ __main__: verify_pdf(/tmp/merged.pdf, expect_pages20, expect_keywords[合同, 金额])如果验证不通过不要急着换库或者查语法先看一下原始 PDF 是否本身就不包含这些文字。比如扫描版 PDF 里可能写着“合同”两个字但它是图片不是文本层所以提取不到。这种情况并不是代码有问题而是数据源的问题。实际项目里我会把校验逻辑封装成一个函数放在每个批处理任务的最后一步。比如批量拆分 50 个 PDF 后统一执行校验程序生成一份报告。这样一旦出现个别文件页数不对可以在报告里精确知道是哪一个文件、哪一段页码范围有问题而不是等用户发现后再追查。9. 常见问题与排查思路PDF 处理脚本看起来很简单实际上在真实项目中栽跟头的人不少。我整理了下面几个高频问题并给出排查思路。问题现象可能原因排查方式解决方案打开 PDF 时报错“File has not been decrypted”文档设置了打开密码用 reader.is_encrypted 判断在 PdfReader 中传入 password提取文本时返回空或 None页面没有文本层属于扫描件用 pdfplumber 查看该页是否含图片改用 OCR 方案提取中文文本乱码PDF 内嵌字体映射表不规范用专业阅读器对比文本层内容改走 OCR 或以图像为准split/merge 得到文件页数为 0页码范围写错或越界打印 len(reader.pages) 检查修正 page_range 的 start/end合并后文件顺序不对页面添加顺序与预想不一致打印每个文件追加时的页号和文件名检查循环顺序运行报 “PyCryptodome is required”处理加密 PDF 缺少加密库安装 pycryptodomepip install pycryptodomepip 安装库时提示 network 超时网络连接不稳定或镜像源慢换国内 pip 镜像pip install xxx -i https://pypi.tuna.tsinghua.edu.cn/simple针对最常见的情况再做一点说明。如果你处理的是中文 PDF文本提取的第一步应该先搜一下文件有没有字体嵌入限制。很多企业内部的 PDF 会用自定义字体子集化转出来之后文字顺序和视觉不完全一致。这时候与其花大力气去调库的参数我更建议沉淀一个思路先抽一小段样本文本人工比对。绝大多数情况下人能看得出来的规则代码都能改出来人看不出来的规则就不要硬调了。还有一点比较重要当你在 Python 中使用 pypdf 创建出来的 PDF 或加密文件有极低概率在某些老版本阅读器中打开时提示错误。这时先不要怀疑代码逻辑可以试试把输出的 PDF 再重新复制一个版本或用 WPS/Adobe 打开一次。哪一步会导致兼容性问题再针对性地调整参数。办公自动化的目标是解决问题不是要写出多漂亮的代码所以保持“输出兼容性优先”的思路更有利于产出。10. 最佳实践与工程建议如果你打算把这些脚本放到日常脚本库中长期使用下面几条工程建议可以帮你省下不少力气。第一统一封装路径与输出规范。不要在每个脚本里硬编码文件路径而是通过一个config.py集中管理输入目录、输出目录和临时目录。这样即使换了电脑或文件目录只需要改一个配置文件不需要把代码翻个底朝天。# 文件路径config.py from pathlib import Path BASE_DIR Path(__file__).parent INPUT_DIR BASE_DIR / input OUTPUT_DIR BASE_DIR / output TEMP_DIR BASE_DIR / temp # 这些目录如果不存在则自动创建 for d in [INPUT_DIR, OUTPUT_DIR, TEMP_DIR]: d.mkdir(parentsTrue, exist_okTrue)第二注意敏感文件的最小留存。PDF 里常常有身份证复印件、合同盖章、财务信息。你处理的中间文件比如提取出的 txt、拆分后的临时 PDF在任务结束后要及时清理。如果你把代码放到服务器上自动跑不要忘了定期清理临时目录。第三适当地记录执行日志。不要只在屏幕上 print最好同时输出到日志文件。尤其当你要批量处理几百份 PDF 时中途任何一个文件报错都需要事后可以回溯。# 文件路径logger.py import logging logging.basicConfig( levellogging.INFO, format%(asctime)s - %(name)s - %(levelname)s - %(message)s, handlers[ logging.FileHandler(pdf_automation.log, encodingutf-8), logging.StreamHandler() ] ) logger logging.getLogger(pdf_tool)第四把 PDF 处理的常用函数建一个自己的工具包。比如pdf_utils.py文件里封装extract_text、split_pdf、merge_pdf、encrypt_pdf其他调度脚本通过 import 复用。不要在多个脚本里重复复制粘贴同一段逻辑。你只需要保证封装粒度适中不要依赖固定的“主流程”因为不同任务组合会很不一样。第五在处理大批量任务前一定要做抽样测试。建议在一个小的测试集上尝试完再批量执行避免整个文件夹一次性跑挂造成文件损坏或输出结果不对。最后想说的一点是尽量把 PDF 处理和 Excel、Word 处理串联起来。很多办公自动化任务并不是孤立的。比如你可能从一个 PDF 合同里提取文本再把关键字段写入 Excel 表也可能先批量合并 PDF再通过邮件或企业微信机器人发送。学会用 Python 把这些环节串起来你的自动化网络才会真正成型效率也会几倍地上升。PDF 处理将成为这整条流水线上坚实的一环。11. 总结与实践建议这篇文章从办公自动化的视角把 PDF 的基础概念、读取信息、文本提取、表格抽取、拆分合并、加解密以及验证方法串成了一整条操作链。你可以照着这些代码去搭一套本地 Python 小工具解决日常“手动点开 PDF 一点点操作”的低效痛点。我们列出的代码都是可以跑的总起点但我强烈建议你从第一个脚本开始亲手动一动。先拿一个简单的 PDF 测试输入和输出只要换一换路径跑通了再扩展。之后再进入一个更复杂的场景比如每月从几十份 PDF 报告中汇总客户出账金额这时候你再把提取好的数据接入 Excel 处理流程你就能体会到所谓办公自动化是什么感觉了。真正让你进步的不是收藏这份代码而是你用这串代码跑通了自己手头的一个麻烦。要想进一步学习你可以考虑研究三个方面PDF 的内部结构与页对象或者学习 pdfplumber 的表格解析原理又或者涉猎 OCR 技术将扫描型 PDF 转换为可检索文档。如果这篇文章对你有帮助建议收藏备用。后续我还会沿着办公自动化这条线写更多 Python 实操教程期待你一起把手里重复、枯燥的文件工作变成几分钟跑完的脚本。
返回列表