首页 » Python编程快速上手 » Python编程快速上手全文在线阅读

《Python编程快速上手》第13章 处理PDF和Word文档

关灯直达底部

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

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

13.1 PDF文档

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

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

13.1.1 从PDF提取文本

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

图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 March 7, 2015 /n      The Board of Elementary and Secondary Education shall provide leadership and create policies for education that expand opportunities for children, empower families and communities, and advance Louisiana in an increasingly competitive global market. BOARD of ELEMENTARY and SECONDARY EDUCATION '  

首先,导入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(0),而不是getPage(42)或getPage(1)。

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

13.1.2 解密PDF

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

 >>> import PyPDF2 >>> pdfReader = PyPDF2.PdfFileReader(open('encrypted.pdf', 'rb'))❶ >>> pdfReader.isEncrypted True >>> pdfReader.getPage(0)❷ Traceback (most recent call last):   File "< pyshell#173>", line 1, in < module>     pdfReader.getPage --snip-- File "C:/Python34/lib/site-packages/PyPDF2/pdf.py", line 1173, in getObject   raise utils.PdfReadError("file has not been decrypted") PyPDF2.utils.PdfReadError: file has not been decrypted❸ >>> pdfReader.decrypt('rosebud') 1 >>> pageObj = pdfReader.getPage(0)  

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

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

13.1.3 创建PDF

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

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

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

2.创建一个新的PdfFileWriter对象。

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

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

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

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

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

13.1.4 拷贝页面

可以利用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对象保存在pdf1File和pdf2File中。调用PyPDF2.PdfFileReader,传入pdf1File,得到一个表示meetingminutes.pdf的PdfFileReader对象❶。再次调用PyPDF2.PdfFileReader,传入pdf2File,得到一个表示meetingminutes2.pdf的PdfFileReader对象❷。然后创建一个新的PdfFileWriter对象,它表示一个空白的PDF文档❸。

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

注意

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

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

13.1.5 旋转页面

