深入学习Python解析并解密PDF文件内容的方法

时间:2023-03-09 00:15:23
深入学习Python解析并解密PDF文件内容的方法

  前面学习了解析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可以安装包。

深入学习Python解析并解密PDF文件内容的方法

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文件:

深入学习Python解析并解密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)

  解析的时候,会主动触发异常(如下):

深入学习Python解析并解密PDF文件内容的方法

  那么,打开文件,我们会发现,实际情况是这样的:

深入学习Python解析并解密PDF文件内容的方法

  既然文件已经加密,那么正常渠道解析,肯定会触发异常,所以此时的重中之重就是解密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', '')

  运行结果如下:

深入学习Python解析并解密PDF文件内容的方法

  新生成的文件如下:

深入学习Python解析并解密PDF文件内容的方法

  打开是这样的:

深入学习Python解析并解密PDF文件内容的方法

  所以 ,这样的话 就可以打开了,也可以解析了,下面继续使用PDF解析文件解析,代码是上面的,结果如下:

深入学习Python解析并解密PDF文件内容的方法

  解析成功,那么会保存为txt格式。

但是这里要注意,我给解密的代码,把密码设置为abc,如下:

深入学习Python解析并解密PDF文件内容的方法

  那么会触发异常,代码结果表示如下:

深入学习Python解析并解密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', '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文件的文档信息

  返回类型DocumentInformation或者None如果不存在。

getFieldstree = Noneretval = Nonefileobj = 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() 有效模式的说明,请参阅。

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

  返回类型strNone如果没有指定。

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