目录



>>> from helper import info; info()
页面更新时间: 2020-07-05 07:29:07
操作系统/OS: Linux-4.19.0-9-amd64-x86_64-with-debian-10.4 ;Python: 3.7.3

5.1. 处理 PDF 文档

PDF和Word文档是二进制文件,所以它们比纯文本文件 要复杂得多。除了文本之外,它们还保存了许多字体、 颜色和布局信息。如果希望程序能读取或写入 PDF 和 Word 文档,需要做的就不只是将它们的文件名 传递给 open()

好在,有一些Python模块。使得处理PDF和Word文档变 得容易。本章将介绍两个这样的模块。

PDF文档

PDF表示Portable Document Format ,使用.pdf文件扩展名。 虽然PDF支持许多功能,但本章将专注于最常做的两件事: 从PDF读取文本内容和从已有的文档生成新的PDF。

用于处理PDF的模块是 PyPDF2 。要安装它, 就从命令行运行 pip install PyPDF2 。这个模块 名称是区分大小写的,所以要确保 y 是小写,其他 字母都是大写(请查看附录A,了解安装第三方模块的 所有细节)。如果该模块安装正确,在交互式环境中 运行 import PyPDF2 ,应该不会显示任何错误。

从 PDF提取文本

PyPDF2没有办法从PDF文档中提取图像、图表或其他 媒体,但它可以提取文本,并将文本返回为Python字符串。 为了开始学习 PyPDF2 的工作原理,我们将它 用于一个示例PDF,如图13-1所示。

image0

图13-1 PDF页面,我们将从中提取文本

有问题的PDF格式

虽然PDF文件对文本布局非常好,让人们很容易打印并阅读,但软件要将它们解析为纯文本却并不容易。 因此, PyPDF2 从PDF提取文本时可能会出错,甚至根本不能打开某些PDF。 遗憾的是,你对此没有什么办法, PyPDF2 可能就是不能处理某些PDF文件。 话虽这样说,我至今没有发现不能用 PyPDF2 打开的PDF文件。

http://nostarch.com/automatestuff/ 下载这个PDF文件,并在交互式环境中输入以下代码:

>>> import PyPDF2
>>> pdfFileObj = open('meetingminutes.pdf', 'rb')
>>> pdfReader = PyPDF2.PdfFileReader(pdfFileObj)
>>> pdfReader.numPages
19
>>> pageObj = pdfReader.getPage(0)
>>> pageObj.extractText()
'OOFFFFIICCIIAALL  BBOOAARRDD  MMIINNUUTTEESS   Meeting of nMarch 7n, 2014n        n     The Board of Elementary and Secondary Education shall provide leadership and ncreate policies for education that expand opportunities for children, empower nfamilies and communities, and advance Louisiana in an increasingly ncompetitive globnal market.n BOARD n of ELEMENTARYn and n SECONDARYn EDUCATIONn  '

首先,导入 PyPDF2 模块。然后以读二进制模式打开 meetingminutes.pdf ,并将它保存在 pdfFileObj 中。 为了取得表示这个PDF的 PdfFileReader 对象,调用 PyPDF2.PdfFileReader() 并向它传入 pdfFileObj 。 将这个 PdfFileReader 对象保存在 pdfReader 中。

该文档的总页数保存在 PdfFileReader 对象的 numPages 属性中。示例PDF文档有19页,但我们 只提取第一页的文本。

要从一页中提取文本,需要通过 PdfFileReader 对象 取得一个 Page 对象,它表示PDF中的一页。可以调用 PdfFileReader 对象的 getPage() 方法,向它 传入感兴趣的页码(在我们的例子中是0),从而取得 Page 对象。

PyPDF2 在取得页面时使用从0开始的下标:第一页是0页, 第二页是1页,以此类推。事情总是这样,即使文档中页面的 页码不同。例如,假定你的PDF是从一个较长的报告中抽取出 3页,它的页码分别是42、43和44,要取得这个文档的第一 页,需要调用 pdfReader.getPage(O) ,而不是 getPage(42)getPage(l)

在取得 Page 对象后,调用它的 extractText() 方法, 返回该页文本的字符串。文本提取并不完美: 该PDF中的文本 Charles E.“Chas”Roemer,President,在函数返 回的字符串中消失了,而且空格有时候也会没有。但是,这种近似 的PDF文本内容,可能对你的程序来说已经足够了。

解密PDF

某些PDF文档有加密功能,以防止别人阅读,只有在打开文档时 提供口令才能阅读。在交互式环境中输入以下代码,处理下载的 PDF,它已经用口令 rosebud 加密:

