乐趣区

关于python: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 名收费(弹出付费信息能够疏忽)。

正在整顿代码和演示数据,群内公布交换。

退出移动版