使用Python開發(fā)的PDF拆分工具
1. 我寫這個工具的原因
我經(jīng)常會把電子書交給 AI 做總結(jié)/問答,但很多 PDF 體積大、頁數(shù)多,如果我想按章節(jié)拆開再喂給 AI,手動操作會非常耗時間。
所以我用 Python 寫了一個小工具,實現(xiàn)了:
- 自動識別“第X章”標(biāo)題(優(yōu)先書簽,沒書簽再掃正文)
- 按章節(jié)自動創(chuàng)建文件夾(文件夾命名帶序號,方便排序)
- 支持整章導(dǎo)出 + 單頁導(dǎo)出(每頁單獨 PDF,便于上傳/AI 處理)
2. 最終效果長什么樣(我想要的輸出)
我拆分后的輸出結(jié)構(gòu)大概是這樣:
輸出目錄
01_第1章_xxx/
01_第1章_xxx.pdf(整章)p0001.pdf p0002.pdf ...(單頁)
02_第2章_xxx/
02_第2章_xxx.pdfp00xx.pdf ...
一句話:我既保留“整章”,也能拿到“每一頁”。
3. 使用方法(我實際是這樣跑的)
我把腳本直接丟進(jìn) PyCharm(你原文寫的 ptcharm 我這里統(tǒng)一寫成 PyCharm)運行即可。
3.1 我做的第 1 步:選擇 PDF 文件
我點擊按鈕 「1. 請選擇你的 PDF 文件」,選中要處理的 PDF。
3.2 我做的第 2 步:選擇輸出位置并開始拆分
我點擊 「2. 請選擇輸出目錄并開始拆分」,選擇一個輸出文件夾,工具就會開始處理,并在下方日志區(qū)域輸出進(jìn)度。
你原來的兩張圖建議保留在這里(效果最直觀)


