Python处理PDF的实用姿势

你不懂得安排自己的人生,会有很多人帮你安排,他们需要你做的事。

PDF文件我们经常用,尤其是这两个场景:

  • 下载参考资料,如各类报告、文档
  • 分享只读资料,方便传播同时保留源文件

场景和模块

所以,对于PDF文件,常见的需求也就是两类:

  • 处理文件本身,属于文件页面级操作,如合并/分拆PDF页面、加/解密、加/去水印;
  • 处理文件内容,属于内容级操作,如提取文字、表格数据、图表等。

目前Python用于处理PDF的模块,主要有3个:

  • PyPDF2:模块成熟,最后一次更新在2年前,适合页面级操作,文字提取效果较差。
  • PDFMiner:擅长文字抽取,目前主分支已停止维护,取而代之的是pdfminer.six
  • pdfplumber:基于pdfminer.six的文本内容抽取工具,使用门槛更低,如支持表格提取。

实战中,可以根据需求的类型选择模块。如果是页面级的操作,就用PyPDF2,如果需要内容抽取,优先使用pdfplumber

对应的模块安装:

  • pip install pypdf2
  • pip install pdfminer.six
  • pip install pdfplumber

下面按使用场景演示3个模块的使用。

PyPDF2

PyPDF2的主要能力在页面级操作,比如:

  • 获取PDF文档基本信息
  • PDF分割及合并
  • PDF的旋转及排序
  • PDF加水印及去水印
  • PDF加密及解密

PyPDF2的核心两个类是PdfFileReaderPdfFileWriter,完成PDF文件的读写操作。

获取PDF文档基本信息
import pathlib
from PyPDF2 import PdfFileReader

path = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')
f_path = path.joinpath('2020-新冠肺炎疫情对中国连锁餐饮行业的影响调研报告-中国连锁经营协会.pdf')
with open(f_path, 'rb') as f:
    pdf = PdfFileReader(f)
    info = pdf.getDocumentInfo()
    cnt_page = pdf.getNumPages()
    is_encrypt = pdf.getIsEncrypted()
print(f'''
作者: {info.author}
创建者: {info.creator}
制作者: {info.producer}
主题: {info.subject}
标题: {info.title}
总页数: {cnt_page}
是否加密: {is_encrypt}
''')
PDF分割及合并
import pathlib
from PyPDF2 import PdfFileReader, PdfFileWriter

path = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')
f_path = path.joinpath('2020-新冠肺炎疫情对中国连锁餐饮行业的影响调研报告-中国连锁经营协会.pdf')
out_path = path.joinpath('002pdf_split_merge.pdf')
out_path_1 = path.joinpath('002pdf_split_half_front.pdf')
out_path_2 = path.joinpath('002pdf_split_half_back.pdf')
# 把文件分为两半
with open(f_path, 'rb') as f, open(out_path_1, 'wb') as f_out1, open(out_path_2, 'wb') as f_out2:
    pdf = PdfFileReader(f)
    pdf_out1 = PdfFileWriter()
    pdf_out2 = PdfFileWriter()
    cnt_pages = pdf.getNumPages()
    print(f'共 {cnt_pages} 页')
    for i in range(cnt_pages):
        if i <= cnt_pages //2:
            pdf_out1.addPage(pdf.getPage(i))
        else:
            pdf_out2.addPage(pdf.getPage(i))
    pdf_out1.write(f_out1)
    pdf_out2.write(f_out2)
# 再把后半个文件与前半个文件合并,后半个文件在前
with open(out_path, 'wb') as f_out:
    cnt_f, cnt_b = pdf_out1.getNumPages(), pdf_out2.getNumPages()
    pdf_out = PdfFileWriter()
    for i in range(cnt_b):
        pdf_out.addPage(pdf_out2.getPage(i))
    for i in range(cnt_f):
        pdf_out.addPage(pdf_out1.getPage(i))
    pdf_out.write(f_out)
PDF的旋转及排序
import pathlib
from PyPDF2 import PdfFileReader, PdfFileWriter

path = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')
f_path = path.joinpath('2020-新冠肺炎疫情对中国连锁餐饮行业的影响调研报告-中国连锁经营协会.pdf')
out_path = path.joinpath('002pdf_rotate.pdf')

with open(f_path, 'rb') as f, open(out_path, 'wb') as f_out:
    pdf = PdfFileReader(f)
    pdf_out = PdfFileWriter()
    page = pdf.getPage(0).rotateClockwise(90)
    pdf_out.addPage(page)
    # 把第二页放到前面
    pdf_out.addPage(pdf.getPage(2))
    page = pdf.getPage(1).rotateCounterClockwise(90)
    pdf_out.addPage(page)
    pdf_out.write(f_out)
PDF加水印及去水印

加图片水印,其实就是在页面中增加一个透明背景的图片,通过页面的mergePage方法即可完成。

