通过Python pypdf库轻松拆分大型PDF文件

pypdf的历史

pypdf最早可以追溯到2005年开源发布,最早名称是"pyPdf",中间的P是大写的,是一个纯python库,这个库一直持续到2010年的pyPdf1.13最后一个版本!

开源其实是一件非常吃力不讨好的事情,在没有商业化的手段,以及没有额外费用的支持下,很难一直靠爱发电。

2011到2016年之间,在此基础上又诞生了一个PyPDF2的分支,这个分支其实是真正走到大众面前的一个库,在很多优秀的python书籍中都能看到该库的身影。PyPDF2从2016年沉寂了几年后,2022年又被一个开发者接管并维护,并且增加了一些功能。

2018到2022年间又围绕PyPDF2陆续诞生了PyPDF3 和 PyPDF4 ,但相对PyPDF2几乎很少有人使用,自然也就没有什么新的发展,岁月的车轮终究碾碎了单纯的开发者!

好在开源的力量是无穷的,正所谓,天下大势,合久必分,分久必合,2023年pypdf回归本源,PyPDF2 被合并回 pypdf,现在的名称全部为小写,成为没有数字的pypdf!

最后,希望我们能看到PyPDF3 和 PyPDF4 的开发者也能加入到社区中,让pypdf这个库能有更好的发展和未来。

最后让我们一起,致敬开源,感恩开源,向优秀的开源开发者学习!

pypdf的安装

pypdf是一个纯python库,安装使用非常简单,只需要使用pip安装即可!

pip install pypdf

pypdf的应用案例

拆分pdf文档思路与分析

拆分一个多页的pdf文档有两种拆分思路:

  1. 按每个拆分的pdf包含多少页自动拆分

​ 这个拆分思路其实是规定了每个将要拆分的小pdf文件由多少页组成的一个方式,很好理解。

​ 计算公式:pdf总页数 / 每个pdf的页数 = 拆分的份数

  1. 按份数拆分

​ 这种就是直接指定将一个pdf拆分成多少份

​ 计算公式:pdf总页数 / 拆分的份数 = 每个pdf的页数

其实,看完这个计算公式之后我们就能发现,其实他们解决的是同一个问题,就是一个简单的除法运算!

这里我们用一个简单的例子来演示一下:

假如,我们现在要拆分一个11页的pdf文件,按照第一种思路按页数拆分,每个小的pdf文件由三页组成!

套用公式就是: 11 / 3 ≈ 3.67,发现没有这个是除不尽的,所以就存在能除尽和除不尽两种现象!

现在我们对应到python的数据格式,其实就可以用一个二维数组来表示~!

# 页码我们从零开始
[[0,1,2],[3,4,5],[6,7,8],[9,10]]

看看这个数据结构,二维数组中的小数组就代表了一个拆分好的pdf文件,如果除不尽拆分分数就要向上进,这里的份数就变成4。

如果非要拆分成3个,则可以选择将最后一个数组与倒数第二个数组合并!

[[0,1,2],[3,4,5],[6,7,8,9,10]]

分析到这里,我们其实已经解决了拆分的所有的问题,无论是按份数拆分还是按页数拆分,最终我们都需要这样的一个数据结构!

所以我们就可以很好的用一个简单的列表推导式结合range方法的一个步长的概念来生成这样的一个数据结构!

[list(range(11))[i:i+3] for i in range(0, 11, 3)]

这个列表推导式非常的简单,只要你对python列表的切片用法以及步长概念理解到位其实就没有任何难度,剩下的就是编程思维的一个问题!

正式拆分

import pathlib
from pypdf import PdfReader, PdfWriter

# 1.实现拆分数据结构的函数
def pdf_split_page_num(pdf_reader:PdfReader, page_num:int):
    """
    pdf_reader是PdfReader类的实例化对象
    page_num是一个整数,即每个pdf包含的页数,也就是数据结构中range的步长
    """
    # pdf所有页数列表,list类型
    pages = pdf_reader.pages
    # 拆分数据结构
    split_pages = [
        list(pages)[i:i+page_num]
        for i in range(0, len(pages), page_num)
    ]
    return split_pages


# 合并列表函数
def merage_list(pdf_list:pdf_split_page_num):
    """
    合并pdf_list 倒数第二和倒数第一的列表
    """
    _pdf_list = pdf_list[:] # copy一份数据
    _pdf_list[-2] = _pdf_list[-2] + _pdf_list[-1] # 修改倒数第二组数据
    del _pdf_list[-1] # 删除倒数第一组数据
    return _pdf_list


