27_PDF格式转换
摘要:PDF 虽然是文档交换的标准格式,但在编辑和数据提取方面却极其不便。用户经常需要将 PDF 转换为可编辑的 Word、可分析的 Excel 或用于演示的 PPT。
本章将深入探讨 Python 生态中实现这些转换的主流方案。我们将使用 pdf2docx 重构文档布局,利用 pdfplumber 提取表格数据,结合 python-pptx 生成演示文稿,并讨论在没有商业级 SDK(如 Adobe/Aspose)的情况下,如何通过开源方案达到生产可用的转换质量。
关联的小程序样例:薄荷百宝箱
27.1 PDF 转 Word (docx)
27.1.1 选型分析:为什么是 pdf2docx?
要将 PDF 转为 Word,最困难的不是提取文本,而是恢复布局(Layout Recovery)。我们需要保持段落、表格、图片在 Word 中的相对位置。
python-docx:只能写 Word,不具备解析 PDF 布局的能力。pdf2docx:这是目前 Python 开源界最成熟的方案。它基于 PyMuPDF (fitz) 提取原始数据,利用算法重建段落、表格和图片,并生成 .docx 文件。27.1.2 核心代码实现
在 app/plugins/pdf/to_word.py 中,我们封装转换逻辑。注意:这是一个计算密集型任务,必须在线程池或独立进程中运行。
import os
from pdf2docx import Converter
from app.utils.file_helper import get_temp_path
def convert_pdf_to_docx(pdf_path: str) -> str:
"""
将 PDF 转换为 Word 文档
:param pdf_path: 输入 PDF 文件路径
:return: 输出 Docx 文件路径
"""
# 1. 确定输出路径
docx_path = pdf_path.replace(".pdf", ".docx")
cv = None
try:
# 2. 初始化转换器
cv = Converter(pdf_path)
# 3. 执行转换
# start=0, end=None: 转换所有页面
# multi_processing=True: 开启多进程加速(注意在 Web 容器中可能需要关闭)
cv.convert(docx_path, start=0, end=None, multi_processing=False)
except Exception as e:
# 转换失败需清理残留文件
if os.path.exists(docx_path):
os.remove(docx_path)
raise RuntimeError(f"PDF转Word失败: {str(e)}")
finally:
# 4. 释放资源
if cv:
cv.close()
return docx_path
常见坑:
pdf2docx 只能将图片插入 Word,无法生成可编辑文本。这时需要先经过 OCR 层(见后续章节)。27.2 PDF 转 Excel
27.2.1 选型分析:为什么是 pdfplumber?
PDF 中的表格本质上只是一堆画线指令和绝对定位的文本。
camelot:擅长有线表格,但依赖 Ghostscript,部署麻烦。pdfplumber:纯 Python 实现,基于 pdfminer.six。它提供了强大的可视化调试工具,能很好地处理有线和无线表格,是提取数据的首选。27.2.2 核心代码实现
我们需要遍历每一页,提取表格数据,然后用 pandas 写入 Excel。
import pdfplumber
import pandas as pd
from typing import List
def convert_pdf_to_excel(pdf_path: str) -> str:
excel_path = pdf_path.replace(".pdf", ".xlsx")
all_tables = []
with pdfplumber.open(pdf_path) as pdf:
for page in pdf.pages:
# extract_tables 返回一个嵌套列表: [[[row1_col1, row1_col2], ...], ...]
tables = page.extract_tables()
for table in tables:
# 清洗数据:去除 None 和换行符
cleaned_table = [
[cell.replace('\n', ' ') if cell else '' for cell in row]
for row in table
]
if cleaned_table:
all_tables.extend(cleaned_table)
if not all_tables:
raise ValueError("未在 PDF 中检测到有效表格")
# 使用 pandas 生成 Excel
df = pd.DataFrame(all_tables)
# 假设第一行是表头(根据业务需求调整)
# df.columns = df.iloc[0]
# df = df[1:]
df.to_excel(excel_path, index=False, header=False)
return excel_path
27.3 PDF 转 PPT
27.3.1 技术难点与妥协
将 PDF 转为可编辑的 PPT(每个元素都是独立的 TextBox/Shape)在开源界是一个极高难度的任务,目前没有成熟的库能完美做到。
工程化方案:“图片化 PPT”。
虽然不可编辑文字,但这满足了 90% 用户“在 PPT 中展示 PDF 内容”的需求。
27.3.2 核心代码实现
依赖:pdf2image (需要安装系统库 poppler) 和 python-pptx。
from pdf2image import convert_from_path
from pptx import Presentation
from pptx.util import Inches
import os
def convert_pdf_to_ppt(pdf_path: str) -> str:
ppt_path = pdf_path.replace(".pdf", ".pptx")
# 1. 将 PDF 转为图片列表 (内存中)
# dpi=150 权衡了清晰度和生成速度
images = convert_from_path(pdf_path, dpi=150)
# 2. 创建 PPT 对象
prs = Presentation()
# 获取默认幻灯片尺寸 (通常是 16:9 或 4:3)
# 这里我们动态调整 PPT 尺寸以适应 PDF 页面
if images:
first_img = images[0]
# python-pptx 默认单位是 Emu,这里简单设置宽高比例
# 实际生产中建议固定 PPT 尺寸,让图片 center_inside
prs.slide_width = int(first_img.width * 9525) # pixel to emu approx
prs.slide_height = int(first_img.height * 9525)
for i, img in enumerate(images):
# 3. 添加空白版式 (BLANK)
slide_layout = prs.slide_layouts[6]
slide = prs.slides.add_slide(slide_layout)
# 4. 保存临时图片文件 (python-pptx 需要文件路径或流)
temp_img_path = f"{pdf_path}_temp_{i}.jpg"
img.save(temp_img_path, 'JPEG')
# 5. 插入图片,铺满幻灯片
try:
slide.shapes.add_picture(
temp_img_path,
left=0, top=0,
width=prs.slide_width,
height=prs.slide_height
)
finally:
if os.path.exists(temp_img_path):
os.remove(temp_img_path)
prs.save(ppt_path)
return ppt_path
27.4 PDF 转图片
27.4.1 场景与选型
用于生成缩略图、长图预览或 OCR 预处理。
pdf2image:封装了 pdftoppm 和 pdftocairo 工具,速度快,支持并发。27.4.2 核心代码实现
from pdf2image import convert_from_path
import zipfile
from io import BytesIO
def convert_pdf_to_images(
pdf_path: str,
fmt: str = 'png',
dpi: int = 200
) -> str:
"""
将 PDF 转为图片压缩包
"""
output_zip = pdf_path.replace(".pdf", ".zip")
# thread_count: 开启多线程加速
images = convert_from_path(pdf_path, dpi=dpi, fmt=fmt, thread_count=4)
with zipfile.ZipFile(output_zip, 'w') as zf:
for i, image in enumerate(images):
# 将 PIL Image 转为 Bytes
img_buffer = BytesIO()
image.save(img_buffer, format=fmt.upper())
# 写入 Zip
zf.writestr(f"page_{i+1:03d}.{fmt}", img_buffer.getvalue())
return output_zip
长图拼接技巧: 如果用户想要“生成长图”,可以使用 PIL (Image.new) 创建一个大画布,将 images 列表中的图片按顺序从上到下粘贴(Paste)上去。
27.5 转换质量控制
开源工具毕竟不是 Adobe Acrobat,面对复杂的 PDF 往往会出现格式错乱。我们需要建立一套质量控制机制。
27.5.1 失败降级策略
以 PDF 转 Word 为例:
pdf2docx 进行可编辑转换。27.5.2 资源限制与保护
PDF 转换极其消耗 CPU 和内存。
pdf2image 依赖系统级进程 pdftoppm。在 Docker 容器中,需确保安装了 poppler-utils,并限制子进程的内存使用。27.5.3 字体问题
PDF 经常包含未嵌入的字体(Subset Fonts)。转换服务器必须安装常用的中文字体(如思源黑体、宋体),否则转换出的 Word 文档会出现乱码或方框。
# Dockerfile 示例:安装中文字体
RUN apt-get update && apt-get install -y fonts-noto-cjk
本章面试题与知识点总结
1. pdf2docx 转换 Word 的原理是什么?为什么它有时候会转换失败?
参考答案:
●原理:pdf2docx首先利用PyMuPDF提取 PDF 中的文本块、图片和矢量绘图指令。然后,它通过基于规则的算法(Rule-based Parsing)分析这些元素的位置关系,重构出段落(Paragraph)、表格(Table)和版式(Section),最后调用python-docx生成文件。●失败原因:●扫描件:没有文本层,无法提取文字。●复杂布局:如报纸的多栏不规则排版,规则算法难以正确判定阅读顺序。●矢量表格:有些表格是用线条画出来的(无结构信息),算法可能无法识别其为 Table。
2. 在 Linux (Docker) 环境下使用 pdf2image 报错 Unable to get page count,怎么排查?
参考答案: 这通常是因为缺少系统依赖
poppler-utils。pdf2image只是一个 Python Wrapper,底层通过subprocess调用pdftoppm或pdftocairo命令。 解决:1.确保 Dockerfile 中包含RUN apt-get install -y poppler-utils。2.检查 PDF 文件是否损坏或加密。
3. 如何处理 PDF 转 Excel 时表格跨页的问题?
参考答案:
pdfplumber是按页处理的,无法自动感知“这个表格跨页了”。 工程实践:1.提取所有页面的表格数据List[List[str]]。2.表头启发式算法:检查每一页表格的第一行。如果第 N+1 页的第一行数据类型与第 N 页的表头不一致,且列数相同,大概率是同一个表格的延续。3.合并:将后续页面的数据行(去除可能的重复表头后)追加到前一个表格的数据集中。
夜雨聆风
