ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

用python-pptx拆解PPTX模板:安全课件批量重排与校验

用python-pptx拆解PPTX模板:安全课件批量重排与校验 简介这份资源是2022年安全生产月主题宣讲课件面向企业安全管理人员、班组长、安全员以及需要组织安全生产月培训的部门负责人核心解决的是围绕遵守安全生产法 当好第一责任人主题开展全员宣讲与培训时的课件准备问题。压缩包为单个pptx文件体积约43.52MB页面内容系统完整可直接用于会议投影、班组学习或线上培训。课件围绕第21个全国安全生产月的背景展开依次梳理安全生产月的由来与历年主题演变、2022年活动主要内容与有关要求、新《安全生产法》及《刑法》条款解读、企业安全生产八大主体责任、安全意识提升以及常用安全生产知识等模块其中对安全生产十五条措施、第一责任人七项职责、以案释法与警示教育、应急救援演练等要求均有具体展开便于宣讲人按章节取用。目前已有183人学习下载适合需要快速搭建培训框架、补充法规解读素材的安全管理从业者参考使用。1. 拿到 2022 安全生产月宣讲课件.pptx先别急着改内容每年五六月安全管理和培训岗的共享盘里都会冒出一批同源文件《2022年安全生产月主题宣讲课件十二.pptx》就是典型的一份二十来页目录里排着什么是安全生产月年度主要内容有关要求新安法及刑法解读企业八大主体责任安全意识提升几个板块末尾还有八安八险这类口号式清单。多数人的第一反应是打开 PowerPoint把封面年份改掉、主题句换掉、目录对齐一下然后另存为下一年——改完发现页码错位、字体串号、配图跑到框外。反直觉的地方在于这份课件真正有价值的不是页面上的文字而是二十几页的版式骨架哪一页用标题和内容版式、目录页用了几个占位符、章节标题统一落在第几号文本框里。把这些关系抽出来它就不再是一份只能手改的 PPT而是一套能参数化的模板。做 EHS 培训、安全月宣贯、部门文档资料批处理的人下面这套拆法都能直接复用。2. PPTX 的 OPC 包结构与 python-pptx 对象模型拆解2.1 先把它当 ZIP 包看.pptx的本质是一套 OPCOpen Packaging Conventions结构ZIP 装部件XML 描述内容.rels关系文件描述部件之间的引用。改后缀解开看一眼心里就有底了。cp 2022年安全生产月主题宣讲课件十二.pptx deck.zip unzip -l deck.zip | head -30输出里稳定出现的是这几类[Content_Types].xml声明部件类型ppt/presentation.xml记录页面顺序和画布尺寸ppt/slides/slide1.xml到slideN.xml是每页正文ppt/slides/_rels/slideN.xml.rels是这一页引用的图片和超链接ppt/slideLayouts/、ppt/slideMasters/、ppt/theme/分别管版式、母版和主题配色docProps/app.xml里还躺着页数、标题等摘要信息。关键在于幻灯片正文里通常只记我用的是哪个版式和覆盖过的字符属性字体、颜色、项目符号默认继承版式与母版。这就解释了为什么后面做文本替换时只要不动 run 的格式属性整页观感基本不会变。2.2 python-pptx 对象模型从 Presentation 到 run对象层级是Presentation → slides → slide.shapes → shape.text_frame → paragraphs → runs。文字真正存在 run 层而一个段落的文字经常被拆成多个 run——一句话中间换了字号或颜色PowerPoint 就会切开。下面这张表是日常最常打交道的几个属性。属性所属层级典型取值与含义slide.slide_layout.name幻灯片标题和内容节标题仅标题shape.shape_type形状TEXT_BOX / PLACEHOLDER / PICTURE / TABLEshape.placeholder_format.idx占位符0 通常是标题1 是正文靠它定位替换目标paragraph.level段落04对应项目符号的缩进层级run.font.size文本块Emu 长度对象用.pt换算磅值run.font.name文本块中文字体常返回 None实际由主题决定提示run 层的size、name返回 None 不代表没设值多半是继承自母版或主题判断格式时不能只看这一层。这里有个必踩的坑SmartArt落在p:graphicFrame里的 diagram和图表python-pptx 不会当作text_frame暴露出来。安全课件里的八安八险五个方面这类清单很多是用 SmartArt 排的抽取时会整块丢失。常见做法是在 PowerPoint 里把它转成普通文本框再处理或者退到 XML 层直接抓a:t节点。2.3 先跑一遍体检脚本在动手替换之前先摸清页数、版式分布和每页文字量比直接开改效率高得多。from pptx import Presentation DECK 2022年安全生产月主题宣讲课件十二.pptx prs Presentation(DECK) # 画布比例决定配图会不会被拉伸 ratio prs.slide_width / prs.slide_height print(画布(EMU):, prs.slide_width, prs.slide_height, →, 16:9 if ratio 1.7 else 4:3) for i, slide in enumerate(prs.slides, 1): text_shapes [s for s in slide.shapes if s.has_text_frame] chars sum(len(s.text_frame.text) for s in text_shapes) print(f{i:02d} | {slide.slide_layout.name:8} | 形状{len(slide.shapes):2} f| 含文本{len(text_shapes):2} | 字符{chars:4}) for s in slide.shapes: if s.is_placeholder and s.has_text_frame: # idx 就是后面做模板替换时的定位键 print(f idx{s.placeholder_format.idx} ftype{s.placeholder_format.type} :: f{s.text_frame.text[:20].strip()})几个参数的用法slide_width / slide_height用于判断画布比例16:9 的母版套 4:3 的图会被压扁每页字符数是快速区分纯图页和纯文页的指标字符数低于 20 的页基本都是全图封面或过渡页打印出来的idx是下一章替换要用的键先记下来。跑一遍再看输出二十几页的课件的结构关系基本就清楚了。3. 把宣讲课件正文抽成结构化数据占位符、层级与标题识别3.1 文本粒度paragraph.text 与 run 拼接的差别paragraph.text会把 XML 里的a:br软换行渲染成垂直制表符\v直接打印会出现乱码而.join(r.text for r in p.runs)又会丢掉软换行把两行挤成一行。稳妥写法是先取整体再归一化raw para.text text raw.replace(\v, \n).strip()另一个高频坑是分段替换如果一个段落的文字被拆在三个 run 里你按 run 逐个replace第一责任人跨在两个 run 之间就永远命中不了。所以规则很简单——先拼整段判断再决定把结果写回哪个 run这点在下一章的替换实现里会直接用到。3.2 标题识别的三条启发式规则课件做多了会发现同一种标题在不同页里可能落在完全不同的容器里。按可靠性排序用三条规则兜底shape.is_placeholder且placeholder_format.idx 0这是最可靠的真标题。该页所有段落中字号最大、且长度不超过 30 字的段落用于章节分隔页。shape.top最小最靠上的文本框用于封面和纯文本框排的目录页。优先级按 1 2 3命中即停。安全类课件的章节页经常是大号字 装饰色块的手工排版标题并不在占位符里第 2 条规则就是为这种情况准备的。字号取值时记得过滤掉 Nonesizes [r.font.size.pt for r in para.runs if r.font.size] size max(sizes) if sizes else 03.3 目录项与章节页对齐目录页里的条目通常带 01 这种前缀和尾部页码直接和章节页标题比对是对不上的需要先剥离import re def clean_toc(text): t re.sub(r^0?\d{1,2}[\s、.]*, , text.strip()) # 去掉 01、2、3. 前缀 t re.sub(r[\s·]*\d{1,2}$, , t) # 去掉尾部页码 return t.strip()把整份文件抽成 JSON后续做比对、做模板填空都靠这份中间产物import json def extract(prs): pages [] for i, slide in enumerate(prs.slides, 1): items [] for shp in slide.shapes: if not shp.has_text_frame: continue for p in shp.text_frame.paragraphs: txt p.text.replace(\v, \n).strip() if not txt: continue sizes [r.font.size.pt for r in p.runs if r.font.size] items.append({ shape_id: shp.shape_id, top_emu: shp.top or 0, # 用于按位置排序 level: p.level, # 项目符号层级 size: max(sizes) if sizes else None, text: txt, }) pages.append({slide: i, layout: slide.slide_layout.name, items: items}) return pages data extract(prs) json.dump(data, open(deck.json, w, encodingutf-8), ensure_asciiFalse, indent2)抽完再补一遍清洗把每页都出现的页脚短句和纯数字页码剔掉否则统计字数、做目录对齐时都会被污染FOOTER 遵守安全生产法 当好第一责任人 # 逐页出现的固定短句 for page in data: page[items] [it for it in page[items] if not re.fullmatch(r\d{1,3}, it[text]) and it[text] ! FOOTER]抽取字段来源用途slide枚举序号与 PDF 页码一一对应layoutslide_layout.name判断这一页能否整页复用shape_id/top_emu形状属性按位置还原阅读顺序levelparagraph.level重建主体责任的五个方面这类层级sizerun.font.size标题识别、字号一致性检查跑完这一步一份二十几页的课件会变成一份可检索的 JSON。想查新安法解读出现在第几页、那一页有几个层级直接对deck.json做过滤即可不用再人工翻页。4. 基于母版批量重排新一期安全宣讲课件4.1 三种替换粒度怎么选同一个需求把 2022 换成 2023有三种做法成本和风险完全不同。粒度做法格式保留适用页型整页重做add_slide(layout)后重填占位符不保留页内个性格式结构固定的章节页占位符替换按placeholder idx定位后改文字完整保留封面、目录页run 级替换拼段→替换→写回runs[0]保留首个 run 的字符格式正文里大面积改年份、主题句选型逻辑很直接占位符能定位到就优先用它因为那是模板设计者留下的接口定位不到、又要改动分布在十几个文本框里的年份数字就用 run 级替换横扫一遍。4.2 保留格式的 run 级替换def replace_text(shape, mapping): 在单个形状内做替换保留段落首个 run 的字符格式 if not shape.has_text_frame: return 0 hit 0 for para in shape.text_frame.paragraphs: if not para.runs: continue old para.text.replace(\v, \n) new old for k, v in mapping.items(): new new.replace(k, v) if new old: continue para.runs[0].text new # 全部内容塞进第一个 run for r in para.runs[1:]: # 其余 run 清空避免文字重复显示 r.text hit 1 return hit参数说明mapping是{旧串: 新串}字典旧串必须和 XML 中的实际字符逐字一致——全角半角、空格有无都要分别写比如第 21 个和第21个是两条不同的规则返回值hit是命中段落数可以用它做替换完整性的第一道判断命中为 0 说明这页根本没这个串。调用时遍历全篇MAPPING { 2022: 2023, 第21个: 第22个, 第 21 个: 第 22 个, } total 0 for slide in prs.slides: for shp in slide.shapes: total replace_text(shp, MAPPING) prs.save(deck_2023.pptx) print(命中段落数:, total)注意年份替换不能无脑全局跑。课件里往往有一张历年全国安全生产月主题的对照表表里从 2002 到 2022 逐年排列那是历史数据不该跟着变。这类页要单独列白名单跳过。4.3 复制版式新增页面与图片关系文件python-pptx 没有现成的copy_slide常见做法是先按原版式加一页空的再把原页的形状元素深拷贝过去。import copy def duplicate_slide(prs, index): src prs.slides[index] dst prs.slides.add_slide(src.slide_layout) for shp in list(dst.shapes): # 清掉新页自带占位符避免叠加 shp._element.getparent().remove(shp._element) for shp in src.shapes: # 深拷贝原页所有形状 dst.shapes._spTree.append(copy.deepcopy(shp._element)) return dst这里的坑在图片slide.xml里图片写的是r:embedrId5真正的映射关系在ppt/slides/_rels/slideN.xml.rels。deepcopy只搬走了引用没搬关系项用 PowerPoint 打开会提示需要修复。做法上分两种纯文字页直接套用上面这段带图页要么手工重建要么把目标图片部件用relate_to挂到新页的 part 上并改写 rId。课件里图片一多与其补关系不如在母版里预留好版式让add_slide一次到位。5. 交付前的校验转 PDF、溢出预警与残留文本比对5.1 无头转 PDF 做渲染检查改完之后第一件事不是发出去是让渲染引擎先看一遍。装了 LibreOffice 的话一条命令就能批量转soffice --headless --convert-to pdf --outdir ./out deck_2023.pptx pdfinfo out/deck_2023.pdf | grep -E Pages|Page sizePages对不上原文件说明 4.3 里复制页出了错Page size变了说明画布比例被动过。这两条比肉眼翻页可靠得多尤其是几十页的课件。5.2 文本框溢出预警主题句长度一变字号不变的情况下就可能顶出框。用字符数和框宽做一次粗略估算能在交付前把风险页捞出来EMU_PER_PT 12700 # 1 磅 12700 EMU def est_overflow(shape, line_ratio1.35): 返回正数表示估算文本高度超出框高单位磅 if not shape.has_text_frame or not shape.width or not shape.height: return None sizes [r.font.size.pt for p in shape.text_frame.paragraphs for r in p.runs if r.font.size] pt max(sizes) if sizes else 18 per_line max(int(shape.width / EMU_PER_PT / pt), 1) # 每行可放几个中文字 lines sum(max(1, -(-len(p.text) // per_line)) for p in shape.text_frame.paragraphs) need_pt lines * pt * line_ratio return round(need_pt - shape.height / EMU_PER_PT, 1)要点在per_line中文按一个字宽约等于字号估算shape.width / EMU_PER_PT把 EMU 换算成磅除以字号就是每行容纳的字数。返回正数就去 PowerPoint 里放大文本框或降字号中英混排时这个估算偏乐观英文单词按半个字宽算更准。5.3 残留文本比对最后一步是查漏改。把新版解开直接在 XML 层搜旧串unzip -o -q deck_2023.pptx -d /tmp/new grep -rno 2022\|第21个\|第 21 个 /tmp/new/ppt/slides/*.xml输出的每一条都是待确认项命中在历史主题对照表里就保留命中在封面或页脚就是漏改。把这条 grep 和 5.2 的溢出检查一起串到替换脚本末尾一次跑完输出两份清单——一份漏改候选一份溢出风险页逐条消化完再交付。本文还有配套的精品资源点击获取
返回列表