@evilking
2019-04-26T03:37:42.000000Z
字数 6114
阅读 2651
Python
在一些正式场合,一般我们都会传 PDF,因为它不可修改,布局分析,方便阅读,在任何电脑上打开视觉效果都是一样的,比如发布求职简历.
对于我们 NLPer 处理 PDF 文件就是常有的事了,比如提取其中的文本和图片.
另外 PDF 在网络传输的时候由于其大小比较大,耗时会比较长,所以要对其进行压缩后传输; 仔细分析后发现,PDF 中文本本身并不是很大,主要是其中的图片比较大.
对于 NLP 操作来说,重要的是 PDF 中的文本内容,所以我们可以删除 PDF 中的图片后再传输,或者压缩其中的图片了再传输,这是两种思路,后面我们代码分别处理.
如果不考虑保留 PDF 的 metadata 信息,一种比较骚的操作是将 PDF 的每一页转换成图片,对图片压缩等处理之后再合并生成 PDF 文件,这也算是压缩了 PDF 文件,只是丢失了字体信息。
处理 PDF 文件的工具很多,比如 PyMuPDF 和 PdfBox.
本文就是用的 PyMuPDF 工具,它是 Python 对 MuPDF 工具的封装.
pip install [--upgrade] PyMuPDF
测试:
#!/usr/bin/pythonimport fitz, sys# 传入pdf文件名fname = sys.argv[1]# 打开文件,创建文件句柄doc = fitz.open(fname)# 循环pdf的每一页for page in doc:# 从 page 生成图片pixmap对象pix = page.getPixmap(alpha=False)# 将 pixmap 写到文件pix.writePNG("page-%i.png" % page.number)
上面这个小例子做了一件事,就是提取 PDF 文件的每一页生成一张 png 图片.
至此工具就按照好了
操作图像的包也有很多,比较常用的是 PyOpenCV 和 PIL 中的 Image.
这里我们已 PIL 为例来介绍,opencv 压缩图像可以参考: https://www.jianshu.com/p/9faeb9b8627a
PIL:Python Imaging Library,已经是Python平台事实上的图像处理标准库了。PIL功能非常强大,但API却非常简单易用。
由于PIL仅支持到Python 2.7,加上年久失修,于是一群志愿者在PIL的基础上创建了兼容的版本,名字叫Pillow,支持最新Python 3.x,又加入了许多新特性,因此,我们可以直接安装使用Pillow。
pip install pillow
如果遇到Permission denied安装失败,请加上sudo重试。
测试:
from PIL import Image# 打开一个jpg图片img = Image.open('test.jpg')# 获得图像尺寸w, h = img.size# 缩放到 50%img.thumbnail((w/2, h/2))print('Resize image to: %sx%s' % (w//2, h//2))# 保存缩放后的图像img.save('thumbnail.jpg', 'jpeg')
PIL 有很多其他的图像操作功能,可自行 google.
PyMuPDF 提取图片有多种方式,比较上层的 api 和 lower level 的 api.
pdf 文件各元素的组织形式是类似于 [xref, ...] 的列表,而图片的组织形式是 [xref, smask, width, height, ...] 这种,比较核心的就是 xref 这个分量,pdf 文件中操作各种对象都需要知道其 xref 索引.
import fitz, osdoc = fitz.open(fname)# page 的数量page_count = len(doc)xreflist = []imglist = []for pno in range(page_count):il = doc.getPageImageList(pno)# 第 0 个表示 xrefimglist.extend([x[0] for x in il])for img in il:xref = img[0]# 因为一个 pdf 可能有多个 xref 索引同一个 imageif xref in xreflist:continuewidth = img[2]height = img[3]if min(width, height) <= dimlimit:continue# 生成 pixmap 对象pix = fitz.Pixmap(doc, xref)if type(pix) is dict: # we got a raw imageext = pix["ext"]imgdata = pix["image"]n = pix["colorspace"]imgfile = os.path.join(imgdir, "img-%i.%s" % (xref, ext))else: # we got a pixmapimgfile = os.path.join(imgdir, "img-%i.png" % xref)n = pix.n# 得到 pixmap 中具体的图像数据imgdata = pix.getPNGData()if len(imgdata) <= abssize:continueif len(imgdata) / (width * height * n) <= relsize:continue# 将图像数据写到图片文件中fout = open(imgfile, "wb")fout.write(imgdata)fout.close()xreflist.append(xref)t1 = time.time()imglist = list(set(imglist))
这是官网中的一个例子,先得到每一页中的图像对象列表,从而获得图像的 xref,然后根据 xref 提取每个 pixmap 中具体的图像二进制数据,然后写到文件中.
虽然上面的代码看着挺复杂,核心步骤没多少,其中利用 xreflist 保存 xref 这步,是因为PDF中可能不同的图片具有同一个 xref 索引,这个操作就是为了处理这种情况的.
import fitz, re# 用于 metadata 查找的正则checkXO = r"/Type(?= */XObject)"checkIM = r"/Subtype(?= */Image)"doc = fitz.open(fname)print('totaling page count: ', doc.pageCount)imgcount = 0# 得到 xref 列表lenXREF = doc._getXrefLength()for xref in range(1, lenXREF):text = doc._getObjectString(xref)isXObject = re.search(checkXO, text)isImage = re.search(checkIM, text)if not isXObject or not isImage:continueimgcount += 1# 根据 xref 索引生成 pixmap 对象pix = fitz.Pixmap(doc, xref)# 转成 RGB 的 pixmappix = fitz.Pixmap(fitz.csRGB, pix)# 保存 pixmap 对象到本地文件pix.writePNG("img%s.png" % imgcount)
上面的代码中调用的是 pymupdf 比较底层的 api,运行速度就比较快.
根据 PDF 文件格式的定义,将其中的每个元素看成是一个 Object 对象,数据结构为 [xref, smask, ...] 这种,其中每个对象都有一个 xref 索引,通过该索引我们就可以操作指定对象了.
所以我们的思路就是先通过某种方法找到目标对象的 xref,然后通过索引得到目标对象,再做处理操作.
先介绍一种比较骚的操作,就是先用 pdf 生成一张张图片了,再对图片进行压缩,然后再将图片合并生成 pdf 文件.
import fitz, os, glob# 将每页pdf缩放、旋转后生成图片def pdf_compress(fname):doc = fitz.open(fname)# 按 a4 纸大小来计算每一页的宽高width, height = fitz.PaperSize("a4")totaling = doc.pageCountfor pg in range(totaling):# 每一页page = doc[pg]zoom = int(100)rotate = int(0)print(page)# 创建缩放、旋转矩阵trans = fitz.Matrix(zoom/100.0, zoom/100.0).preRotate(rotate)# 利用 matrix 创建 pixmap 对象,内部做了shupm = page.getPixmap(matrix=trans, alpha=False)lurl = 'pdf/%s.jpg' % str(pg + 1)pm.writePNG(lurl)doc.close()# 将生成的图片合并生成pdf文件def pic_2_pdf():# 打开一个 pdf 文件句柄doc = fitz.open()# 对指定目录下的图片按名称排序访问for img in sorted(glob.glob("pdf/*")):print(img)# 打开一个图片文件句柄imgdoc = fitz.open(img)# 将图片转成 pdf 字节流pdfbytes = imgdoc.convertToPDF()# 转成 pdf page对象imgpdf = fitz.open("pdf", pdfbytes)# 插入 doc 文档中doc.insertPDF(imgpdf)if os.path.exists("newpdf.pdf"):os.remove("newpdf.pdf")# 真正把生成好的 pdf 对象写到磁盘doc.save("newpdf.pdf")doc.close()
在压缩pdf的过程中因为使用了缩放,所以生成的图片大小会比较小; 但由于是把图片合并成 pdf,所以最后的pdf文件每一页就是一张图片,丢失了里面的字体信息,如果再要提取里面的字体,就需要通过 OCR 来识别了.
pdf 的结构组织形式就是按一个个 Object 拼起来的,所以我们可以找到其中的图片对象(找到图片对象的 xref),直接删除.
import fitz, recheckXO = r"/Type(?= */XObject)" # finds "/Type/XObject"checkIM = r"/Subtype(?= */Image)" # finds "/Subtype/Image"doc = fitz.open(fname)xreflen = doc._getXrefLength()for xref in range(1, xreflen):# 上面都是找图片对象的 xref# 也可以使用文章上面介绍的 getPageImageList() 方法text = doc._getObjectString(xref)isXObject = re.search(checkXO, text)isImage = re.search(checkIM, text)if not isXObject or not isImage:continue# 直接删除该图片对象doc._deleteObject(xref)# 将修改后的 pdf 写会磁盘doc.save("pdf/newpdf.pdf", clean=True)
这段代码就做了两件事,查找图片的 xref,通过 xref 删除该图片对象.
这么操作后生成的 pdf 去掉了图片,同时保留了 pdf 原有的文本字体信息,文件大小因为去掉了图片所以大大缩小.
还可以将 PDF 中的图片提取出来进行压缩,压缩完了再写回 PDF,从而实现 PDF 的压缩.
from PIL import Image, ImageFilterimport fitz, recheckXO = r"/Type(?= */XObject)" # finds "/Type/XObject"checkIM = r"/Subtype(?= */Image)" # finds "/Subtype/Image"doc = fitz.open(fname)xreflen = doc._getXrefLength()for xref in range(1, xreflen):text = doc._getObjectString(xref)isXObject = re.search(checkXO, text)isImage = re.search(checkIM, text)if not isXObject or not isImage:continue# 得到 xref 对应的对象字节流, 这里就是图像对象字节流stream = doc._getXrefStream(xref)if stream:pix = fitz.Pixmap(doc, xref)pix = fitz.Pixmap(fitz.csRGB, pix)# 根据 pixmap 生成 Image 对象img = Image.frombytes(“RGB”, [pix.width, pix.height], pix.samples)# img.show()# 插值缩放图片, 可减少图片大小img = img.resize((pix.width, pix.height), Image.ANTIALIAS)# 高通滤波压缩图片img = img.filter(ImageFilter.GaussianBlur)# updateStream 要求是字节数组或者二进制字符串stream = img.tobytes()# 修改 pdf 中的图片对象doc._updateStream(xref, stream)# 将修改后的 pdf 写会磁盘doc.save("pdf/newpdf.pdf", clean=True)
上一个方法是直接删掉了图片,但如果我们想保留图片又想压缩文件,就需要将图片压缩.
上面是将 pixmap 对象生成 Image 对象,利用 PIL 工具来压缩图片,压缩图片方法很多,其中 resize() 和 filter() 用的比较多,如果不考虑 RGB 颜色的影响,还可以将图像转成灰度图像,从而进行压缩。图像压缩部分涉及到图像处理的知识,读者可以自行百度.
比较重要的是 Document._updateStream(xref, stream) 方法,PyMuPDF 中对 pdf 的修改,内部基本都是调用的这个方法,更新 xref 对应的对象.
对 pdf 的操作可以压缩文件大小,减少网络传输时间,如果要对 pdf 中的头像进行提取,也涉及到 PDF 图片提取的问题.
PyMuPDF 工具中还有很多其他有意思的方法,比如查找里面的特定文本,给它加上下划线等等; 还有类似放大镜一样的,选择某个区域进行剪裁放大; 或者给 pdf 文件加上批注,这些都可以通过该工具实现,读者可以学习官网手册了解.
官网: https://pymupdf.readthedocs.io/en/latest/faq/