import pathlib
from PyPDF2 import PdfFileReader, PdfFileWriter

path = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')
f_path = path.joinpath('2020-新冠肺炎疫情对中国连锁餐饮行业的影响调研报告-中国连锁经营协会.pdf')
wm_path = path.joinpath('watermark.pdf')
en_path = path.joinpath('002pdf_with_watermark_en.pdf')
out_path = path.joinpath('002pdf_with_watermark.pdf')

with open(f_path, 'rb') as f, open(wm_path, 'rb') as f_wm, open(out_path, 'wb') as f_out:
    pdf = PdfFileReader(f)
    pdf_wm = PdfFileReader(f_wm)
    pdf_out = PdfFileWriter()
    wm_cn_page = pdf_wm.getPage(0)
    wm_en_page = pdf_wm.getPage(1)
    cnt_pages = pdf.getNumPages()
    for i in range(cnt_pages):
        page = pdf.getPage(i)
        page.mergePage(wm_cn_page)
        pdf_out.addPage(page)
    pdf_out.write(f_out)

去水印,就比较复杂,需要根据不同情况具体分析。因为水印可能是文字、图片或者各种组合,关键是识别出特征。

去水印的3个常见思路参考:

  1. 找到特征词后替换,适合英文文档,但不适用于中文等CJK字符。
  2. 把PDF页转成图片后,用图像算法去水印,但这样会破坏文件原信息结构。
  3. 根据水印大小位置特征,找到所有元素后删除。这是更推荐的方式。

第3种方式效果最好,但如果碰到一些复杂的文档水印,就非常考验耐心。

你得一个个识别操作命令,一边替换一边检查效果,直到水印成功去除。

但,未必剩下的所有页都可以用同样特征模式来消除,因为这份PDF可能经过多人加水印,已经包含多种加水印方式。

所以,去水印并没有一种100%安全有效(不错删信息)且通用的方法。

加水印、去水印本质上是一种攻防策略

比如一些工具推出去水印功能,一旦公开,加水印方就能识别并避开它的去除方法。

最后,尊重版权,是每个人应有的态度。

除了学习外,正式使用时,应该遵守内容创作方的规则。

PDF加密解密

PDF里的密码,分为用户密码和所有者密码。

PyPDF2里提供了基本的加密功能,“防君子不防小人”。

如果打开PDF文件后,复制了新文件,那新文件就不受所有者密码的约束,可被修改。

import pathlib
from PyPDF2 import PdfFileReader, PdfFileWriter

path = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')
f_path = path.joinpath('2020-新冠肺炎疫情对中国连锁餐饮行业的影响调研报告-中国连锁经营协会.pdf')
out_path_encrypt = path.joinpath('002pdf_encrypt.pdf')
out_path_decrypt = path.joinpath('002pdf_decrypt.pdf')

with open(f_path, 'rb') as f, open(out_path_encrypt, 'wb') as f_out:
    pdf = PdfFileReader(f)
    pdf_out = PdfFileWriter()
    cnt_pages = pdf.getNumPages()
    for i in range(cnt_pages):
        page = pdf.getPage(i)
        pdf_out.addPage(page)
    pdf_out.encrypt('123456', owner_pwd='654321')
    pdf_out.write(f_out)
# 重新读取加密文件并生成解密文件
with open(out_path_encrypt, 'rb') as f, open(out_path_decrypt, 'wb') as f_out:
    pdf = PdfFileReader(f)
    if not pdf.isEncrypted:
        print('文件未被加密')
    else:
        success = pdf.decrypt('123456')
        # if not success:
        pdf_out = PdfFileWriter()
        pdf_out.appendPagesFromReader(pdf)
        pdf_out.write(f_out)

pdfminer.six

PDFMiner的操作门槛比较高,需要部分了解PDF的文档结构模型,适合定制开发复杂的内容处理工具。

平时直接用PDFMiner比较少,这里只演示基本的文档内容操作:

import pathlib
from pdfminer.pdfparser import PDFParser
from pdfminer.pdfdocument import PDFDocument
from pdfminer.pdfpage import PDFPage
from pdfminer.pdfinterp import PDFResourceManager
from pdfminer.pdfinterp import PDFPageInterpreter
from pdfminer.pdfdevice import PDFDevice
from pdfminer.layout import LAParams, LTTextBox, LTFigure, LTImage
from pdfminer.converter import PDFPageAggregator

path = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')
f_path = path.joinpath('2020-新冠肺炎疫情对中国连锁餐饮行业的影响调研报告-中国连锁经营协会.pdf')

