python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python处理PDF

Python处理PDF文档的两大功能库(PyPDF2/pdfplumber)的使用指南

作者:MarkHD

PyPDF2擅长PDF文档操作,而pdfplumber专注于高质量文本和表格数据提取,本文将对比Python中两个PDF处理库PyPDF2和pdfplumber的主要功能与适用场景,希望对大家有所帮助

一、工欲善其事:PyPDF2 vs pdfplumber,如何选择?

在处理PDF之前,我们需要理解不同库的定位。Python生态中有多个PDF处理库,各有所长。根据我们的学习目标,主要聚焦于两个库:

功能/库PyPDF2(及其继任者pypdf)pdfplumber
主要用途PDF操作修改:合并、拆分、旋转、加密、添加水印精确提取结构化数据,特别是表格和文本
文本提取能力基础(对复杂布局支持有限)高级(保留布局信息)
表格提取❌ 不支持✅ 内置强大功能
文本位置/坐标❌ 不支持✅ 支持
PDF操作✅ 合并、拆分、加密、水印❌ 只读,不支持修改
处理速度一般(基于pdfminer)
典型场景文档整理、批量处理、添加水印数据提取、发票解析、报表分析

选择建议

开始之前,请确保安装所需库:

pip install pypdf2 pdfplumber

注意:PyPDF2的后续版本已更名为pypdf,API有所调整。本文基于稳定版PyPDF2编写,代码同样适用于pypdf(需注意类名变更,如PdfFileReader变为PdfReader)。

二、PDF文本提取——让机器人“读懂”PDF

2.1 为什么PDF文本提取有难度

PDF本质上是描述页面外观的指令集合,而不是像Word或HTML那样包含结构化文本。这意味着:

因此,选择正确的工具至关重要。

2.2 使用PyPDF2提取基础文本

PyPDF2提供了基础的文本提取功能,适合简单的、布局规整的PDF。

import PyPDF2

def extract_text_with_pypdf2(pdf_path):
    """使用PyPDF2提取PDF文本"""
    with open(pdf_path, 'rb') as file:
        # 创建PDF阅读器对象
        reader = PyPDF2.PdfReader(file)
        
        # 获取总页数
        num_pages = len(reader.pages)
        print(f"总页数: {num_pages}")
        
        # 提取每一页文本
        full_text = ""
        for page_num in range(num_pages):
            page = reader.pages[page_num]
            text = page.extract_text()
            full_text += f"\n--- 第 {page_num + 1} 页 ---\n{text}"
        
        return full_text

# 使用示例
text = extract_text_with_pypdf2("report.pdf")
print(text[:500])  # 打印前500个字符

局限性:PyPDF2的文本提取基于PDF内部的文本绘制指令,对于复杂排版(如多列、表格、页眉页脚 交错),提取结果可能出现乱序或丢失。

2.3 使用pdfplumber高质量提取文本

pdfplumber基于pdfminer.six构建,但提供了更友好的API和更好的布局分析能力。

import pdfplumber

def extract_text_with_pdfplumber(pdf_path):
    """使用pdfplumber提取PDF文本,保留布局"""
    full_text = ""
    with pdfplumber.open(pdf_path) as pdf:
        print(f"总页数: {len(pdf.pages)}")
        
        for i, page in enumerate(pdf.pages):
            # 提取文本(自动处理布局)
            text = page.extract_text()
            full_text += f"\n--- 第 {i+1} 页 ---\n{text}"
            
            # 可选:提取单词及其位置信息
            words = page.extract_words()
            if words:
                print(f"第{i+1}页包含{len(words)}个单词")
                # 第一个单词的坐标信息示例
                first_word = words[0]
                print(f"首个单词: '{first_word['text']}' 位置: ({first_word['x0']}, {first_word['top']})")
    
    return full_text

# 使用示例
text = extract_text_with_pdfplumber("复杂报表.pdf")

优势:pdfplumber能够更好地理解页面布局,提取的文本顺序更符合人类阅读习惯。

2.4 实战:提取表格数据并结构化

pdfplumber的核心优势在于表格提取。它能够自动识别表格的边界和单元格结构。

import pdfplumber
import pandas as pd

