查看: 310|回复: 3

Python批量导出PDF图片:PyMuPDF与pypdf实现

[复制链接]
发表于 4 小时前 | 显示全部楼层 |阅读模式
提取PDF里的图片看起来只是“遍历页面、把图片存下来”,真正动手时会遇到三类麻烦:图片名带斜杠无法直接当文件名、同一张图在多页被引用导致重复保存、以及“要原始素材”还是“要带标注的视觉快照”分不清。下面按 pypdf、PyMuPDF、pdfplumber 三种实现整理一套可落地的脚本,并给出参数含义与常见坑的排查思路。

一、环境准备与方案定位

原文用 conda 建独立环境,Python 版本固定为 3.11,安装 pypdf:
  1. conda create -n pdf2img python=3.11 -y
  2. conda activate pdf2img
  3. conda install -y pypdf
复制代码

三个库的定位差别很明确:pypdf 是纯 Python 实现,安装即用、无系统级 C 依赖,适合图片格式标准、不需要裁剪的场景;PyMuPDF(导入名为 fitz)功能最完整,能按交叉引用表直接取出嵌入图片的原始字节,保留 JPEG/PNG/TIFF 等原始格式并处理 CMYK 到 RGB 的转换;pdfplumber 不能直接取原始嵌入图片,但能拿到图片在页面上的精确边界框,适合按位置裁剪渲染。

二、pypdf 方案:最轻量的纯 Python 实现

核心接口是 page.images,它直接返回图片文件对象,对象上有 name 和 data 两个关键属性。原文实现里做了三处工程化处理,值得单独说明:

1)文件名清洗。img.name 可能是 /I1.png 或 FormXob.xxx.jpg 这类带斜杠的形式,直接拼接路径会出错,所以先 replace("\\", "/") 再取 os.path.basename。
2)扩展名兜底。用 os.path.splitext 拆出主干与后缀,后缀为空时统一补 .png,避免生成无扩展名文件。
3)跨页去重。用 hash(img.data) 作为键维护一个 seen 集合,同一张图被多页引用时只落盘一次。

