在日常办公和开发任务中,PDF按页拆分是一个非常常见的需求:从200页的培训教材里抽取15到30页,或者将合同按章节拆成独立文件分发给不同负责人。手动在阅读器里选择页面再导出的方式效率很低,而且每次只能处理一段范围。用Python脚本可以直接完成指定页码提取、逐页拆分、按块大小拆分以及按书签拆分为章节,适合批量处理和自动化流程。
实现思路并不复杂:通过pypdf库的PdfReader读取源PDF页面,再用PdfWriter将选中的页面写入新的PDF文件。整个过程只涉及页面对象的复用和写入操作,不涉及重新渲染,所以速度很快,也不会损失原始PDF内容。
环境准备方面,需要安装pypdf,可以执行以下命令:如果还需要解析非标准书签,可以额外安装pdfplumber:
下面给出完整实现代码,包含四种拆分方式:提取指定页码范围、逐页拆分、按固定页数分块、按书签章节拆分。- import os
- from pypdf import PdfReader, PdfWriter
- from pathlib import Path
- # ==================== 方案 1:提取指定页码范围 ====================
- def extract_pages(input_pdf, start_page, end_page, output_file):
- """
- 从 PDF 中提取指定范围的页面
- 参数:
- input_pdf: 源 PDF 文件
- start_page: 起始页码(从 1 开始)
- end_page: 结束页码(包含)
- output_file: 输出文件名
- """
- reader = PdfReader(input_pdf)
- total = len(reader.pages)
- # 验证页码范围
- if start_page < 1 or end_page > total:
- print(f"错误: 文件共 {total} 页,请求范围 {start_page}-{end_page}")
- return
- writer = PdfWriter()
- for i in range(start_page - 1, end_page): # 转为 0-based 索引
- writer.add_page(reader.pages[i])
- with open(output_file, 'wb') as f:
- writer.write(f)
- print(f"已提取第 {start_page}-{end_page} 页 → {output_file}")
- # ==================== 方案 2:逐页拆分 ====================
- def split_pdf_by_page(input_pdf, output_dir="逐页拆分"):
- """
- 将 PDF 的每一页拆分为独立文件
- """
- reader = PdfReader(input_pdf)
- output_path = Path(output_dir)
- output_path.mkdir(parents=True, exist_ok=True)
- base_name = Path(input_pdf).stem
- for i, page in enumerate(reader.pages):
- writer = PdfWriter()
- writer.add_page(page)
- output_file = output_path / f"{base_name}_第{i+1}页.pdf"
- with open(output_file, 'wb') as f:
- writer.write(f)
- print(f"逐页拆分完成: {len(reader.pages)} 页 → {output_dir}/")
- # ==================== 方案 3:按指定块大小拆分 ====================
- def split_pdf_by_chunks(input_pdf, chunk_size, output_dir="分块拆分"):
- """
- 按指定页数拆分(如每 10 页一个文件)
- 参数:
- input_pdf: 源 PDF
- chunk_size: 每个文件的页数
- output_dir: 输出目录
- """
- reader = PdfReader(input_pdf)
- output_path = Path(output_dir)
- output_path.mkdir(parents=True, exist_ok=True)
- total = len(reader.pages)
- base_name = Path(input_pdf).stem
- chunk_count = 0
- for start in range(0, total, chunk_size):
- end = min(start + chunk_size, total)
- writer = PdfWriter()
- for i in range(start, end):
- writer.add_page(reader.pages[i])
- output_file = output_path / f"{base_name}_第{start+1}-{end}页.pdf"
- with open(output_file, 'wb') as f:
- writer.write(f)
- chunk_count += 1
- print(f"已拆分: 第 {start+1}-{end} 页")
- print(f"\n分块拆分完成: {chunk_count} 个文件")
- # ==================== 方案 4:按章节/书签拆分 ====================
- def split_pdf_by_outline(input_pdf, output_dir="按章节拆分"):
- """
- 按 PDF 的书签(大纲)拆分为独立章节文件
- """
- reader = PdfReader(input_pdf)
- output_path = Path(output_dir)
- output_path.mkdir(parents=True, exist_ok=True)
- base_name = Path(input_pdf).stem
- outline = reader.outline
- # 提取书签信息
- bookmarks = []
- for item in outline:
- if isinstance(item, dict) and '/Title' in item:
- title = item['/Title']
- page_num = None
- if '/Dest' in item:
- dest = item['/Dest']
- if isinstance(dest, list) and dest[0]:
- page_num = reader.get_destination_page_number(item)
- bookmarks.append({'title': title, 'page': page_num})
- if not bookmarks:
- print("该 PDF 没有书签/大纲")
- return
- # 按书签拆分
- for idx, bm in enumerate(bookmarks):
- if bm['page'] is None:
- continue
- start = bm['page']
- # 下一个书签的页码 - 1 作为结束
- if idx + 1 < len(bookmarks) and bookmarks[idx + 1]['page'] is not None:
- end = bookmarks[idx + 1]['page'] - 1
- else:
- end = len(reader.pages) - 1
- writer = PdfWriter()
- for i in range(start, end + 1):
- writer.add_page(reader.pages[i])
- safe_title = bm['title'].replace('/', '_').replace('\\', '_')
- output_file = output_path / f"{safe_title}.pdf"
- with open(output_file, 'wb') as f:
- writer.write(f)
- print(f"已拆分章节: {bm['title']} (第 {start+1}-{end+1} 页)")
- print(f"\n按章节拆分完成: {output_dir}/")
- # ==================== 使用示例 ====================
- if __name__ == "__main__":
- PDF_FILE = "培训教材_200页.pdf"
- # 方案 1:提取第 15-30 页
- extract_pages(PDF_FILE, 15, 30, "培训教材_核心章节.pdf")
- # 方案 2:逐页拆分
- # split_pdf_by_page(PDF_FILE)
- # 方案 3:每 10 页一个文件
- # split_pdf_by_chunks(PDF_FILE, chunk_size=10)
- # 方案 4:按书签拆分
- # split_pdf_by_outline(PDF_FILE)
复制代码
代码说明:extract_pages函数的start_page和end_page均从1开始计数,内部通过start_page-1转换为pypdf的0基索引。输出前会校验页码范围,避免越界。逐页拆分会在指定目录下生成“原文件名_第N页.pdf”的独立文件。分块拆分需要传入chunk_size参数,例如chunk_size=10表示每10页生成一个文件,最后一个文件可能不足10页。按书签拆分则依赖PDF内置大纲,提取出章节标题和对应起始页码,并把每个书签到下一个书签之间的页面归入同一章节;标题中的特殊字符会被替换为下划线,避免生成非法文件名。
实际使用中可能遇到两个典型问题。
问题一:提取后页面空白。这通常出现在扫描件或加密PDF上,pypdf无法直接读取加密内容。可以在创建reader之后先判断并解密:- reader = PdfReader(input_pdf)
- if reader.is_encrypted:
- reader.decrypt("")
复制代码 空字符串解密适用于无密码的加密文件;如果PDF本身有打开密码,则需要传入对应密码。
问题二:按书签拆分时没有提取到书签。部分PDF的书签可能存储在非标准位置,pypdf的outline属性无法读取。此时可以改用pdfplumber尝试获取更完整的目录信息:- import pdfplumber
- with pdfplumber.open(pdf_file) as pdf:
- toc = pdf.toc
- print(toc)
复制代码 pdfplumber会尽量解析文档结构中的书签数据,拿到目录后可以再结合页码信息自行实现拆分逻辑。
总结一下四种方式的特点:- 拆分方式 函数 说明
- 指定范围 extract_pages() 提取 start-end 页
- 逐页拆分 split_pdf_by_page() 每页一个文件
- 分块拆分 split_pdf_by_chunks() 每 N 页一个文件
- 按书签拆分 split_pdf_by_outline() 按章节拆分
复制代码 如果你的业务场景是“只取中间某一段”,用extract_pages最直接;如果需要将整份文档拆成多个固定页数的小文件,split_pdf_by_chunks更合适;而按书签拆分则适合结构清晰的电子书或技术手册,可以一次性得到按章节组织的独立PDF。这套逻辑同样可以扩展到批量处理多个PDF文件,只需在外层加一个文件遍历循环即可。 |