1. 项目缘起为什么需要自己动手处理PDF在日常工作中我经常遇到这样的场景市场同事发来一份几十页的竞品分析报告PDF需要我快速提取其中的核心数据和图表法务部门给了一堆合同扫描件要求批量核对关键条款甚至自己写的技术文档也需要从中抽取代码片段和架构图进行复用。如果每次都手动打开PDF复制、粘贴、另存为效率低下不说还容易出错。市面上当然有各种在线的PDF转换工具但涉及到公司内部文档、敏感数据时上传到第三方平台总让人心里不踏实。而且当处理需求变得复杂比如需要按特定规则提取某一类信息或者要集成到自动化流程中时通用工具往往就力不从心了。这时Python的优势就凸显出来了。它就像一个万能工具箱通过几个强大的库我们就能编程实现对PDF文档的“解剖”——精准地提取出纯文本内容或者像外科手术般分离出内嵌的图片。这不仅仅是简单的格式转换更是实现文档自动化处理、信息抽取乃至构建知识库的第一步。今天我就结合自己多次“踩坑”的经验手把手带你用Python搞定PDF的文本与图片提取让你拥有一个随时待命、安全可靠的私人文档处理助手。2. 工欲善其事环境搭建与核心库选型在开始写代码之前选对工具至关重要。Python处理PDF的库不少但各有侧重。经过多次项目实践我主要推荐以下两个组合它们几乎能覆盖90%的提取需求。2.1 核心武器库PyMuPDF 与 pdfplumberPyMuPDF (fitz)这是一个功能极其强大且速度飞快的库。它基于成熟的MuPDF引擎不仅支持文本、图片提取还能处理注释、表单、甚至进行文档渲染和转换。它的文本提取精度高能保留文字的位置信息这对于需要分析文档版式的场景如提取表格数据非常有帮助。安装也简单pip install PyMuPDF在代码中我们通常以import fitz的方式导入因为它的模块名是fitz。pdfplumber如果说PyMuPDF是瑞士军刀那么pdfplumber更像是为“信息抽取”量身定制的精密仪器。它底层也部分依赖PyMuPDF但提供了更友好、更Pythonic的API尤其在处理表格提取和获取详细的字符、线框等布局信息方面表现非常出色。对于复杂的、排版不规则的PDF用它来提取文本和定位元素往往更省心。pip install pdfplumber为什么不选PyPDF2或pdfminer你可能也听说过这两个库。PyPDF2对某些加密或特殊编码的PDF支持不够好文本提取功能相对基础pdfminer.sixpdfminer的Python 3版本虽然强大但API较为复杂。对于大多数提取任务PyMuPDF和pdfplumber的组合在性能、易用性和功能上取得了更好的平衡。2.2 辅助工具包Pillow 与 正则表达式Pillow (PIL)这是Python事实上的图像处理标准库。当我们从PDF中提取出图片的原始数据通常是字节流后需要用Pillow将其打开、查看或保存为常见的图片格式如PNG, JPG。pip install Pillow正则表达式 (re)Python内置的re模块。提取出文本后我们经常需要进一步清洗和筛选比如找出所有的邮箱地址、电话号码或者特定格式的日期。这时正则表达式就是最锋利的“手术刀”。2.3 环境配置要点与避坑指南这里有一个新手极易踩坑的点库的版本兼容性。特别是PyMuPDF它的API在早期版本和较新版本之间有变化。如果你在网上找到一段2019年的代码直接复制运行很可能报错。重要提示建议使用较新的稳定版本。在撰写本文时我使用的环境是 PyMuPDF1.23.8, pdfplumber0.10.3, Pillow10.1.0。在安装前可以先使用pip list查看现有版本或用pip install --upgrade进行更新。另一个常见问题是处理中文PDF时出现乱码。这通常不是因为提取库的问题而是PDF内部字体编码导致的。PyMuPDF和pdfplumber在提取时通常会尝试使用正确的编码但如果你遇到乱码可以尝试在提取文本时指定编码或者检查PDF本身是否使用了非标准的嵌入字体。一个简单的判断方法是用Adobe Acrobat Reader等专业软件打开PDF如果能正常显示那么用我们的Python库提取也应该是正常的如果 Reader 打开就是乱码那问题出在PDF文件本身。3. 庖丁解牛深入PDF文档结构在动手写代码前花几分钟理解PDF的内部结构会让你在遇到问题时更加从容。你可以把一个PDF文件想象成一栋大楼。文档 (Document)大楼本身。对应代码中的doc fitz.open(“file.pdf”)。页面 (Page)大楼里的每一层楼。一个文档包含多个页面页码通常从0开始。页面元素每一层楼里的各种物件。这主要包括两大类文本块 (Text Block/Span)由文字组成的段落、句子或单词。它们不仅有内容还有精确的坐标x0, y0, x1, y1定义其位置和边界框。图像对象 (Image Object)嵌入的图片。在PDF内部图片可能以多种格式如JPEG, PNG, 或原始的位图数据存储并带有位置、变换矩阵等信息。提取文本和图片的过程本质上就是让程序遍历这栋“大楼”的每一层页面然后识别并记录下所有的“文本物件”和“图片物件”。为什么理解坐标很重要因为PDF是一种基于坐标的页面描述语言。文本“Hello”和一张图片在PDF里并没有直接的“上下左右”关系而是通过坐标定位。当我们想提取特定区域的文字比如只提取摘要部分或者想把文本和图片按原始版面关系重组时就必须依赖这些坐标信息。PyMuPDF和pdfplumber在提取时都提供了丰富的位置数据这是它们比简单复制粘贴强大得多的地方。4. 实战演练一使用 PyMuPDF 进行高精度提取PyMuPDF速度极快适合处理大型PDF文档。我们来一步步拆解如何使用它。4.1 文本提取从基础到进阶最基本的文本提取一行代码就能获取整个页面的纯文本import fitz # 这就是PyMuPDF def extract_text_with_pymupdf(pdf_path): text # 1. 打开文档 doc fitz.open(pdf_path) for page_num in range(len(doc)): page doc[page_num] # 2. 提取当前页所有文本最简单的方式 text page.get_text() doc.close() return textpage.get_text()默认使用“文本”模式它会尝试按照阅读顺序拼接文字但对于多栏排版或复杂布局的页面顺序可能会乱。进阶按“块”提取并保留位置信息为了获得更好的结构和精度我强烈推荐使用get_text(“blocks”)或get_text(“dict”)模式。def extract_text_blocks_with_pymupdf(pdf_path): doc fitz.open(pdf_path) all_blocks [] for page_num, page in enumerate(doc): # 获取文本块每个块是一个元组 (x0, y0, x1, y1, “文本内容”, 块号, 类型) blocks page.get_text(“blocks”) for block in blocks: x0, y0, x1, y1, block_text, block_no, block_type block if block_type 0: # 类型0通常代表文本块 # 你可以在这里根据坐标(x0, y0)对块进行排序以还原版面阅读顺序 all_blocks.append({ “page”: page_num, “bbox”: (x0, y0, x1, y1), “text”: block_text.strip() }) doc.close() return all_blocks这种方式提取的文本每个“块”都带有其边界框坐标。你可以根据y坐标垂直位置对所有块进行排序就能较好地还原多栏文档的阅读顺序这是处理报告、论文PDF的关键技巧。4.2 图片提取捕获每一个像素用PyMuPDF提取图片思路是遍历页面的所有显示对象筛选出图像对象然后将其原始像素数据导出。import fitz from PIL import Image import io def extract_images_with_pymupdf(pdf_path, output_folder): doc fitz.open(pdf_path) image_count 0 for page_num in range(len(doc)): page doc[page_num] # 1. 获取页面的图像列表 image_list page.get_images(fullTrue) # fullTrue 获取高清图 for img_index, img_info in enumerate(image_list): xref img_info[0] # 图像在PDF中的交叉引用号 base_image doc.extract_image(xref) image_bytes base_image[“image”] # 图像的二进制数据 image_ext base_image[“ext”] # 图像格式如 “jpeg”, “png” # 2. 使用Pillow打开并保存图像 try: image Image.open(io.BytesIO(image_bytes)) image_filename f“{output_folder}/page_{page_num1}_img_{img_index1}.{image_ext}” image.save(image_filename) print(f“Saved: {image_filename}”) image_count 1 except Exception as e: print(f“Failed to save image on page {page_num1}, index {img_index}: {e}”) doc.close() print(f“Total images extracted: {image_count}”)关键点解析与避坑page.get_images(fullTrue)这个fullTrue参数非常重要。如果设为False获取的可能是预览图或低分辨率图。设为True才能拿到嵌入的最高质量图像。doc.extract_image(xref)这是真正将图像数据从PDF中“解包”出来的步骤。返回的字典中“image”键对应二进制数据“ext”键对应文件扩展名。图片重复问题PDF中同一张图片可能在多个页面被引用但只存储一次。get_images可能会列出这些引用。上述代码通过xref提取同一张图片只会保存一次。但如果你发现提取的图片数量远超预期可能是页面上的矢量图形或背景也被识别为图像对象需要进行更复杂的过滤。保存格式我们直接使用了PDF中存储的原始格式image_ext。但有时这个格式可能是“jpx”(JPEG 2000) 等不常见的格式Pillow可能无法直接保存。一个更稳健的做法是统一用Pillow转换为PNG或JPG格式保存。5. 实战演练二使用 pdfplumber 进行精细化操作pdfplumber的API设计更贴近数据分析的思维它把页面看作一个由字符、线条、矩形等组成的平面。5.1 文本提取获得字符级控制import pdfplumber def extract_text_with_pdfplumber(pdf_path): all_text “” with pdfplumber.open(pdf_path) as pdf: for page in pdf.pages: # 提取页面所有文本 text page.extract_text() if text: all_text text “\n\n” # 页面间加空行分隔 # 更高级提取所有字符及其位置 # chars page.chars # for char in chars: # print(char[“text”], char[“x0”], char[“top”], char[“fontname”]) return all_textpage.extract_text()方法已经内置了不错的布局分析算法对于大多数文档效果很好。但pdfplumber的真正威力在于page.chars、page.words、page.lines等属性它们提供了页面最底层的元素让你可以实现极其精细的控制比如自己写算法来识别表格。5.2 图片提取与可视化分析pdfplumber提取图片的方式和PyMuPDF类似但它还提供了一个非常实用的功能可视化调试。import pdfplumber from PIL import Image def extract_images_with_pdfplumber(pdf_path, output_folder): with pdfplumber.open(pdf_path) as pdf: for page_num, page in enumerate(pdf.pages): # 获取图片对象 images page.images for img_info in images: # img_info 是一个字典包含 ‘stream’, ‘x0’, ‘top’, ‘x1’, ‘bottom’ 等 if ‘stream’ in img_info: raw_data img_info[“stream”].get_rawdata() if raw_data: try: image Image.open(io.BytesIO(raw_data)) # 根据坐标等信息生成文件名 bbox (img_info[“x0”], img_info[“top”], img_info[“x1”], img_info[“bottom”]) filename f“{output_folder}/page_{page_num1}_img_at_{int(bbox[0])}_{int(bbox[1])}.png” image.save(filename) except Exception as e: print(f“Error processing image on page {page_num1}: {e}”) # 【实用技巧】可视化页面元素用于调试 # im page.to_image(resolution150) # 将页面渲染为图片 # im.debug_tablefinder() # 在图片上绘制出检测到的表格线框 # im.save(“debug_page.png”)page.to_image()功能非常强大它能把PDF页面转换成一个PIL图像对象然后你可以调用.debug_tablefinder()在上面画出它识别出的表格线或者用.draw_rects()画出文本块的边界这对于调试复杂的提取规则、验证坐标是否正确时是无可替代的利器。6. 综合应用与性能优化掌握了基本技能后我们来看几个实际场景的综合应用和提升效率的技巧。6.1 场景一批量处理与信息结构化假设你需要从几百份产品说明书PDF中提取“产品型号”和“技术参数”部分。这些信息在每份文档中的位置大致固定比如都在第二页的顶部。import fitz import re def batch_extract_specific_area(pdf_folder_path, target_area): “““ target_area: 一个字典定义要提取的区域例如 {‘page_num’: 1, ‘rect’: (50, 100, 400, 300)} rect 是 (x0, y0, x1, y1)单位是点point。 “““ results [] for pdf_file in os.listdir(pdf_folder_path): if pdf_file.endswith(“.pdf”): doc fitz.open(os.path.join(pdf_folder_path, pdf_file)) page doc[target_area[‘page_num’]] # 使用给定的矩形区域提取文本 rect fitz.Rect(target_area[‘rect’]) text_in_area page.get_text(“text”, cliprect) # clip参数用于指定区域 # 使用正则表达式从提取的文本中匹配型号和参数 model_match re.search(r‘Model[:]\s*(\S)’, text_in_area) param_match re.search(r‘Voltage[:]\s*([\d\.\-~]V)’, text_in_area) results.append({ ‘file’: pdf_file, ‘model’: model_match.group(1) if model_match else ‘N/A’, ‘voltage’: param_match.group(1) if param_match else ‘N/A’ }) doc.close() return results这个例子结合了区域提取(clip参数) 和正则表达式实现了半结构化的信息抽取是自动化文档处理的核心模式。6.2 场景二提取图片并重命名有时我们需要根据图片所在页面的上下文文本来给图片命名。例如提取论文中的图表并命名为“图1-1 系统架构.png”。import fitz def extract_images_with_contextual_name(pdf_path, output_folder): doc fitz.open(pdf_path) for page_num in range(len(doc)): page doc[page_num] # 先提取页面文本用于寻找图片标题 full_text page.get_text(“text”) # 这里可以写更复杂的逻辑比如寻找“图”、“Figure”等关键词附近的文字 # 简单示例假设图片标题在图片上方50点的位置 image_list page.get_images(fullTrue) for img_index, img_info in enumerate(image_list): xref img_info[0] base_image doc.extract_image(xref) # 获取图片的大致位置这里简化处理用第一个图片对象的引用位置 # 实际中需要更精确地关联图片对象和其坐标 image_bbox page.get_image_bbox(img_info) # 注意这是一个简化表述实际需用page.get_image_rects(xref) # 根据图片坐标在全文文本中寻找附近的文字作为标题此处逻辑需自定义 potential_caption find_nearby_text(full_text, image_bbox) filename sanitize_filename(f“page{page_num1}_{potential_caption or img_index}”) # … 保存图片文件名用filename doc.close()这个场景的难点在于精准关联图片和其标题文本。PDF中图片和文字是独立的对象没有直接的“所属关系”。通常需要通过坐标位置进行启发式匹配比如假设标题位于图片上方20-50点的矩形区域内。这需要反复调试没有一劳永逸的通用规则。6.3 性能优化与内存管理处理成百上千页的PDF时性能和内存是关键。逐页处理及时关闭始终使用with语句pdfplumber或在循环结束后手动doc.close()(PyMuPDF)确保文件句柄被释放。选择性读取如果只处理特定页面不要遍历所有页。直接用doc[page_index]访问。避免重复提取如果需要同时获取文本和图片最好在一次页面遍历中完成而不是分别调用两次get_text和get_images。对于超大型PDF考虑使用PyMuPDF因为它的C语言后端在速度上有绝对优势。如果内存吃紧可以分批处理比如每处理50页就保存一次中间结果然后释放内存。7. 疑难杂症与排查指南即使掌握了上面的方法在实际操作中你还是会遇到各种“怪事”。下面是我总结的几个常见问题及排查思路。问题一提取的文本全是乱码或顺序错乱。排查首先用专业的PDF阅读器如Adobe Acrobat Reader打开检查显示是否正常。如果正常则问题可能出在布局复杂尝试使用get_text(“blocks”)或get_text(“dict”)模式然后根据(x0, y0)坐标对文本块进行排序先按y坐标再按x坐标。字体编码问题极少见。可以尝试在get_text()时指定编码如page.get_text(encoding“utf-8”)但通常库会自动处理。工具推荐使用pdfplumber的page.to_image().debug_tablefinder()功能可视化查看文本块的划分是否准确。问题二提取的图片数量远多于实际可见的图片。原因PDF中的Logo、水印、图标、甚至某些字体渲染的阴影或特效都可能被识别为图像对象。page.get_images()会列出所有这些对象。解决你需要进行过滤。常见的过滤策略有按尺寸过滤太小的图片比如宽度或高度小于50像素很可能是图标或装饰。if image_width 100 and image_height 100: # 只保存较大的图片 save_image(image)按位置过滤如果水印固定在页眉页脚可以排除特定矩形区域内的图片。按xref去重同一个图片xref在多个页面被引用get_images会列出多次但extract_image提取一次即可。问题三有些“文字”明明在PDF里看得到却提取不出来。原因这些“文字”很可能不是真正的文本而是扫描件图片或者是由矢量路径绘制而成的图形文字。判断在PDF阅读器中尝试用鼠标选择这些文字。如果选不中那就是图片。解决对于扫描件你需要使用OCR光学字符识别技术。可以先用我们上面的方法把图片提取出来然后使用Tesseract等OCR引擎对图片进行识别。这就是为什么“java tesseract.doocr 提取文本信息”会成为相关搜索词。在Python中你可以使用pytesseract库来调用Tesseract。import pytesseract from PIL import Image # 假设我们已经从PDF中提取了一张图片并保存为 ‘extracted_image.png’ image Image.open(‘extracted_image.png’) text_from_scanned_image pytesseract.image_to_string(image, lang‘chi_simeng’) # 中英文混合识别 print(text_from_scanned_image)处理扫描件PDF是一个更大的话题涉及图像预处理去噪、二值化、纠偏等和OCR引擎调优足以单独写一篇博文。问题四处理加密或受保护的PDF。情况如果PDF有打开密码PyMuPDF在open时可以传入密码fitz.open(“file.pdf”, password“userpass”)。限制如果PDF有复制/打印限制所有者密码即使你知道打开密码也可能无法提取内容。这属于数字版权管理DRM绕过它通常违反软件许可协议和可能的法律法规。对于这类文档最合规的方式是联系文档提供者获取无限制的版本。掌握了这些核心方法、应用场景和排错技巧你基本上就能应对绝大多数PDF内容提取的需求了。从简单的复制文本到复杂的自动化信息抽取Python都能为你提供强大而灵活的工具链。关键在于理解文档结构选择合适的库并针对具体问题设计提取策略。