利用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) {'/Contents': [IndirectObject(961, 0), IndirectObject(962, 0), --snip-- } >>> pdfWriter = PyPDF2.PdfFileWriter >>> pdfWriter.addPage(page)❸>>> resultPdfFile = open('rotatedPage.pdf', 'wb') >>> pdfWriter.write(resultPdfFile) >>> resultPdfFile.close >>> minutesFile.close  

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

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

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

13.1.6 叠加页面

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.pdf的PdfFileReader对象❶。调用getPage(0),取得第一页的Page对象,并将它保存在minutesFirstPage中❷。然后生成了watermark.pdf的PdfFileReader对象❸,并在minutesFirstPage上调用mergePage❹。传递给mergePage的参数,是watermark.pdf第一页的Page对象。

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

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

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

13.1.7 加密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,都必须输入这个口令。在确保文件的拷贝被正确加密后,你可能会删除原来的未加密的文件。

13.2 项目:从多个PDF中合并选择的页面

假定你有一个很无聊的任务,需要将几十个PDF文件合并成一个PDF文件。每一个文件都有一个封面作为第一页,但你不希望合并后的文件中重复出现这些封面。即使有许多免费的程序可以合并PDF,很多也只是简单的将文件合并在一起。让我们来写一个Python程序,定制需要合并到PDF中的页面。

总的来说,该程序需要完成:

  • 找到当前工作目录中所有PDF文件。
  • 按文件名排序,这样就能有序地添加这些PDF。
  • 除了第一页之外,将每个PDF的所有页面写入输出的文件。

从实现的角度来看,代码需要完成下列任务:

  • 调用os.listdir,找到当前工作目录中的所有文件,去除掉非PDF文件。
  • 调用Python的sort列表方法,对文件名按字母排序。
  • 为输出的PDF文件创建PdfFileWriter对象。
  • 循环遍历每个PDF文件,为它创建PdfFileReader对象。
  • 针对每个PDF文件,循环遍历每一页,第一页除外。
  • 将页面添加到输出的PDF。
  • 将输出的PDF写入一个文件,名为<em>allminutes.pdf</em>。

针对这个项目,打开一个新的文件编辑器窗口,将它保存为combinePdfs.py。

第1步:找到所有PDF文件

首先,程序需要取得当前工作目录中所有带.pdf扩展名的文件列表,并对它们排序。让你的代码看起来像这样:

 #! python3 # combinePdfs.py - Combines all the PDFs in the current working directory into # into a single PDF.❶ import PyPDF2, os # Get all the PDF filenames. pdfFiles =  for filename in os.listdir('.'):     if filename.endswith('.pdf'):❷ pdfFiles.append(filename)❸ pdfFiles.sort(key/str.lower)❹ pdfWriter = PyPDF2.PdfFileWriter # TODO: Loop through all the PDF files. # TODO: Loop through all the pages (except the first) and add them. # TODO: Save the resulting PDF to a file.  

在#!行和介绍程序做什么的描述性注释之后,代码导入了os和PyPDF2模块❶。os.listdir('.')调用将返回当前工作目录中所有文件的列表。代码循环遍历这个列表,将带有.pdf扩展名的文件添加到pdfFiles中❷。然后,列表按照字典顺序排序,调用sort时带有key/str.lower关键字参数❸。

代码创建了一个PdfFileWriter对象,保存合并后的PDF页面❹。最后,一些注释语句简要描述了剩下的程序。

第2步:打开每个PDF文件

现在,程序必须读取pdfFiles中的每个PDF文件。在程序中加入以下代码:

#! python3# combinePdfs.py - Combines all the PDFs in the current working directory into# a single PDF.import PyPDF2, os# Get all the PDF filenames.pdfFiles = --snip--# Loop through all the PDF files.for filename in pdfFiles:    pdfFileObj = open(filename, 'rb')    pdfReader = PyPDF2.PdfFileReader(pdfFileObj)    # TODO: Loop through all the pages (except the first) and add them.# TODO: Save the resulting PDF to a file.  

针对每个PDF文件,循环内的代码调用open,以'wb'作为第二个参数,用读二进制的模式打开文件。open调用返回一个 File 对象,它被传递给PyPDF2.PdfFileReader,创建针对那个PDF文件的PdfFileReader对象。

第3步:添加每一页

针对每个PDF文件,需要循环遍历每一页,第一页除外。在程序中添加以下代码:

 #! python3 # combinePdfs.py - Combines all the PDFs in the current working directory into # a single PDF. import PyPDF2, os --snip-- # Loop through all the PDF files. for filename in pdfFiles: --snip--     # Loop through all the pages (except the first) and add them.❶     for pageNum in range(1, pdfReader.numPages): pageObj = pdfReader.getPage(pageNum) pdfWriter.addPage(pageObj) # TODO: Save the resulting PDF to a file.  

for循环内的代码将每个Page对象拷贝到PdfFileWriter对象。要记住,你需要跳过第一页。因为PyPDF2认为0是第一页,所以循环应该从1开始❶,然后向上增长到pdfReader.numPages中的整数,但不包括它。

第4步:保存结果

在这些嵌套的for循环完成后,pdfWriter变量将包含一个PdfFileWriter对象,合并了所有PDF的页面。最后一步是将这些内容写入硬盘上的一个文件。在程序中添加以下代码:

#! python3# combinePdfs.py - Combines all the PDFs in the current working directory into# a single PDF.import PyPDF2, os--snip--# Loop through all the PDF files.for filename in pdfFiles:--snip--    # Loop through all the pages (except the first) and add them.    for pageNum in range(1, pdfReader.numPages):    --snip--# Save the resulting PDF to a file.pdfOutput = open('allminutes.pdf', 'wb')pdfWriter.write(pdfOutput)pdfOutput.close  

向open传入'wb',以写二进制的模式打开输出PDF文件allminutes.pdf。然后,将得到的File对象传给write方法,创建实际的PDF文件。调用close方法,结束程序。

第5步:类似程序的想法

能够利用其他PDF文件的页面创建PDF文件,这让你的程序能完成以下任务:

  • 从PDF文件中截取特定的页面。
  • 重新调整PDF文件中页面的次序。
  • 创建一个PDF文件,只包含那些具有特定文本的页面。文本由extractText来确定。

13.3 Word文档

利用python-docx模块,Python可以创建和修改Word文档,它带有.docx文件扩展名。运行pip install python-docx,可以安装该模块(附录A介绍了安装第三方模块的细节)。

注意

OSI参考模型最初是在1983年由国际标准化组织出版,标准号为ISO 7498。在第一次用pip安装python-docx时,注意要安装python-docx,而不是docx。安装名称docx是指另一个模块,本书没有介绍。但是,在导入python-docx模块时,需要执行import docx,而不是import python-docx。

如果你没有Word软件,LibreOffice Writer和OpenOffice Writer都是免费的替代软件,它们可以在Windows、OS X和Linux上打开.docx文件。可以分别从https://www.libreoffice.org和http://openoffice.org下载它们。python-docx的完整文档在https://python-docx.readthedocs.org/。尽管有针对OS X平台的Word版本,但本章将使用Windows平台的Word。

和纯文本相比,.docx文件有很多结构。这些结构在python-docx中用3种不同的类型来表示。在最高一层,Document对象表示整个文档。Document对象包含一个Paragraph对象的列表,表示文档中的段落(用户在Word文档中输入时,如果按下回车,新的段落就开始了)。每个Paragraph对象都包含一个Run对象的列表。图13-4中的单句段落有4个Run对象。

图13-4 一个Paragraph对象中识别的Run对象

Word文档中的文本不仅仅是字符串。它包含与之相关的字体、大小、颜色和其他样式信息。在Word中,样式是这些属性的集合。一个Run对象是相同样式文本的延续。当文本样式发生改变时,就需要一个新的Run对象。

13.3.1 读取Word文档

让我们尝试使用python-docx模块。从http://nostarch.com/automatestuff/下载demo.docx,并将它保存在当前工作目录中。然后在交互式环境中输入以下代码:

>>> import docx❶ >>> doc = docx.Document('demo.docx')❷ >>> len(doc.paragraphs)7❸ >>> doc.paragraphs[0].text'Document Title'❹ >>> doc.paragraphs[1].text'A plain paragraph with some bold and some italic'❺ >>> len(doc.paragraphs[1].runs)4❻ >>> doc.paragraphs[1].runs[0].text'A plain paragraph with some '❼ >>> doc.paragraphs[1].runs[1].text'bold'❽ >>> doc.paragraphs[1].runs[2].text' and some '❾ >>> doc.paragraphs[1].runs[3].text'italic'  

在❶行,我们在Python中打开了一个.docx文件,调用docx.Document,传入文件名demo.docx。这将返回一个Document对象,它有paragraphs属性,是Paragraph对象的列表。如果我们对doc.paragraphs调用len,将返回7。这告诉我们,该文档有7个Paragraph对象❷。每个Paragraph对象都有一个text属性,包含该段中文本的字符串(没有样式信息)。这里,第一个text属性包含'DocumentTitle'❸,第二个包含'A plain paragraph with some bold and some italic'❹。

每个Paragraph对象也有一个runs属性,它是Run对象的列表。Run对象也有一个text属性,包含那个延续中的文本。我们看看第二个Paragraph对象中的text属性,'A plain paragraph with some bold and some italic'。对这个Paragraph对象调用len,结果告诉我们有4个Run对象❺。第一个对象包含'A plain paragraph with some '❻。然后,文本变为粗体样式,所以’bold’开始了一个新的Run对象❼。在这之后,文本又回到了非粗体的样式,这导致了第三个Run对象,' and some '❽。最后,第四个对象包含'italic',是斜体样式❾。

有了python-docx,Python程序就能从.docx文件中读取文本,像其他的字符串值一样使用它。

13.3.2 从.docx文件中取得完整的文本

如果你只关心Word文档中的文本,不关心样式信息,就可以利用getText函数。它接受一个.docx文件名,返回其中文本的字符串。打开一个新的文件编辑器窗口,输入以下代码,并保存为readDocx.py:

#! python3import docxdef getText(filename):    doc = docx.Document(filename)    fullText =     for para in doc.paragraphs:fullText.append(para.text)    return '/n'.join(fullText)  

getText函数打开了Word文档,循环遍历paragraphs列表中的所有Paragraph对象,然后将它们的文本添加到fullText列表中。循环结束后,fullText中的字符串连接在一起,中间以换行符分隔。

readDocx.py程序可以像其他模块一样导入。现在如果你只需要Word文档中的文本,就可以输入以下代码:

>>> import readDocx>>> print(readDocx.getText('demo.docx'))Document TitleA plain paragraph with some bold and some italicHeading, level 1Intense quotefirst item in unordered listfirst item in ordered list  

也可以调整getText,在返回字符串之前进行修改。例如,要让每一段缩进,就将文件中的append调用替换为:

fullText.append(' ' + para.text)   

要在段落之间增加空行,就将join调用代码改成:

return '/n/n'.join(fullText)   

可以看到,只需要几行代码,就可以写出函数,读取.docx文件,根据需要返回它的内容字符串。

13.3.3 设置Paragraph和Run对象的样式

在Windows平台的Word中,你可以按下Ctrl-Alt-Shift-S,显示样式窗口并查看样式,如图13-5所示。在OS X上,可以点击ViewStyles菜单项,查看样式窗口。

Word和其他文字处理软件利用样式,保持类似类型的文本在视觉展现上一致,并易于修改。例如,也许你希望将内容段落设置为11点,Times New Roman,左对齐,右边不对齐的文本。可以用这些设置创建一种样式,将它赋给所有的文本段落。然后,如果稍后想改变文档中所有内容段落的展现形式,只要改变这种样式,所有段落都会自动更新。

图13-5 在Windows平台上按下Ctrl-Alt-Shift-S,显示样式窗口

对于Word文档,有3种类型的样式:段落样式可以应用于Paragraph对象,字符样式可以应用于Run对象,链接的样式可以应用于这两种对象。可以将Paragraph和Run对象的style属性设置为一个字符串,从而设置样式。这个字符串应该是一种样式的名称。如果style被设置为None,就没有样式与Paragraph或Run对象关联。

默认Word样式的字符串如下:

在设置style属性时,不要在样式名称中使用空格。例如,样式名称可能是Subtle Emphasis,你应该将属性设置为字符串'SubtleEmphasis',而不是'Subtle Emphasis'。包含空格将导致Word误读样式名称,并且应用失败。

如果对Run对象应用链接的样式,需要在样式名称末尾加上'Char'。例如,对Paragraph对象设置Quote链接的样式,应该使用paragraphObj.style = 'Quote'。但对于Run对象,应该使用runObj.style = 'QuoteChar'。

在当前版本的python-docx (0.7.4)中,只能使用默认的Word样式,以及打开的文件中已有的样式,不能创建新的样式,但这一点在将来的模块版本中可能会改变。

13.3.4 创建带有非默认样式的Word文档

如果想要创建的Word文档使用默认样式以外的样式,就需要打开一个空白Word文档,通过点击样式窗口底部的New Style按钮,自己创建样式(图13-6展示了Windows平台上的情形)。

图13-6 新建样式按扭(左边)和“根据格式设置创建新样式”对话框(右边)

这将打开“Creat New Style from Formatting”对话框,在这里可以输入新样式。然后,回到交互式环境,用docx.Document打开这个空白Word文档,利用它作为Word文档的基础。这种样式的名称现在就可以被python-docx使用了。

13.3.5 Run属性

通过text属性,Run可以进一步设置样式。每个属性都可以被设置为3个值之一:True(该属性总是启用,不论其他样式是否应用于该Run)、False(该属性总是禁用)或None(默认使用该Run被设置的任何属性)。

表13-1列出了可以在Run对象上设置的text属性。

表13-1 Run对象的text属性

属性

描述

bold

文本以粗体出现

italic

文本以斜体出现

underline

文本带下划线

strike

文本带删除线

double_strike

文本带双删除线

all_caps

文本以大写首字母出现

small_caps

文本以大写首字母出现,小写字母小两个点

shadow

文本带阴影

outline

文本以轮廓线出现,而不是实心

rtl

文本从右至左书写

imprint

文本以刻入页面的方式出现

emboss

文本以凸出页面的方式出现

例如,为了改变demo.docx的样式,在交互式环境中输入以下代码:

>>> doc = docx.Document('demo.docx')>>> doc.paragraphs[0].text'Document Title'>>> doc.paragraphs[0].style'Title'>>> doc.paragraphs[0].style = 'Normal'>>> doc.paragraphs[1].text'A plain paragraph with some bold and some italic'>>> (doc.paragraphs[1].runs[0].text, doc.paragraphs[1].runs[1].text, doc.paragraphs[1].runs[2].text, doc.paragraphs[1].runs[3].text)('A plain paragraph with some ', 'bold', ' and some ', 'italic')>>> doc.paragraphs[1].runs[0].style = 'QuoteChar'>>> doc.paragraphs[1].runs[1].underline = True>>> doc.paragraphs[1].runs[3].underline = True>>> doc.save('restyled.docx')  

这里,我们使用了text和style属性,以便容易地看到文档的段落中有什么。我们可以看到,很容易将段落划分成Run,并单独访问每个Run。所以我们取得了第二段中的第一、第二和第四个Run,设置每个Run的样式,将结果保存到一个新文档。

文件顶部的单词Document Title将具有Normal样式,而不是Title样式。针对文本A plain paragraph的Run对象,将具有QuoteChar样式。针对单词bold和italic的两个Run对象,它们的underline属性设置为True。图13-7展示了文件中段落和Run的样式看起来的样子。

图13-7 restyled.docx文件

访问https://python-docx.readthedocs.org/en/latest/user/styles.html,你可以看到,python-docx使用样式的更完整文档。

13.3.6 写入Word文档

在交互式环境中输入以下代码:

>>> import docx>>> doc = docx.Document>>> doc.add_paragraph('Hello world!')< docx.text.Paragraph object at 0x0000000003B56F60>>>> doc.save('helloworld.docx')  

要创建自己的.docx文件,就调用docx.Document,返回一个新的、空白的Word Document对象。Document对象的add_paragraph方法将一段新文本添加到文档中,并返回添加的Paragraph对象的引用。在添加完文本之后,向Document对象的save方法传入一个文件名字符串,将Document对象保存到文件。

这将在当前工作目录中创建一个文件,名为helloworld.docx。如果打开它,就像图13-8的样子。

图13-8 利用add_paragraph('Hello world!')创建的Word文档

可以用新的段落文本,再次调用add_paragraph方法,添加段落。或者,要在已有段落的末尾添加文本,可以调用Paragraph对象的add_run方法,向它传入一个字符串。在交互式环境中输入以下代码:

>>> import docx>>> doc = docx.Document>>> doc.add_paragraph('Hello world!')< docx.text.Paragraph object at 0x000000000366AD30>>>> paraObj1 = doc.add_paragraph('This is a second paragraph.')>>> paraObj2 = doc.add_paragraph('This is a yet another paragraph.')>>> paraObj1.add_run(' This text is being added to the second paragraph.')< docx.text.Run object at 0x0000000003A2C860>>>> doc.save('multipleParagraphs.docx')  

得到的文本如图13-9所示。请注意,文本This text is being added to the second paragraph.被添加到paraObj1中的Paragraph对象中,它是添加到doc中的第二段。add_paragraph和add_run分别返回Paragraph和Run对象,这样你就不必多花一步来提取它们。

图13-9 添加了多个Paragraph和Run对象的文档

要记住,对于python-docx的0.5.3版本,新的Paragraph对象只能添加在文档的末尾,新的Run对象只能添加在Paragraph对象的末尾。

可以再次调用save方法,保存所做的变更。

add_paragraph和add_run都接受可选的第二个参数,它是表示Paragraph或Run对象样式的字符串。例如:

>>> doc.add_paragraph('Hello world!', 'Title')  

这一行添加了一段,文本是Hello world!,样式是Title。

13.3.7 添加标题

调用add_heading将添加一个段落,并使用一种标题样式。在交互式环境中输入以下代码:

>>> doc = docx.Document>>> doc.add_heading('Header 0', 0)< docx.text.Paragraph object at 0x00000000036CB3C8>>>> doc.add_heading('Header 1', 1)< docx.text.Paragraph object at 0x00000000036CB630>>>> doc.add_heading('Header 2', 2)< docx.text.Paragraph object at 0x00000000036CB828>>>> doc.add_heading('Header 3', 3)< docx.text.Paragraph object at 0x00000000036CB2E8>>>> doc.add_heading('Header 4', 4)< docx.text.Paragraph object at 0x00000000036CB3C8>>>> doc.save('headings.docx')  

add_heading的参数,是一个标题文本的字符串,以及一个从0到4的整数。整数0表示标题是Title样式,这用于文档的顶部。整数1到4是不同的标题层次,1是主要的标题,4是最低层的子标题。add_heading返回一个Paragraph对象,让你不必多花一步从Document对象中提取它。

得到的headings.docx文件如图13-10所示。

图13-10 带有标题0到4的headings.docx文档

13.3.8 添加换行符和换页符

要添加换行符(而不是开始一个新的段落),可以在Run对象上调用add_break方法,换行符将出现在它后面。如果希望添加换页符,可以将docx.text.WD_BREAK.PAGE作为唯一的参数,传递给add_break,就像下面代码中间所做的一样:

 >>> doc = docx.Document >>> doc.add_paragraph('This is on the first page!') < docx.text.Paragraph object at 0x0000000003785518>❶ >>> doc.paragraphs[0].runs[0].add_break(docx.text.WD_BREAK.PAGE) >>> doc.add_paragraph('This is on the second page!') < docx.text.Paragraph object at 0x00000000037855F8> >>> doc.save('twoPage.docx')  

这创建了一个两页的Word文档,第一页上是This is on the first page!,第二页上是This is on the second page!。虽然在文本This is on the first page!之后,第一页还有大量的空间,但是我们在第一段的第一个Run之后插入分页符,强制下一段落出现在新的页面中❶。

13.3.9 添加图像

Document对象有一个add_picture方法,让你在文档末尾添加图像。假定当前工作目录中有一个文件zophie.png,你可以输入以下代码,在文档末尾添加zophie.png,宽度为1英寸,高度为4厘米(Word可以同时使用英制和公制单位):

>>> doc.add_picture('zophie.png', width=docx.shared.Inches(1),height=docx.shared.Cm(4))< docx.shape.InlineShape object at 0x00000000036C7D30>  

第一个参数是一个字符串,表示图像的文件名。可选的width和height关键字参数,将设置该图像在文档中的宽度和高度。如果省略,宽度和高度将采用默认值,即该图像的正常尺寸。

你可能愿意用熟悉的单位来指定图像的高度和宽度,诸如英寸或厘米。所以在指定 width 和 height 关键字参数时,可以使用 docx.shared.Inches和docx.shared.Cm函数。

13.4 小结

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

Word文档更可靠,可以用python-docx模块来读取。可以通过Paragraph和Run对象来操作Word文档中的文本。可以设置这些对象的样式,尽管必须使用默认的样式,或文档中已有的样式。可以添加新的段落、标题、换行换页符和图像,尽管只能在文档的末尾。

在处理PDF和Word文档时有很多限制,这是因为这些格式的本意是很好地展示给人看,而不是让软件易于解析。下一章将探讨存储信息的另外两种常见格式:JSON和CSV文件。这些格式是设计给计算机使用的。你会看到,Python处理这些格式要容易得多。

13.5 习题

1.不能将PDF文件名的字符串传递给PyPDF2.PdfFileReader函数。应该向该函数传递什么?

2.PdfFileReader和PdfFileWriter需要的File对象,应该以何种模式打开?

3.如何从PdfFileReader对象中取得第5页的Page对象?

4.什么PdfFileReader变量保存了PDF文档的页数?

5.如果PdfFileReader对象表示的PDF文档是用口令swordfish加密的,应该先做什么,才能从中取得Page对象?

6.使用什么方法来旋转页面?

7.什么方法返回文件demo.docx的Document对象?

8.Paragraph对象和Run对象之间的区别是什么?

9.doc变量保存了一个Document对象,如何从中得到Paragraph对象的列表?

10.哪种类型的对象具有bold、underline、italic、strike和outline变量?

11.bold变量设置为True、False或None,有什么区别?

12.如何为一个新Word文档创建Document对象?

13.doc变量保存了一个Document对象,如何添加一个文本是'Hello there!'的段落?

14.哪些整数表示Word文档中可用的标题级别?

13.6 实践项目

作为实践,编程完成下列任务。

13.6.1 PDF偏执狂

利用第9章的os.walk函数编写一个脚本,遍历文件夹中的所有PDF(包含子文件夹),用命令行提供的口令对这些PDF加密。用原来的文件名加上_encrypted.pdf后缀,保存每个加密的PDF。在删除原来的文件之前,尝试用一个程序读取并解密该文件,确保它被正确的加密。

然后编写一个程序,找到文件夹中所有加密的PDF文件(包括它的子文件夹),利用提供的口令,创建PDF的解密拷贝。如果口令不对,程序应该打印一条消息,并继续处理下一个PDF文件。

13.6.2 定制邀请函,保存为Word文档

假设你有一个客人名单的文本文件。这个guests.txt文件每行有一个名字,像下面这样:

Prof. PlumMiss ScarletCol. MustardAl SweigartRoboCop  

写一个程序,生成定制邀请函的Word文档,如图13-11所示。

图13-11 定制的邀请函脚本生成的Word文档

因为python-docx只能使用Word文档中已经存在的样式,所以你必须先将这些样式添加到一个空白Word文件中,然后用python-docx打开该文件。在生成的Word文档中,每份邀请函应该占据一页,所以在每份邀请函的最后一段调用add_break,添加分页符。这样,你只需要打开一份Word文档,就能打印所有的邀请函。

你可以从http://nostarch.com/automatestuff/下载示例guests.txt文件。

13.6.3 暴力PDF口令破解程序

假定有一个加密的PDF文件,你忘记了口令,但记得它是一个英语单词。尝试猜测遗忘的口令是很无聊的任务。作为替代,你可以写一个程序,尝试用所有可能的英语单词来解密这个PDF文件,直到找到有效的口令。这称为暴力口令攻击。从http://nostarch.com/automatestuff/下载文本文件dictionary.txt。这个字典文件包含44000多个英语单词,每个单词占一行。

利用第8章学过的文件读取技巧来读取这个文件,创建一个单词字符串的列表。然后循环遍历这个列表中的每个单词,将它传递给decrypt方法,如果这个方法返回整数 0,口令就是错的,程序应该继续尝试下一个口令。如果decrypt返回1,程序就应该终止循环,打印出破解的口令。你应该尝试每个单词的大小写形式(在我的笔记本上,遍历来自字典文件的所有88000个大小写单词,只要几分钟时间。这就是不应该使用简单英语单词作为口令的原因)。