4. 環(huán)境與依賴(我用的配置)
- Windows 10/11 均可
- Python 3.x(建議 3.8+)
- 依賴庫:
pypdf
我安裝依賴的方式:
pip install pypdf
注意:如果 PDF 是掃描版圖片(沒有可提取的文字),正文識別可能會失敗,這種情況需要先 OCR,否則工具無法“讀到章節(jié)標(biāo)題”。
5. 我在代碼里做了什么(原理簡述)
為了讓工具對不同 PDF 更“穩(wěn)”,我設(shè)計了兩套識別策略:
5.1 優(yōu)先方案:從 PDF 書簽(outline)識別章節(jié)
如果 PDF 自帶書簽(目錄),我就直接讀取書簽并找出匹配 “第X章” 的標(biāo)題,然后拿到對應(yīng)頁碼作為章節(jié)起點。
優(yōu)點:速度快、準(zhǔn)確率高。
5.2 備用方案:掃描正文文本猜章節(jié)起始頁
如果沒有書簽,我會逐頁提取文本,然后用正則匹配 第X章,并做了兩層過濾:
- 過濾“目錄頁”(包含 目錄/Contents 等)
- 過濾類似
標(biāo)題......頁碼的目錄行
優(yōu)點:即使沒書簽,只要正文可提取文字,也能拆分。
6. 我可以調(diào)的關(guān)鍵參數(shù)(想改行為就看這里)
代碼最上面配置區(qū)我留了兩個重點:
CHAPTER_PATTERN:章節(jié)匹配正則(默認(rèn)支持:第1章 / 第 1 章 / 第一章 / 第十四章)EXPORT_SINGLE_PAGES:是否導(dǎo)出單頁 PDF(默認(rèn) True)
如果你不想生成單頁 PDF,把 EXPORT_SINGLE_PAGES = False 就行。
7. 常見問題(我自己踩過的坑)
Q1:提示“未識別到任何章節(jié)標(biāo)題”
通常是三類原因:
- PDF 沒書簽 + 正文提取不到文本(掃描版)
- 章節(jié)標(biāo)題不是“第X章”這種格式(需要改正則)
- 章節(jié)標(biāo)題出現(xiàn)在頁面太靠后(正文識別里有閾值
m.start() > 400)
解決思路:我會先確認(rèn) PDF 是不是可復(fù)制文字;不行就 OCR;標(biāo)題格式不一致就改正則。
Q2:為什么我的章節(jié)起始頁不準(zhǔn)?
如果 PDF 正文排版很特殊(比如章標(biāo)題不在頁首、頁眉重復(fù)干擾),可能會誤判。
這種情況我會優(yōu)先找“帶書簽”的版本,或者適當(dāng)調(diào)整正文識別的閾值。
Q3:輸出文件名為什么會有下劃線?
我在 sanitize_filename() 里把 Windows 不允許的字符替換成 _,避免出現(xiàn)“無法創(chuàng)建文件”的問題。
8. 完整代碼(可直接復(fù)制運行)
我把完整代碼放在這里,復(fù)制到 PyCharm 直接運行即可。
# -*- coding: utf-8 -*-
import re
from pathlib import Path
from pypdf import PdfReader, PdfWriter
import tkinter as tk
from tkinter import filedialog, messagebox
# ================= 配置區(qū)域 =================
# 章節(jié)標(biāo)題匹配規(guī)則(適合:第1章 / 第 1 章 / 第一章 / 第十四章 等)
CHAPTER_PATTERN = re.compile(
r"第\s*[一二三四五六七八九十百千0-9]+\s*章[^\n\r]*"
)
# 是否額外按“頁”拆成單頁 PDF
EXPORT_SINGLE_PAGES = True
# ==========================================
# GUI 全局對象占位
root = None
log_text = None
def sanitize_filename(name: str) -> str:
"""
去掉 Windows 不支持的文件名字符。
"""
return re.sub(r'[\\/:*?"<>|]', "_", name)
# ---------- 方案一:優(yōu)先從 PDF 書簽(outline) 中找章節(jié) ----------
def find_chapters_from_outline(reader: PdfReader):
"""
從 PDF 書簽(outline) 中找出章節(jié):
- 遍歷所有書簽
- 標(biāo)題里匹配 CHAPTER_PATTERN(第X章……)
- 獲取對應(yīng)頁碼
返回: [{title: '第1章 xxx', start: 0}, ...]
"""
chapters = []
# 兼容不同版本的 pypdf:有的叫 outline,有的叫 outlines
try:
outlines = reader.outline
except Exception:
try:
outlines = reader.outlines
except Exception:
outlines = None
if not outlines:
return []
def walk(items):
for item in items:
# 子列表:繼續(xù)遞歸
if isinstance(item, list):
walk(item)
else:
# 嘗試拿書簽標(biāo)題
try:
title = item.title
except AttributeError:
title = str(item)
if not isinstance(title, str):
title = str(title)
# 標(biāo)題里不含“第X章”就跳過
if not CHAPTER_PATTERN.search(title):
continue
# 拿到書簽指向的頁碼
try:
page_num = reader.get_destination_page_number(item)
except Exception:
continue
chapters.append({"title": title.strip(), "start": page_num})
walk(outlines)
# 去重、排序(同一頁只保留一個章節(jié))
unique = {}
for ch in chapters:
if ch["start"] not in unique:
unique[ch["start"]] = ch
chapters = sorted(unique.values(), key=lambda c: c["start"])
return chapters
# ---------- 方案二:從正文文本中猜章節(jié)(備用) ----------
def find_chapters_from_text(reader: PdfReader):
"""
掃描整個 PDF 正文,猜每一章的“起始頁”以及章節(jié)標(biāo)題。
規(guī)則大致是:
- 排除“目錄/contents”頁面
- 一頁內(nèi)允許有多個“第X章”,逐個判斷
- 只要某個匹配出現(xiàn)在頁面較前面,且所在行不像目錄行(標(biāo)題 + ...... + 頁碼) 就認(rèn)為是章節(jié)開始
返回: [{title: '第1章 xxx', start: 0}, ...]
"""
chapters = []
num_pages = len(reader.pages)
for i in range(num_pages):
page = reader.pages[i]
text = page.extract_text() or ""
if not text.strip():
continue
# 跳過目錄頁(粗略判斷即可)
head = text[:100]
if "目錄" in head or "Contents" in head or "CONTENTS" in head:
continue
# 遍歷此頁所有匹配 "第X章"
for m in CHAPTER_PATTERN.finditer(text):
# 要求標(biāo)題出現(xiàn)在頁面比較靠前的位置
if m.start() > 400: # 這個閾值可以按需要微調(diào)
continue
# 找到這一行的文本內(nèi)容
lines = text.splitlines()
line_of_match = ""
char_pos = 0
for line in lines:
next_pos = char_pos + len(line) + 1 # 粗略算上換行
if m.start() < next_pos:
line_of_match = line
break
char_pos = next_pos
# 目錄行一般是:標(biāo)題 + 一串點 + 頁碼
# 例如:第1章 人際關(guān)系的構(gòu)成..................1
if re.search(r"[\.·…]{3,}\s*\d+\s*$", line_of_match):
# 像目錄的行,忽略
continue
title = m.group(0).strip()
# 同一頁只認(rèn)一個章節(jié)起點
if not any(ch["start"] == i for ch in chapters):
chapters.append({"title": title, "start": i})
break # 當(dāng)前頁已經(jīng)找到一個章節(jié)了,后面不再找
return chapters
# ---------- 包一層:帶 logger 的 find_chapters ----------
def find_chapters(reader: PdfReader, logger=print):
chapters = find_chapters_from_outline(reader)
if chapters:
logger("? 使用 PDF 書簽識別章節(jié)")
return chapters
logger("?? 此 PDF 沒有可用書簽,改用正文文本識別章節(jié)")
chapters = find_chapters_from_text(reader)
return chapters
def fill_chapter_ranges(chapters, num_pages):
"""
根據(jù) start 頁自動計算每章的 end 頁。
修改 chapters 列表,增加 end 字段。
"""
for idx, ch in enumerate(chapters):
start = ch["start"]
if idx < len(chapters) - 1:
end = chapters[idx + 1]["start"] - 1
else:
end = num_pages - 1
ch["end"] = end
return chapters
def split_pdf_by_chapters(pdf_path, output_root, logger=None):
"""
真正拆分 PDF 的函數(shù),所有信息通過 logger 輸出到日志區(qū)域
"""
if logger is None:
logger = print
pdf_path = Path(pdf_path)
output_root = Path(output_root)
if not pdf_path.exists():
msg = f"PDF 文件不存在: {pdf_path}"
logger(msg)
raise FileNotFoundError(msg)
reader = PdfReader(str(pdf_path))
num_pages = len(reader.pages)
book_name = pdf_path.name
logger(f"開始處理:{book_name}")
logger(f"總頁數(shù):{num_pages}")
# 1. 找章節(jié)
chapters = find_chapters(reader, logger=logger)
if not chapters:
msg = "未識別到任何章節(jié)標(biāo)題,請檢查:PDF 是否有書簽/正文是否能提取文字/正則是否合適。"
logger(msg)
raise ValueError(msg)
logger(f"共識別到 {len(chapters)} 章:")
for idx, ch in enumerate(chapters, start=1):
logger(f" 第{idx}章 → {ch['title']}(起始頁:{ch['start'] + 1})")
# 2. 填充每章的結(jié)束頁
chapters = fill_chapter_ranges(chapters, num_pages)
# 3. 創(chuàng)建輸出根目錄
output_root.mkdir(parents=True, exist_ok=True)
logger(f"輸出目錄:{output_root}")
# 4. 按章節(jié)導(dǎo)出
for idx, ch in enumerate(chapters, start=1):
title = ch["title"]
start_page = ch["start"] # 0-based
end_page = ch["end"] # 0-based
page_count = end_page - start_page + 1
safe_title = sanitize_filename(title)
chapter_dir = output_root / f"{idx:02d}_{safe_title}"
chapter_dir.mkdir(parents=True, exist_ok=True)
logger("")
logger(f"==== 處理章節(jié) {idx}: {title} ====")
logger(f"頁碼范圍: {start_page + 1} - {end_page + 1}(共 {page_count} 頁)")
logger(f"章節(jié)輸出目錄: {chapter_dir}")
# 4.1 導(dǎo)出“整章一個 PDF”
chapter_writer = PdfWriter()
for p in range(start_page, end_page + 1):
chapter_writer.add_page(reader.pages[p])
chapter_pdf_path = chapter_dir / f"{idx:02d}_{safe_title}.pdf"
with open(chapter_pdf_path, "wb") as f:
chapter_writer.write(f)
logger(f" ? 已生成整章 PDF: {chapter_pdf_path.name}")
# 4.2 可選:每一頁單獨導(dǎo)出
if EXPORT_SINGLE_PAGES:
for p in range(start_page, end_page + 1):
writer = PdfWriter()
writer.add_page(reader.pages[p])
# 頁碼用 1 開始,且補零對齊,例如 p0001.pdf
page_label = f"p{p + 1:04d}.pdf"
single_page_path = chapter_dir / page_label
with open(single_page_path, "wb") as f:
writer.write(f)
logger(" ? 已生成單頁 PDF 文件(按頁命名)")
logger("")
logger("?? 拆分完成!")
# ========= GUI 部分(兩個按鈕 + 日志框) =========
selected_pdf_file = ""
selected_output_dir = ""
def append_log(msg: str):
"""寫日志到 Text,并自動滾動"""
if log_text is None:
print(msg)
return
log_text.config(state="normal")
log_text.insert(tk.END, msg + "\n")
log_text.see(tk.END)
log_text.config(state="disabled")
# 刷新一下界面,讓日志滾動更及時
if root is not None:
root.update_idletasks()
def choose_pdf():
"""按鈕1:選擇 PDF 文件"""
global selected_pdf_file
path = filedialog.askopenfilename(
title="請選擇 PDF 文件",
filetypes=[("PDF 文件", "*.pdf"), ("所有文件", "*.*")]
)
if path:
selected_pdf_file = path
label_pdf.config(text=f"已選擇 PDF:{path}")
append_log(f"已選擇 PDF 文件:{path}")
def choose_output_and_run():
"""按鈕2:選擇輸出目錄并開始拆分"""
global selected_output_dir, selected_pdf_file
if not selected_pdf_file:
messagebox.showwarning("提示", "請先選擇 PDF 文件!")
return
path = filedialog.askdirectory(title="請選擇輸出目錄")
if not path:
return
selected_output_dir = path
label_output.config(text=f"輸出目錄:{path}")
append_log("")
append_log(f"輸出目錄設(shè)置為:{path}")
append_log("開始拆分,請稍候...\n")
# 清理一下舊的錯誤提示
try:
split_pdf_by_chapters(selected_pdf_file, selected_output_dir, logger=append_log)
messagebox.showinfo("完成", "拆分完成!\n請到輸出目錄查看各章節(jié)文件夾。")
except Exception as e:
append_log(f"? 出錯:{e}")
messagebox.showerror("錯誤", f"處理過程中出現(xiàn)錯誤:\n{e}")
if __name__ == "__main__":
# 創(chuàng)建窗口
root = tk.Tk()
root.title("PDF 章節(jié)拆分工具")
root.geometry("400x400") # 固定為 400x400 的窗口
root.resizable(False, False)
# 上半部分:按鈕區(qū)域
btn_frame = tk.Frame(root)
btn_frame.pack(padx=10, pady=10, fill="x")
# 按鈕1:選擇 PDF
btn_pdf = tk.Button(btn_frame, text="1. 請選擇你的 PDF 文件", command=choose_pdf)
btn_pdf.pack(fill="x")
label_pdf = tk.Label(btn_frame, text="尚未選擇 PDF 文件", anchor="w")
label_pdf.pack(fill="x", pady=(5, 10))
# 按鈕2:選擇輸出目錄 + 開始拆分
btn_output = tk.Button(btn_frame, text="2. 請選擇輸出目錄并開始拆分", command=choose_output_and_run)
btn_output.pack(fill="x")
label_output = tk.Label(btn_frame, text="尚未選擇輸出目錄", anchor="w")
label_output.pack(fill="x", pady=(5, 0))
# 下半部分:日志輸出區(qū)域(帶滾動條)
log_frame = tk.Frame(root)
log_frame.pack(padx=10, pady=10, fill="both", expand=True)
log_text = tk.Text(log_frame, state="disabled")
log_text.pack(side="left", fill="both", expand=True)
scrollbar = tk.Scrollbar(log_frame, command=log_text.yview)
scrollbar.pack(side="right", fill="y")
log_text.config(yscrollcommand=scrollbar.set)
append_log("日志初始化完成。")
append_log("提示:先選擇 PDF 文件,再選擇輸出目錄開始拆分。")
root.mainloop()
到此這篇關(guān)于使用Python開發(fā)的PDF拆分工具的文章就介紹到這了,更多相關(guān)Python PDF拆分工具內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺談Python中os模塊及shutil模塊的常規(guī)操作
這篇文章主要介紹了淺談Python中os模塊及shutil模塊的常規(guī)操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-04-04

