Python自动化办公【PDF文件自动化】

PDF 文件可以分为可编辑型PDF 文件与扫描型PDF 文件,内容可以复制,是可编辑型PDF文件,反之则是扫描型PDF 文件。简单理解扫描型PDF文件是由一张张图像构建而成。

读取PDF文件内容

1.1 PDF文件原理简析

PDF 文件是日常工作中经常使用的文件格式之一,因为部分功能与Word文档类似,所以人们时常将PDF文件与Word文档联系在一起,但实际上PDF文件与Word文档完全不同,虽然两者在实际用途上有很多相同之处,但这只是表面现象,从实现原理层面来看,PDF文件与Word文档的差异远大于Word文档与Excel表格的差异。
PDF文件与Word文档在出现之初的定位就不相同,使用Word文档的主要目的是方便编辑,我们可以在Word文档上对内容轻松地编写和修改,但在不同操作系统下或用不同的Word编辑软件打开同一个Word文档,其样式可能存在差异;而PDF文件则不同使用PDF文件的目的是便于展示与传播,在任何操作系统下或使用任何PDF软件打开同一个PDF文件,其样式几乎没有差异。
PDF 文件结构主要由四大部分构成,分别是文件头(Header)、文件主体(Body)、交叉引用表(Cross-Reference Table)和文件尾(Trailer)。

(1)文件头:描述当前PDF文件遵从的PDF规范的版本号,它出现在PDF文件的第一行。
(2)文件主体:由一系列PDF对象构成,包括页面、文字、字体、图像等,每个对象都有唯一的ID编号。
(3)交叉引用表:指PDF文件中的对象索引表,存储了不同对象间的引用关系,这些对象可以相互引用、相互包含。
(4)文件尾:声明了交叉引用表的位置并描述了文件的根对象(Catalog),如果当前PDF文件是级、加密文件,那么文件尾还会保存加密信息。

通常,PDF软件在读取PDF文件内容时,首先会获取文件头中的版本号等信息,然后读取文件尾中的信息,并获取交叉引用表的地址,最后借助交叉引用表解析PDF文件中所有对象的包含与引用关系,并将其绘制出来。

PDF 文件结构的不同部分都有较强的依赖性,因此修改PDF文件中的部分内容就很容易影响其他内容,这也是PDF文件难以编辑的本质原因。

如果确实需要对PDF文件内容进行修改,建议找到PDF文件对应的源文件,在源文件上进行修改,它可能是Markdown 文件、Word 文档等,修改完成后再重新生成新的PDF文件,直接对PDF文件进行修改,会对PDF文件原有结构造成“污染”,如果修改内容较多,就容易造成PDF文件布局格式混乱,以及文件太大等问题。

1.2 读取PDF文件中文字

pymupdf 库可以轻松实现对PDF文件的读写操作,在使用前,需要先通过pip3进行安装:

pip3 install pymupdf

出于历史原因,在使用pymupdf 库时导入库名为 fitz,这一点需要注意。
pymupdf 库提供了getText() 方法,可以将PDF文件中某页的文字内容提取出来,同时只需要循环遍历整个PDF文件,便可将所有的内容提取出来:

import fitz
from pathlib import Path

# pip install pymupdf 由于历史原因 使用pympdf时 导入的库名为 fitz

pdf_path = Path("1.pdf")


def extract_all_ducument_text():
  # 提取PDF中所有的文字 缺点: 提取之后没有顺序
  # 打开pdf文件
  pdf = fitz.open(pdf_path)
  content = ""
  for page in pdf:
    # 获取文字
    text = page.getText()
    content += text
  with open("1.txt", "w") as f:
    f.write(content)

extract_all_ducument_text()

我们可以尝试尝试利用 pymupdf 库的块提取机制来解决提取内容的顺序问题。通过提取机制,可以获取很多额外信息,其中包括每块文字的bbox(bounding box,边界框)位置。

bbox 其实就是一个矩形框,它可以通过矩形的左上角和右下角来确定唯一的一个矩形。

利用提取机制获取的额外信息可以对每一页中获取的块进行排序,从而获取具有正常阅读顺序的文字:

import fitz
from pathlib import Path

pdf_path = Path("2.pdf")
# 从bbox按顺序获取文字
def extract_all_document_text_by_block():
  # 提取PDF中的所有文字 通过区块读取文字 实现顺序读取
  # block中有很多信息可以用于排序 从而获取正确的顺序
  pdf = fitz.open(pdf_path)
  content = []
  for page in pdf:
    # 获取文本块
    blocks = page.getText("blockes")
    print(blocks)
    # 对bbox的y0坐标进行排序 以获取正常的顺序
    # blocks = sorted(blocks, key=lambda x:x[1])
    content.extend(blocks)
  with open("2.txt", "w") as f:
    # content = "\n".join([_[4] for _ in content])
    f.write(str(content))

extract_all_document_text_by_block()

因为PDF 文件中文字是一行行顺序排列下来的,所以文本框对应的bbox的左上角的 y 坐标是不同的(右下角的y 坐标也不同),通过对文本框 y 坐标的排序,便可以获取具有正确阅读顺序的文字。
此外,pymupdf 库还提供了 page.getTextWords方法,可以获取每页中的文字框信息,该方法会返回与 getText('blocks') 方法一致的元组,只是其bbox 位置是具体的某单个文字的位置,而 blocks_data 也仅是单个文字的内容。该方法可以对一些复杂的PDF文件进行内容提取。

1.3 从PDF文件中提取图像

一个PDF文件通常会包含图像元素,图像作为PDF文件中的对象也会记录在交叉引用表中 。
在 pymupdf 库中,可以通过相应的方法获取交叉引用表中记录的对象ID 编号,并将其称为 xref整数。
如果知道了 xref 整数,就可以通过 fitz.Pimxmap(pdf,xref)方法获取相应的像素图。该方法执行速度非常快,但无法判断已获取的像素图的原始格式(如.png、.jpg等)。

import fitz
from pathlib import Path


imgdir = Path("images")

# 文件不存在 需要创建
if not imgdir.is_dir():
  imgdir.mkdir(parents=True)

def get_all_images(pdfpath):
  # 获取pdf中所有的图片
  pdf = fitz.open(pdfpath)

  xreflist = []
  for page_num in range(len(pdf)):
    # 获取某页所有的图片数据
    imgs = pdf.getPageImageList(page_num)
    for img in imgs:
      xref = img[0]
      if xref in xreflist:
        # 已经处理过 跳过
        continue
      # 获取图片信息
      pix = recoverpix(pdf, img)
      # 获取原始图像
      if isinstance(pix, dict):
        # 图像扩展名
        ext = pix["ext"]
        # 图像原始数据
        imgdata = pix["image"]
        # 图像颜色通道
        n = pix["colorspace"]
        # 图像保存路径
        imgfile = imgdir.joinpath(f"img-{xref}.{ext}")
      else:
        # 图像保存路径
        imgfile = imgdir.joinpath(f"img-{xref}.png")
        n = pix.n
        imgdata = pix.getImageData()
      if len(imgdata) <= 2048:
        # 图像大小至少大于或等于2KB 否则忽略
        continue
      # 保存图像
      print(imgfile)
      with open(imgfile, 'wb') as f:
        f.write(imgdata)
      # 不再重复处理相同的xref
      xreflist.append(xref)
      print(f"{imgfile} save")
      
  def main():
  pdfpath = Path("3.pdf")
  get_all_images(pdfpath)

至此,读取图像的大体框架已编写完成,但获取图像信息的 recoverpix 方法还没有编写:

def getimage(pix):
  # 像素色彩空间不为4 表示没有透明层
  if pix.colorspace.n != 4:
    return pix
  tpix = fitz.Pixmap(fitz.csRGB, pix)
  return tpix