def extract_tables_to_dataframe(pdf_path):
    """提取PDF中的表格并转为DataFrame"""
    all_tables = []
    
    with pdfplumber.open(pdf_path) as pdf:
        for page_num, page in enumerate(pdf.pages):
            # 提取当前页的所有表格
            tables = page.extract_tables()
            
            for table_num, table in enumerate(tables):
                print(f"第{page_num+1}页,表格{table_num+1},共{len(table)}行")
                
                # 转为DataFrame(假设第一行为表头)
                if len(table) > 0:
                    df = pd.DataFrame(table[1:], columns=table[0])
                    all_tables.append({
                        'page': page_num + 1,
                        'table_num': table_num + 1,
                        'dataframe': df
                    })
    
    return all_tables

# 优化表格提取的参数设置
def extract_tables_with_settings(pdf_path):
    """使用自定义参数优化表格提取"""
    results = []
    
    with pdfplumber.open(pdf_path) as pdf:
        for page in pdf.pages:
            # 自定义表格检测策略
            table_settings = {
                "vertical_strategy": "lines",  # 基于线条检测列
                "horizontal_strategy": "lines",  # 基于线条检测行
                "snap_tolerance": 5,  # 线条对齐容差
                "edge_min_length": 3,  # 最小线段长度
            }
            
            # 或者使用"text"策略(当表格没有明显边框时)
            # table_settings = {
            #     "vertical_strategy": "text",
            #     "horizontal_strategy": "text",
            # }
            
            table = page.extract_table(table_settings)
            if table:
                results.append(table)
    
    return results

# 使用示例
tables = extract_tables_to_dataframe("财务报表.pdf")
for item in tables:
    print(item['dataframe'].head())

参数调优技巧

2.5 处理扫描件PDF(OCR集成)

pdfplumber本身不支持OCR,但可以与pytesseract结合处理扫描件:

import pdfplumber
import pytesseract
from PIL import Image

def ocr_scanned_pdf(pdf_path):
    """对扫描件PDF进行OCR识别"""
    text_results = []
    
    with pdfplumber.open(pdf_path) as pdf:
        for i, page in enumerate(pdf.pages):
            # 将PDF页面转为图像
            im = page.to_image(resolution=300)
            
            # 使用pytesseract进行OCR
            text = pytesseract.image_to_string(im.original, lang='chi_sim+eng')  # 中英文
            text_results.append({
                'page': i + 1,
                'text': text
            })
    
    return text_results

三、合并与拆分PDF——文件管理的自动化

3.1 批量合并PDF文件

合并PDF是办公中最常见的需求之一。比如将多个部门的周报合并为一份总报告。

import os
from PyPDF2 import PdfMerger

def merge_pdfs(input_folder, output_filename, pattern=None):
    """
    合并指定文件夹中的所有PDF文件
    
    Args:
        input_folder: 包含PDF文件的文件夹路径
        output_filename: 输出文件名
        pattern: 文件名过滤模式,如"report_"开头的文件
    """
    merger = PdfMerger()
    merged_count = 0
    
    # 获取文件夹下所有PDF文件
    for filename in os.listdir(input_folder):
        if filename.lower().endswith('.pdf'):
            if pattern and pattern not in filename:
                continue
                
        file_path = os.path.join(input_folder, filename)
        print(f"正在添加: {filename}")
        
        try:
            merger.append(file_path)
            merged_count += 1
        except Exception as e:
            print(f"添加文件 {filename} 时出错: {e}")
    
    # 写入合并后的文件
    if merged_count > 0:
        merger.write(output_filename)
        merger.close()
        print(f"合并完成!共合并 {merged_count} 个文件,保存为: {output_filename}")
    else:
        print("未找到可合并的PDF文件")

# 按指定顺序合并(指定文件列表)
def merge_pdfs_by_list(file_list, output_filename):
    """按指定文件列表顺序合并PDF"""
    merger = PdfMerger()
    
    for file_path in file_list:
        if os.path.exists(file_path):
            print(f"添加: {os.path.basename(file_path)}")
            merger.append(file_path)
        else:
            print(f"文件不存在: {file_path}")
    
    merger.write(output_filename)
    merger.close()
    print(f"合并完成!保存为: {output_filename}")

# 使用示例
merge_pdfs('./月度报告', '2025年一季度合并报告.pdf')
merge_pdfs_by_list(['封面.pdf', '目录.pdf', '正文.pdf', '附录.pdf'], '完整报告.pdf')

3.2 拆分PDF文件

有时我们需要从大型PDF中提取特定页面,比如只保留合同的第一页(签字页)。

from PyPDF2 import PdfReader, PdfWriter