with open(f_path, 'rb') as f:
    parser = PDFParser(f)
    doc = PDFDocument(parser)
    rsrcmgr = PDFResourceManager()
    laparams = LAParams()
    device = PDFPageAggregator(rsrcmgr, laparams=laparams)
    interpreter = PDFPageInterpreter(rsrcmgr, device)
    for page in PDFPage.create_pages(doc):
        interpreter.process_page(page)
        layout = device.get_result()
        for x in layout:
            # 获取文本对象
            if isinstance(x, LTTextBox):
                print(x.get_text().strip())
            # 获取图片对象
            if isinstance(x,LTImage):
                print('这里获取到一张图片')
            # 获取 figure 对象
            if isinstance(x,LTFigure):
                print('这里获取到一个 figure 对象')

虽然pdfminer使用门槛较高,但遇到复杂情况,最后还得用它。目前开源模块中,它对PDF的支持应该是最全的了。

下面这个pdfplumber就是基于pdfminer.six开发的模块,降低了使用门槛。

pdfplumber

相比pdfminer.sixpdfplumber提供了更便捷的PDF内容抽取接口。

日常工作中常用的操作,比如:

  • 提取PDF内容,保存到txt文件
  • 提取PDF中的表格到Excel
  • 提取PDF中的图片
  • 提取PDF中的图表
提取PDF内容,保存到txt文件
import pathlib
import pdfplumber

path = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')
f_path = path.joinpath('2020-新冠肺炎疫情对中国连锁餐饮行业的影响调研报告-中国连锁经营协会.pdf')
out_path = path.joinpath('002pdf_out.txt')

with pdfplumber.open(f_path) as pdf, open(out_path ,'a') as txt:
    for page in pdf.pages:
        textdata = page.extract_text()
        txt.write(textdata)
提取PDF中的表格到Excel
import pathlib
import pdfplumber
from openpyxl import Workbook

path = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')
f_path = path.joinpath('2020-新冠肺炎疫情对中国连锁餐饮行业的影响调研报告-中国连锁经营协会.pdf')
out_path = path.joinpath('002pdf_excel.xlsx')

wb = Workbook()
sheet = wb.active
with pdfplumber.open(f_path) as pdf:
    for i in range(19, 22):
        page = pdf.pages[i]
        table = page.extract_table()
        for row in table:
            sheet.append(row)
wb.save(out_path)

上面用到了openpyxl的功能创建了一个Excel文件,后面会有单独文章介绍它。

提取PDF中的图片
import pathlib
import pdfplumber
from PIL import Image

path = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')
f_path = path.joinpath('2020-疫情影响下的中国社区趋势研究-艾瑞.pdf')
out_path = path.joinpath('002pdf_images.png')
with pdfplumber.open(f_path) as pdf, open(out_path, 'wb') as fout:
    page = pdf.pages[10]
    # for img in page.images:
    im = page.to_image()
    im.save(out_path, format='PNG')
    imgs = page.images
    for i, img in enumerate(imgs):
        size = img['width'], img['height']
        data = img['stream'].get_data()
        out_path = path.joinpath(f'002pdf_images_{i}.png')
        with open(out_path, 'wb') as fimg_out:
            fimg_out.write(data)

上面用到了PILPillow)的功能处理图片。

提取PDF中的图表

图表与图像不同,指的是类似直方图、饼图之类的数据生成图。

import pathlib
import pdfplumber
from PIL import Image

path = list(pathlib.Path.cwd().parents)[1].joinpath('data/automate/002pdf')
f_path = path.joinpath('2020-新冠肺炎疫情对中国连锁餐饮行业的影响调研报告-中国连锁经营协会.pdf')
out_path = path.joinpath('002pdf_figures.png')
with pdfplumber.open(f_path) as pdf, open(out_path, 'wb') as fout:
    page = pdf.pages[7]
    im = page.to_image()
    im.save(out_path, format='PNG')
    figures = page.figures
    for i, fig in enumerate(figures):
        size = fig['width'], fig['height']
        crop = page.crop((fig['x0'], fig['top'], fig['x1'], fig['bottom']))
        img_crop = crop.to_image()
        out_path = path.joinpath(f'002pdf_figures_{i}.png')
        img_crop.save(out_path, format='png')
    im.draw_rects(page.extract_words(), stroke='yellow')
    im.draw_rects(page.images, stroke='blue')
    im.draw_rects(page.figures)
im # show in notebook

总结

本文介绍了PDF的常见使用场景,以及Python处理PDF的3个主要模块。

补充一点,PDF标准规范由Adobe公司主导。

平时我们不需要参考规范,但如果遇到一些较复杂的场景,尤其是模块没有直接支持,就只能硬着头皮翻阅文档了。文档是公开的,可以去搜索引擎搜索关键词:pdf_reference_1-7.pdf

最后,建个学习群,有兴趣的可以加入,前100名免费(弹出付费信息可以忽略)。

正在整理代码和演示数据,群内发布交流。

程一初6

你可能感兴趣的:(python,自动化,文件处理)