def recoverpix(pdf, item):
  # 恢复图片 处理不同类型的图片 处理遮罩层
  xref = item[0]
  # xref 对应的遮罩层
  smask = item[0]
  if smask == 0:
    # 没有遮罩层 直接导出
    return pdf.extractImage(xref)

  pix1 = fitz.Pixmap(pdf, xref)
  pix2 = fitz.Pixmap(pdf, smask)
  # 完整性判断
  if not all([
    # 像素矩形相同
    pix1.irect  == pix2.irect,
    # 像素图都没有Alpha层
    pix1.alpha == pix2.alpha == 0,
    # pix2像素图每像素只有一维
    pix2.n == 1
  ]):
    pix2 = None
    return getimage(pix1)

  # 复制pix1 也用于添加alpha值
  pix = fitz.Pixmap(pix1)
  pix.setAlpha(pix2.samples)
  pix1 = pix2 = None
  return getimage(pix)

1.4 从PDF 文件中提取表格

PDF 文件中通常会有表格元素,如何提取表格元素中的内容呢?这里的“提取”有两层含义,一是提取表格中的文字,二是提取的文字依旧保持正常的顺序。
仔细观察PDF文件中的表格,可以发现表格都存在边界,有些表格的边界是可见的,而有些表格的边界是不可见的,我们如何猜测表格中不可见边界的位置呢?
pdfplumber 库提供的 extract_table 方法可以轻松提取PDF 文件中某页的所有表格,对于缺少边界的表格,pdfplumber 库会利用文本位置信息进行猜测,从而定位出不可见边界的位置。在使用 pdfplumber 库前需要通过pip3 进行安装。

pip3 install pdfplumber

使用 pdfplumber 库提取表格的方法非常简单,代码如下:

from pathlib import Path
import pdfplumber
import pandas as pd


def use_pdfplumber(pdfpath):
  pdf = pdfplumber.open(pdfpath)
  # 获取具有表格的某页pdf
  p0 = pdf.pages[0]
  # 获取pdf中的表格
  try:
    table = p0.extract_table()
    df = pd.DataFrame(table[1:], columns=table[0])
    df.to_csv("table1.csv")

  except Exception as e:
    print("无法解析pdf中的表格")
    raise e

pdfpath = Path("1.pdf")
print(pdfpath)
use_pdfplumber(pdfpath)

pdfplumber 库类似的库还有 camelot 库,该库同样需要安装:

pip3 install camelot

因为 camelot 库依赖 opencv-python 库,所以还需要安装 opencv-python 库:

pip3 install opencv-python

camelot 库提取 PDF 文件表格的代码非常简单,核心代码如下:

from pathlib import Path
import camelot
import cv2

def use_camelot(pdfpath):
  tables = camelot.read_pdf(str(pdfpath))
  tables.export("table2.csv", f="csv", compress=True)

pdfpath = Path("2.pdf")
print(pdfpath)
use_camelot(pdfpath)

虽然使用 pdfplumber 库与 camelot 库可以轻松提取 PDF 文件中的表格,但是很多表格布局独特,难以通过上述方法提取。经过多次试验,pdfplumber 库与 camelot 库可以提取具有标准样式的表格,以及部分无边界但文字对齐的表格。

对于一些PDF 文件中特殊布局的表格,首先,可以利用 pymupdf 库逐字提取,然后根据字体所在的位置编写相应的过滤提取逻辑。

2 PDF 文件基本操作

通过 Python 还可以对 PDF 文件进行多种基本操作,如添加文字、生成目录、加密等。通过 PyPDF4 第三方库来实现。

PyPDF4 与 PyPDF2 在功能上相似且同样易用,但需要注意两个库并不兼容。通过 pip3 便可以安装 PyPDF4:

pip3 install PyPDF4

2.1 给PDF 文件添加文字

通过前面内容的介绍,我们掌握了如何从 PDF 文件中读取数据,而与之对应的给 PDF 文件中添加文字也是常见的额需求,通过 pymupdf 库即可实现对文字的添加。
因为 pymupdf 库对 PDF 文件的操作接近底层,所以在添加文字时需要进行相应的设置,如指定文字的插入位置、文字使用的字体等。

import fitz
import PyPDF4


# 写入文字
# 创建新的pdf
pdf = fitz.open()
# 新的一页pdf
page = pdf.newPage()

start_x = 50
start_y = 50

# [内容, 字体]
# china-s 黑体
# china-ss 宋体
# china-t 繁体
texts = [["Hello PDF!"], ], ["你好", "china-ss"]