>>> import PyPDF2
>>> pdfReader =PyPDF2.PdfFileReader(open('watermark.pdf', 'rb'))
>>> # pdf Reader.isEncrypted
>>> pdfReader.getPage(0)
{'/Type': '/Page',
 '/Parent': {'/Type': '/Pages', '/Count': 1, '/Kids': [IndirectObject(3, 0)]},
 '/Resources': {'/Font': {'/F1': {'/Type': '/Font',
    '/Subtype': '/TrueType',
    '/Name': '/F1',
    '/BaseFont': '/ABCDEE+Calibri',
    '/Encoding': '/WinAnsiEncoding',
    '/FontDescriptor': {'/Type': '/FontDescriptor',
     '/FontName': '/ABCDEE+Calibri',
     '/Flags': 32,
     '/ItalicAngle': 0,
     '/Ascent': 750,
     '/Descent': -250,
     '/CapHeight': 750,
     '/AvgWidth': 521,
     '/MaxWidth': 1743,
     '/FontWeight': 400,
     '/XHeight': 250,
     '/StemV': 52,
     '/FontBBox': [-503, -250, 1240, 750],
     '/FontFile2': {'/Filter': '/FlateDecode', '/Length1': 168868}},
    '/FirstChar': 32,
    '/LastChar': 32,
    '/Widths': [226]}},
  '/ExtGState': {'/GS7': {'/Type': '/ExtGState', '/BM': '/Normal', '/CA': 1},
   '/GS8': {'/Type': '/ExtGState', '/BM': '/Normal', '/CA': 0.50196},
   '/GS11': {'/Type': '/ExtGState', '/BM': '/Normal', '/ca': 0.49804}},
  '/XObject': {'/Image9': {'/Type': '/XObject',
    '/Subtype': '/Image',
    '/Width': 1237,
    '/Height': 304,
    '/ColorSpace': '/DeviceRGB',
    '/BitsPerComponent': 8,
    '/Interpolate': <PyPDF2.generic.BooleanObject at 0x7fb8c1442cf8>,
    '/SMask': {'/Type': '/XObject',
     '/Subtype': '/Image',
     '/Width': 1237,
     '/Height': 304,
     '/ColorSpace': '/DeviceGray',
     '/Matte': [0, 0, 0],
     '/BitsPerComponent': 8,
     '/Interpolate': <PyPDF2.generic.BooleanObject at 0x7fb8c14475f8>,
     '/Filter': '/FlateDecode'},
    '/Filter': '/FlateDecode'}},
  '/ProcSet': ['/PDF', '/Text', '/ImageB', '/ImageC', '/ImageI']},
 '/MediaBox': [0, 0, 612, 792],
 '/Contents': {'/Filter': '/FlateDecode'},
 '/Group': {'/Type': '/Group', '/S': '/Transparency', '/CS': '/DeviceRGB'},
 '/Tabs': '/S',
 '/StructParents': 0}
>>> # pdfReader.decrypt('rosebud')
>>> pageObj = pdfReader.getPage(0)

所有 PdfFileReader 对象都有一个 isEncrypted 属性, 如果PDF是加密的,它就是 True ,如果不是,它就是 False 。在文件用正确的口令解密之前,尝试调用函数来 读取文件,将会导致错误。

要读取加密的PDF,就调用 decrypt() 函数,传入口令字 符串。在用正确的口令调用 decrypt() 后,你会看到调用 getPage() 不再导致错误。如果提供了错误的口令, decrypt() 函数将返回0,并且 getPage() 会继续 失败。请注意, decrypt() 方法只解密了PdfFileReader 对象, 而不是实际的PDF文件。在程序中止后,硬盘上的文件仍然是 加密的。程序下次运行时,仍然需要再次调用 decrypt()

创建PDF

PyPDF2 中,与 PdfFileReader 对象相对的是 PdfFileWriter 对象,它可以创建一个新的PDF文件。 但 PyPDF2 不能将任意文本写入PDF,就像 Python 可以写入纯文本文件那样。 PyPDF2 写入PDF的能力, 仅限于从其他PDF中拷贝页面、旋转页面、重叠页面和加密 文件。

模块不允许直接编辑PDF。必须创建一个新的PDF,然后从已有 的文档拷贝内容。本节的例子将遵循这种一般方式:

  1. 打开一个或多个已有的PDF(源PDF),得到 PdfFileReader对象。

  • 创建一个新的 PdfFileWriter 对象。

  • 将页面从 PdfFileReader 对象拷贝到 PdfFileWriter 对象中。

  • 最后,利用 PdfFileWriter 对象写入输出的PDF。

