pdfplumber、pypdf2 常用方法总结

这两天学习了一些处理 PDF 文档的方法,网上查找资料的过程中发现很多处理 PDF 文件的库,多方尝试后推荐两个比较好用的。
若处理对象是 PDF 文档本身,则推荐使用 pypdf2,如对 PDF 文档进行分割, 合并, 插入等操作.
若处理对象是 PDF 文档中的文本,表格等内容,则推荐使用 pdfplumber.

pypdf2

PdfFileMerger。

该类用来合并 pdf 文件,该类的构造方法有一个参数:PdfFileMerger(strict=True),注意这里的参数后面会介绍:

常用方法:

addBookmark(title, pagenum, parent=None):给 pdf 添加一个书签,title 是书签的标题,pagenum 是该书签指向的页面。

append(fileobj, bookmark=None, pages=None, import_bookmarks=True):将指定的 fileobj 文件添加到文件的末尾,bookmark 是赎前,pages 可以使用 (start, stop[, step]) 或者一个 Page Range 来设定将 fileobj 中的指定范围的页面进行添加。

merge(position, fileobj, bookmark=None, pages=None, import_bookmarks=True):与 append 方法类似,不过可以使用 position 参数指定添加的位置。

write(fileobj):将数据写入到文件中。

使用的时候可以创建一个 PdfFileMerger 实例,然后使用 append 或者 merge 将想要融合的 pdf 文件依次添加进去,最后使用 write 保存即可。

def merge_pdf():
    # 创建一个用来合并文件的实例
    pdf_merger = PdfFileMerger()

    # 首先添加一个Week1_1.pdf文件
    pdf_merger.append('Week1_1.pdf')
    # 然后在第0页后面添加ex1.pdf文件
    pdf_merger.merge(0, 'ex1.pdf')
    # 添加书签
    pdf_merger.addBookmark('这是一个书签', 1)
    # 将其写入到文件中
    pdf_merger.write('merge_pdf.pdf')

下面看一下 PdfFileMerger(strict=True) 中的这个参数:

官方对这个参数的解释:

strict (bool) – Determines whether user should be warned of all problems and also causes some correctable problems to be fatal. Defaults to True.

确定是否应该警告用户所有问题,并且还会导致一些可纠正的问题。

刚开始感觉这个参数就是用来是否警告用户一些错误的,直接使用默认即可,但是当本人尝试合并带中文的 pdf 时,出现了如下错误:

Traceback (most recent call last):
  File "I:\python3.6\lib\site-packages\PyPDF2\generic.py", line 484, in readFromStream
    return NameObject(name.decode('utf-8'))
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc8 in position 10: invalid continuation byte

During handling of the above exception, another exception occurred:

PyPDF2.utils.PdfReadError: Illegal character in Name Object

在源码包中使用 utf 解码的时候出错了,尝试修改此处源码,让其使用 gbk,但是还出现了其他的错误。最后发现当把构造函数中的 strict 设置为 False 时,控制台会打印下面的错误:

PdfReadWarning: Illegal character in Name Object [generic.py:489]

但是两个文件成功的合并了,并且大概看了下合并后的文件有时好又是坏,同样的代码运行多次,有时候能够正常处理中文,但有时候中文乱码。

除了列出的方法还有一些其他的方法,比如添加书签、添加链接等等,可以参考官方文档。

PdfFileReader。

该类主要提供了对 pdf 文件的读操作,其构造方法为:

PdfFileReader(stream, strict=True, warndest=None, overwriteWarnings=True)

第一个参数可以传入一个文件流,或者一个文件路径。后面三个参数都是用来设置警告的处理方式,直接使用默认的即可。

得到实例之后,就可以对 pdf 进行一些操作了。主要的有以下几个操作:

  • decrypt(password):如果 pdf 文件加密的话,可以使用该方法对其解密。

  • getDocumentInfo():检索 pdf 文件的一些信息。其返回值为一个 DocumentInformation 类型,直接输出的话会得到类似下面的信息:

{'/Author':'Markus Richter', '/Creator': 'Microsoft® Word 2019', '/CreationDate':"D:20190819160317+08'00'", '/ModDate': "D:20190819160317+08'00'",'/Producer': 'Microsoft® Word 2019'}

getNumPages():这个获取 pdf 文件中的页数。

getPage(pageNumber):会得到 pdf 文件中对应的 pageNumber 页数的页面对象,返回值为 PageObject 实例。在得到 PageObject 实例之后就可以将其加添、插入等操作。

  • getPageNumber(page):与上面的方法对立,可以传入 PageObject 实例,然后得到该实例是 pdf 文件中第几页的。

  • getOutlines(node=None, outlines=None):检索文档中出现的文档大纲。

  • isEncrypted:记录该 pdf 是否加密。如果文件本身加密,即使在使用解密 decrypt 方法之后,还是会返回 true。

  • numPages:pdf 总共的页数,相当于访问 getNumPages() 的只读属性。

PdfFileWriter 。

该类支持对 pdf 文件进行写操作,通常是使用 PdfFileReader 读取一些 pdf 数据,然后使用该类进行一些操作。

