1. 程式人生 > >深入學習Python解析並解密PDF檔案內容的方法

深入學習Python解析並解密PDF檔案內容的方法

  前面學習瞭解析PDF文件,並寫入文件的知識,那篇文章的名字為深入學習Python解析並讀取PDF檔案內容的方法。

  但是最近出現了一個新問題,就是上面使用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

三: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檔案路徑的字串。

  • strictbool) - 確定是否應該警告使用者所有問題並且還會導致一些可糾正的問題致命。預設為True
  • warndest - 記錄警告的目的地(預設為 sys.stderr)。
  • overwriteWarningsbool) - 確定是否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檔案的文件資訊

getFieldstree = Noneretval = Nonefileobj = None 

  如果此PDF包括互動式表單欄位,則提取欄位資料,該樹和retval的引數是遞迴使用。

  引數:fileobj  用於在找到的所有互動式表單欄位上寫入報告的檔案物件(通常是文字檔案)

  返回:一個字典,其中每個鍵是一個欄位名稱,每個值都是一個個Field物件。預設情況下,對映名稱用於鍵。

  返回型別:dict  或者None無法找到表單資料。

getFormTextFields()

  使用文字資料從文件中檢索表單域(輸入,下拉列表)

getNameDestinations(tree=None,retval=None)

  檢索文件中存在的指定目標

  返回型別:字典

getNumPages()

  計算此PDF檔案中的頁面。

  返回:頁面

  返回型別:INT

  引發PDFReadError:如果檔案已加密且限制阻止此操作。

getOutlines(node=None,outlines=None)

  檢查文件中存在的文件大綱。

getPageLayout()

  獲取頁面佈局,有關setPageLayout() 有效佈局的說明,請參閱參考資料。

  返回:目前正在使用的頁面佈局

  返回型別:str None如果沒有指定。

getPageMode()

  獲取頁面佈局,有關setPageMode() 有效模式的說明,請參閱。

  返回:目前正在使用的頁面模式。

  返回型別strNone如果沒有指定。

getPageNumber()

  檢索給定PageObject的頁面。

  返回:頁碼或如果找不到頁面,則為-1

  返回型別:INT

getXmpMetadata()

  從PDF文件跟目錄中檢索XMP(可擴充套件元資料平臺)資料。

  返回型別XmpInformation或者 None如果在文件根目錄中未找到元資料。

isEncrypted

  只讀布林屬性,顯示此PDF檔案是否已加密。請注意,即使decrypt()呼叫該方法,此屬性(如果為true)仍將保持為true 。

namedDestinations

numPages

outlines

pageLayout

pageMode

pages

xmpMetadata

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