跳转到主要内容

快速导航

顶层函数

当你需要从”文件级任务”快速开始(打开、合并、批量渲染)时,优先看这一节。

打开 PDF 文档

打开一个 PDF 文档,返回 Document 实例。 接口签名: sopdf.open(path, password, *, stream)
sopdf.open(
    path: str | pathlib.Path | None = None,
    password: str | None = None,
    *,
    stream: bytes | None = None,
) -> Document
path
str | Path | None
默认值:"None"
PDF 文件的路径,与 stream 二选一。
password
str | None
默认值:"None"
加密 PDF 的密码,无需密码时传 None
stream
bytes | None
默认值:"None"
从内存字节打开,与 path 二选一。
返回值: Document — 打开的文档对象。
异常触发条件
PasswordError文档需要密码,但未提供或密码错误。
FileDataError文件损坏或无法解析为有效 PDF。
# 从文件路径打开
doc = sopdf.open("report.pdf")

# 打开加密文档
doc = sopdf.open("secure.pdf", password="hunter2")

# 从内存字节打开
with open("report.pdf", "rb") as f:
    doc = sopdf.open(stream=f.read())

# 推荐:使用上下文管理器自动释放资源
with sopdf.open("report.pdf") as doc:
    print(doc.page_count)

合并多个 PDF 文件

将多个 PDF 文件按顺序合并为一个输出文件。 接口签名: sopdf.merge(inputs, output)
sopdf.merge(
    inputs: list[str | pathlib.Path],
    output: str | pathlib.Path,
) -> None
inputs
list[str | Path]
待合并的 PDF 文件路径列表,按列表顺序拼接。
output
str | Path
输出文件的目标路径。
异常触发条件
ValueErrorinputs 列表为空。
PasswordError某个输入文件需要密码。
FileDataError某个输入文件无法读取。
sopdf.merge(
    ["intro.pdf", "body.pdf", "appendix.pdf"],
    output="book.pdf",
)

批量渲染页面为图像字节

批量将一组页面渲染为图像字节。 接口签名: sopdf.render_pages(pages, *, dpi, format, alpha, parallel)
sopdf.render_pages(
    pages: list[Page],
    *,
    dpi: int = 72,
    format: str = "png",
    alpha: bool = False,
    parallel: bool = False,
) -> list[bytes]
pages
list[Page]
待渲染的页面对象列表,通常来自 doc.pages
dpi
int
默认值:"72"
渲染分辨率(每英寸点数)。常用值:72(屏幕预览)、150(高清)、300(印刷)。
format
str
默认值:"\"png\""
输出图像格式,"png""jpeg"
alpha
bool
默认值:"False"
是否包含透明通道(仅 PNG 有效)。
parallel
bool
默认值:"False"
是否使用多进程并行渲染。开启后可绕过 GIL,多核机器上大幅提速。
推荐参数组合
场景推荐参数
屏幕预览dpi=72, format="png", alpha=False, parallel=False
高质量导出dpi=150, format="png", alpha=False, parallel=False
大文档提速dpi=300, format="png", alpha=False, parallel=True
返回值: list[bytes] — 与 pages 一一对应的编码图像字节列表。
with sopdf.open("report.pdf") as doc:
    # 顺序渲染
    images = sopdf.render_pages(doc.pages, dpi=150)

    # 多进程并行渲染(大文档推荐)
    images = sopdf.render_pages(doc.pages, dpi=300, parallel=True)

批量渲染页面并写入文件

批量渲染页面并将结果写入目录,文件名为 page_0.pngpage_1.png 等。 接口签名: sopdf.render_pages_to_files(pages, output_dir, *, dpi, format, alpha, parallel)
sopdf.render_pages_to_files(
    pages: list[Page],
    output_dir: str | pathlib.Path,
    *,
    dpi: int = 72,
    format: str = "png",
    alpha: bool = False,
    parallel: bool = False,
) -> None
pages
list[Page]
待渲染的页面对象列表。
output_dir
str | Path
输出目录路径,不存在时自动创建。
dpi
int
默认值:"72"
渲染分辨率(每英寸点数)。
format
str
默认值:"\"png\""
输出图像格式,"png""jpeg"
alpha
bool
默认值:"False"
是否包含透明通道(仅 PNG 有效)。
parallel
bool
默认值:"False"
是否使用多进程并行渲染。
推荐参数组合
场景推荐参数
批量预览图dpi=72, format="png", parallel=False
文档归档图dpi=150, format="png", parallel=False
多核高吞吐dpi=300, format="png", parallel=True
with sopdf.open("report.pdf") as doc:
    sopdf.render_pages_to_files(doc.pages, "output/", dpi=150, parallel=True)