# 2. 按份数拆分,即拆分成多少份
def pdf_split_num(pdf_reader:PdfReader, pdf_num:int, is_merage=True):
    """
    pdf_reader是PdfReader类的实例化对象
    pdf_num是拆分的份数,整数
    is_merage布尔值,除不尽的情况下是否合并最后两个数组,默认合并
    """
    # pdf所有页数列表,list类型
    pages = pdf_reader.pages
    # 每个pdf由多少页组成,取整数部分
    pdf_page_nums = len(pages) // pdf_num

    # 这里就要判断是否合并的问题
    if is_merage:
        pdf_list = merage_list(pdf_split_page_num(pdf_reader, pdf_page_nums))
    else:
        pdf_list = pdf_split_page_num(pdf_reader, pdf_page_nums)
    return pdf_list


# 3. 按页数拆分,即自动计算拆分份数
def pdf_split_page(pdf_reader:PdfReader, page_num:int, is_merage=False):
    """
    pdf_reader是PdfReader类的实例化对象
    page_num是一个整数,即每个pdf包含的页数
    is_merage是否合并,默认不合并
    """
    pdf_list = pdf_split_page_num(pdf_reader, page_num)
    if is_merage:
        pdf_list = merage_list(pdf_list)
    return pdf_list


# 以上部分只是实现了数据结构的拆分并没有真正的去拆分保存写入文件
# 4. 保存函数
def save(pdf_list:list, wirte_path:pathlib.Path):
    """保存

    Args:
        pdf_list (list): pdf_split_num 或 pdf_split_page函数的结果
        wirte_path (pathlib.Path): 拆分后保存的路径
    """

    # 如果拆分后保存的路径不存在则自动创建
    if not wirte_path.exists():
        wirte_path.mkdir(parents=True, exist_ok=True)

    for index, page_objs in enumerate(pdf_list):  
        # 创建空白pdf
        pdf_wirte = PdfWriter()
        # 遍历内部数组结构
        for page in page_objs:
            # 逐页添加进去
            pdf_wirte.add_page(page)
        # 写入
        with open(f"{wirte_path}/{index}.pdf", 'wb') as f:
            pdf_wirte.write(f)

# 最终的使用
if __name__ == "__main__":
    reader_pdf = PdfReader("merage_pdf/merge.pdf")
    wirte_path = pathlib.Path("splitpdf")
    pdf_list = pdf_split_num(reader_pdf, 2, True)
    # pdf_list = pdf_split_page(reader_pdf, 5, False)
    save(pdf_list, wirte_path)

这个只是一个pypdf使用的一个小小的案例,非常适合python初学者去练手,在学习完基础的数据操作之后就需要寻找生活中遇到的实际问题一个一个的去解决,去思考才能使得编程具有价值!

2024年3月11日优化更新

import pathlib
from pypdf import PdfReader, PdfWriter


def pdf_split_page_num(pdf_reader: PdfReader, page_num: int):
    """ 按照页数拆分pdf文档
    :param pdf_reader: PdfReader
    :param page_num: int
    :return: list
    """
    pages = pdf_reader.pages  #list
    return [
        list(pages)[i: i+page_num] 
        for i in range(0, len(pages), page_num)
    ]


# 按份数拆分pdf文档
def pdf_split_num(pdf_reader: PdfReader, pdf_num: int):
    """_summary_

    Args:
        pdf_reader (PdfReader): _description_
        pdf_num (int): _description_
    """

    pages = pdf_reader.pages

    if len(pages) < pdf_num:
        raise ValueError("pdf文件页数小于指定份数")

    # 计算每一个pdf文件里边包含多少个页面
    page_num = len(pages) // pdf_num + 1 if len(pages) % pdf_num else len(pages) / pdf_num
    return pdf_split_page_num(pdf_reader, page_num)


# 按页数拆分
def pdf_split_page(pdf_reader: PdfReader, page_num:int):
    """_summary_

    Args:
        pdf_reader (PdfReader): _description_
        page_num (int): _description_
    """
    # pages = pdf_reader.pages

    return pdf_split_page_num(pdf_reader, page_num)


def save(pdf_list:list, save_path:pathlib.Path):
    # 先创建文件夹
    if not save_path.exists():
        save_path.mkdir(parents=True, exist_ok=True)

    for i, pdf in enumerate(pdf_list):
        # print(i, pdf)
        # 创建pdf文件
        pdf_writer = PdfWriter()
        for page in pdf:
            pdf_writer.add_page(page)

        with open(f"{save_path / f"pdf_{i}.pdf"}", "wb") as f:
            pdf_writer.write(f)


if __name__ == "__main__":

    pdf_reader = PdfReader("merage_img_all.pdf")
    save_path = pathlib.Path("split_pdf")
    pdf_list = pdf_split_num(pdf_reader, 3)

    save(pdf_list, save_path)