完整代码如下:
  1. import os
  2. import pypdf
  3. def extract_images_from_pdf(pdf_path, output_dir="extracted_images"):
  4.     os.makedirs(output_dir, exist_ok=True)
  5.     try:
  6.         pdf_reader = pypdf.PdfReader(pdf_path)
  7.     except FileNotFoundError:
  8.         print(f"错误: 找不到PDF文件 '{pdf_path}'")
  9.         return
  10.     except Exception as e:
  11.         print(f"打开PDF失败: {e}")
  12.         return
  13.     print(f"PDF文件: {pdf_path}")
  14.     print(f"总页数: {len(pdf_reader.pages)}")
  15.     image_count = 0
  16.     seen = set()
  17.     for page_num, page in enumerate(pdf_reader.pages):
  18.         print(f"\n处理第 {page_num + 1} 页...")
  19.         try:
  20.             for img in page.images:
  21.                 safe_name = os.path.basename(img.name.replace("\", "/"))
  22.                 stem, ext = os.path.splitext(safe_name)
  23.                 if not ext:
  24.                     ext = ".png"
  25.                 filename = f"page_{page_num + 1}_{stem}{ext}"
  26.                 filepath = os.path.join(output_dir, filename)
  27.                 key = hash(img.data)
  28.                 if key in seen:
  29.                     print(f" 跳过重复图片: {filename}")
  30.                     continue
  31.                 seen.add(key)
  32.                 try:
  33.                     with open(filepath, "wb") as f:
  34.                         f.write(img.data)
  35.                 except Exception as e:
  36.                     print(f" 保存失败: {e}")
  37.                     continue
  38.                 image_count += 1
  39.                 print(f" 发现图片 {image_count}: {filename} ({len(img.data)} 字节)")
  40.         except Exception as e:
  41.             print(f" 处理第 {page_num + 1} 页时出错: {e}")
  42.             continue
  43.     print(f"\n提取完成!共保存 {image_count} 张图片")
  44.     print(f"图片保存在: {os.path.abspath(output_dir)}")
  45. def main():
  46.     pdf_path = input("请输入PDF文件路径: ").strip().strip('"').strip("'")
  47.     if not os.path.exists(pdf_path):
  48.         print(f"错误: 文件 '{pdf_path}' 不存在")
  49.         return
  50.     if not pdf_path.lower().endswith(".pdf"):
  51.         print("警告: 文件可能不是PDF格式")
  52.     output_dir = input("请输入输出目录 (默认为 'extracted_images'): ").strip()
  53.     if not output_dir:
  54.         output_dir = "extracted_images"
  55.     extract_images_from_pdf(pdf_path, output_dir)
  56. if __name__ == "__main__":
  57.     main()
复制代码

运行方式为 python pdf_extract_fixed.py。需要注意,输入路径时额外调用 strip('"') 与 strip("'") 是为了兼容从资源管理器“复制为路径”带引号粘贴的情况,不处理会直接命中“文件不存在”。

该方案的局限在兼容性:pypdf 对部分特殊编码或 CMYK 图片的处理不如 PyMuPDF 稳定,提取结果可能在格式或色彩上出现偏差。

三、PyMuPDF 方案:直接取原始字节,画质无损

思路是先 page.get_images(full=True) 拿到页面上的图片清单,其中 img[0] 是 xref,即图片在 PDF 交叉引用表中的 ID;再把 xref 交给 doc.extract_image(xref),返回字典里 image 是原始字节、ext 是原始扩展名。因为不重新编码,所以画质不损失,命名按“页号 + 图序 + 原始扩展名”组织:
  1. import fitz  # PyMuPDF
  2. import os
  3. def extract_images_pymupdf(pdf_path, output_dir="extracted_images"):
  4.     os.makedirs(output_dir, exist_ok=True)
  5.     doc = fitz.open(pdf_path)
  6.     saved = []
  7.     for page_num in range(len(doc)):
  8.         page = doc.load_page(page_num)
  9.         images = page.get_images(full=True)
  10.         for img_index, img in enumerate(images):
  11.             xref = img[0]
  12.             base_image = doc.extract_image(xref)
  13.             image_bytes = base_image["image"]
  14.             image_ext = base_image["ext"]
  15.             filename = f"page_{page_num + 1}_img_{img_index + 1}.{image_ext}"
  16.             filepath = os.path.join(output_dir, filename)
  17.             with open(filepath, "wb") as f:
  18.                 f.write(image_bytes)
  19.             saved.append(filepath)
  20.             print(f"已保存: {filename} ({len(image_bytes) / 1024:.1f} KB)")
  21.     doc.close()
  22.     return saved
  23. if __name__ == "__main__":
  24.     extract_images_pymupdf("your_file.pdf", "output_images")
复制代码

若同一张图被多页引用,get_images(full=True) 会在每一页都返回同一个 xref,此时用 xref 集合去重比按字节比较更省内存:
  1. seen_xrefs = set()
  2. for page in doc:
  3.     for img in page.get_images(full=True):
  4.         xref = img[0]
  5.         if xref in seen_xrefs:
  6.             continue
  7.         seen_xrefs.add(xref)
  8.         # 此处调用 doc.extract_image(xref) 提取
复制代码

四、进阶:需要保留叠加标注时改为区域渲染

如果图片上方叠加了红框、文字说明等非嵌入元素,直接 extract_image 只会拿到原始图片,叠加内容全部丢失。这时应先用 page.get_image_rects(xref) 取图片在页面上的矩形区域,再用 get_pixmap(clip=rect, dpi=300) 渲染该区域。dpi 控制输出分辨率,clip 保证只渲染图片所在范围而不是整页:
  1. def extract_images_with_annotations(pdf_path, output_dir="extracted_with_annotations"):
  2.     os.makedirs(output_dir, exist_ok=True)
  3.     doc = fitz.open(pdf_path)
  4.     for page_num in range(len(doc)):
  5.         page = doc.load_page(page_num)
  6.         for img_index, img in enumerate(page.get_images(full=True)):
  7.             xref = img[0]
  8.             rects = page.get_image_rects(xref)
  9.             for rect in rects:
  10.                 pix = page.get_pixmap(clip=rect, dpi=300)
  11.                 filename = f"page_{page_num + 1}_img_{img_index + 1}_annotated.png"
  12.                 pix.save(os.path.join(output_dir, filename))
  13.     doc.close()
复制代码

五、pdfplumber 方案:按页面坐标裁剪渲染

pdfplumber 的 page.images 提供每张图的边界信息,但它的坐标原点在左下角,而渲染用的 Pillow 以左上角为原点,所以必须做一次纵向翻转:新的上边界为 page_height - img["y1"],新的下边界为 page_height - img["y0"],然后 page.crop(bbox).to_image(resolution=300) 得到快照并保存为 PNG。
  1. import pdfplumber
  2. import os
  3. def extract_images_pdfplumber(pdf_path, output_dir="pdfplumber_images", resolution=300):
  4.     os.makedirs(output_dir, exist_ok=True)
  5.     saved = []
  6.     with pdfplumber.open(pdf_path) as pdf:
  7.         for page_num, page in enumerate(pdf.pages):
  8.             page_height = page.height
  9.             for img_index, img in enumerate(page.images):
  10.                 bbox = (
  11.                     img["x0"],
  12.                     page_height - img["y1"],
  13.                     img["x1"],
  14.                     page_height - img["y0"],
  15.                 )
  16.                 cropped = page.crop(bbox)
  17.                 image_obj = cropped.to_image(resolution=resolution)
  18.                 filename = f"page_{page_num + 1}_img_{img_index + 1}.png"
  19.                 filepath = os.path.join(output_dir, filename)
  20.                 image_obj.save(filepath)
  21.                 saved.append(filepath)
  22.                 print(f"已保存: {filename}")
  23.     return saved
复制代码

代价要提前说清:这种方式得到的是页面区域的像素快照,不是原始嵌入图片。输出格式被统一成 PNG,文件体积通常比原始图片大,清晰度由 resolution 参数决定,与原始图片固有分辨率无关。

六、四个容易踩的坑

第一,重复提取。同一 xref 在多页出现时会被重复返回,PyMuPDF 用 xref 集合去重,pypdf 用图片字节哈希去重,思路一致但成本不同。

第二,“提取原始图片”与“截取页面区域”是两件事。嵌入对象用 extract_image(xref) 得到无损原图,但会丢标注;扫描件或叠加过标注的截图属于页面渲染结果的一部分,只能用 get_pixmap(clip=bbox) 或 page.crop() 渲染区域。选哪个取决于要“原始素材”还是“视觉快照”。

第三,加密 PDF。有打开密码的文件需要在 fitz.open() 后调用 doc.authenticate(password),或打开时直接传 password 参数;仅限制编辑、打印的加密 PDF 通常仍可直接提取。

第四,大文件的内存占用。不要一次性把所有 base_image["image"] 都留在内存里,按页处理、处理完即释放,是官方建议的稳妥做法。

综合来看:追求画质和无损原始格式就选 PyMuPDF;部署环境不能装 C 依赖、图片格式规范就选 pypdf;需要按版面精确截图则用 pdfplumber。
回复

使用道具 举报

发表于 4 小时前 | 显示全部楼层

Re: Python批量导出PDF图片:PyMuPDF与pypdf实现

这篇整理挺实用的,尤其是先把 pypdf、PyMuPDF、pdfplumber 的定位讲清楚,后面选型就不容易走偏。pypdf 那段里文件名清洗、扩展名兜底和跨页去重这三处确实是实际跑起来才会踩到的坑,代码也做了异常兜底,拿来改改就能用。一个小建议:去重键用 hash(img.data) 虽然省事,但理论上存在碰撞,如果对完整性要求高,可以换成对图片字节做摘要,或者日志里把重复出现的页码也带上,后面排查会更方便。期待后面 PyMuPDF 和 pdfplumber 的落地细节。
回复 支持 反对

使用道具 举报

发表于 4 小时前 | 显示全部楼层

Re: Python批量导出PDF图片:PyMuPDF与pypdf实现

这篇整理得挺实用的,思路很清楚:先讲三种库的定位差异,再落到 pypdf 的具体实现,尤其是文件名清洗、扩展名兜底和跨页去重这三处工程化处理,确实是实际跑脚本时容易踩的坑。pypdf 方案适合轻量、标准图片的场景,代码里对异常和重复图片也做了处理,整体可落地。不过首帖代码末尾好像截断了,路径校验那行没贴完,建议补全后再发一版,方便直接复制运行。另外去重用 hash(img.data) 简单够用,如果担心极端碰撞可以换成 hashlib 摘要。期待后续 PyMuPDF 和 pdfplumber 的部分,尤其是原始素材提取和按边界框裁剪渲染的对比。
回复 支持 反对

使用道具 举报

发表于 4 小时前 | 显示全部楼层

Re: Python批量导出PDF图片:PyMuPDF与pypdf实现

感谢分享,pypdf 这段写得很扎实,尤其是那三处工程化处理,都是实际跑起来才会踩到的坑。文件名清洗这块我深有体会,之前导出时遇到 img.name 是 /I1.png 这种,直接拼路径就报错,后来也是 basename 解决的。跨页去重也很关键,像公司模板 PDF 每页页眉都有同一个 logo,不去重的话能导出上百份一模一样的图。 有个小建议:去重键用 hash(img.data) 在同一进程里没问题,但如果 PDF 里图片很大、数量又多,可以换成 hashlib.md5(img.data).hexdigest(),内存占用和碰撞风险都更可控一些。另外扩展名兜底补 .png 那里,如果原始数据其实是 JPEG 但没后缀,补成 .png 会导致部分看图软件识别异常,可以考虑用 imghdr 或 PIL 嗅探一下真实格式再定后缀,当然纯脚本场景下问题不大。 另外正文好像到 main 函数那里截断了,PyMuPDF 和 pdfplumber 的部分还没贴全。挺想看看 PyMuPDF 按交叉引用表取原始字节那段,以及 CMYK 转 RGB 具体是怎么处理的,方便的话补一下后续内容?
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-11-10 08:23 , Processed in 0.025724 second(s), 28 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部