ABSP第13章:處理pdf和word文檔
最近處理word文檔比較多,剛剛忙完所以想先翻譯這一章
處理pdf和word文檔
pdf和word文檔都是二進位文件,所以和純文本比起來處理這兩種文件要複雜一些。處理文字以外,這兩種文件還存儲著大量的字體,顏色還有排版信息。如果你想讓自己的程序可以讀取/寫入word文檔或者pdf文檔的話,除了簡單的open(),你還需要其他的東西。
好在python提供了好用的模塊讓處理pdf和word文檔變得簡單。這一章會介紹兩個這樣的模塊:PyPDF2和Python-Docx。
pdf文檔
pdf的全稱是portable document format(可移動文檔格式),擴展名是.pdf。儘管pdf有很多的特性,但是這一章主要講解兩個最常用的:從pdf讀取文字以及怎麼利用現有文檔新建pdf文檔。
我們用來處理pdf的模塊是PyPDF2。要安裝這個模塊的話,在命令行環境下運行pip install PyPDF2,注意區分大小寫(附錄A裡面有關於安裝第三方模塊的進一步說明)。如果模塊安裝成功的話,在python互動編程環境下面輸入import PyPDF2是不會有報錯信息的。
問題多多的pdf格式
雖然pdf格式在文字的排版方面可以方便人類閱讀,列印,但是等到被軟體分析轉化成純文本的時候就沒那麼方便了。事實上,PyPDF2可能會在提取文字的時候出錯,有時候甚至就什麼文字都無法提取。不幸的是,真要是遇到這樣的情況,你也沒啥辦法。有些pdf文檔就是沒法用PyPDF2打開的。不過我現在還沒有遇到這麼奇葩的pdf就是了。
從PDF提取文本
PyPDF2不能從pdf裡面提取圖片、圖表或者其他的媒體資料。但是它可以從pdf中提取文本並用字元串的形式傳回。我們會用下圖所展示的pdf開始我們的學習。
[000065.jpg]
meetingminutes.pdf在隨書文件中,下載地址如下:
附加文件
百度網盤分流:鏈接: https://pan.baidu.com/s/1eSnfcps 密碼: wix8
>>> 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模塊。之後用open()在二進位讀取模式下(read binary)打開meetingminutes.pdf,將文件對象存儲在pdfFileObj裡面。PyPDF2的PdfFileReader()接受了pdfFileObj這個參數,生成了一個PdfFileReader對象,存入pdfReader變數里。
pdfReader現在作為PdfFileReader對象具有很多和文檔相關的屬性。比如numPages保存的就是文檔的頁數[代碼1]。從返回結果可以看到文檔有19頁,不過現在我們只提取第一頁的內容。
要想從某一頁裡面提取文字,你首先要拿到這一頁的page對象。PdfFileReader里的一個Page對象對應著一篇文檔里的一頁。要獲取一個page對象,對相應的PdfFileReader使用getPage()方法[代碼2],傳入參數是頁碼,只不過是從0開始算,所以第一頁就是0。
PyPDF2對頁數的編碼是0為初始的:第一頁是0,第二頁是1,以此類推。在任何情況下均是如此,即便你在pdf閱讀器裡面看到的頁碼並不是從1開始。比如,你的pdf是一個多頁報告中的三頁節選,閱讀器里你看到的頁碼分別是42,43和44,如果要獲得第一頁,你應該用pdfReader.getPage(0),不是getPage(42)或者getPage(1)。
當你拿到page對象以後,使用它的extractText()方法就可以提取當前頁面的文字了[代碼3]。當然了,這也不是完美的解決方案,比如豎排的Charles E. 「Chas」 Roemer, President沒有被提取出來,另外回車和空格有些奇怪。不過不管怎樣,對於很多程序應用來說,弄成這樣已經差不多了。
破解PDF密碼
有些pdf文檔加了密,只要沒有密碼就沒法打開,這樣一來也就沒法提取文字了。隨書附件中的encrypted.pdf就是被加密的pdf,密碼是rosebud。嘗試下面的代碼打開encrypted.pdf。
>>> import PyPDF2>>> pdfReader = PyPDF2.PdfFileReader(open(encrypted.pdf, rb))? >>> pdfReader.isEncryptedTrue>>> pdfReader.getPage(0)? Traceback (most recent call last): File "<pyshell#173>", line 1, in <module> pdfReader.getPage() --snip-- File "C:Python34libsite-packagesPyPDF2pdf.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屬性。如果這個屬性是True,說明pdf被加密,否則屬性為false[代碼1]。如果在文件沒有解密以前嘗試讀取內容就會出錯[代碼2]。
真要讀取加密的pdf,首先要用decrypt()方法解密,當然要把密碼作為參數傳給decrypt才行。在你把密碼交給decrypt()以後,你可以看到調用getPage()已經不會報錯了。如果密碼是錯的,decrypt()會返回0,然後getPage()還是沒法用。需要注意的是decrypt()只是解開PdfFileReader的密碼,實際上原來的pdf文件是沒有被解密的,所以你不用擔心在python裡面跑過一遍以後pdf的密碼沒了,另外下次如果你要再打開這個加密文件,仍舊需要用decrypt()解密一次。
生成PDF
PyPDF2除了PdfFileReader以外還有一個PdfFileWriter對象,可以用來生成pdf文檔。不過PyPDF2所謂的生成pdf並不是把純文本轉換成pdf,而是只能把做下面的事情(1)圖片轉化成pdf,(2)旋轉頁面,(3)頁面重疊以及(4)加密文件
PyPDF2不提供直接對pdf進行編輯的功能。所以如果你需要「編輯」pdf的話,就需要建立一個新的pdf,把原來的文件拷貝進去。這一部分的例子都會採用下面的流程。
- 打開一個或者多個pdf,交給PdfFileReader()對象
- 生成新的PDFFileWriter對象
- 從PdfFileReader拷貝page對象交給PdfFileWriter對象
- 最後,用PdfFileWriter對象輸出PDF
建立PdfFileWriter對象僅僅只是在Python裡面新建一個代表PDF文檔的值,在硬碟里並沒有生成新的pdf文件。如果要生成pdf文件,必須用PdfFileWriter的write()方法。
一般來說,如果看到PdfFileWriter這樣的開頭第一個字母就是大寫的,代表的是類;而第一個字母如果是小寫的,比如pdfFileWriter,還有前面程序里寫的pdfReader,這些是對象。這些對象是類的instance,就像白馬和黑馬都是馬的instance一樣。
Write()方法的參數是已經用write-binary模式打開的File對象。你可以通過python的open()函數獲取這樣的file對象,當然要傳入兩個參數,第一個是需要打開文件的文件名,第二個就是wb這個二進位寫入模式名。
如果這聽起來有點難懂,先不要擔心,接下來的程序裡面我們會演示這些是怎麼工作的。
複製頁面
你可以用PyPDF2從一個pdf往另外一個pdf裡面複製頁面。這樣你就可以實現pdf的合併,去掉不需要的頁面或者對頁面重新排序了。
在隨書文件附件里找到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()
上面的程序里,首先用open打開兩個pdf文檔(rb模式是read-binary模式),交給pdf1File和pdf2File。之後用PyPDF2.PdfFileReader()讀取內容,交給pdf1Reader[1]和pdf2Reader[2]。
以pdf1Reader為例,pdf1Reader拿到的實際文件是meetingminutes.pdf, 這個文件經由PyPDF2.PdfFileReader(pdf1File) ,以參數的形式交給PdfFileReader方法,生成了一個pdfReader,命名為pdf1Reader。
在下一步循環讀取頁面以前,先用PyPDF2.PDFFileWriter()新建一個pdfFileWriter()寫入對象[3]。隨後用兩個循環,分別把pdf1Reader以及pdf2Reader的所有頁面先後寫進新建的pdfWriter。首先看一下循環體:
pageObj = pdf1Reader.getPage(pageNum)pdfWriter.addPage(pageObj)
循環體第一行用getPage()獲取第pageNum頁(啊,實際上是pageNum+1頁),第二行把剛才獲取的這一頁加入(使用addPage()方法)pdfWriter對象。
而循環是怎麼定義的呢:
for pageNum in range(pdf1Reader.numPages):
首先,pdf1Reader.numPages獲取的是pdf1Reader代表的pdf文件(meetingminutes.pdf)的頁數,比如在我們的這個情況下,是19。所以現在上面的循環相當於
for pageNum in range(19):
由於前面跳過了循環一塊的翻譯,如果看到這裡還是不清楚的話可以去看一下原文關於這一張的內容。
總之,上面的循環把meetingsminutes.pdf從第1頁到最後一頁遍歷了一次,並且把每一頁都加進了pdfWriter
[6][7]這個循環和剛才針對pdf1Reader的循環是差不多的
經過這兩個循環,pdfWriter現在包含了兩個文件的所有頁面。之後在[8]我們用wb(writing binary)模式打開combineminutes.pdf,然後用pdfWriter.write(pdfOutputFile)把pdfWriter的內容,寫進pdfOutputFile所代表的文件。最後把打開的三個文件逐個關閉。
說明:
PyPDF2是不能往pdf文件中間插頁面的,addPage()只能在pdf末尾添加新的頁面。
現在你把兩個pdf合併起來,生成了一個新的pdf。注意在程序裡面,我們打開的兩個pdf是用rb模式打開,而寫入的pdf是用wb模式打開的。
頁面旋轉
PyPDF還可以對PDF頁面作90°旋轉。rotateClockwise()和rotateCounterClockwise()分別可以順時針和逆時針旋轉頁面。90,180,270三個整數可以作為參數傳入。嘗試下面代碼:
>>> 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()
上面的代碼裡面,[1]獲取第一頁,之後用rotateClockwise(90)把這一頁旋轉90°。我們接下來新建了一個pdfWriter,向pdfWriter添加了剛才的這一頁,並且用open()在wb模式下面打開rotatedPage.pdf[3],將pdfWriter的內容寫入roatedPage.pdf文件。
新建的rotatedPage.pdf只有一頁,是原先meetingminutes.pdf第一頁旋轉90°的結果。另外在上面的代碼裡面,rotateClockwise()這個方法返回了很多的內容,這個不用管。
頁面重疊
PyPDF2可以把兩頁pdf疊加,這個功能在添加logo,時間戳或者水印的時候很有用。用python可以非常簡單的向多個pdf文件添加水印。
找到隨書附件中的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[1],然後用getPage(0)就可以獲取meetingminutes.pdf的第一頁,[2]把這一頁交給minutesFirstPage。
接下來,[3]讀取watermark.pdf,交給PyPDF2.PdfFileReader()生成另外一個pdfReader。同樣對這個命名為pdfWatermarkReader的pdfReader使用getPage(0)獲取第一頁。[4] 同樣在這一行代碼,將minutesFirstPage和watermarkReader.getPage(0)這兩頁pdf用mergePage()這個方法合併。minutesFirstPage現在變成了這兩頁的疊加,minuetesFirstPage目前就被加了水印。
直接看代碼更加簡潔,對吧
在完成了水印頁的生成以後,[5]生成一個PdfFileWriter,[6]給它添加第一頁:我們剛剛建立起來的水印頁。
[7]從第2頁開始,把剩下的頁面loop一遍,將剩下的各個頁面加入PdfFileWriter。最後用wb模式打開文件,寫入,逐個關閉。
PDF加密
PdfFileWriter除了生成pdf以外還可以對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()
在[1]以前而之前的代碼沒什麼太大的區別,而在用wb模式打開新文件以前,[1]加入了encrypt(),並且將swordfish作為密碼傳入。PDF支持兩種密碼,一種是用戶密碼,第二種是擁有者密碼。第一種密碼決定你能不能看這個pdf的內容,第二種則是決定你能不能修改pdf的各種屬性,能不能列印、注釋之類的。encrypt()接受兩個參數,第一個參數設置的是用戶密碼,第二個參數設置擁有者密碼,如果只提供一個參數則同時設置兩個密碼。
在上面的代碼裡面,我們把meetingminutes.pdf的所有頁面複製進PdfFileWriter對象,在[1]將這個PdfFileWriter對象加密,用wb模式新打開一個叫做encryptedminutes.pdf的文件,把加密的內容寫入該文件。接下來如果有人要看著個pdf文檔就需要先輸入swordfish這個密碼才能行。接下來你可能要手動刪掉原來未加密的文檔避免泄密了。
關於上面提到的用wb模式打開文件。就像在之前所提到的一樣,在寫入模式下面用open的時候,python不會因為目標文件不存在而報錯,python只會幫你新建一個文件
項目:將多個文檔中的選定頁面合併成一個文件
假如說你現在想把多個pdf格式的報告合併到一個文件裡面,每個pdf都有封面,封底,或者其他的一些附加頁面。當然了,我知道你可以用acrobat pro或者foxit之類的軟體進行pdf合併,但是,假如現在你需要把每個pdf的封面和封底都去掉然後才合併呢?這個時侯就寫一寫python代碼就可以解決這個問題。
老規矩,先草擬一個需求大綱:
- 找到當前工作目錄下的所有pdf文件
- 整理文件名,讓文件可以按順序合併
- 將每個文件,去除不需要的頁面(第一頁)後加入到合併輸出文件中
而要實現上面的功能,你的代碼需要:
- 使用os.listdir()將當前工作目錄中的所有文件列出,從裡面找到pdf文件,過濾所有非pdf文件
- 使用python的sort()方法將文件名按照字元順序排序
- 生成一個PdfFileWriter製作輸出pdf
- 遍歷所有pdf文件,每個pdf文件生成一個PDFFileReader
- 遍歷每個pdf文件的每一頁(第一頁除外)
- 將需要的頁面加入輸出pdf
- 將輸出pdf寫入硬碟,起名為allminutes.pdf
這個項目我們將代碼命名為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.
上面的代碼裡面,[1]導入os和PyPDF2兩個模塊,用os.listdir(.)獲取當前文件夾(也就是當前工作目錄)下面的所有文件列表,遍歷這個列表,找到其中擴展名為.pdf的文件(endwith(.pdf),這個是string類型的一個方法,返回字元串是否以參數制定的文字結束)。如果文件擴展名是.pdf,那麼久將文件名加入pdfFiles這個列表中[2]。完成遍歷以後,對pdfFiles這個列表sort(),依據key是str.lower[3]。
str.lower實際上是str.lower(),屬於string類型的一個方法。在上面的代碼裡面,key參數接收的不再是一個值,而是一個函數,這樣一來str.lower()的參數又從哪裡來呢?
在上面代碼的最後,我們新建了一個PdfFileWriter。
第2步:打開所有pdf文件
現在pdfFiles包含了當前目錄下所有pdf文件的文件名,把下面的代碼加入程序中:
for filename in pdfFiles: pdfFileObj = open(filename, rb) pdfReader = PyPDF2.PdfFileReader(pdfFileObj) # TODO: Loop through all the pages (except the first) and add them.
上面的代碼把pdfFiles所有的文件名都用來交給open()用rb模式打開,pdfFileObj這個文件句柄交給PdfFileReader()生成PDFFileReader。接下來需要做的就是把pdfReader裡面的每一頁都拿出來遍歷一次了:
第3步:遍歷每一頁
緊接著剛才的# TODO: 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)
上面的代碼通過range(1,...)跳過了第一頁,將這個頁面範圍里每一頁都用getPage提出來,用addPage加入pdfWriter。
順便說一下,第2步和第3步合在一起是下面這樣的,注意是一個2層循環:
for filename in pdfFiles: pdfFileObj = open(filename, rb) pdfReader = PyPDF2.PdfFileReader(pdfFileObj) # TODO: 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)
如果需要去掉的頁面不是第一頁而是第二頁怎麼辦?
第4步:保存文件
當上面的兩層循環完成以後,所有文件的所有頁面(去除第一頁)都加入了pdfWriter,現在就只需要把這個對象寫入硬碟就好了。
# Save the resulting PDF to a file.pdfOutput = open(allminutes.pdf, wb)pdfWriter.write(pdfOutput)pdfOutput.close()
仍舊是老樣子,wb模式打開,寫入,close()
其他類似的應用
現在你可以自己嘗試著拓展一下了,嘗試用剛才提到的工具完成下面的工作:
- 將某一頁從pdf裡面去除
- 將pdf文件頁面重新排序
- 將pdf里包含特定文字的頁面提取出來形成一個新的pdf文件。特定文字使用extractText()獲取
- 根據pdf頁面奇偶性決定是否在最後一頁插入空白頁(可以自己建一個空白頁pdf),這一個應用在整理文檔附件的時候尤其有用,因為很多時候你會希望附件里各個文件的第一頁是在奇數頁上(也就是書本打開以後的右側,而不是左側)
13章上半部分到此為止,後半部分是關於word的內容和課後習題,遲些再翻譯。
推薦閱讀:
