@evilking
2019-04-26T11:37:42.000000Z
字数 6114
阅读 2398
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/python
import 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, os
doc = fitz.open(fname)
# page 的数量
page_count = len(doc)
xreflist = []
imglist = []
for pno in range(page_count):
il = doc.getPageImageList(pno)
# 第 0 个表示 xref
imglist.extend([x[0] for x in il])
for img in il:
xref = img[0]
# 因为一个 pdf 可能有多个 xref 索引同一个 image
if xref in xreflist:
continue
width = 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 image
ext = pix["ext"]
imgdata = pix["image"]
n = pix["colorspace"]
imgfile = os.path.join(imgdir, "img-%i.%s" % (xref, ext))
else: # we got a pixmap
imgfile = os.path.join(imgdir, "img-%i.png" % xref)
n = pix.n
# 得到 pixmap 中具体的图像数据
imgdata = pix.getPNGData()
if len(imgdata) <= abssize:
continue
if 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:
continue
imgcount += 1
# 根据 xref 索引生成 pixmap 对象
pix = fitz.Pixmap(doc, xref)
# 转成 RGB 的 pixmap
pix = 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.pageCount
for 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 对象,内部做了shu
pm = 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, re
checkXO = 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, ImageFilter
import fitz, re
checkXO = 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/