网站分类目录查询,代理记账公司利润大吗,电子购物网站建设,网页设计实训报告范例Python 一键拆分 PDF#xff1a;按“目录/章节”建文件夹 每页单独导出#xff08;支持书签识别正文识别#xff09; 文章目录Python 一键拆分 PDF#xff1a;按“目录/章节”建文件夹 每页单独导出#xff08;支持书签识别正文识别#xff09;1. 我写这个工…Python 一键拆分 PDF按“目录/章节”建文件夹 每页单独导出支持书签识别正文识别文章目录Python 一键拆分 PDF按“目录/章节”建文件夹 每页单独导出支持书签识别正文识别1. 我写这个工具的原因2. 最终效果长什么样我想要的输出3. 使用方法我实际是这样跑的3.1 我做的第 1 步选择 PDF 文件3.2 我做的第 2 步选择输出位置并开始拆分4. 环境与依赖我用的配置5. 我在代码里做了什么原理简述5.1 优先方案从 PDF 书签outline识别章节5.2 备用方案扫描正文文本猜章节起始页6. 我可以调的关键参数想改行为就看这里7. 常见问题我自己踩过的坑Q1提示“未识别到任何章节标题”Q2为什么我的章节起始页不准Q3输出文件名为什么会有下划线8. 完整代码可直接复制运行1. 我写这个工具的原因我经常会把电子书交给 AI 做总结/问答但很多 PDF体积大、页数多如果我想按章节拆开再喂给 AI手动操作会非常耗时间。所以我用 Python 写了一个小工具实现了自动识别“第X章”标题优先书签没书签再扫正文按章节自动创建文件夹文件夹命名带序号方便排序支持整章导出 单页导出每页单独 PDF便于上传/AI 处理2. 最终效果长什么样我想要的输出我拆分后的输出结构大概是这样输出目录01_第1章_xxx/01_第1章_xxx.pdf整章p0001.pdf p0002.pdf ...单页02_第2章_xxx/02_第2章_xxx.pdfp00xx.pdf ...一句话我既保留“整章”也能拿到“每一页”。3. 使用方法我实际是这样跑的我把脚本直接丢进 PyCharm你原文写的 ptcharm 我这里统一写成 PyCharm运行即可。3.1 我做的第 1 步选择 PDF 文件我点击按钮「1. 请选择你的 PDF 文件」选中要处理的 PDF。3.2 我做的第 2 步选择输出位置并开始拆分我点击「2. 请选择输出目录并开始拆分」选择一个输出文件夹工具就会开始处理并在下方日志区域输出进度。你原来的两张图建议保留在这里效果最直观4. 环境与依赖我用的配置Windows 10/11 均可Python 3.x建议 3.8依赖库pypdf我安装依赖的方式pipinstallpypdf注意如果 PDF 是扫描版图片没有可提取的文字正文识别可能会失败这种情况需要先 OCR否则工具无法“读到章节标题”。5. 我在代码里做了什么原理简述为了让工具对不同 PDF 更“稳”我设计了两套识别策略5.1 优先方案从 PDF 书签outline识别章节如果 PDF 自带书签目录我就直接读取书签并找出匹配 “第X章” 的标题然后拿到对应页码作为章节起点。优点速度快、准确率高。5.2 备用方案扫描正文文本猜章节起始页如果没有书签我会逐页提取文本然后用正则匹配第X章并做了两层过滤过滤“目录页”包含 目录/Contents 等过滤类似标题......页码的目录行优点即使没书签只要正文可提取文字也能拆分。6. 我可以调的关键参数想改行为就看这里代码最上面配置区我留了两个重点CHAPTER_PATTERN章节匹配正则默认支持第1章 / 第 1 章 / 第一章 / 第十四章EXPORT_SINGLE_PAGES是否导出单页 PDF默认 True如果你不想生成单页 PDF把EXPORT_SINGLE_PAGES False就行。7. 常见问题我自己踩过的坑Q1提示“未识别到任何章节标题”通常是三类原因PDF 没书签 正文提取不到文本扫描版章节标题不是“第X章”这种格式需要改正则章节标题出现在页面太靠后正文识别里有阈值m.start() 400解决思路我会先确认 PDF 是不是可复制文字不行就 OCR标题格式不一致就改正则。Q2为什么我的章节起始页不准如果 PDF 正文排版很特殊比如章标题不在页首、页眉重复干扰可能会误判。这种情况我会优先找“带书签”的版本或者适当调整正文识别的阈值。Q3输出文件名为什么会有下划线我在sanitize_filename()里把 Windows 不允许的字符替换成_避免出现“无法创建文件”的问题。8. 完整代码可直接复制运行我把完整代码放在这里复制到 PyCharm 直接运行即可。# -*- coding: utf-8 -*-importrefrompathlibimportPathfrompypdfimportPdfReader,PdfWriterimporttkinterastkfromtkinterimportfiledialog,messagebox# 配置区域 # 章节标题匹配规则适合第1章 / 第 1 章 / 第一章 / 第十四章 等CHAPTER_PATTERNre.compile(r第\s*[一二三四五六七八九十百千0-9]\s*章[^\n\r]*)# 是否额外按“页”拆成单页 PDFEXPORT_SINGLE_PAGESTrue# # GUI 全局对象占位rootNonelog_textNonedefsanitize_filename(name:str)-str: 去掉 Windows 不支持的文件名字符。 returnre.sub(r[\\/:*?|],_,name)# ---------- 方案一优先从 PDF 书签(outline) 中找章节 ----------deffind_chapters_from_outline(reader:PdfReader): 从 PDF 书签(outline) 中找出章节 - 遍历所有书签 - 标题里匹配 CHAPTER_PATTERN第X章…… - 获取对应页码 返回: [{title: 第1章 xxx, start: 0}, ...] chapters[]# 兼容不同版本的 pypdf有的叫 outline有的叫 outlinestry:outlinesreader.outlineexceptException:try:outlinesreader.outlinesexceptException:outlinesNoneifnotoutlines:return[]defwalk(items):foriteminitems:# 子列表继续递归ifisinstance(item,list):walk(item)else:# 尝试拿书签标题try:titleitem.titleexceptAttributeError:titlestr(item)ifnotisinstance(title,str):titlestr(title)# 标题里不含“第X章”就跳过ifnotCHAPTER_PATTERN.search(title):continue# 拿到书签指向的页码try:page_numreader.get_destination_page_number(item)exceptException:continuechapters.append({title:title.strip(),start:page_num})walk(outlines)# 去重、排序同一页只保留一个章节unique{}forchinchapters:ifch[start]notinunique:unique[ch[start]]ch chapterssorted(unique.values(),keylambdac:c[start])returnchapters# ---------- 方案二从正文文本中猜章节备用 ----------deffind_chapters_from_text(reader:PdfReader): 扫描整个 PDF 正文猜每一章的“起始页”以及章节标题。 规则大致是 - 排除“目录/contents”页面 - 一页内允许有多个“第X章”逐个判断 - 只要某个匹配出现在页面较前面且所在行不像目录行(标题 ...... 页码) 就认为是章节开始 返回: [{title: 第1章 xxx, start: 0}, ...] chapters[]num_pageslen(reader.pages)foriinrange(num_pages):pagereader.pages[i]textpage.extract_text()orifnottext.strip():continue# 跳过目录页粗略判断即可headtext[:100]if目录inheadorContentsinheadorCONTENTSinhead:continue# 遍历此页所有匹配 第X章forminCHAPTER_PATTERN.finditer(text):# 要求标题出现在页面比较靠前的位置ifm.start()400:# 这个阈值可以按需要微调continue# 找到这一行的文本内容linestext.splitlines()line_of_matchchar_pos0forlineinlines:next_poschar_poslen(line)1# 粗略算上换行ifm.start()next_pos:line_of_matchlinebreakchar_posnext_pos# 目录行一般是标题 一串点 页码# 例如第1章 人际关系的构成..................1ifre.search(r[\.·…]{3,}\s*\d\s*$,line_of_match):# 像目录的行忽略continuetitlem.group(0).strip()# 同一页只认一个章节起点ifnotany(ch[start]iforchinchapters):chapters.append({title:title,start:i})break# 当前页已经找到一个章节了后面不再找returnchapters# ---------- 包一层带 logger 的 find_chapters ----------deffind_chapters(reader:PdfReader,loggerprint):chaptersfind_chapters_from_outline(reader)ifchapters:logger(✅ 使用 PDF 书签识别章节)returnchapters logger(⚠️ 此 PDF 没有可用书签改用正文文本识别章节)chaptersfind_chapters_from_text(reader)returnchaptersdeffill_chapter_ranges(chapters,num_pages): 根据 start 页自动计算每章的 end 页。 修改 chapters 列表增加 end 字段。 foridx,chinenumerate(chapters):startch[start]ifidxlen(chapters)-1:endchapters[idx1][start]-1else:endnum_pages-1ch[end]endreturnchaptersdefsplit_pdf_by_chapters(pdf_path,output_root,loggerNone): 真正拆分 PDF 的函数所有信息通过 logger 输出到日志区域 ifloggerisNone:loggerprintpdf_pathPath(pdf_path)output_rootPath(output_root)ifnotpdf_path.exists():msgfPDF 文件不存在:{pdf_path}logger(msg)raiseFileNotFoundError(msg)readerPdfReader(str(pdf_path))num_pageslen(reader.pages)book_namepdf_path.name logger(f开始处理{book_name})logger(f总页数{num_pages})# 1. 找章节chaptersfind_chapters(reader,loggerlogger)ifnotchapters:msg未识别到任何章节标题请检查PDF 是否有书签/正文是否能提取文字/正则是否合适。logger(msg)raiseValueError(msg)logger(f共识别到{len(chapters)}章)foridx,chinenumerate(chapters,start1):logger(f 第{idx}章 →{ch[title]}起始页{ch[start]1})# 2. 填充每章的结束页chaptersfill_chapter_ranges(chapters,num_pages)# 3. 创建输出根目录output_root.mkdir(parentsTrue,exist_okTrue)logger(f输出目录{output_root})# 4. 按章节导出foridx,chinenumerate(chapters,start1):titlech[title]start_pagech[start]# 0-basedend_pagech[end]# 0-basedpage_countend_page-start_page1safe_titlesanitize_filename(title)chapter_diroutput_root/f{idx:02d}_{safe_title}chapter_dir.mkdir(parentsTrue,exist_okTrue)logger()logger(f 处理章节{idx}:{title})logger(f页码范围:{start_page1}-{end_page1}共{page_count}页)logger(f章节输出目录:{chapter_dir})# 4.1 导出“整章一个 PDF”chapter_writerPdfWriter()forpinrange(start_page,end_page1):chapter_writer.add_page(reader.pages[p])chapter_pdf_pathchapter_dir/f{idx:02d}_{safe_title}.pdfwithopen(chapter_pdf_path,wb)asf:chapter_writer.write(f)logger(f ✅ 已生成整章 PDF:{chapter_pdf_path.name})# 4.2 可选每一页单独导出ifEXPORT_SINGLE_PAGES:forpinrange(start_page,end_page1):writerPdfWriter()writer.add_page(reader.pages[p])# 页码用 1 开始且补零对齐例如 p0001.pdfpage_labelfp{p1:04d}.pdfsingle_page_pathchapter_dir/page_labelwithopen(single_page_path,wb)asf:writer.write(f)logger( ✅ 已生成单页 PDF 文件按页命名)logger()logger( 拆分完成)# GUI 部分两个按钮 日志框 selected_pdf_fileselected_output_dirdefappend_log(msg:str):写日志到 Text并自动滚动iflog_textisNone:print(msg)returnlog_text.config(statenormal)log_text.insert(tk.END,msg\n)log_text.see(tk.END)log_text.config(statedisabled)# 刷新一下界面让日志滚动更及时ifrootisnotNone:root.update_idletasks()defchoose_pdf():按钮1选择 PDF 文件globalselected_pdf_file pathfiledialog.askopenfilename(title请选择 PDF 文件,filetypes[(PDF 文件,*.pdf),(所有文件,*.*)])ifpath:selected_pdf_filepath label_pdf.config(textf已选择 PDF{path})append_log(f已选择 PDF 文件{path})defchoose_output_and_run():按钮2选择输出目录并开始拆分globalselected_output_dir,selected_pdf_fileifnotselected_pdf_file:messagebox.showwarning(提示,请先选择 PDF 文件)returnpathfiledialog.askdirectory(title请选择输出目录)ifnotpath:returnselected_output_dirpath label_output.config(textf输出目录{path})append_log()append_log(f输出目录设置为{path})append_log(开始拆分请稍候...\n)# 清理一下旧的错误提示try:split_pdf_by_chapters(selected_pdf_file,selected_output_dir,loggerappend_log)messagebox.showinfo(完成,拆分完成\n请到输出目录查看各章节文件夹。)exceptExceptionase:append_log(f❌ 出错{e})messagebox.showerror(错误,f处理过程中出现错误\n{e})if__name____main__:# 创建窗口roottk.Tk()root.title(PDF 章节拆分工具)root.geometry(400x400)# 固定为 400x400 的窗口root.resizable(False,False)# 上半部分按钮区域btn_frametk.Frame(root)btn_frame.pack(padx10,pady10,fillx)# 按钮1选择 PDFbtn_pdftk.Button(btn_frame,text1. 请选择你的 PDF 文件,commandchoose_pdf)btn_pdf.pack(fillx)label_pdftk.Label(btn_frame,text尚未选择 PDF 文件,anchorw)label_pdf.pack(fillx,pady(5,10))# 按钮2选择输出目录 开始拆分btn_outputtk.Button(btn_frame,text2. 请选择输出目录并开始拆分,commandchoose_output_and_run)btn_output.pack(fillx)label_outputtk.Label(btn_frame,text尚未选择输出目录,anchorw)label_output.pack(fillx,pady(5,0))# 下半部分日志输出区域带滚动条log_frametk.Frame(root)log_frame.pack(padx10,pady10,fillboth,expandTrue)log_texttk.Text(log_frame,statedisabled)log_text.pack(sideleft,fillboth,expandTrue)scrollbartk.Scrollbar(log_frame,commandlog_text.yview)scrollbar.pack(sideright,filly)log_text.config(yscrollcommandscrollbar.set)append_log(日志初始化完成。)append_log(提示先选择 PDF 文件再选择输出目录开始拆分。)root.mainloop()