def split_pdf_by_pages(input_pdf, output_pattern, pages_to_extract):
    """
    提取指定页面并保存为新文件
    
    Args:
        input_pdf: 输入PDF路径
        output_pattern: 输出文件名模式,如"提取_第{page}页.pdf"
        pages_to_extract: 要提取的页码列表(从1开始)
    """
    reader = PdfReader(input_pdf)
    total_pages = len(reader.pages)
    
    for page_num in pages_to_extract:
        if page_num < 1 or page_num > total_pages:
            print(f"页码 {page_num} 超出范围(1-{total_pages})")
            continue
        
        writer = PdfWriter()
        # 注意:页码从0开始索引
        writer.add_page(reader.pages[page_num - 1])
        
        output_file = output_pattern.format(page=page_num)
        with open(output_file, 'wb') as output_pdf:
            writer.write(output_pdf)
        
        print(f"已提取第 {page_num} 页,保存为: {output_file}")

def split_pdf_every_n_pages(input_pdf, n):
    """每n页拆分成一个文件(如每10页一个文件)"""
    reader = PdfReader(input_pdf)
    total_pages = len(reader.pages)
    
    for start in range(0, total_pages, n):
        end = min(start + n, total_pages)
        writer = PdfWriter()
        
        for page_num in range(start, end):
            writer.add_page(reader.pages[page_num])
        
        output_file = f"拆分_{start+1}-{end}.pdf"
        with open(output_file, 'wb') as output_pdf:
            writer.write(output_pdf)
        
        print(f"已生成: {output_file} (第{start+1}-{end}页)")

# 使用示例
split_pdf_by_pages('年度报告.pdf', '第{page}页_摘要.pdf', [1, 5, 10])
split_pdf_every_n_pages('超大文档.pdf', 20)

3.3 实战:智能拆分工具

结合pdfplumber的文本查找功能,我们可以实现更智能的拆分——根据内容自动切分。

import pdfplumber
from PyPDF2 import PdfReader, PdfWriter

def split_pdf_by_keyword(input_pdf, keyword, output_prefix):
    """
    根据关键字出现的页码拆分PDF
    找到关键字出现的所有页码,每个关键页及其后续页直到下一个关键页之前,作为一个独立文件
    """
    # 第一步:找出所有包含关键字的页码
    keyword_pages = []
    
    with pdfplumber.open(input_pdf) as pdf:
        for i, page in enumerate(pdf.pages):
            text = page.extract_text()
            if keyword in text:
                keyword_pages.append(i)  # 保存0-based索引
                print(f"第{i+1}页包含关键字: {keyword}")
    
    if not keyword_pages:
        print(f"未找到包含关键字'{keyword}'的页面")
        return
    
    # 第二步:根据关键页拆分
    reader = PdfReader(input_pdf)
    total_pages = len(reader.pages)
    
    for idx, start_page in enumerate(keyword_pages):
        writer = PdfWriter()
        
        # 确定结束页:下一个关键页的前一页,或最后一页
        end_page = keyword_pages[idx + 1] - 1 if idx + 1 < len(keyword_pages) else total_pages - 1
        
        # 添加从start_page到end_page的所有页
        for page_num in range(start_page, end_page + 1):
            writer.add_page(reader.pages[page_num])
        
        # 保存文件
        output_file = f"{output_prefix}_第{idx+1}段_页{start_page+1}-{end_page+1}.pdf"
        with open(output_file, 'wb') as output_pdf:
            writer.write(output_pdf)
        
        print(f"已生成: {output_file}")

# 使用示例:按"第一章"、"第二章"拆分书籍
split_pdf_by_keyword('Python教程.pdf', '第', '章节')

四、添加水印与高级操作——让PDF更专业

4.1 添加文字水印(使用reportlab+PyPDF2)

PyPDF2本身不支持创建新内容,因此添加水印的通用方法是:先用reportlab创建水印PDF,再与原文件合并。

from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import letter
from PyPDF2 import PdfReader, PdfWriter

def create_watermark_pdf(watermark_text, output_path, page_size=letter):
    """
    创建水印PDF文件
    """
    c = canvas.Canvas(output_path, pagesize=page_size)
    width, height = page_size
    
    # 设置透明度和旋转
    c.setFillAlpha(0.3)  # 透明度30%
    c.setFont("Helvetica", 60)
    c.setFillColorRGB(0.5, 0.5, 0.5)  # 灰色
    
    # 旋转45度,居中显示
    c.saveState()
    c.translate(width/2, height/2)
    c.rotate(45)
    c.drawCentredString(0, 0, watermark_text)
    c.restore()
    
    c.save()
    print(f"水印文件已创建: {output_path}")