# 生成 output/page_0.png, output/page_1.png, ...
返回顶部

文档对象操作

当你已经拿到 Document 实例,想做页级管理、拆分、合并、保存时,重点看这一节。 Document 表示一个已打开的 PDF 文档。不应直接构造,始终通过 sopdf.open() 获取。

属性

总页数

成员: doc.page_countlen(doc)
doc.page_count -> int
文档的总页数(只读)。
len(doc) -> int
len(doc) 等同于 doc.page_count

元数据

doc.metadata -> Metadata
文档元数据,通过 Metadata 代理对象读写。
# 读取
print(doc.metadata.title)
print(doc.metadata.creation_datetime)  # Python datetime 对象

# 写入(懒加载 pikepdf,标记文档为脏)
doc.metadata.title  = "Annual Report 2025"
doc.metadata.author = "Kevin Qiu"
doc.save("updated.pdf")

文档大纲

doc.outline -> Outline
文档大纲(目录),以 Outline 树对象返回。文档无书签时返回 len == 0 的空大纲。读取使用 pypdfium2,无 pikepdf 开销。
for item in doc.outline.items:
    print(f"[p{item.page + 1}] {item.title}")

flat = doc.outline.to_list()  # 与 PyMuPDF get_toc() 格式兼容的扁平列表

加密状态

doc.is_encrypted -> bool
文档是否设有密码保护(只读)。即使提供了正确密码并成功打开,该属性仍返回 True

页面序列

doc.pages -> _PageList
所有页面的惰性序列(只读)。支持迭代和切片,常与 render_pages() 配合使用。

页面访问

按索引获取页面

接口签名: doc[index] / doc.load_page(index)
doc[index: int] -> Page
doc.load_page(index: int) -> Page
通过 0-based 索引获取页面。支持负数索引(doc[-1] 为最后一页)。
异常触发条件
PageError索引超出范围。
first_page = doc[0]
last_page  = doc[-1]
third_page = doc.load_page(2)

迭代

for page in doc:
    print(page.number)

分割

按页拆分文档

接口签名: doc.split(pages, output)
doc.split(
    pages: list[int],
    output: str | pathlib.Path | None = None,
) -> Document
从当前文档中提取指定页面,返回一个新的 Document 对象。
pages
list[int]
待提取的页面 0-based 索引列表,顺序与列表顺序一致。
output
str | Path | None
默认值:"None"
若提供,则同时将新文档写入该路径;否则仅在内存中返回。
返回值: Document — 包含指定页面的新文档对象。
# 提取前 3 页并保存
chapter = doc.split(pages=[0, 1, 2], output="chapter1.pdf")

# 仅在内存中提取,不写磁盘
excerpt = doc.split(pages=[4, 5, 6])

逐页拆分为文件

接口签名: doc.split_each(output_dir)
doc.split_each(output_dir: str | pathlib.Path) -> None
将文档的每一页分别保存为独立的 PDF 文件,文件名格式为 page_0.pdfpage_1.pdf 等。
output_dir
str | Path
输出目录路径,不存在时自动创建。
doc.split_each("pages/")
# 生成 pages/page_0.pdf, pages/page_1.pdf, ...

合并

追加文档页面

接口签名: doc.append(other)
doc.append(other: Document) -> None
将另一个文档的所有页面追加到当前文档末尾。调用后文档被标记为”已修改”,需调用 save()to_bytes() 持久化。
other
Document
被追加的文档对象。
with sopdf.open("part1.pdf") as doc_a, sopdf.open("part2.pdf") as doc_b:
    doc_a.append(doc_b)
    doc_a.save("combined.pdf")

保存

保存到文件

