16 KiB
16 KiB
PDF 处理高级参考
本文档包含主要技能说明中未涵盖的高级 PDF 处理功能、详细示例和附加库。
pypdfium2 库(Apache/BSD 许可证)
概述
pypdfium2 是 PDFium(Chromium 的 PDF 库)的 Python 绑定。它非常适合快速 PDF 渲染、图片生成,并且可以作为 PyMuPDF 的替代方案。
将 PDF 渲染为图片
import pypdfium2 as pdfium
from PIL import Image
# 加载 PDF
pdf = pdfium.PdfDocument("document.pdf")
# 将页面渲染为图片
page = pdf[0] # 第一页
bitmap = page.render(
scale=2.0, # 更高分辨率
rotation=0 # 不旋转
)
# 转换为 PIL Image
img = bitmap.to_pil()
img.save("page_1.png", "PNG")
# 处理多个页面
for i, page in enumerate(pdf):
bitmap = page.render(scale=1.5)
img = bitmap.to_pil()
img.save(f"page_{i+1}.jpg", "JPEG", quality=90)
使用 pypdfium2 提取文本
import pypdfium2 as pdfium
pdf = pdfium.PdfDocument("document.pdf")
for i, page in enumerate(pdf):
text = page.get_text()
print(f"第 {i+1} 页文本长度:{len(text)} 个字符")
JavaScript 库
pdf-lib(MIT 许可证)
pdf-lib 是一个功能强大的 JavaScript 库,用于在任何 JavaScript 环境中创建和修改 PDF 文档。
加载和操作现有 PDF
import { PDFDocument } from 'pdf-lib';
import fs from 'fs';
async function manipulatePDF() {
// 加载现有 PDF
const existingPdfBytes = fs.readFileSync('input.pdf');
const pdfDoc = await PDFDocument.load(existingPdfBytes);
// 获取页数
const pageCount = pdfDoc.getPageCount();
console.log(`文档共有 ${pageCount} 页`);
// 添加新页面
const newPage = pdfDoc.addPage([600, 400]);
newPage.drawText('由 pdf-lib 添加', {
x: 100,
y: 300,
size: 16
});
// 保存修改后的 PDF
const pdfBytes = await pdfDoc.save();
fs.writeFileSync('modified.pdf', pdfBytes);
}
从零开始创建复杂 PDF
import { PDFDocument, rgb, StandardFonts } from 'pdf-lib';
import fs from 'fs';
async function createPDF() {
const pdfDoc = await PDFDocument.create();
// 添加字体
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);
const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
// 添加页面
const page = pdfDoc.addPage([595, 842]); // A4 尺寸
const { width, height } = page.getSize();
// 添加带样式的文本
page.drawText('发票 #12345', {
x: 50,
y: height - 50,
size: 18,
font: helveticaBold,
color: rgb(0.2, 0.2, 0.8)
});
// 添加矩形(页眉背景)
page.drawRectangle({
x: 40,
y: height - 100,
width: width - 80,
height: 30,
color: rgb(0.9, 0.9, 0.9)
});
// 添加表格类内容
const items = [
['项目', '数量', '单价', '合计'],
['小工具', '2', '$50', '$100'],
['小装置', '1', '$75', '$75']
];
let yPos = height - 150;
items.forEach(row => {
let xPos = 50;
row.forEach(cell => {
page.drawText(cell, {
x: xPos,
y: yPos,
size: 12,
font: helveticaFont
});
xPos += 120;
});
yPos -= 25;
});
const pdfBytes = await pdfDoc.save();
fs.writeFileSync('created.pdf', pdfBytes);
}
高级合并与拆分操作
import { PDFDocument } from 'pdf-lib';
import fs from 'fs';
async function mergePDFs() {
// 创建新文档
const mergedPdf = await PDFDocument.create();
// 加载源 PDF
const pdf1Bytes = fs.readFileSync('doc1.pdf');
const pdf2Bytes = fs.readFileSync('doc2.pdf');
const pdf1 = await PDFDocument.load(pdf1Bytes);
const pdf2 = await PDFDocument.load(pdf2Bytes);
// 从第一个 PDF 复制页面
const pdf1Pages = await mergedPdf.copyPages(pdf1, pdf1.getPageIndices());
pdf1Pages.forEach(page => mergedPdf.addPage(page));
// 从第二个 PDF 复制特定页面(第 0、2、4 页)
const pdf2Pages = await mergedPdf.copyPages(pdf2, [0, 2, 4]);
pdf2Pages.forEach(page => mergedPdf.addPage(page));
const mergedPdfBytes = await mergedPdf.save();
fs.writeFileSync('merged.pdf', mergedPdfBytes);
}
pdfjs-dist(Apache 许可证)
PDF.js 是 Mozilla 的 JavaScript 库,用于在浏览器中渲染 PDF。
基本的 PDF 加载与渲染
import * as pdfjsLib from 'pdfjs-dist';
// 配置 worker(对性能很重要)
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdf.worker.js';
async function renderPDF() {
// 加载 PDF
const loadingTask = pdfjsLib.getDocument('document.pdf');
const pdf = await loadingTask.promise;
console.log(`已加载 PDF,共 ${pdf.numPages} 页`);
// 获取第一页
const page = await pdf.getPage(1);
const viewport = page.getViewport({ scale: 1.5 });
// 渲染到 canvas
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
const renderContext = {
canvasContext: context,
viewport: viewport
};
await page.render(renderContext).promise;
document.body.appendChild(canvas);
}
提取带坐标的文本
import * as pdfjsLib from 'pdfjs-dist';
async function extractText() {
const loadingTask = pdfjsLib.getDocument('document.pdf');
const pdf = await loadingTask.promise;
let fullText = '';
// 从所有页面提取文本
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const textContent = await page.getTextContent();
const pageText = textContent.items
.map(item => item.str)
.join(' ');
fullText += `\n--- 第 ${i} 页 ---\n${pageText}`;
// 获取带坐标的文本以供高级处理
const textWithCoords = textContent.items.map(item => ({
text: item.str,
x: item.transform[4],
y: item.transform[5],
width: item.width,
height: item.height
}));
}
console.log(fullText);
return fullText;
}
提取注释和表单
import * as pdfjsLib from 'pdfjs-dist';
async function extractAnnotations() {
const loadingTask = pdfjsLib.getDocument('annotated.pdf');
const pdf = await loadingTask.promise;
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const annotations = await page.getAnnotations();
annotations.forEach(annotation => {
console.log(`注释类型:${annotation.subtype}`);
console.log(`内容:${annotation.contents}`);
console.log(`坐标:${JSON.stringify(annotation.rect)}`);
});
}
}
高级命令行操作
poppler-utils 高级功能
提取带边界框坐标的文本
# 提取带边界框坐标的文本(对结构化数据至关重要)
pdftotext -bbox-layout document.pdf output.xml
# XML 输出中包含每个文本元素的精确坐标
高级图片转换
# 转换为指定分辨率的 PNG 图片
pdftoppm -png -r 300 document.pdf output_prefix
# 以高分辨率转换指定页面范围
pdftoppm -png -r 600 -f 1 -l 3 document.pdf high_res_pages
# 转换为 JPEG 并设置质量
pdftoppm -jpeg -jpegopt quality=85 -r 200 document.pdf jpeg_output
提取嵌入图片
# 提取所有嵌入图片及元数据
pdfimages -j -p document.pdf page_images
# 列出图片信息(不提取)
pdfimages -list document.pdf
# 以原始格式提取图片
pdfimages -all document.pdf images/img
qpdf 高级功能
复杂页面操作
# 将 PDF 拆分为多组页面
qpdf --split-pages=3 input.pdf output_group_%02d.pdf
# 使用复杂页码范围提取特定页面
qpdf input.pdf --pages input.pdf 1,3-5,8,10-end -- extracted.pdf
# 合并来自多个 PDF 的特定页面
qpdf --empty --pages doc1.pdf 1-3 doc2.pdf 5-7 doc3.pdf 2,4 -- combined.pdf
PDF 优化与修复
# 优化 PDF 以便网络传输(线性化以实现流式传输)
qpdf --linearize input.pdf optimized.pdf
# 移除未使用对象并压缩
qpdf --optimize-level=all input.pdf compressed.pdf
# 尝试修复损坏的 PDF 结构
qpdf --check input.pdf
qpdf --fix-qdf damaged.pdf repaired.pdf
# 显示详细 PDF 结构以进行调试
qpdf --show-all-pages input.pdf > structure.txt
高级加密
# 添加带特定权限的密码保护
qpdf --encrypt user_pass owner_pass 256 --print=none --modify=none -- input.pdf encrypted.pdf
# 检查加密状态
qpdf --show-encryption encrypted.pdf
# 移除密码保护(需要密码)
qpdf --password=secret123 --decrypt encrypted.pdf decrypted.pdf
高级 Python 技术
pdfplumber 高级功能
提取带精确坐标的文本
import pdfplumber
with pdfplumber.open("document.pdf") as pdf:
page = pdf.pages[0]
# 提取所有文本及坐标
chars = page.chars
for char in chars[:10]: # 前 10 个字符
print(f"字符:'{char['text']}' 位置 x:{char['x0']:.1f} y:{char['y0']:.1f}")
# 按边界框提取文本(左、上、右、下)
bbox_text = page.within_bbox((100, 100, 400, 200)).extract_text()
自定义设置的高级表格提取
import pdfplumber
import pandas as pd
with pdfplumber.open("complex_table.pdf") as pdf:
page = pdf.pages[0]
# 使用自定义设置提取复杂布局的表格
table_settings = {
"vertical_strategy": "lines",
"horizontal_strategy": "lines",
"snap_tolerance": 3,
"intersection_tolerance": 15
}
tables = page.extract_tables(table_settings)
# 表格提取的可视化调试
img = page.to_image(resolution=150)
img.save("debug_layout.png")
reportlab 高级功能
创建带表格的专业报告
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib import colors
# 示例数据
data = [
['产品', 'Q1', 'Q2', 'Q3', 'Q4'],
['小工具', '120', '135', '142', '158'],
['小装置', '85', '92', '98', '105']
]
# 创建带表格的 PDF
doc = SimpleDocTemplate("report.pdf")
elements = []
# 添加标题
styles = getSampleStyleSheet()
title = Paragraph("季度销售报告", styles['Title'])
elements.append(title)
# 添加带高级样式的表格
table = Table(data)
table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.grey),
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 14),
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('BACKGROUND', (0, 1), (-1, -1), colors.beige),
('GRID', (0, 0), (-1, -1), 1, colors.black)
]))
elements.append(table)
doc.build(elements)
复杂工作流
从 PDF 提取图形/图片
方法一:使用 pdfimages(最快)
# 以原始质量提取所有图片
pdfimages -all document.pdf images/img
方法二:使用 pypdfium2 + 图片处理
import pypdfium2 as pdfium
from PIL import Image
import numpy as np
def extract_figures(pdf_path, output_dir):
pdf = pdfium.PdfDocument(pdf_path)
for page_num, page in enumerate(pdf):
# 渲染高分辨率页面
bitmap = page.render(scale=3.0)
img = bitmap.to_pil()
# 转换为 numpy 以进行处理
img_array = np.array(img)
# 简单的图形检测(非白色区域)
mask = np.any(img_array != [255, 255, 255], axis=2)
# 查找轮廓并提取边界框
# (此处已简化——实际实现需要更复杂的检测)
# 保存检测到的图形
# ……具体实现取决于实际需求
带错误处理的批量 PDF 处理
import os
import glob
from pypdf import PdfReader, PdfWriter
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def batch_process_pdfs(input_dir, operation='merge'):
pdf_files = glob.glob(os.path.join(input_dir, "*.pdf"))
if operation == 'merge':
writer = PdfWriter()
for pdf_file in pdf_files:
try:
reader = PdfReader(pdf_file)
for page in reader.pages:
writer.add_page(page)
logger.info(f"已处理:{pdf_file}")
except Exception as e:
logger.error(f"处理 {pdf_file} 失败:{e}")
continue
with open("batch_merged.pdf", "wb") as output:
writer.write(output)
elif operation == 'extract_text':
for pdf_file in pdf_files:
try:
reader = PdfReader(pdf_file)
text = ""
for page in reader.pages:
text += page.extract_text()
output_file = pdf_file.replace('.pdf', '.txt')
with open(output_file, 'w', encoding='utf-8') as f:
f.write(text)
logger.info(f"已从以下文件提取文本:{pdf_file}")
except Exception as e:
logger.error(f"从 {pdf_file} 提取文本失败:{e}")
continue
高级 PDF 裁剪
from pypdf import PdfWriter, PdfReader
reader = PdfReader("input.pdf")
writer = PdfWriter()
# 裁剪页面(左、下、右、上,单位:点)
page = reader.pages[0]
page.mediabox.left = 50
page.mediabox.bottom = 50
page.mediabox.right = 550
page.mediabox.top = 750
writer.add_page(page)
with open("cropped.pdf", "wb") as output:
writer.write(output)
性能优化技巧
1. 处理大型 PDF
- 使用流式方法而非将整个 PDF 加载到内存
- 使用
qpdf --split-pages拆分大文件 - 使用 pypdfium2 逐页处理
2. 文本提取
pdftotext -bbox-layout是纯文本提取最快的方案- 使用 pdfplumber 处理结构化数据和表格
- 对于非常大的文档,避免使用
pypdf.extract_text()
3. 图片提取
pdfimages比渲染页面快得多- 预览使用低分辨率,最终输出使用高分辨率
4. 表单填写
- pdf-lib 比大多数替代方案更好地保留表单结构
- 处理前预先验证表单字段
5. 内存管理
# 分块处理 PDF
def process_large_pdf(pdf_path, chunk_size=10):
reader = PdfReader(pdf_path)
total_pages = len(reader.pages)
for start_idx in range(0, total_pages, chunk_size):
end_idx = min(start_idx + chunk_size, total_pages)
writer = PdfWriter()
for i in range(start_idx, end_idx):
writer.add_page(reader.pages[i])
# 处理块
with open(f"chunk_{start_idx//chunk_size}.pdf", "wb") as output:
writer.write(output)
常见问题排查
加密的 PDF
# 处理密码保护的 PDF
from pypdf import PdfReader
try:
reader = PdfReader("encrypted.pdf")
if reader.is_encrypted:
reader.decrypt("password")
except Exception as e:
print(f"解密失败:{e}")
损坏的 PDF
# 使用 qpdf 修复
qpdf --check corrupted.pdf
qpdf --replace-input corrupted.pdf
文本提取问题
# 对扫描件回退到 OCR
import pytesseract
from pdf2image import convert_from_path
def extract_text_with_ocr(pdf_path):
images = convert_from_path(pdf_path)
text = ""
for i, image in enumerate(images):
text += pytesseract.image_to_string(image)
return text
许可证信息
- pypdf:BSD 许可证
- pdfplumber:MIT 许可证
- pypdfium2:Apache/BSD 许可证
- reportlab:BSD 许可证
- poppler-utils:GPL-2 许可证
- qpdf:Apache 许可证
- pdf-lib:MIT 许可证
- pdfjs-dist:Apache 许可证