def add_watermark_to_pdf(input_pdf, watermark_pdf, output_pdf):
    """
    为PDF文件添加水印
    """
    # 读取水印PDF
    watermark_reader = PdfReader(watermark_pdf)
    watermark_page = watermark_reader.pages[0]
    
    # 读取原PDF
    reader = PdfReader(input_pdf)
    writer = PdfWriter()
    
    # 为每一页添加水印
    for page_num in range(len(reader.pages)):
        page = reader.pages[page_num]
        page.merge_page(watermark_page)  # 合并水印
        writer.add_page(page)
    
    # 保存结果
    with open(output_pdf, 'wb') as output_file:
        writer.write(output_file)
    
    print(f"水印添加完成!保存为: {output_pdf}")

# 支持中文的水印
def create_chinese_watermark(watermark_text, output_path, font_path='simsun.ttc'):
    """
    创建支持中文的水印PDF
    """
    from reportlab.pdfbase import pdfmetrics
    from reportlab.pdfbase.ttfonts import TTFont
    
    # 注册中文字体
    pdfmetrics.registerFont(TTFont('SimSun', font_path))
    
    c = canvas.Canvas(output_path, pagesize=letter)
    width, height = letter
    
    c.setFillAlpha(0.2)
    c.setFont('SimSun', 50)
    c.setFillColorRGB(0.7, 0.7, 0.7)
    
    # 居中旋转
    c.saveState()
    c.translate(width/2, height/2)
    c.rotate(45)
    c.drawCentredString(0, 0, watermark_text)
    c.restore()
    
    c.save()

# 使用示例
create_watermark_pdf("CONFIDENTIAL", "watermark.pdf")
add_watermark_to_pdf("report.pdf", "watermark.pdf", "report_confidential.pdf")

create_chinese_watermark("绝密", "chinese_watermark.pdf")
add_watermark_to_pdf("合同.pdf", "chinese_watermark.pdf", "合同_带水印.pdf")

4.2 旋转页面

from PyPDF2 import PdfReader, PdfWriter

def rotate_pdf_pages(input_pdf, output_pdf, rotation_degree=90, pages=None):
    """
    旋转PDF页面
    pages: 要旋转的页码列表,None表示所有页
    """
    reader = PdfReader(input_pdf)
    writer = PdfWriter()
    
    for page_num in range(len(reader.pages)):
        page = reader.pages[page_num]
        
        if pages is None or (page_num + 1) in pages:
            page.rotate(rotation_degree)
        
        writer.add_page(page)
    
    with open(output_pdf, 'wb') as output_file:
        writer.write(output_file)
    
    print(f"旋转完成!保存为: {output_pdf}")

# 使用示例:将第2页旋转90度
rotate_pdf_pages("扫描件.pdf", "扫描件_校正.pdf", rotation_degree=90, pages=[2])

4.3 加密与解密PDF

from PyPDF2 import PdfReader, PdfWriter

def encrypt_pdf(input_pdf, output_pdf, user_password, owner_password=None):
    """添加密码保护"""
    reader = PdfReader(input_pdf)
    writer = PdfWriter()
    
    # 复制所有页面
    for page in reader.pages:
        writer.add_page(page)
    
    # 加密
    writer.encrypt(user_password, owner_password)
    
    with open(output_pdf, 'wb') as output_file:
        writer.write(output_file)
    
    print(f"加密完成!保存为: {output_pdf}")

def decrypt_pdf(input_pdf, output_pdf, password):
    """解密PDF(需要知道密码)"""
    reader = PdfReader(input_pdf)
    
    if reader.is_encrypted:
        reader.decrypt(password)
    
    writer = PdfWriter()
    for page in reader.pages:
        writer.add_page(page)
    
    with open(output_pdf, 'wb') as output_file:
        writer.write(output_file)
    
    print(f"解密完成!保存为: {output_pdf}")

# 使用示例
encrypt_pdf("财务报告.pdf", "财务报告_加密.pdf", "123456")
decrypt_pdf("财务报告_加密.pdf", "财务报告_解密.pdf", "123456")

五、实战案例:构建自动化PDF处理流水线

让我们综合三天所学,构建一个完整的PDF处理流程:从多个PDF中提取表格数据,合并分析结果,并添加水印和密码保护。

import os
import pandas as pd
import pdfplumber
from PyPDF2 import PdfMerger, PdfReader, PdfWriter
from reportlab.pdfgen import canvas