接口签名: doc.save(path, *, compress, garbage, linearize)
doc.save(
    path: str | pathlib.Path,
    *,
    compress: bool = True,
    garbage: bool = False,
    linearize: bool = False,
) -> None
将文档写入磁盘。
path
str | Path
目标文件路径。
compress
bool
默认值:"True"
是否压缩内容流,可显著减小文件体积。
garbage
bool
默认值:"False"
是否生成对象流(object streams),进一步压缩结构数据。
linearize
bool
默认值:"False"
是否线性化 PDF,优化网络顺序读取(Fast Web View)。
# 普通保存(压缩默认开启)
doc.save("output.pdf")

# 最大压缩
doc.save("output.pdf", compress=True, garbage=True)

# 去除加密(以正确密码打开后保存)
doc.save("unlocked.pdf")

导出为字节

接口签名: doc.to_bytes(*, compress)
doc.to_bytes(compress: bool = True) -> bytes
将文档序列化为字节,不写入磁盘。适用于在内存中处理或通过网络传输 PDF。
compress
bool
默认值:"True"
是否压缩内容流。
返回值: bytes — 完整的 PDF 文件字节内容。
pdf_bytes = doc.to_bytes()

# 在 Flask 中作为响应直接返回
from flask import Response
return Response(doc.to_bytes(), mimetype="application/pdf")

生命周期

关闭文档

接口签名: doc.close()
doc.close() -> None
关闭文档,释放所有文件句柄和内存资源。推荐使用 with 语句自动管理,避免手动调用。

上下文管理器

with sopdf.open("file.pdf") as doc:
    ...
# 退出 with 块时自动调用 close()
返回顶部

页面对象操作

当你要在单页维度做渲染、文本提取、文本检索时,使用这一节的方法。 Page 表示文档中的单个页面。通过 doc[i]doc.load_page(i) 获取,不应直接构造。

属性

页面序号

成员: page.number
page.number -> int
页面的 0-based 索引(只读)。

页面尺寸

成员: page.rect
page.rect -> Rect
页面尺寸,单位为 PDF 点(1 pt = 1/72 英寸)(只读)。rect.widthrect.height 为页面的宽高。

页面旋转角度

成员: page.rotation
page.rotation -> int          # 读取当前旋转角度
page.rotation = degrees: int  # 设置旋转角度
页面旋转角度,取值为 090180270 之一(可读写)。
异常触发条件
PageError设置了非 0/90/180/270 的值。

渲染

渲染为图像字节

接口签名: page.render(*, dpi, format, alpha)
page.render(
    *,
    dpi: int = 72,
    format: str = "png",
    alpha: bool = False,
) -> bytes
将页面渲染为图像字节。
dpi
int
默认值:"72"
渲染分辨率(每英寸点数)。72 适合屏幕预览,300 适合印刷质量。
format
str
默认值:"\"png\""
输出格式,"png""jpeg"
alpha
bool
默认值:"False"
是否包含透明通道(Alpha)。仅 PNG 格式有效;JPEG 不支持透明度。
推荐参数组合
场景推荐参数
页面预览dpi=72, format="png", alpha=False
清晰截图dpi=150, format="png", alpha=False
打印级导出dpi=300, format="png", alpha=False
返回值: bytes — 编码后的图像字节(PNG 或 JPEG)。
png_bytes  = page.render(dpi=150)
jpeg_bytes = page.render(dpi=150, format="jpeg")
png_alpha  = page.render(dpi=72, alpha=True)

渲染并保存图像

接口签名: page.render_to_file(path, *, dpi, format, alpha)
page.render_to_file(
    path: str | pathlib.Path,
    *,
    dpi: int = 72,
    format: str = "png",
    alpha: bool = False,
) -> None
渲染页面并将图像写入文件。参数含义与 render() 完全一致。
path
str | Path
输出文件路径(含扩展名)。
dpi
int
默认值:"72"
渲染分辨率(每英寸点数)。
format
str
默认值:"\"png\""
输出格式,"png""jpeg"
alpha
bool
默认值:"False"
是否包含透明通道(仅 PNG)。
page.render_to_file("page0.png", dpi=300)
page.render_to_file("page0.jpg", dpi=150, format="jpeg")

文本提取

提取纯文本

接口签名: page.get_text(*, rect)
page.get_text(
    *,
    rect: Rect | None = None,
) -> str
提取页面的纯文本内容。
rect
Rect | None
默认值:"None"
仅提取该矩形区域内的文本;为 None 时提取整页。
返回值: str — 提取到的纯文本字符串。
full_text = page.get_text()

