PDF 文件可以分为可编辑型PDF 文件与扫描型PDF 文件,内容可以复制,是可编辑型PDF文件,反之则是扫描型PDF 文件。简单理解扫描型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文件布局格式混乱,以及文件太大等问题。
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文件进行内容提取。
一个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)
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 库逐字提取,然后根据字体所在的位置编写相应的过滤提取逻辑。
通过 Python 还可以对 PDF 文件进行多种基本操作,如添加文字、生成目录、加密等。通过 PyPDF4 第三方库来实现。
PyPDF4 与 PyPDF2 在功能上相似且同样易用,但需要注意两个库并不兼容。通过 pip3
便可以安装 PyPDF4:
pip3 install PyPDF4
通过前面内容的介绍,我们掌握了如何从 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 文件。
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()
有时需要对 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个步骤会经常用到。
在分享具有机密性质的 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)
将多个 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)
给 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")