前面學習了解析PDF文檔,並寫入文檔的知識,那篇文章的名字為深入學習Python解析並讀取PDF文件內容的方法。
鏈接如下:https://www.cnblogs.com/wj-1314/p/9429816.html
但是最近出現了一個新問題,就是上面使用pdfminer這個庫只能解析正常的PDF內容,然而在實際情況中,公司的一些文檔可能是加密的,那么如何處理加密的PDF文件,就是本文學習的重點。
在網上查找資料,發現pypdf2可以實現對pdf文件進行加密,解密,所以就學習了一下這個庫,並留下筆記。
首先說明pypdf2是Python3版本的,在之前的Python2版本有一個對應的pypdf庫,但是本文下載了pypdf2這個庫,在Python2 運行時沒有報錯的。
注意:所有修改操作均無法再原文件中操作,只能將修改的結果寫入新文件中。
一:PyPDF2介紹
PyPDF2是源自pyPdf項目的純python PDF工具包。它目前由Phaseit,Inc。維護。PyPDF2可以從PDF文件中提取數據,或者操縱現有的PDF來生成新文件。PyPDF2與Python版本2.6,2.7和3.2 - 3.5兼容。
作為PDF工具包構建的Pure-Python庫。它能夠:
- 提取文檔信息(標題,作者,......)
- 逐頁拆分文檔
- 逐頁合並文檔
- 裁剪頁面
- 將多個頁面合並為一個頁面
- 加密和解密PDF文件
通過Pure-Python,它應該在任何Python平台上運行,而不依賴於外部庫。它也可以完全在StringIO對象而不是文件流上工作,允許在內存中進行PDF操作。因此,它是管理或操作PDF的網站的有用工具。
而本文主要學習加密解密PDF文件。
二:PyPDF2安裝
2.1 下載
在https://pypi.org/project/PyPDF2/ 中搜索PyPDF2 1.26.0可以安裝包。
2.2 在Linux安裝壓縮包命令如下:
cd /data && tar -xvf PyPDF2-1.26.0.tar.gz cd PyPDF2-1.26.0 python setup.py install
2.3 直接安裝
pip install pypdf2
2.4 PyPDF的官方文檔:https://pythonhosted.org/PyPDF2/
三:PyPDF 的使用目的
首先 我這里有一個加密的PDF文件:
那么我使用上一篇文章的代碼(如下):
#coding:utf-8 import importlib import sys import time importlib.reload(sys) time1 = time.time() from pdfminer.pdfparser import PDFParser, PDFDocument from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter from pdfminer.converter import PDFPageAggregator from pdfminer.layout import LTTextBoxHorizontal, LAParams from pdfminer.pdfinterp import PDFTextExtractionNotAllowed text_path = r'5b931164edc09a226b3a12c4.pdf' def parse(): '''解析PDF文本,並保存到TXT文件中''' fp = open(text_path, 'rb') # 用文件對象創建一個PDF文檔分析器 parser = PDFParser(fp) # 創建一個PDF文檔 doc = PDFDocument() # 連接分析器,與文檔對象 parser.set_document(doc) doc.set_parser(parser) # 提供初始化密碼,如果沒有密碼,就創建一個空的字符串 doc.initialize() # 檢測文檔是否提供txt轉換,不提供就忽略 if not doc.is_extractable: raise PDFTextExtractionNotAllowed else: # 創建PDF,資源管理器,來共享資源 rsrcmgr = PDFResourceManager() # 創建一個PDF設備對象 laparams = LAParams() device = PDFPageAggregator(rsrcmgr, laparams=laparams) # 創建一個PDF解釋其對象 interpreter = PDFPageInterpreter(rsrcmgr, device) # 循環遍歷列表,每次處理一個page內容 # doc.get_pages() 獲取page列表 for page in doc.get_pages(): interpreter.process_page(page) # 接受該頁面的LTPage對象 layout = device.get_result() # 這里layout是一個LTPage對象 里面存放着 這個page解析出的各種對象 # 一般包括LTTextBox, LTFigure, LTImage, LTTextBoxHorizontal 等等 # 想要獲取文本就獲得對象的text屬性, for x in layout: if (isinstance(x, LTTextBoxHorizontal)): with open(r'2.txt', 'a') as f: results = x.get_text() print(results) f.write(results + "\n") if __name__ == '__main__': parse() time2 = time.time() print("總共消耗時間為:", time2 - time1)
解析的時候,會主動觸發異常(如下):
那么,打開文件,我們會發現,實際情況是這樣的:
既然文件已經加密,那么正常渠道解析,肯定會觸發異常,所以此時的重中之重就是解密PDF文件,然后再去解析即可。
如何解密呢? 話不多說,直接看代碼。
如果不知道密碼,最好設置為空,這樣的話 大多數就可以解析,代碼如下:
# coding:utf-8 import os from PyPDF2 import PdfFileReader from PyPDF2 import PdfFileWriter def get_reader(filename, password): try: old_file = open(filename, 'rb') print('run jiemi1') except Exception 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 decrypt_pdf(filename, password, decrypted_filename=None): """ 將加密的文件及逆行解密,並生成一個無需密碼pdf文件 :param filename: 原先加密的pdf文件 :param password: 對應的密碼 :param decrypted_filename: 解密之后的文件名 :return: """ # 生成一個Reader和Writer print('run jiemi') 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')) decrypt_pdf(r'5b931164edc09a226b3a12c4.pdf', '')
運行結果如下:
新生成的文件如下:
打開是這樣的:
所以 ,這樣的話 就可以打開了,也可以解析了,下面繼續使用PDF解析文件解析,代碼是上面的,結果如下:
解析成功,那么會保存為txt格式。
但是這里要注意,我給解密的代碼,把密碼設置為abc,如下:
那么會觸發異常,代碼結果表示如下:
# coding:utf-8 import os from PyPDF2 import PdfFileReader from PyPDF2 import PdfFileWriter def get_reader(filename, password): try: old_file = open(filename, 'rb') print('run jiemi1') except Exception 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 decrypt_pdf(filename, password, decrypted_filename=None): """ 將加密的文件及逆行解密,並生成一個無需密碼pdf文件 :param filename: 原先加密的pdf文件 :param password: 對應的密碼 :param decrypted_filename: 解密之后的文件名 :return: """ # 生成一個Reader和Writer print('run jiemi') 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')) decrypt_pdf(r'5b931164edc09a226b3a12c4.pdf', 'abc')
四:PyPDF2的理論介紹
PyPDF2 包含了 PdfFileReader PdfFileMerger PageObject PdfFileWriter 四個常用的主要 Class。
具體分析:
PyPDF2 將讀與寫分成兩個類來操作:
from PyPDF2 import PdfFileWriter, PdfFileReader writer = PdfFileWriter() reader = PdfFileReader(open("document1.pdf", "rb"))
官方實例:
from PyPDF2 import PdfFileWriter, PdfFileReader output = PdfFileWriter() input1 = PdfFileReader(open("document1.pdf", "rb")) # print how many pages input1 has: print "document1.pdf has %d pages." % input1.getNumPages() # add page 1 from input1 to output document, unchanged output.addPage(input1.getPage(0)) # add page 2 from input1, but rotated clockwise 90 degrees output.addPage(input1.getPage(1).rotateClockwise(90)) # add page 3 from input1, rotated the other way: output.addPage(input1.getPage(2).rotateCounterClockwise(90)) # alt: output.addPage(input1.getPage(2).rotateClockwise(270)) # add page 4 from input1, but first add a watermark from another PDF: page4 = input1.getPage(3) watermark = PdfFileReader(open("watermark.pdf", "rb")) page4.mergePage(watermark.getPage(0)) output.addPage(page4) # add page 5 from input1, but crop it to half size: page5 = input1.getPage(4) page5.mediaBox.upperRight = ( page5.mediaBox.getUpperRight_x() / 2, page5.mediaBox.getUpperRight_y() / 2 ) output.addPage(page5) # add some Javascript to launch the print window on opening this PDF. # the password dialog may prevent the print dialog from being shown, # comment the the encription lines, if that's the case, to try this out output.addJS("this.print({bUI:true,bSilent:false,bShrinkToFit:true});") # encrypt your new PDF and add a password password = "secret" output.encrypt(password) # finally, write "output" to document-output.pdf outputStream = file("PyPDF2-output.pdf", "wb") output.write(outputStream)
五 :PdfFileReader類
class PyPDF2.PdfFileReader(stream,strict = True,warndest = None, overwriteWarnings = True )
初始化PdfFileReader對象。此操作可能需要一些時間,因為PDF流的交叉引用表被讀入內存。
參數:
stream - File對象或支持類似於File對象的標准讀取和搜索方法的對象。也可以是表示PDF文件路徑的字符串。
- strict(bool) - 確定是否應該警告用戶所有問題並且還會導致一些可糾正的問題致命。默認為
True
。 - warndest - 記錄警告的目的地(默認為
sys.stderr
)。 - overwriteWarnings(bool) - 確定是否
warnings.py
使用自定義實現覆蓋Python的 模塊(默認為True
)。
decrypt(密碼)
使用帶有PDF標准加密處理程序的加密/安全PDF文件時,此功能將允許解密文件。它根據文檔的用戶密碼和所有者密碼檢查給定的密碼,如果密碼正確,則存儲生成的解密密鑰。
哪個密碼匹配無關緊要。兩個密碼都提供了正確的解密密鑰,允許文檔與此庫一起使用。
參數:password(str) 要匹配的密碼
返回:0
如果密碼失敗,1
密碼是否與用戶密碼匹配,密碼2
是否與所有者密碼匹配。
返回類型: INT
引發NotImplementedError:如果文檔使用不受支持的加密方法。
documentInfo
訪問給定Destination對象的頁碼
getDestinationPageNumber(destination)
檢索PDF文件的文檔信息字典(如果存在)。請注意,某些PDF文件使用元數據流而不是docinfo詞典,此功能不會訪問這些元數據流。
返回:頁碼或者如果找不到頁面的話 則為-1
返回類型:INT
getDocumentInfo()
檢索PDF文件的文檔信息字典(如果存在)。請注意,某些PDF文件使用元數據流而不是docinfo詞典,此功能不會訪問這些元數據流。
返回:該PDF文件的文檔信息
返回類型:DocumentInformation
或者None
如果不存在。
getFields
(tree = None,retval = None,fileobj = None )
如果此PDF包括交互式表單字段,則提取字段數據,該樹和retval的參數是遞歸使用。
參數:fileobj 用於在找到的所有交互式表單字段上寫入報告的文件對象(通常是文本文件)
返回:一個字典,其中每個鍵是一個字段名稱,每個值都是一個個Field
對象。默認情況下,映射名稱用於鍵。
返回類型:dict 或者None無法找到表單數據。
getFormTextFields()
使用文本數據從文檔中檢索表單域(輸入,下拉列表)
getNameDestinations(tree=None,retval=None)
檢索文檔中存在的指定目標
返回:將名稱映射到的字典 Destinations
。
返回類型:字典
getNumPages()
計算此PDF文件中的頁面。
返回:頁面
返回類型:INT
引發PDFReadError:如果文件已加密且限制阻止此操作。
getOutlines(node=None,outlines=None)
檢查文檔中存在的文檔大綱。
返回:一個PageObject
實例。
返回類型:PageObject
getPageLayout()
獲取頁面布局,有關setPageLayout()
有效布局的說明,請參閱參考資料。
返回:目前正在使用的頁面布局
返回類型:str None如果沒有指定。
getPageMode()
獲取頁面布局,有關setPageMode()
有效模式的說明,請參閱。
返回:目前正在使用的頁面模式。
返回類型:str
,None
如果沒有指定。
getPageNumber()
檢索給定PageObject的頁面。
參數:page(PageObject) - 獲取頁碼的頁面。應該是一個實例PageObject
返回:頁碼或如果找不到頁面,則為-1
返回類型:INT
getXmpMetadata()
從PDF文檔跟目錄中檢索XMP(可擴展元數據平台)數據。
返回:XmpInformation
可用於從文檔訪問XMP元數據的實例
返回類型:XmpInformation
或者 None
如果在文檔根目錄中未找到元數據。
isEncrypted
只讀布爾屬性,顯示此PDF文件是否已加密。請注意,即使decrypt()
調用該方法,此屬性(如果為true)仍將保持為true 。
namedDestinations
訪問該getNamedDestinations()
函數的只讀屬性 。
numPages
訪問該getNumPages()
函數的只讀屬性 。
outlines
-
只讀屬性訪問
getOutlines()
功能。
pageLayout
訪問該getPageLayout()
方法的只讀屬性 。
pageMode
訪問該getPageMode()
方法的只讀屬性 。
pages
只讀屬性,它根據getNumPages()
和 getPage()
方法模擬列表 。
xmpMetadata
訪問該getXmpMetadata()
函數的只讀屬性 。
PDF讀取操作:
# encoding:utf-8 from PyPDF2 import PdfFileReader, PdfFileWriter readFile = 'C:/ learn.pdf' # 獲取 PdfFileReader 對象 pdfFileReader = PdfFileReader(readFile) # 或者這個方式:pdfFileReader = PdfFileReader(open(readFile, 'rb')) # 獲取 PDF 文件的文檔信息 documentInfo = pdfFileReader.getDocumentInfo() print('documentInfo = %s' % documentInfo) # 獲取頁面布局 pageLayout = pdfFileReader.getPageLayout() print('pageLayout = %s ' % pageLayout) # 獲取頁模式 pageMode = pdfFileReader.getPageMode() print('pageMode = %s' % pageMode) xmpMetadata = pdfFileReader.getXmpMetadata() print('xmpMetadata = %s ' % xmpMetadata) # 獲取 pdf 文件頁數 pageCount = pdfFileReader.getNumPages() print('pageCount = %s' % pageCount) for index in range(0, pageCount): # 返回指定頁編號的 pageObject pageObj = pdfFileReader.getPage(index) print('index = %d , pageObj = %s' % (index, type(pageObj))) # <class 'PyPDF2.pdf.PageObject'> # 獲取 pageObject 在 PDF 文檔中處於的頁碼 pageNumber = pdfFileReader.getPageNumber(pageObj) print('pageNumber = %s ' % pageNumber)
六: PDFFileWriter類
這個類支持PDF文件,給出其他類生成的頁面。
屬性和方法 | 描述 |
---|---|
addAttachment(fname,fdata) | 在 PDF 中嵌入文件 |
addBlankPage(width= None,height=None) | 追加一個空白頁面到這個 PDF 文件並返回它 |
addBookmark(title,pagenum,parent=None, color=None,bold=False,italic=False,fit=’/fit,*args’) |
|
addJS(javascript) | 添加將在打開此 PDF 是啟動的 javascript |
addLink(pagenum,pagedest,rect,border=None,fit=’/fit’,*args) | 從一個矩形區域添加一個內部鏈接到指定的頁面 |
addPage(page) | 添加一個頁面到這個PDF 文件,該頁面通常從 PdfFileReader 實例獲取 |
getNumpages() | 頁數 |
getPage(pageNumber) | 從這個 PDF 文件中檢索一個編號的頁面 |
insertBlankPage(width=None,height=None,index=0) | 插入一個空白頁面到這個 PDF 文件並返回它,如果沒有指定頁面大小,就使用最后一頁的大小 |
insertPage(page,index=0) | 在這個 PDF 文件中插入一個頁面,該頁面通常從 PdfFileReader 實例獲取 |
removeLinks() | 從次數出中刪除連接盒注釋 |
removeText(ignoreByteStringObject = False) | 從這個輸出中刪除圖像 |
write(stream) | 將添加到此對象的頁面集合寫入 PDF 文件 |
PDF寫入操作
def addBlankpage(): readFile = 'C:/study.pdf' outFile = 'C:/copy.pdf' pdfFileWriter = PdfFileWriter() # 獲取 PdfFileReader 對象 pdfFileReader = PdfFileReader(readFile) # 或者這個方式:pdfFileReader = PdfFileReader(open(readFile, 'rb')) numPages = pdfFileReader.getNumPages() for index in range(0, numPages): pageObj = pdfFileReader.getPage(index) pdfFileWriter.addPage(pageObj) # 根據每頁返回的 PageObject,寫入到文件 pdfFileWriter.write(open(outFile, 'wb')) pdfFileWriter.addBlankPage() # 在文件的最后一頁寫入一個空白頁,保存至文件中 pdfFileWriter.write(open(outFile,'wb'))
結果是:在寫入的 copy.pdf 文檔的最后最后一頁寫入了一個空白頁。
分割文檔(取第五頁之后的頁面)
def splitPdf(): readFile = 'C:/learn.pdf' outFile = 'C://copy.pdf' pdfFileWriter = PdfFileWriter() # 獲取 PdfFileReader 對象 pdfFileReader = PdfFileReader(readFile) # 或者這個方式:pdfFileReader = PdfFileReader(open(readFile, 'rb')) # 文檔總頁數 numPages = pdfFileReader.getNumPages() if numPages > 5: # 從第五頁之后的頁面,輸出到一個新的文件中,即分割文檔 for index in range(5, numPages): pageObj = pdfFileReader.getPage(index) pdfFileWriter.addPage(pageObj) # 添加完每頁,再一起保存至文件中 pdfFileWriter.write(open(outFile, 'wb'))
合並文檔
def mergePdf(inFileList, outFile): ''' 合並文檔 :param inFileList: 要合並的文檔的 list :param outFile: 合並后的輸出文件 :return: ''' pdfFileWriter = PdfFileWriter() for inFile in inFileList: # 依次循環打開要合並文件 pdfReader = PdfFileReader(open(inFile, 'rb')) numPages = pdfReader.getNumPages() for index in range(0, numPages): pageObj = pdfReader.getPage(index) pdfFileWriter.addPage(pageObj) # 最后,統一寫入到輸出文件中 pdfFileWriter.write(open(outFile, 'wb'))
PageObject類
class PyPDF2.pdf.PageObject(pdf = None,indirectRef = None )
此類表示 PDF 文件中的單個頁面,通常這個對象是通過訪問 PdfFileReader 對象的 getPage() 方法來得到的,也可以使用 createBlankPage() 靜態方法創建一個空的頁面。
參數:
- pdf : 頁面所屬的 PDF 文件。
- indirectRef:將源對象的原始間接引用存儲在其源 PDF 中。
PageObject 對象的屬性和方法
屬性或方法 | 描述 |
---|---|
static createBlankPage(pdf=None,width=None,height=None) | 返回一個新的空白頁面 |
extractText() | 找到所有文本繪圖命令,按照他們在內容流中提供的順序,並提取文本 |
getContents() | 訪問頁面內容,返回 Contents 對象或 None |
rotateClockwise(angle) | 順時針旋轉 90 度 |
scale(sx,sy) | 通過向其內容應用轉換矩陣並更新頁面大小 |
粗略讀取PDF文本內容
def getPdfContent(filename): pdf = PdfFileReader(open(filename, "rb")) content = "" for i in range(0, pdf.getNumPages()): pageObj = pdf.getPage(i) extractedText = pageObj.extractText() content += extractedText + "\n" # return content.encode("ascii", "ignore") return content