# 仅提取特定区域
region = Rect(0, 0, 300, 100)
header_text = page.get_text(rect=region)

提取文本块

接口签名: page.get_text_blocks(*, rect, format)
page.get_text_blocks(
    *,
    rect: Rect | None = None,
    format: str = "list",
) -> list
提取带边界框的结构化文本块。
rect
Rect | None
默认值:"None"
仅提取该矩形区域内的文本块;为 None 时提取整页。
format
str
默认值:"\"list\""
返回格式:"list" 返回 TextBlock 对象列表;"dict" 返回字典列表,每项含 "text""rect" 键。
返回值: format="list"list[TextBlock]format="dict"list[dict],每项形如 {"text": "...", "rect": {"x0": ..., "y0": ..., "x1": ..., "y1": ...}}
blocks = page.get_text_blocks()
for block in blocks:
    print(block.text, block.rect)

# 以字典格式返回(便于 JSON 序列化)
dicts = page.get_text_blocks(format="dict")

文本搜索

搜索文本位置

接口签名: page.search(query, *, match_case)
page.search(
    query: str,
    *,
    match_case: bool = False,
) -> list[Rect]
在页面上搜索文本,返回所有命中位置的矩形区域列表。
query
str
要搜索的文本字符串。
match_case
bool
默认值:"False"
是否区分大小写,默认不区分。
返回值: list[Rect] — 每个命中位置的边界矩形列表,未找到时返回空列表。
hits = page.search("invoice")
for rect in hits:
    print(f"在 {rect} 处找到匹配")

# 区分大小写
hits = page.search("PDF", match_case=True)

搜索文本并返回上下文块

接口签名: page.search_text_blocks(query, *, match_case)
page.search_text_blocks(
    query: str,
    *,
    match_case: bool = False,
) -> list[dict]
搜索文本,同时返回每处命中的精确矩形及其所在的完整文本块上下文。
query
str
要搜索的文本字符串。
match_case
bool
默认值:"False"
是否区分大小写。
返回值: list[dict],每个元素包含:
类型说明
"text"str命中所在文本块的完整文本内容。
"rect"Rect命中所在文本块的边界矩形。
"match_rect"Rect命中关键词本身的精确边界矩形。
results = page.search_text_blocks("total amount")
for r in results:
    print(r["text"])        # 包含关键词的完整段落
    print(r["match_rect"])  # 关键词精确位置
返回顶部

数据类型

当你需要理解返回值结构(如 RectTextBlockMetadata)或做二次处理时,参考这一节。

Rect

表示一个矩形区域,坐标单位为 PDF 点(pt,1 pt = 1/72 英寸)。坐标系以页面左上角为原点,x 向右增大,y 向下增大。
Rect(x0: float, y0: float, x1: float, y1: float)
构造参数
参数类型说明
x0float左边界(左上角 x 坐标)。
y0float上边界(左上角 y 坐标)。
x1float右边界(右下角 x 坐标)。
y1float下边界(右下角 y 坐标)。
核心属性(常用)
属性类型说明
x0float左边界。
y0float上边界。
x1float右边界。
y1float下边界。
widthfloat矩形宽度,等于 x1 - x0
heightfloat矩形高度,等于 y1 - y0
所有几何运算均返回新的 Rect 实例,原对象不可变。
r = Rect(10, 20, 200, 300)
print(r.width)    # 190.0
print(r.height)   # 280.0

# 判断包含
print(r.contains(Rect(50, 50, 100, 100)))  # True
print(r.contains((50, 50)))                # True(点)

# 交集
a = Rect(0, 0, 100, 100)
b = Rect(50, 50, 150, 150)
print(a.intersect(b))  # Rect(50, 50, 100, 100)

# 解包
x0, y0, x1, y1 = r

TextBlock

表示页面上一个带边界框的文本块。
TextBlock(text: str, rect: Rect)
属性 / 方法类型说明
textstr文本块的文字内容。
rectRect文本块在页面上的边界矩形。
to_dict()dict转换为 {"text": ..., "rect": {"x0": ..., "y0": ..., "x1": ..., "y1": ...}}
blocks = page.get_text_blocks()
for block in blocks:
    print(block.text)
    print(block.rect.width, block.rect.height)
    print(block.to_dict())

Metadata