for text in texts:
  # 文字内容的起点
  p = fitz.Point(start_x, start_y)
  rc = page.insertText(
    p,
    text[0],
    fontname=text[1] if len(text) == 2 else "helv",
    fontsize=11,
    # rotate角度 其他可以用值 90, 180, 270
    rotate=0
  )
  # 下移
  start_y += 20

pdf.save("text.pdf")

因为 PDF 文件格式的复杂性,所以我们需要通过较为复杂的程序才能生成比较美观的 PDF 文件,相较于编写复杂的 PDF 文件生成程序,建议通过程序生成美观的 Word 文件,然后将 Word 文件转为 PDF 文件。

2.2 为 PDF 文件生成大纲

pymupdf 库提供了 getToC 方法与 setToC方法操作当前PDF 文件的大纲,其中 getToC 方法会获取当前 PDF 文件中的大纲,该方法会返回一个二维列表,形式为 [[lvl,title,page,dest],…],其含义如下:
(1)lvl 表示大纲的层级数,即一级目录、二级目录等。
(2)title 表示大纲的标题。
(3)page 表示当前大纲对应的页码,即单击该大纲会跳转到 PDF 文件中的那一页。
(4)dest 表示大纲的详细信息,只有当 getToC 方法的 simple 参数为 False 时才会返回。

而 setToC 方法可以为 PDF 文件设置新的大纲,因此利用 getToC 方法与 setToC 方法便可以为 PDF 文件生成新的大纲:

import fitz

# 生成大纲
pdf = fitz.open("text2.pdf")
# 获取pdf的大纲
toc = pdf.getToC()
print(toc)
# 清空
toc = []
# 添加
toc.append([1, "标签1", 1])
toc.append([1, "标签2", 2])
pdf.setToC(toc)
print(toc)
# 保存
pdf.saveIncr()

2.3 旋转 PDF 页面

有时需要对 PDF 文件中内容进行旋转才能达到更好的阅读效果。PyPDF4 库提供了 rotateClockwise 方法,可以轻松地旋转 PDF 文件中的内容:

import PyPDF4

# 旋转PDF页面
pdfReader = PyPDF4.PdfFileReader("text2.pdf")
# 获取第一页
page = pdfReader.getPage(0)
# 页面旋转90度
page.rotateClockwise(90)

pdfWriter = PyPDF4.PdfFileWriter()
pdfWriter.addPage(page)

with open("text2-rotation.pdf", "wb") as f:
  pdfWriter.write(f)

仔细观察旋转 PDF 文件内容的代码,可以发现利用 PyPDF4 库操作 PDF 文件可以大致分为以下3步:
(1)实例化 PdfFileReader 类,该类会将 PDF 文件的信息读入内存中。
(2)利用 PdfFileReader 类提供的方法对内存中的 PDF 文件信息进行修改。
(3)实例化 PdfFileWriter 类,将内存中修改后的 PDF 文件数据写入硬盘中。
其实,PyPDF4库对 PDF 文件的大部分操作都可以分为这3步,所以在后续的代码编写中,这3个步骤会经常用到。

2.4 加密 PDF 文件

在分享具有机密性质的 PDF 文件时,我们希望该文件不被第三方看到,此时就需要对 PDF 文件进行加密操作。通过 PyPDF4 库可以轻松实现对 PDF 文件的加密:

import PyPDF4

# 加密PDF
pdfReader = PyPDF4.PdfFileReader("text.pdf")
pdfWriter = PyPDF4.PdfFileWriter()

# 将内容读取并添加到pdfWriter中
for pagenum in range(pdfReader.numPages):
  pdfWriter.addPage(pdfReader.getPage(pagenum))

# 加密
pdfWriter.encrypt("123456")
with open("text-encrypt.pdf", "wb") as f:
  pdfWriter.write(f)

2.5 合并 PDF 文件

将多个 PDF 文件合并成一个大的 PDF 文件是常见的需求,利用 PyPDF4 库可以轻松将多个 PDF 文件合并:

import PyPDF4