创建一个PdfFileWriter 对象,只是在Python中创建了 一个代表PDF文档的值,这并没有创建实际的PDF文件, 要实际生成文件,必须调用 PdfFileWriter 对象的 write() 方法。

write() 方法接受一个普通的 File 对象,它以写 二进制的模式打开。你可以用两个参数调用 Python 的 open() 函数,得到这样的 File 对象:一个是要 打开的PDF文件名字符串,一个是 'wb',表明文件应该 以写二进制的模式打开。

如果这听起来有些令人困惑,不用担心,在接下来的代码 示例中,你会看到这种工作方式。

拷贝页面

可以利用 PyPDF2 ,从一个PDF文档拷贝页面到另一个PDF 文档。这让你能够组合多个PDF文件,去除不想要的页面,或 调整页面的次序。

从http://nostarch.com/automatestuff 下载 meetingminutes.pdf和meetingminutes2.pdf, 放在当前工作目录中。在交互式环境中输入以下代码:

>>> import PyPDF2
>>> pdf1File = open('meetingminutes.pdf', 'rb')
>>> pdf2File = open('meetingminutes2.pdf', 'rb')
>>> pdf1Reader = PyPDF2.PdfFileReader(pdf1File)
>>> pdf2Reader = PyPDF2.PdfFileReader(pdf2File)
>>> pdfWriter = PyPDF2.PdfFileWriter()
>>>
>>> for pageNum in range(pdf1Reader.numPages):
>>>     pageObj = pdf1Reader.getPage(pageNum)
>>>     pdfWriter.addPage(pageObj)
>>> for pageNum in range(pdf2Reader.numPages):
>>>     pageObj = pdf2Reader.getPage(pageNum)
>>>     pdfWriter.addPage(pageObj)
>>>
>>> pdfOutputFile = open('combinedminutes.pdf', 'wb')
>>> pdfWriter.write(pdfOutputFile)
>>> pdfOutputFile.close()
>>> pdf1File.close()
>>> pdf2File.close()

以读二进制的模式打开两个PDF文件,将得到的两个: File 对象保存在 pdf1Filepdf2File中。 调用 PyPDF2.PdfFiIeReader() ,传入 pdf1File , 得到一个表示 meetingminutes.pdfPdfFileReader 对象。再次调用 PyPDF2.PdfFileReader() ,传入 pdf2File, 得到一个表示 meetingminutes2.pdfPdfFileReader 对象。然后创建一个新的 PdfFileWriter 对象,它表示一个空白的PDF文档。

接下来,从两个源PDF拷贝所有的页面,将它们添加到 PdfFileWriter 对象。在 PdfFileReader 对象 上调用 getPage() ,取得 Page 对象。然后 将这个 Page 对象传递给 PdfFileWriteraddPage() 方法。这些步骤先是针对 pdf1Reader 进行, 然后再针对 pdf2Reader 进行。在拷贝页面完成后, 向 PdfFileWriterwrite() 方法传入一个 File 对象,写入一个新的PDF文档,名为 combinedminutes.pdf。

注意

PyPDF2 不能在 PdfFileWriter 对象中间插入页面, addPage() 方法只能够在末尾添加页面。

现在你创建了一个新的 PDF 文件,将来自 meetingminutes.pdfmeetingminutes2.pdf 的页面组合在一个文档中。要记住,传递给 PyPDF2.PdfFileWriter()File 对象,需要以读二进制的方式打开。即使用'rb’作为 open() 的第二个参数。类似的,传 入 pdfWriter.write()File对象需要以写二进制的模式打开,即使用'wb'

旋转页面

利用 rotateClockwise()rotateCounterClockwise() 方法,PDF文档的页面也可以旋转90度的整数倍。向这些 方法传入整数90、180或270就可以了。在交互式环 境中输入以下代码,同时将 meetingminutes.pdf 放在当前工作目录中:

>>> import PyPDF2
>>> minutesFile = open('meetingminutes.pdf','rb')
>>> pdfReader = PyPDF2.PdfFileReader(minutesFile)
>>> page = pdfReader.getPage(0)
>>> page.rotateClockwise(90)
>>> print()
>>> pdfWriter = PyPDF2.PdfFileWriter()
>>> pdfWriter.addPage(page)
>>> resultPdfFile = open('rotatedPage.pdf', 'wb')
>>> pdfWriter.write(resultPdfFile)
>>> resultPdfFile.close()
>>> minutesFile.close()

这里,我们使用 getPag(O) 来选择PDF的第一页, 然后对该页调用 rotateClockwise(90)。我们 将旋转过的页面写入一个新的PDF文档,并保存为 rotatedPage.pdf

