
先说个很多人容易踩的误区提到Python处理PDF很多人第一反应是“直接用PDF编辑器不就行了”但真正到了批量操作、内容提取、数据清洗这一步手动处理基本就是灾难。比如你手上有一百份格式统一的扫描件合同要抽出关键字段做台账比如你要把几十个PDF按目录拆分成独立章节再比如你想把一堆表格PDF里的数据汇总到Excel。这种场景下Python的价值不在于“编辑一个PDF”而在于把PDF当做一个数据源、一条流水线来处理。这篇文章会围绕Python处理PDF的几类核心操作展开文本提取、图片提取、表格解析、格式转换、合并拆分、加密解密、水印添加、OCR识别。每个模块我都会给出可直接运行的代码、选型思路以及实际项目中容易翻车的细节。适合两类读者一是被PDF重复操作折磨的办公自动化需求方二是想系统掌握PDF处理技术栈的开发者。1. 动手之前先选对库Python处理PDF的几大主力工具对比PDF处理的库非常多但选错库会把简单事情搞复杂。我在早期项目里就吃过亏——用某个人气很高的库去做表格提取结果面对带合并单元格的复杂表格直接崩掉最后花了一整晚重构。先把主力工具分分类大家按场景选型。1.1 高配核心pypdf 与 PyMuPDF 的定位差异处理PDF绕不开两个名字pypdf和PyMuPDF。先说历史沿革早期有一个库叫PyPDF2后来维护者重构之后把项目改名为pypdf如果你的老代码还在用from PyPDF2 import PdfReader现在装pypdf也能正常运行因为兼容了旧导入路径但新代码建议直接写from pypdf import PdfReader。pypdf是纯Python实现优点是跨平台、无外部依赖、安装简单适合做PDF的“结构性操作”——合并、拆分、旋转、裁剪、加密解密、读取元数据和书签。缺点是它对PDF内容流的解析能力一般别指望它做精细的文本定位。PyMuPDF是需要重点关注的库虽然安装包名是PyMuPDF但导入名是fitz这里曾经坑过不少人。它底层绑定的是MuPDF这个C语言渲染引擎性能极强。它最拿手的是两件事一是把PDF页面渲染成图片二是按坐标块提取文本和图像。配合正则表达式做定向抓取比pypdf的extract_text()输出可控得多。对比维度pypdfPyMuPDF底层实现纯PythonC扩展MuPDF结构操作合并/拆分/加密强一般文本提取精度较弱常混排较强支持坐标块提取页面渲染为图片不支持强安装依赖无需要编译好的wheel学习曲线低中结构操作优先用pypdf内容提取和渲染优先用PyMuPDF。这不是“选一个”的问题而是两个都要会。1.2 内容提取特长生pdfplumber 与 pdfminer.six如果核心需求是从PDF中抽表格pdfplumber是最成熟的选择。它的底层解析器用的是pdfminer.six但pdfplumber在pdfminer基础上做了大量优化直接暴露了extract_table()和extract_tables()这两个接口还能控制表格线的识别策略。pdfminer.six本身定位是低层工具适合高级用户自定义解析流程如果你只是想快速抽表直接上pdfplumber。pdfplumber提取表格的原理并不神秘它先解析页面上的线条包括水平线和垂直线把线条交叉形成的矩形网格填上文本内容最终拼成二维表格。这也就解释了它的局限性——如果表格没有画线或者线条是图片形式的扫描件pdfplumber就失灵了。这种情况要落到OCR方案我在后面专门讲。1.3 生成场景的梯队reportlab、fpdf2 各自擅长什么说完读取再说生成。从零创建PDF最常见的是reportlab老牌、功能全、支持复杂的绝对定位和图形绘制缺点是API设计偏古老写起来不够直观。如果你要生成的是简单报表、发票样式的PDF用reportlab非常稳。另一个轻量方案是fpdf2API更现代化、文档也友好但复杂布局能力不如reportlab。我的建议很直接要做动态版式、大批量出证直接学reportlab它帮你避免很多底层坑。1.4 安装与版本踩坑记录安装命令如下建议用虚拟环境pip install pypdf pdfplumber PyMuPDF reportlab pdf2docx这里有三个常见的坑第一PyMuPDF导入名不是PyMuPDF而是import fitz看到ModuleNotFoundError: No module named fitz不用慌。第二新版pypdf对旧版PyPDF2的API做了调整比如PdfFileReader这种类名在新版中变成了PdfReader网上很多老教程混着用会报错。第三pdfplumber依赖pdfminer.six而pdfminer.six更新频繁偶尔会遇到pdfplumber对某个pdfminer版本不兼容症状是调用extract_text()时抛TypeError。解决办法很简单把pdfminer.six固定到pdfplumber要求的版本即可看它的setup.py为准。2. 从PDF里“薅”内容文本、图片、表格三种提取实战2.1 文本提取先跑通最简单的再处理“难啃”的先看最简单场景——数字化生成的PDF即文本可选的PDF不是扫描件。用pypdf提取全文只需要几行from pypdf import PdfReader reader PdfReader(demo.pdf) for page in reader.pages: text page.extract_text() or print(text)这个方案对简单的单栏文本有效但对双栏排版、页眉页脚混排的效果就不太理想因为extract_text()是整页一股脑输出的阅读顺序会乱。这时候换成PyMuPDF的按块提取方式import fitz doc fitz.open(demo.pdf) for page in doc: blocks page.get_text(blocks) # 每个块是(x0, y0, x1, y1, text, block_no, block_type) for b in blocks: if b[6] ! 0: # 跳过图片块 continue print(f坐标({b[0]:.1f}, {b[1]:.1f}) - {b[4]})拿到每个文本块的坐标后你可以按列排序、按区域过滤比如只提取页面左上角的标题或者只取某一坐标范围内的正文内容。这在处理复杂版面的纸质扫描转换PDF时非常实用。2.2 图片提取别再截图了直接拉原图PDF里嵌的图片很多人是打开PDF软件放大后截图分辨率一塌糊涂。用PyMuPDF可以精准取回原始嵌入图片。import fitz doc fitz.open(with_images.pdf) for page_num in range(len(doc)): page doc[page_num] images page.get_images(fullTrue) for img_index, img in enumerate(images): xref img[0] base_image doc.extract_image(xref) image_bytes base_image[image] ext base_image[ext] with open(fpage{page_num1}_img{img_index1}.{ext}, wb) as f: f.write(image_bytes)extract_image()拿到的就是嵌在PDF里的原图字节流。不过需要注意有些PDF设计者会对图片套一层裁剪遮罩mask单独提取出来整图会带黑色背景或透明通道异常。遇到这种情况建议先渲染整个页面区域再用像素处理的方式裁剪后续会聊到。2.3 表格提取把“铁板一块”拆成行和列表格提取是PDF操作里最能体现“开箱即用与真实阵痛”的部分。拿pdfplumber举例import pdfplumber with pdfplumber.open(table_demo.pdf) as pdf: first_page pdf.pages[0] tables first_page.extract_tables() for table in tables: for row in table: print(row)看着简单但真实项目中会遇到几个问题。第一pdfplumber默认用页面上的所有线条生成表格如果页面有线框用于装饰会被误判成表格解决方案是传入vertical_strategy和horizontal_strategy参数改成text策略让系统根据文本位置推断表格边界。第二合并单元格提取后会出现重复文本或None值需要写清理逻辑。我自己的经验是处理复杂表格时绝不只依赖一次extract_tables()而是结合page.lines和page.rects先画一个页面网格可视化再做针对性参数调整。必要时用page.crop()把表格区域切出来单独解析准确率会明显上升。2.4 提取后数据清洗的必要性提取出文本或表格之后别急着入库。PDF中的空格可能是为了对齐而填充的换行符可能出现在句子中间。我通常会在提取后做一次统一清洗流程合并断行、去空格、处理全半角符号、剔除页眉页脚特征文本。用正则表达式处理时注意PDF文本常含有特殊连字符和不可见字符优先用Unicode规范化。3. PDF转Word、转图片多种转换思路解析3.1 很多人想要的PDF转Word怎么做才不像“奥利给”PDF转Word是搜索热词里的大户但说实话不存在完美转换。文字版PDF转Word还能做到格式基本还原扫描版PDF不做OCR是不可能直接转成可编辑Word的所谓“识别”其实是OCR加版面重排。工具层面我推荐用pdf2docx它基于PyMuPDF和python-docx实现对文本型PDF的还原度相当高能保留基本样式、表格和图片。pip install pdf2docxfrom pdf2docx import Converter pdf_file demo.pdf docx_file demo.docx cv Converter(pdf_file) cv.convert(docx_file, start0, endNone) cv.close()一个合理的预期管理pdf2docx对单栏文本、标准表格的PDF效果很好但对复杂排版、艺术字体、特殊符号的处理会出现偏位。如果你接触的PDF大多来自Word或WPS导出这个工具的性价比极高。如果你的转换需求不是整篇转换而是从PDF里挑几页转出来Converter的start和end参数可以直接指定页码范围不用先把PDF裁开。3.2 PDF转图片渲染成PNG/JPG到底有什么用PDF转图片有两大典型场景做OCR预处理以及防止代码被直接复制走的内容分发。PyMuPDF把页面渲染成图片非常简单import fitz doc fitz.open(demo.pdf) zoom_x 2.0 # 2倍分辨率 zoom_y 2.0 mat fitz.Matrix(zoom_x, zoom_y) for page in doc: pix page.get_pixmap(matrixmat) pix.save(fpage_{page.number1}.png)这里的zoom参数直接控制输出DPI2.0对应约144 DPI3.0约216 DPI。如果只是预览1.5足够如果后续要送OCR建议至少2.0以上。别小看这个参数我曾试过用1.0的zoom导出图片OCR小字号中文识别率惨不忍睹调高到2.5后准确率明显改善。3.3 批量转换与文件整理批量处理是Python处理PDF的最大优势。一个常见的自动化场景把某个目录下所有PDF按文件名转换成同名的Word或图片并归档到指定目录。from pathlib import Path from pdf2docx import Converter src_dir Path(./pdfs) out_dir Path(./converted) out_dir.mkdir(exist_okTrue) for pdf_path in src_dir.glob(*.pdf): out_path out_dir / (pdf_path.stem .docx) cv Converter(str(pdf_path)) cv.convert(str(out_path)) cv.close() print(f已转换: {pdf_path.name})如果是成百上千个文件加一个ThreadPoolExecutor做多线程也能跑但要注意pdf2docx的转换过程比较吃内存建议分批处理一次控制在20个文件以内。4. 合并、拆分、旋转、加密不换格式也能完成的日常操作4.1 合并多个PDF顺序与书签问题合并PDF是最基础也最高频的操作。一个典型的业务场景把多份扫描件合并成一个完整合同。pypdf的实现方式如下from pypdf import PdfWriter, PdfReader writer PdfWriter() pdf_files [a.pdf, b.pdf, c.pdf] for file in pdf_files: reader PdfReader(file) for page in reader.pages: writer.add_page(page) with open(merged.pdf, wb) as out: writer.write(out)需要重点处理的是书签目录问题。直接用上面的代码合并生成的文件没有书签面板读者翻页很不方便。如果你在做一个几十页的合并PDF建议给每个文件的开头页面添加书签from pypdf import PdfWriter, PdfReader writer PdfWriter() pdf_files [a.pdf, b.pdf, c.pdf] for index, file in enumerate(pdf_files): reader PdfReader(file) start_page len(writer.pages) for page in reader.pages: writer.add_page(page) writer.add_outline_item(fPart {index1}: {file}, start_page) with open(merged_with_bookmarks.pdf, wb) as out: writer.write(out)还有个细节容易被忽视合并PDF时如果原始文件的页面尺寸不一致比如第一部分是A4第二部分是A3扫描件合并后查看器会自动缩放显示打印时会出现页面大小错乱。此时可以在添加页面前用page.scale_to(width, height)统一页面尺寸。4.2 拆分指定页灵活抽取子文件拆分PDF常见有两种需求按页码范围切割以及按页数规律抽选。看代码from pypdf import PdfReader, PdfWriter reader PdfReader(source.pdf) total_pages len(reader.pages) # 提取第2页到第5页注意页码从0开始 writer PdfWriter() for page_num in range(1, 5): writer.add_page(reader.pages[page_num]) with open(extracted.pdf, wb) as out: writer.write(out)从财务对账场景来看经常需要“抽奇数页”因为很多扫描合同是正反面分别扫描的正面是盖章页背面是条款页全处理完后就要按奇偶拆成两组文件。控制循环步长即可实现。4.3 页面旋转、裁剪与方向修正扫描件方向错乱的问题非常常见特别是手机扫描的文档经常出现某些页面旋转了90度或180度。pypdf的旋转操作from pypdf import PdfReader, PdfWriter reader PdfReader(rotated_source.pdf) writer PdfWriter() for page in reader.pages: page.rotate(90) # 顺时针旋转90度。也可用rotate_clockwise writer.add_page(page) with open(rotated_fixed.pdf, wb) as out: writer.write(out)判断页面是否旋转、应该旋转多少度现实中不能靠猜。推荐先提取每页的前几个文本块的坐标分析文本方向后再决定是否旋转。PyMuPDF可以拿到页面原始旋转属性page.rotation # 返回0, 90, 180, 270如果页面自带的rotation信息不对或者压根没写rotation字段是0但内容实际是横着排的那你可能需要先渲染成图片用视觉方向判断。自动判断文本方向是个偏复杂的CV问题简单的场景可以直接肉眼预览几个页面再写死旋转逻辑。4.4 加密、解密与权限控制加密PDF分为两种打开密码User Password和权限密码Owner Password。用pypdf设置密码和权限from pypdf import PdfWriter, PdfReader reader PdfReader(source.pdf) writer PdfWriter() for page in reader.pages: writer.add_page(page) writer.encrypt( user_passworduser123, owner_passwordowner456, permissions_flag0b1100, # 禁止打印、禁止修改 ) with open(encrypted.pdf, wb) as out: writer.write(out)解密有打开密码的文件from pypdf import PdfReader reader PdfReader(encrypted.pdf) if reader.is_encrypted: reader.decrypt(user123) for page in reader.pages: text page.extract_text() print(text)需要提醒一句你只能解自己有权限处理的文件。很多打印店和公司内部文件的加密权限是自己设置的解密逻辑是做自动化归档用的别拿来做突破访问控制的事。从技术上讲某些非标准加密的PDF比如部分国内的电子发票PDFpypdf是解不开的会抛FileNotDecryptedError这种属于供应商自研加密方案需要使用专用SDK处理。5. 水印、OCR识别和扫描件处理PDF的进阶玩法5.1 给PDF批量加文字水印和图片水印给PDF加水印通常有两种实现路线。路线一用reportlab先生成一个透明背景的水印PDF再用pypdf把它叠加到目标页面上。路线二直接使用PyMuPDF在每页的指定坐标写入文字。这里说路线一因为它的通用性更强不依赖PyMuPDF的渲染坐标。先弄一个水印源from reportlab.pdfgen import canvas from reportlab.lib.units import cm c canvas.Canvas(watermark.pdf, pagesize(595, 842)) # A4 c.setFont(Helvetica, 40) c.setFillColorRGB(0.5, 0.5, 0.5, 0.3) # 半透明灰色 c.translate(297, 420) # 移动到页面中心 c.rotate(45) c.drawCentredString(0, 0, CONFIDENTIAL) c.save()再把这个水印PDF和原始PDF叠加from pypdf import PdfReader, PdfWriter reader PdfReader(target.pdf) watermark PdfReader(watermark.pdf) wm_page watermark.pages[0] writer PdfWriter() for page in reader.pages: page.merge_page(wm_page) writer.add_page(page) with open(watermarked.pdf, wb) as out: writer.write(out)这里merge_page()会把水印层的透明效果保留下来适用于合同、设计稿、内部文件的防泄露场景。5.2 扫描件PDF的OCR识别把“死”文件变“活”扫描版PDF本身没有文本层任何直接提取文本的方法都返回空字符串。要让它可搜索、可提取唯一的途径是OCR。两个主力方案pytesseractGoogle Tesseract和PaddleOCR。Tesseract是老牌方案支持100多种语言中文识别效果中规中矩安装时除了pip还要额外装系统级程序Windows下要安装tesseract.exe并配置环境变量。PaddleOCR是百度开源的中文识别效果更佳pip安装即可但模型加载会更重量级。我的典型处理流程是先用PyMuPDF把PDF页面渲染成高分辨率图片再用OCR引擎识别文本最后把识别结果连同位置信息写入一个新的可搜索PDF或者直接生成TXT/JSON数据。以PaddleOCR为例识别并输出JSONimport fitz import json from paddleocr import PaddleOCR ocr PaddleOCR(use_angle_clsTrue, langch) # 支持中英文 doc fitz.open(scan.pdf) result_all [] for page_num in range(len(doc)): page doc[page_num] pix page.get_pixmap(matrixfitz.Matrix(2.5, 2.5)) img_path ftemp_page_{page_num1}.png pix.save(img_path) result ocr.ocr(img_path, clsTrue) for line in result: if line is None: continue for item in line: box item[0] text item[1][0] conf item[1][1] result_all.append({ page: page_num 1, text: text, bbox: box, confidence: conf }) with open(ocr_result.json, w, encodingutf-8) as f: json.dump(result_all, f, ensure_asciiFalse, indent2)OCR结果的置信度过滤很关键。低于0.7的文本块基本可以丢掉了或者在导出前标记为“疑似识别错误”。实际项目中扫描件的清晰度参差不齐识别结果总会有误差算法模型再好也救不了一张拍歪拍糊的原件。前期做图像预处理转灰度、锐化、二值化往往比调模型参数更有效。5.3 模板化批量提取特定字段掌握了文本提取和OCR之后最实用的进阶玩法是做模板化字段提取。比如我从几十张格式统一的报销单PDF里提取日期、金额、报销人三个字段。思路是先用pdfplumber打开一份样本定位目标字段的坐标区域然后批量处理所有文件时只对相同坐标区域调用extract_text()或extract_words()。这就是“坐标模板”方案只要版式不变准确率非常高。import pdfplumber regions { date: (50, 100, 200, 130), # (x0, y0, x1, y1) amount: (300, 100, 500, 130), name: (50, 140, 200, 170), } with pdfplumber.open(batch_01.pdf) as pdf: page pdf.pages[0] for field, (x0, y0, x1, y1) in regions.items(): crop page.crop((x0, y0, x1, y1)) text crop.extract_text() print(f{field}: {text})这套方案的关键在于拿到准确的坐标。第一次的坐标往往不准建议先用page.to_image(resolution150)渲染页面并手动标注然后再写模板。版式只要错位1毫米提取结果就可能串字段。6. 踩坑实录编码、字体、坐标和页面尺寸那些问题6.1 提取出来全是乱码怎么办PDF文本提取乱码的典型原因有两个。第一是字体子集化加自定义编码PDF标准允许字体内部用非Unicode的CID映射很多国内厂商的PDF生成工具喜欢这么干。提取工具读不到ToUnicode映射表时extract_text()就会吐出类似乱码或者空文本。第二个原因是字体缺失尤其是比较偏的字体系统没有对应字体库时PyMuPDF对部分文本块的解析会异常。最优解法是转路线如果必须取文字试试pdfplumber的extract_words()它输出的粒度更细偶尔能救回来。不行就上OCR。OCR对这种情况几乎是降维打击因为不管底层字体编码多怪渲染成图片后文字特征还在。6.2 PDF坐标体系与常规图像坐标的差异PDF的坐标系统与常规图像不同原点是左下角(0,0)x轴向右增长y轴向上增长。很多初学者用PyMuPDF的get_text(blocks)拿到坐标后直接去别的图像库画框结果文本框全部上下颠倒因为图像库的坐标原点是左上角y轴向下增长。转换公式很简单image_y page_height - pdf_y用PyMuPDF渲染页面时page.rect.height就是页面高度换算一下即可。这个坑我在做“根据PDF文本定位信息在截图里画高亮框”时踩过折腾了半小时才发现是坐标系问题。6.3 表格提取时线条缺失、合并单元格错位用pdfplumber提取复杂表时经常遇到“表格线有一根没识别出来”导致整列错位。此时把策略改成显式配置table page.extract_table({ vertical_strategy: lines, horizontal_strategy: lines, snap_tolerance: 3, # 线条吸附容差单位pt })如果表格线是花色的、浅色的或者虚线lines策略可能识别不到改用explicit_vertical_lines和explicit_horizontal_lines手动传入线坐标。合并单元格的处理没有银弹我的经验是解析后写一个后处理函数把跨行跨列的单元格按第一出现的行列对齐并把空值填成上一行的值。6.4 页面尺寸不一致导致的奇怪问题合并或截取PDF页面后如果页面尺寸各不相同某些PDF阅读器显示时会出现“页面很大但内容很小”的怪象。最好的方案是在页面写入前统一尺寸from pypdf import PdfReader, PdfWriter reader PdfReader(source.pdf) writer PdfWriter() for page in reader.pages: if page.mediabox.width page.mediabox.height: page.rotate(90) # 统一为竖向 writer.add_page(page) with open(normalized.pdf, wb) as out: writer.write(out)mediabox是PDF页面的物理尺寸单位为ptA4大约是595x842letter大约是612x792。不同来源的文件混在一起时先标准化尺寸和方向能避免后面打印、合并时的一堆刁钻问题。6.5 嵌入式字体与文件体积膨胀给PDF加水印、合并页面后文件体积可能会异常膨胀。这是因为merge_page()有时会把水印页的字体资源整体带入目标文档多次合并后字体资源被重复嵌入。解决方法是处理完所有操作后用PyMuPDF的垃圾回收机制做一次文档瘦身import fitz doc fitz.open(merged_raw.pdf) doc.subset_fonts() # 尝试压缩字体子集 doc.garbage_collect() doc.save(merged_compressed.pdf, garbage4, deflateTrue)garbage4是最激进的重写策略会用重新序列化整个文档的方式清理无用对象。但要注意对加密的PDF先解密再压缩否则可能报错。7. 把脚本封装成顺手的小工具经验与建议走到这里基础的PDF处理能力你已经全部掌握了。最后一个建议别每次都在命令行里敲交互式代码要把这些能力封装成函数或脚本形成自己的工具库。比如我会在本地维护一个pdf_toolkit.py里面放十来个常用函数extract_text(path, pages)、extract_images(path, output_dir)、split_pdf(path, ranges)、merge_pdfs(file_list)、add_watermark(src, wm, output)。团队协作的时候把这套工具包做成命令行CLI或者简单的图形界面能极大减少重复沟通成本。我见过很多运营人员对着一百多份PDF手工操作教她们用脚本之后日常处理时间从小时级降到分钟级。这就是技术解决实际问题的直接价值。一个小技巧是给所有函数加异常兜底和日志输出PDF文件来源五花八门有些文件损坏、有些权限受限、有些非标准实现处理批量任务时一个坏文件可能会中断整个流程。我的习惯是用try...except跳过并记录失败文件等批量结束后统一查看错误报告。最后再分享一个关于页面预览的调试经验。处理PDF时如果你不确定页面当前的样子不要反复猜测参数直接渲染页面缩略图看一眼最有说服力。写代码的过程就是和PDF格式斗智斗勇的过程保持这个习惯能帮你省掉大量的猜测时间。