PDF Document Info 字典的读/写代理。通过 doc.metadata 获取,不应直接构造。 读取路径(零 pikepdf 开销):每个属性调用 pypdfium2.get_metadata_dict() 并在自动同步后返回。 写入路径(懒加载 pikepdf):每个 setter 调用 _ensure_pike(),写入 pike_doc.docinfo 并将文档标记为脏,下次读取时自动同步。 核心字段(常用)
属性类型说明
titlestr | None文档标题(/Title)。可读写。
authorstr | None作者姓名(/Author)。可读写。
subjectstr | None文档主题(/Subject)。可读写。
keywordsstr | None搜索关键词(/Keywords)。可读写。
PDF 日期格式: D:YYYYMMDDHHmmSSOHH'mm'(前缀 D: 和时区均可选)。
with sopdf.open("report.pdf") as doc:
    meta = doc.metadata

    # 读取字段
    print(meta.title)
    print(meta.creation_datetime)  # datetime(2024, 1, 1, 12, 0, tzinfo=...)

    # 写入
    meta.title  = "新标题"
    meta.author = "Kevin Qiu"
    doc.save("updated.pdf")

    # 字典风格读取(向后兼容)
    d = meta.to_dict()
    print(d["title"])
    print(meta["author"])

OutlineItem

文档大纲中的单个书签节点(不可变)。
@dataclass(frozen=True)
class OutlineItem:
    title:    str
    page:     int                          # 0-based;-1 = 无目标页
    level:    int                          # 0 = 顶层
    children: tuple[OutlineItem, ...] = ()
属性 / 方法类型说明
titlestr阅读器目录面板中显示的书签标签。
pageint0-based 目标页码;无目标页时为 -1
levelint嵌套深度;0 = 顶层项。
childrentuple[OutlineItem, ...]子节点(frozen tuple)。
to_dict()dict递归序列化为普通字典。

Outline

只读大纲树管理器。通过 doc.outline 获取,不应直接构造。首次访问时一次性构建,使用 pypdfium2 的 TOC 数据,无需 pikepdf 初始化。
成员返回值说明
itemslist[OutlineItem]顶层大纲节点(每个可能含嵌套 children)。
to_list()list[dict]深度优先扁平化列表,每项格式 {"level": int, "title": str, "page": int}。与 PyMuPDF get_toc() 兼容。
len(outline)int所有层级节点的总数。
iter(outline)遍历顶层节点。
bool(outline)bool文档有至少一个大纲节点时为 True
with sopdf.open("textbook.pdf") as doc:
    outline = doc.outline
    print(outline)          # Outline(top_level=2, total=4)
    print(bool(outline))    # True

    # 递归树遍历
    def print_tree(items, indent=0):
        for item in items:
            print("  " * indent + f"[p{item.page + 1}] {item.title}")
            print_tree(item.children, indent + 1)

    print_tree(outline.items)

    # 扁平列表(与 PyMuPDF 兼容)
    for row in outline.to_list():
        print(f"{'  ' * row['level']}{row['title']}  →  p{row['page'] + 1}")
返回顶部

异常

当你要把 PDF 处理流程接入线上服务,做稳定性与错误恢复设计时,先看这一节。 所有异常均继承自 PDFError,后者继承自内置 RuntimeError
RuntimeError
└── PDFError
    ├── PasswordError
    ├── FileDataError
    └── PageError
异常类触发场景处理建议可恢复
PDFError所有 sopdf 异常的基类,可用于统一捕获。在最外层兜底记录日志并统一提示。视子类而定
PasswordError打开加密 PDF 时密码缺失或错误。重新请求密码,限制重试次数。
FileDataErrorPDF 文件损坏、格式非法或无法解析。提示用户重新上传或更换文件来源。
PageError页面索引超出范围,或设置了非法旋转角度(非 0/90/180/270)。在调用前校验页码与角度范围。
推荐捕获顺序: 先捕获具体异常(PasswordError / FileDataError / PageError),最后再捕获 PDFError 作为兜底。
import sopdf

try:
    doc = sopdf.open("file.pdf", password="wrong")
except sopdf.PasswordError:
    print("密码错误")
except sopdf.FileDataError:
    print("文件损坏")
except sopdf.PDFError as e:
    print(f"PDF 错误:{e}")
返回顶部