得到的PDF文件有一个页面,顺时针旋转了90度,如图 13-2所示。 rotateClockwise()rotateCounterClockwise() 的返回值包含许多信息, 你可以忽略。

image0

图13-2 rotatedPage.pdf文件,页面顺时针旋转了90度

叠加页面

PyPDF2 也可以将一页的内容叠加到另一页上,这可以 用来在页面上添加公司标志、时间戳或水印。利用Python, 很容易为多个文件添加水印,并且只针对程序指定的页面添加。

从http://nostarch.com/automatestuff/ 下载watermark.pdf,将它和meetingminutes.pdf—起放在 当前工作目录中。然后在交互式环境中输入以下代码:

>>> import PyPDF2
>>> minutesFile = open('meetingminutes.pdf', 'rb')
>>> pdfReader = PyPDF2.PdfFileReader(minutesFile)
>>> minutesFirstPage = pdfReader.getPage(0)
>>> pdfWatermarkReader = PyPDF2.PdfFileReader(open('watermark.pdf','rb'))
>>> minutesFirstPage.mergePage(pdfWatermarkReader.getPage(0))
>>> pdfWriter = PyPDF2.PdfFileWriter()
>>> pdfWriter.addPage(minutesFirstPage)
>>>
>>> for pageNum in range(1, pdfReader.numPages):
>>>     pageObj =pdfReader.getPage(pageNum)
>>>     pdfWriter.addPage(pageObj)
>>>
>>> resultPdfFile = open('watermarkedCover.pdf', 'wb')
>>> pdfWriter.write(resultPdfFile)
>>> minutesFile.close()
>>> resultPdfFile.close()

这里我们生成了 meetingminutes.pdfPdfFileReader 对象。调用 getPage(O) ,取得第一页的 Page 对象, 并将它保存在 minutesFirstPage 中。然后生成了 watermark.pdfPdffileReader 对象, 并在 minutesFirstPage 上调用mergePage()。 传递给 mergePage() 的参数,是 watermark.pdf 第一页的 Page 对象。

既然我们已经在 minutesFirstPage 上调用了 mergePage()minutesFirstPage 就代 表加了水印的第-一页。我们创建一个 PdfFileWriter 对象,并加入加了水印的第一页。 然后循环遍历 meetingminutes.pdf 的剩余页面, 将它们添加到 PdfFileWriter 对象中。 最后,我们打开一个新的PDF文件 watermarkedCover.pdf ,并将 PdffileWriter 的内容写入该文件。

图 13-3 展示了结果。新的PDF文件 watermarkedCover.pdf ,包含 meetingminutes.pdf 的全部内容,并在第一页加了水印。

image0

图13-3 最初的PDF(左边)、水印PDF(中间) 以及合并的PDF(右边)

加密PDF

PdfFileWriter 对象也可以为PDF文档进行加密。 在交互式环境中输入以下代码:

>>> import PyPDF2
>>> pdfFile = open('meetingminutes.pdf','rb')
>>> pdfReader = PyPDF2.PdfFileReader(pdfFile)
>>> pdfWriter = PyPDF2.PdfFileWriter()
>>> for pageNum in range(pdfReader.numPages):
>>>     pdfWriter.addPage(pdfReader.getPage(pageNum))
>>>
>>> pdfWriter.encrypt('swordfish')
>>> resultPdf = open('encryptedminutes.pdf', 'wb')
>>> pdfWriter.write(resultPdf)
>>> resultPdf.close()

在调用 write() 方法保存文件之前, 调用 encrypt() 方法,传入口令字符串。 PDF可以有一个用户口令(允许查看这个PDF)和一个 拥有者口令(允许设置打印、注释、提取文本和其他功能 的许可)。用户口令和拥有者口令分别是 encrypt()的 第一个和第二个参数。如果只传入一个字符串给 encrypt() ,它将作为两个口令。

在这个例子中,我们将 meetingminutes.pdf 的页面 拷贝到 PdfFileWriter 对象。用口令 swordfish 加密了 PdfFileWriter ,打开了一个名为 encryptedminutes.pdf 的新PDF,将PdfFileWriter 的内容写入新PDF。任何人要查看 encryptedminutes.pdf, 都必须输入这个口令。在确保文件的拷贝被正确加密后, 你可能会删除原来的未加密的文件。

文本信息不仅仅是纯文本文件,实际上,很有可能更经常遇到的是 PDF和Word文档。可以利用 PyPDF2 模块来读写PDF文档。遗憾 的是,从PDF文档读取文本并非总是能得到完美转换的字符串,因为 PDF文档的格式很复杂,某些PDF可能根本读不出来。在这种情况下, 你就不太走运了,除非将来 PyPDF2 更新,支持更多的PDF功能。