# 合并pdf
pdfReaders = [PyPDF4.PdfFileReader("text.pdf"), PyPDF4.PdfFileReader("text2.pdf")]
pdfWriter = PyPDF4.PdfFileWriter()
for pdfReader in pdfReaders:
  for pagenum in range(pdfReader.numPages):
    page = pdfReader.getPage(pagenum)
    pdfWriter.addPage(page)

# 持久化
with open("text_text2.pdf", "wb") as f:
  pdfWriter.write(f)

2.6 给 PDF 文件添加水印

给 PDF 文件添加水印的本质就是将水印文件合并到PDF 文件中。
添加水印的第一步是生成水印文件,可以通过 reportlab 库来生成水印文件。reportlab 库可以快速创建 PDF 文件,以及各种位图和矢量图,在使用前需要通过 pip3 进行安装:

pip3 install reportlab

安装完成后便可以使用 reportlab 库,通过 reportlab 库生成水印文件的代码如下:

from reportlab.pdfgen import canvas
from reportlab.lib.units import cm

# 给PDF添加水印
def create_watermark(content):
  # 创建水印
  file_name = "watermark.pdf"
  # 创建水印画布
  c = canvas.Canvas(file_name, pagesize = (30 * cm, 30 * cm))
  # 移动坐标原点[坐标系左下为(0, 0)]
  c.translate(10 * cm, 2 * cm)
  # 设置字体
  c.setFont("Helvetica", 80)
  # 指定描边的颜色
  c.setStrokeColorRGB(0, 1, 0)
  # 指定填充颜色
  c.setFillColorRGB(0, 1, 0)
  # 旋转45度 坐标系被转换
  c.rotate(45)
  # 指定填充颜色
  c.setFillColorRGB(0.6, 0, 0)
  # 设置透明度 1为不透明
  c.setFillAlpha(0.2)
  # 绘制文本
  c.drawString(3 * cm, 0 * cm, content)
  # 设置透明度
  c.setFillAlpha(0.4)
  # 关闭并保存文件
  c.save()
  return file_name

水印文件创建好后便可以通过 PyPDF4 库将该 PDF 文件与要添加水印的 PDF 文件合并:

from reportlab.pdfgen import canvas
from reportlab.lib.units import cm
from pathlib import Path
from PyPDF4 import PdfFileReader, PdfFileWriter


# 给PDF添加水印
def create_watermark(content):
  # 创建水印
  file_name = "watermark.pdf"
  # 创建水印画布
  c = canvas.Canvas(file_name, pagesize = (30 * cm, 30 * cm))
  # 移动坐标原点[坐标系左下为(0, 0)]
  c.translate(10 * cm, 2 * cm)
  # 设置字体
  c.setFont("Helvetica", 80)
  # 指定描边的颜色
  c.setStrokeColorRGB(0, 1, 0)
  # 指定填充颜色
  c.setFillColorRGB(0, 1, 0)
  # 旋转45度 坐标系被转换
  c.rotate(45)
  # 指定填充颜色
  c.setFillColorRGB(0.6, 0, 0)
  # 设置透明度 1为不透明
  c.setFillAlpha(0.2)
  # 绘制文本
  c.drawString(3 * cm, 0 * cm, content)
  # 设置透明度
  c.setFillAlpha(0.4)
  # 关闭并保存文件
  c.save()
  return file_name


def add_watermark(input_pdf, output):
  # 添加水印
  watermark = Path("watermark.pdf")
  
  # 如果文件不存在则创建水印
  if not watermark.is_file():
    create_watermark("Python!!!")
  
  watermark_obj = PdfFileReader(str(watermark))
  watermark_page = watermark_obj.getPage(0)
  
  pdf_reader = PdfFileReader(input_pdf)
  pdf_writer = PdfFileWriter()
  
  # 给所有页面添加水印
  for page in range(pdf_reader.getNumPages()):
    page = pdf_reader.getPage(page)
    page.mergePage(watermark_page)
    pdf_writer.addPage(page)
  
  with open(output, "wb") as out:
    pdf_writer.write(out)

add_watermark("text.pdf", "text-watermark.pdf")
  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值