创建该类的实例时不需要参数。

其主要的方法有:

  • addAttachment(fname, fdata):向 pdf 添加文件。

  • addBlankPage(width=None, height=None):给 pdf 添加一个空白页到最后,如果没有指定大小就使用当前 Weiter 中 pdf 最后一页的大小。

  • addPage(page):添加 page 到 pdf 中,通常这个 page 是由上面的 Reader 获取的。

  • appendPagesFromReader(reader, after_page_append=None):将 reader 中的数据拷贝到当前的 Writer 实例中,并且如果指定 after_page_append 的话,最后还有回掉该函数并且将 writer 中的数据传入其中。

  • encrypt(user_pwd, owner_pwd=None, use_128bit=True):将 pdf 进行加密,其中官方说 userpwd 是允许用户使用一些限制的权限打开 pdf 文件,也就是使用该密码的话可能会有一些限制,但是本人并没有在文档中找到设置权限的内容。而 ownerpwd 则是允许用户无限制的使用。第三个参数是是否使用 128 位加密。

  • getNumPages():得到 pdf 页数。

  • getPage(pageNumber):得到对应页数的 Page,是一个 PageObject 对象,可以使用上面的 addPage 方法将 page 进行添加。

  • insertPage(page, index=0):将 page 添加到 pdf 中,index 指定的是被插入的位置。

  • write(stream):将该 Writer 中的内容写入到文件中。

汇总代码:

from PyPDF2 import PdfFileReader, PdfFileMerger, PdfFileWriter


def get_reader(filename, password):
    try:
        old_file = open(filename, 'rb')
    except IOError as err:
        print('文件打开失败!' + str(err))
        return None

    # 创建读实例
    pdf_reader = PdfFileReader(old_file, strict=False)

    # 解密操作
    if pdf_reader.isEncrypted:
        if password is None:
            print('%s文件被加密,需要密码!' % filename)
            return None
        else:
            if pdf_reader.decrypt(password) != 1:
                print('%s密码不正确!' % filename)
                return None
    if old_file in locals():
        old_file.close()
    return pdf_reader


def encrypt_pdf(filename, new_password, old_password=None, encrypted_filename=None):
    """
    对filename所对应的文件进行加密,并生成一个新的文件
    :param filename: 文件对应的路径
    :param new_password: 对文件加密使用的密码
    :param old_password: 如果旧文件进行了加密,需要密码
    :param encrypted_filename: 加密之后的文件名,省却时使用filename_encrypted;
    :return:
    """
    # 创建一个Reader实例
    pdf_reader = get_reader(filename, old_password)

    if pdf_reader is None:
        return

    # 创建一个写操作的实例
    pdf_writer = PdfFileWriter()
    # 从之前Reader中将数据写入到Writer中
    pdf_writer.appendPagesFromReader(pdf_reader)

    # 重新使用新密码加密
    pdf_writer.encrypt(new_password)

    if encrypted_filename is None:
        # 使用旧文件名 + encrypted 作为新的文件名
        encrypted_filename = "".join(filename.split('.')[:-1]) + '_' + 'encrypted' + '.pdf'

    pdf_writer.write(open(encrypted_filename, 'wb'))


def decrypt_pdf(filename, password, decrypted_filename=None):
    """
    将加密的文件及逆行解密,并生成一个无需密码pdf文件
    :param filename: 原先加密的pdf文件
    :param password: 对应的密码
    :param decrypted_filename: 解密之后的文件名
    :return:
    """

    # 生成一个Reader和Writer
    pdf_reader = get_reader(filename, password)
    if pdf_reader is None:
        return
    if not pdf_reader.isEncrypted:
        print('文件没有被加密,无需操作!')
        return
    pdf_writer = PdfFileWriter()

    pdf_writer.appendPagesFromReader(pdf_reader)

    if decrypted_filename is None:
        decrypted_filename = "".join(filename.split('.')[:-1]) + '_' + 'decrypted' + '.pdf'

    # 写入新文件
    pdf_writer.write(open(decrypted_filename, 'wb'))


def split_by_pages(filename, pages, password=None):
    """
    将文件按照页数进行平均分割
    :param filename: 所要分割的文件名
    :param pages: 分割之后每个文件对应的页数
    :param password: 如果文件加密,需要进行解密操作
    :return:
    """
    # 得到Reader
    pdf_reader = get_reader(filename, password)
    if pdf_reader is None:
        return
    # 得到总的页数
    pages_nums = pdf_reader.numPages

    if pages <= 1:
        print('每份文件必须大于1页!')
        return

    # 得到切分之后每个pdf文件的页数
    pdf_num = pages_nums // pages + 1 if pages_nums % pages else int(pages_nums / pages)

    print('pdf文件被分为%d份,每份有%d页!' % (pdf_num, pages))

    # 依次生成pdf文件
    for cur_pdf_num in range(1, pdf_num + 1):
        # 创建一个新的写实例
        pdf_writer = PdfFileWriter()
        # 生成对应的文件名称
        split_pdf_name = "".join(filename)[:-1] + '_' + str(cur_pdf_num) + '.pdf'
        # 计算出当前开始的位置
        start = pages * (cur_pdf_num - 1)
        # 计算出结束的位置,如果是最后一份就直接返回最后的页数,否则用每份页数*已经分好的文件数
        end = pages * cur_pdf_num if cur_pdf_num != pdf_num else pages_nums
        # print(str(start) + ',' + str(end))
        # 依次读取对应的页数
        for i in range(start, end):
            pdf_writer.addPage(pdf_reader.getPage(i))
        # 写入文件
        pdf_writer.write(open(split_pdf_name, 'wb'))


