[关闭]
@evilking 2019-04-26T11:37:42.000000Z 字数 6114 阅读 2398

Python

PDF 操作

在一些正式场合,一般我们都会传 PDF,因为它不可修改,布局分析,方便阅读,在任何电脑上打开视觉效果都是一样的,比如发布求职简历.

对于我们 NLPer 处理 PDF 文件就是常有的事了,比如提取其中的文本和图片.

另外 PDF 在网络传输的时候由于其大小比较大,耗时会比较长,所以要对其进行压缩后传输; 仔细分析后发现,PDF 中文本本身并不是很大,主要是其中的图片比较大.

对于 NLP 操作来说,重要的是 PDF 中的文本内容,所以我们可以删除 PDF 中的图片后再传输,或者压缩其中的图片了再传输,这是两种思路,后面我们代码分别处理.

如果不考虑保留 PDF 的 metadata 信息,一种比较骚的操作是将 PDF 的每一页转换成图片,对图片压缩等处理之后再合并生成 PDF 文件,这也算是压缩了 PDF 文件,只是丢失了字体信息。

工具

处理 PDF 文件的工具很多,比如 PyMuPDF 和 PdfBox.

本文就是用的 PyMuPDF 工具,它是 Python 对 MuPDF 工具的封装.

安装 PyMuPDF

  1. pip install [--upgrade] PyMuPDF

测试:

  1. #!/usr/bin/python
  2. import fitz, sys
  3. # 传入pdf文件名
  4. fname = sys.argv[1]
  5. # 打开文件,创建文件句柄
  6. doc = fitz.open(fname)
  7. # 循环pdf的每一页
  8. for page in doc:
  9. # 从 page 生成图片pixmap对象
  10. pix = page.getPixmap(alpha=False)
  11. # 将 pixmap 写到文件
  12. pix.writePNG("page-%i.png" % page.number)

上面这个小例子做了一件事,就是提取 PDF 文件的每一页生成一张 png 图片.

至此工具就按照好了

安装 PIL

操作图像的包也有很多,比较常用的是 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。

  1. pip install pillow

如果遇到Permission denied安装失败,请加上sudo重试。