class PDFAutomationPipeline:
    """PDF自动化处理流水线"""
    
    def __init__(self, input_folder, output_folder):
        self.input_folder = input_folder
        self.output_folder = output_folder
        os.makedirs(output_folder, exist_ok=True)
    
    def extract_all_tables(self):
        """从所有PDF中提取表格"""
        all_data = []
        
        for filename in os.listdir(self.input_folder):
            if filename.endswith('.pdf'):
                file_path = os.path.join(self.input_folder, filename)
                print(f"处理文件: {filename}")
                
                with pdfplumber.open(file_path) as pdf:
                    for page_num, page in enumerate(pdf.pages):
                        tables = page.extract_tables()
                        for table_num, table in enumerate(tables):
                            # 假设第一行是表头
                            if len(table) > 0:
                                df = pd.DataFrame(table[1:], columns=table[0])
                                all_data.append({
                                    'source': filename,
                                    'page': page_num + 1,
                                    'table': table_num + 1,
                                    'data': df
                                })
        
        return all_data
    
    def create_summary_pdf(self, all_tables, summary_pdf):
        """创建汇总PDF(使用reportlab绘制简单汇总)"""
        from reportlab.lib.pagesizes import A4
        from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table as RLTable
        from reportlab.lib.styles import getSampleStyleSheet
        
        doc = SimpleDocTemplate(summary_pdf, pagesize=A4)
        styles = getSampleStyleSheet()
        story = []
        
        # 添加标题
        story.append(Paragraph("数据汇总报告", styles['Title']))
        story.append(Spacer(1, 12))
        
        # 汇总每个表格的基本信息
        for idx, item in enumerate(all_tables[:5]):  # 只显示前5个表格
            story.append(Paragraph(f"表格 {idx+1}: 来源 {item['source']} 第{item['page']}页", styles['Heading2']))
            
            # 将DataFrame转为列表
            data = item['data'].values.tolist()
            if data:
                # 添加表头
                table_data = [item['data'].columns.tolist()] + data[:5]  # 只显示前5行
                t = RLTable(table_data)
                story.append(t)
                story.append(Spacer(1, 12))
        
        doc.build(story)
        print(f"汇总PDF已创建: {summary_pdf}")
    
    def run_pipeline(self):
        """执行完整流程"""
        print("=== 步骤1: 提取所有表格 ===")
        tables = self.extract_all_tables()
        print(f"共提取 {len(tables)} 个表格")
        
        print("\n=== 步骤2: 生成汇总PDF ===")
        summary_path = os.path.join(self.output_folder, "汇总报告.pdf")
        self.create_summary_pdf(tables, summary_path)
        
        print("\n=== 步骤3: 添加水印 ===")
        # 创建水印
        watermark_path = os.path.join(self.output_folder, "watermark.pdf")
        create_watermark_pdf("内部资料", watermark_path)
        
        # 添加水印
        watermarked_path = os.path.join(self.output_folder, "汇总报告_带水印.pdf")
        add_watermark_to_pdf(summary_path, watermark_path, watermarked_path)
        
        print("\n=== 步骤4: 加密保护 ===")
        final_path = os.path.join(self.output_folder, "最终报告_加密.pdf")
        encrypt_pdf(watermarked_path, final_path, "secure123")
        
        print(f"\n✅ 所有处理完成!最终文件: {final_path}")

# 使用流水线
pipeline = PDFAutomationPipeline("./输入文件夹", "./输出文件夹")
pipeline.run_pipeline()

六、性能对比与最佳实践

6.1 性能对比

根据官方性能测试,不同库在处理大文件时差异显著:

因此,如果追求极致性能,尤其是处理大文件,可以考虑PyMuPDF。但本文聚焦的PyPDF2和pdfplumber在API友好度和特定功能(如表格提取)上仍有不可替代的优势。

6.2 避坑指南

版本兼容性:PyPDF2 3.x版本已弃用PdfFileReader等旧类名,改用PdfReader/PdfWriter。本文代码兼容新旧版本。

内存管理:处理大文件时,及时释放对象:

reader = PdfReader(open('large.pdf', 'rb'))
# 处理完成后
del reader

异常处理:PDF文件可能损坏或加密,添加异常捕获:

try:
    reader = PdfReader(file)
except PyPDF2.errors.PdfReadError as e:
    print(f"PDF读取错误: {e}")

中文支持

表格提取调优:如果默认参数提取效果不佳,尝试调整snap_tolerance和策略。

以上就是Python处理PDF文档的两大功能库(PyPDF2/pdfplumber)的使用指南的详细内容,更多关于Python处理PDF的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文