def split_by_num(filename, nums, password=None):
    """
    将pdf文件分为nums份
    :param filename: 文件名
    :param nums: 要分成的份数
    :param password: 如果需要解密,输入密码
    :return:
    """
    pdf_reader = get_reader(filename, password)
    if not pdf_reader:
        return

    if nums < 2:
        print('份数不能小于2!')
        return

    # 得到pdf的总页数
    pages = pdf_reader.numPages

    if pages < nums:
        print('份数不应该大于pdf总页数!')
        return

    # 计算每份应该有多少页
    each_pdf = pages // nums

    print('pdf共有%d页,分为%d份,每份有%d页!' % (pages, nums, each_pdf))

    for num in range(1, nums + 1):
        pdf_writer = PdfFileWriter()
        # 生成对应的文件名称
        split_pdf_name = "".join(filename)[:-1] + '_' + str(num) + '.pdf'
        # 计算出当前开始的位置
        start = each_pdf * (num - 1)
        # 计算出结束的位置,如果是最后一份就直接返回最后的页数,否则用每份页数*已经分好的文件数
        end = each_pdf * num if num != nums else pages
        print(str(start) + ',' + str(end))
        for i in range(start, end):
            pdf_writer.addPage(pdf_reader.getPage(i))
        pdf_writer.write(open(split_pdf_name, 'wb'))


def merger_pdf(filenames, merged_name, passwords=None):
    """
    传进来一个文件列表,将其依次融合起来
    :param filenames: 文件列表
    :param passwords: 对应的密码列表
    :return:
    """
    # 计算共有多少文件
    filenums = len(filenames)
    # 注意需要使用False 参数
    pdf_merger = PdfFileMerger(False)

    for i in range(filenums):
        # 得到密码
        if passwords is None:
            password = None
        else:
            password = passwords[i]
        pdf_reader = get_reader(filenames[i], password)
        if not pdf_reader:
            return
        # append默认添加到最后
        pdf_merger.append(pdf_reader)

    pdf_merger.write(open(merged_name, 'wb'))


def insert_pdf(pdf1, pdf2, insert_num, merged_name, password1=None, password2=None):
    """
    将pdf2全部文件插入到pdf1中第insert_num页
    :param pdf1: pdf1文件名称
    :param pdf2: pdf2文件名称
    :param insert_num: 插入的页数
    :param merged_name: 融合后的文件名称
    :param password1: pdf1对应的密码
    :param password2: pdf2对应的密码
    :return:
    """
    pdf1_reader = get_reader(pdf1, password1)
    pdf2_reader = get_reader(pdf2, password2)

    # 如果有一个打不开就返回
    if not pdf1_reader or not pdf2_reader:
        return
    # 得到pdf1的总页数
    pdf1_pages = pdf1_reader.numPages
    if insert_num < 0 or insert_num > pdf1_pages:
        print('插入位置异常,想要插入的页数为:%d,pdf1文件共有:%d页!' % (insert_num, pdf1_pages))
        return
    # 注意需要使用False参数,可能会出现中文乱码的情况
    m_pdf = PdfFileMerger(False)
    m_pdf.append(pdf1)
    m_pdf.merge(insert_num, pdf2)
    m_pdf.write(open(merged_name, 'wb'))


if __name__ == '__main__':
     encrypt_pdf('ex1.pdf', 'leafage')
    decrypt_pdf('ex1123_encrypted.pdf', 'leafage')
    split_by_pages('ex1.pdf', 5)
    split_by_num('ex2.pdf', 3)
    merger_pdf(['ex1.pdf', 'ex2.pdf'], 'merger.pdf')
    insert_pdf('ex1.pdf', 'ex2.pdf', 10, 'pdf12.pdf')

pdfplumber

本库最重要的应用是提取页面上的文本和表格,用法如下:

import pdfplumber
import pandas as pd

with pdfplumber.open("path/to/file.pdf") as pdf:
    first_page = pdf.pages[0]
    # 获取文本,直接得到字符串,包括了换行符【与PDF上的换行位置一致,而不是实际的“段落”】
    print(first_page.extract_texts()) 
    # 获取本页全部表格,也可以使用extract_table()获得单个表格
    for table in p0.extract_tables(): 
        #得到的table是嵌套list类型,转化成DataFrame更加方便查看和分析 
        df = pd.DataFrame(table[1:], columns=table[0]) 
        print(df)

pdfplumber 还可以获得页面上的所有单词、直线、方格、乃至曲线的位置信息,具体可以看看官网的说明:https://github.com/jsvine/pdfplumber