测试:

  1. from PIL import Image
  2. # 打开一个jpg图片
  3. img = Image.open('test.jpg')
  4. # 获得图像尺寸
  5. w, h = img.size
  6. # 缩放到 50%
  7. img.thumbnail((w/2, h/2))
  8. print('Resize image to: %sx%s' % (w//2, h//2))
  9. # 保存缩放后的图像
  10. img.save('thumbnail.jpg', 'jpeg')

PIL 有很多其他的图像操作功能,可自行 google.

提取 PDF 中的图片

PyMuPDF 提取图片有多种方式,比较上层的 api 和 lower level 的 api.

pdf 文件各元素的组织形式是类似于 [xref, ...] 的列表,而图片的组织形式是 [xref, smask, width, height, ...] 这种,比较核心的就是 xref 这个分量,pdf 文件中操作各种对象都需要知道其 xref 索引.

high level api

  1. import fitz, os
  2. doc = fitz.open(fname)
  3. # page 的数量
  4. page_count = len(doc)
  5. xreflist = []
  6. imglist = []
  7. for pno in range(page_count):
  8. il = doc.getPageImageList(pno)
  9. # 第 0 个表示 xref
  10. imglist.extend([x[0] for x in il])
  11. for img in il:
  12. xref = img[0]
  13. # 因为一个 pdf 可能有多个 xref 索引同一个 image
  14. if xref in xreflist:
  15. continue
  16. width = img[2]
  17. height = img[3]
  18. if min(width, height) <= dimlimit:
  19. continue
  20. # 生成 pixmap 对象
  21. pix = fitz.Pixmap(doc, xref)
  22. if type(pix) is dict: # we got a raw image
  23. ext = pix["ext"]
  24. imgdata = pix["image"]
  25. n = pix["colorspace"]
  26. imgfile = os.path.join(imgdir, "img-%i.%s" % (xref, ext))
  27. else: # we got a pixmap
  28. imgfile = os.path.join(imgdir, "img-%i.png" % xref)
  29. n = pix.n
  30. # 得到 pixmap 中具体的图像数据
  31. imgdata = pix.getPNGData()
  32. if len(imgdata) <= abssize:
  33. continue
  34. if len(imgdata) / (width * height * n) <= relsize:
  35. continue
  36. # 将图像数据写到图片文件中
  37. fout = open(imgfile, "wb")
  38. fout.write(imgdata)
  39. fout.close()
  40. xreflist.append(xref)
  41. t1 = time.time()
  42. imglist = list(set(imglist))

这是官网中的一个例子,先得到每一页中的图像对象列表,从而获得图像的 xref,然后根据 xref 提取每个 pixmap 中具体的图像二进制数据,然后写到文件中.

虽然上面的代码看着挺复杂,核心步骤没多少,其中利用 xreflist 保存 xref 这步,是因为PDF中可能不同的图片具有同一个 xref 索引,这个操作就是为了处理这种情况的.

lower level api

  1. import fitz, re
  2. # 用于 metadata 查找的正则
  3. checkXO = r"/Type(?= */XObject)"
  4. checkIM = r"/Subtype(?= */Image)"
  5. doc = fitz.open(fname)
  6. print('totaling page count: ', doc.pageCount)
  7. imgcount = 0
  8. # 得到 xref 列表
  9. lenXREF = doc._getXrefLength()
  10. for xref in range(1, lenXREF):
  11. text = doc._getObjectString(xref)
  12. isXObject = re.search(checkXO, text)
  13. isImage = re.search(checkIM, text)
  14. if not isXObject or not isImage:
  15. continue
  16. imgcount += 1
  17. # 根据 xref 索引生成 pixmap 对象
  18. pix = fitz.Pixmap(doc, xref)
  19. # 转成 RGB 的 pixmap
  20. pix = fitz.Pixmap(fitz.csRGB, pix)
  21. # 保存 pixmap 对象到本地文件
  22. pix.writePNG("img%s.png" % imgcount)

上面的代码中调用的是 pymupdf 比较底层的 api,运行速度就比较快.

根据 PDF 文件格式的定义,将其中的每个元素看成是一个 Object 对象,数据结构为 [xref, smask, ...] 这种,其中每个对象都有一个 xref 索引,通过该索引我们就可以操作指定对象了.

所以我们的思路就是先通过某种方法找到目标对象的 xref,然后通过索引得到目标对象,再做处理操作.

压缩 PDF

pdf 转图片

先介绍一种比较骚的操作,就是先用 pdf 生成一张张图片了,再对图片进行压缩,然后再将图片合并生成 pdf 文件.

  1. import fitz, os, glob
  2. # 将每页pdf缩放、旋转后生成图片
  3. def pdf_compress(fname):
  4. doc = fitz.open(fname)
  5. # 按 a4 纸大小来计算每一页的宽高
  6. width, height = fitz.PaperSize("a4")
  7. totaling = doc.pageCount
  8. for pg in range(totaling):
  9. # 每一页
  10. page = doc[pg]
  11. zoom = int(100)
  12. rotate = int(0)
  13. print(page)
  14. # 创建缩放、旋转矩阵
  15. trans = fitz.Matrix(zoom/100.0, zoom/100.0).preRotate(rotate)
  16. # 利用 matrix 创建 pixmap 对象,内部做了shu
  17. pm = page.getPixmap(matrix=trans, alpha=False)
  18. lurl = 'pdf/%s.jpg' % str(pg + 1)
  19. pm.writePNG(lurl)
  20. doc.close()
  21. # 将生成的图片合并生成pdf文件
  22. def pic_2_pdf():
  23. # 打开一个 pdf 文件句柄
  24. doc = fitz.open()
  25. # 对指定目录下的图片按名称排序访问
  26. for img in sorted(glob.glob("pdf/*")):
  27. print(img)
  28. # 打开一个图片文件句柄
  29. imgdoc = fitz.open(img)
  30. # 将图片转成 pdf 字节流
  31. pdfbytes = imgdoc.convertToPDF()
  32. # 转成 pdf page对象
  33. imgpdf = fitz.open("pdf", pdfbytes)
  34. # 插入 doc 文档中
  35. doc.insertPDF(imgpdf)
  36. if os.path.exists("newpdf.pdf"):
  37. os.remove("newpdf.pdf")
  38. # 真正把生成好的 pdf 对象写到磁盘
  39. doc.save("newpdf.pdf")
  40. doc.close()

在压缩pdf的过程中因为使用了缩放,所以生成的图片大小会比较小; 但由于是把图片合并成 pdf,所以最后的pdf文件每一页就是一张图片,丢失了里面的字体信息,如果再要提取里面的字体,就需要通过 OCR 来识别了.

删除 PDF 中的图片对象

pdf 的结构组织形式就是按一个个 Object 拼起来的,所以我们可以找到其中的图片对象(找到图片对象的 xref),直接删除.

  1. import fitz, re
  2. checkXO = r"/Type(?= */XObject)" # finds "/Type/XObject"
  3. checkIM = r"/Subtype(?= */Image)" # finds "/Subtype/Image"
  4. doc = fitz.open(fname)
  5. xreflen = doc._getXrefLength()
  6. for xref in range(1, xreflen):
  7. # 上面都是找图片对象的 xref
  8. # 也可以使用文章上面介绍的 getPageImageList() 方法
  9. text = doc._getObjectString(xref)
  10. isXObject = re.search(checkXO, text)
  11. isImage = re.search(checkIM, text)
  12. if not isXObject or not isImage:
  13. continue
  14. # 直接删除该图片对象
  15. doc._deleteObject(xref)
  16. # 将修改后的 pdf 写会磁盘
  17. doc.save("pdf/newpdf.pdf", clean=True)

这段代码就做了两件事,查找图片的 xref,通过 xref 删除该图片对象.

这么操作后生成的 pdf 去掉了图片,同时保留了 pdf 原有的文本字体信息,文件大小因为去掉了图片所以大大缩小.

压缩 PDF 中的图片对象

还可以将 PDF 中的图片提取出来进行压缩,压缩完了再写回 PDF,从而实现 PDF 的压缩.

  1. from PIL import Image, ImageFilter
  2. import fitz, re
  3. checkXO = r"/Type(?= */XObject)" # finds "/Type/XObject"
  4. checkIM = r"/Subtype(?= */Image)" # finds "/Subtype/Image"
  5. doc = fitz.open(fname)
  6. xreflen = doc._getXrefLength()
  7. for xref in range(1, xreflen):
  8. text = doc._getObjectString(xref)
  9. isXObject = re.search(checkXO, text)
  10. isImage = re.search(checkIM, text)
  11. if not isXObject or not isImage:
  12. continue
  13. # 得到 xref 对应的对象字节流, 这里就是图像对象字节流
  14. stream = doc._getXrefStream(xref)
  15. if stream:
  16. pix = fitz.Pixmap(doc, xref)
  17. pix = fitz.Pixmap(fitz.csRGB, pix)
  18. # 根据 pixmap 生成 Image 对象
  19. img = Image.frombytes(“RGB”, [pix.width, pix.height], pix.samples)
  20. # img.show()
  21. # 插值缩放图片, 可减少图片大小
  22. img = img.resize((pix.width, pix.height), Image.ANTIALIAS)
  23. # 高通滤波压缩图片
  24. img = img.filter(ImageFilter.GaussianBlur)
  25. # updateStream 要求是字节数组或者二进制字符串
  26. stream = img.tobytes()
  27. # 修改 pdf 中的图片对象
  28. doc._updateStream(xref, stream)
  29. # 将修改后的 pdf 写会磁盘
  30. 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/

提取图片: http://blog.topspeedsnail.com/archives/9404

PIL: https://www.kancloud.cn/thinkphp/python-guide/39428

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注