ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

python-pptx 解析与批量生成安全生产月 PPTX 课件

2026/9/17 14:16:25 拓冰建站 浏览量
python-pptx 解析与批量生成安全生产月 PPTX 课件 简介这份课件面向企业安全管理人员、班组负责人及安全生产培训组织者围绕2022年全国第21个“安全生产月”展开帮助使用者在内部宣讲、班前会或专题培训中快速组织一次内容完整的安全教育。包内为1个pptx演示文稿压缩包约43.52MB图文版面与章节配图齐全可直接投影或按需增删修改。课件按七个模块编排依次讲解安全生产月的由来与历年主题演变、2022年“遵守安全生产法 当好第一责任人”的活动主要内容与有关要求、新《安全生产法》及《刑法》相关条款解读、企业安全生产八大主体责任、安全意识提升以及常用安全生产知识脉络清晰、层次分明。其中对安全生产十五条措施、第一责任人七项职责、以案释法与警示教育等内容均有展开便于宣讲时逐条对照。目前已有183人学习下载适合需要现成宣贯素材、又想贴合当年政策口径的安全从业者参考使用。1. 一份叫「2022年安全生产月主题宣讲课件十二.pptx」的文件为什么要用代码去处理名字里带个「十二」说明前面至少还有一到十一。安全宣讲课件往往就是这么攒出来的每换一次场地、换一批案例就在上一版基础上复制一份改几页最后谁也说不清哪一版是最新的。这类文件的典型特征是页数多、图片多、文本框位置随版本漂移人工核对一遍要大半天。用 python-pptx 把 pptx 当数据处理能一次拿到每页标题、正文和备注也能反过来按模板批量产出新版本。适合两类人要归档、比对、复用宣讲材料的培训管理者以及需要把课件灌进内部培训系统的后端和运维同学。.pptx 本质是个 zip这点决定了后面所有解析思路。2. 拆开 pptx 的 OPC 包python-pptx 读安全生产月宣讲课件的正确入口2.1 pptx 不是二进制黑盒zip 里到底装了什么把「2022年安全生产月主题宣讲课件十二.pptx」当压缩包看结构立刻清晰。它遵循 OPCOpen Packaging Conventions页面内容、关系、媒体资源分开存放纯文本 XML。# 只看目录结构不解压 unzip -l 2022年安全生产月主题宣讲课件十二.pptx | grep -E slides|notesSlides|media | head -20输出里会出现ppt/slides/slide1.xml、ppt/slides/_rels/slide1.xml.rels、ppt/notesSlides/notesSlide1.xml、ppt/media/image1.png这几类路径。这里有个坑slideN.xml的文件名序号不等于放映顺序。真正的顺序记录在ppt/presentation.xml的p:sldIdLst里每个p:sldId用r:id指向ppt/_rels/presentation.xml.rels中的具体文件。python-pptx 的prs.slides已经按放映顺序排好所以遍历时不要自己去按文件名排序否则页序会和讲稿对不上。想验证这一点可以对照着看from pptx import Presentation import zipfile path 2022年安全生产月主题宣讲课件十二.pptx prs Presentation(path) # slide_width / slide_height 单位是 EMU914400 EMU 1 英寸 2.54 cm print(页数:, len(prs.slides), 画布:, prs.slide_width, prs.slide_height) with zipfile.ZipFile(path) as z: raw sorted(n for n in z.namelist() if n.startswith(ppt/slides/slide) and n.endswith(.xml)) print(包内文件顺序:, raw[:5])参数说明EMU 是 OOXML 的长度单位16:9 画布默认 12192000 × 68580004:3 是 9144000 × 6858000。后面凡是设文本框宽高、位置都得用Emu()或Pt()包一层直接写整数会被当成 EMU 处理位置会错到屏幕外。字号用Pt1 磅 12700 EMU这个换算在反推字号时要用。2.2 最小读取脚本遍历每一页的 shape生产环境里最容易踩的一点是slide.shapes只返回顶层形状组合Group里的文本框必须递归进去。一份排版讲究的宣讲课件封面和流程图几乎一定是组合漏掉递归就会丢掉大半文字。from pptx import Presentation from pptx.enum.shapes import MSO_SHAPE_TYPE def walk(shapes, depth0): 深度优先遍历形状组合形状继续往下钻 for sh in shapes: yield sh, depth if sh.shape_type MSO_SHAPE_TYPE.GROUP: yield from walk(sh.shapes, depth 1) prs Presentation(2022年安全生产月主题宣讲课件十二.pptx) for i, slide in enumerate(prs.slides, start1): print(f slide {i} | layout{slide.slide_layout.name}) for sh, d in walk(slide.shapes): kind TEXT if sh.has_text_frame else str(sh.shape_type) print( * d, sh.shape_id, sh.name, kind)逻辑说明walk用生成器而不是递归收集列表页数上百时内存占用差别明显shape_id是页面内唯一且跨版本相对固定的标识用它做版本比对比用形状名字可靠——形状名字会被 PowerPoint 自动改成「文本框 27」这种带序号的值换一版就变。不同类型形状的取文本方式差别很大处理前先对号入座形状类型判断方式取文本 / 取内容普通文本框、占位符sh.has_text_framesh.text_frame.text组合形状sh.shape_type MSO_SHAPE_TYPE.GROUP递归sh.shapes表格sh.has_tablesh.table.rows遍历cell.text图表sh.has_chart无文本需读sh.chart.plots图片、图标sh.shape_type MSO_SHAPE_TYPE.PICTURE只有sh.image.blob/sh.image.sha12.3 三个最容易漏文本的位置第一是备注页。宣讲课件的讲稿通常写在备注里而备注不在slide.shapes里必须走slide.notes_slide.notes_text_frame且要用slide.has_notes_slide先判断否则部分页会抛异常。第二是表格。表格是 GraphicFrame没有has_text_frame用统一遍历时会被if not sh.has_text_frame: continue直接跳过。安全宣讲课件里的「隐患对照表」「责任分工表」恰恰是信息密度最高的部分。第三是母版与版式上的固定文字比如页脚「XX 公司安全环保部」。这些在slide.slide_layout和prs.slide_master上不在页面里。要不要抽取决于你拿这些数据干什么——做全量归档就抽做页面比对就跳过。for i, slide in enumerate(prs.slides, start1): if slide.has_notes_slide: print(i, 备注:, slide.notes_slide.notes_text_frame.text[:60]) for sh in slide.shapes: if sh.has_table: # 表格单独处理 for row in sh.table.rows: print(i, [c.text.strip() for c in row.cells])3. 把宣讲内容抽成可检索的 JSON / Markdown3.1 抽取规则标题、正文、备注怎么分区分标题和正文有两种做法。规范做的课件用版式占位符可以直接用sh.is_placeholder配合sh.placeholder_format.type PP_PLACEHOLDER.TITLE判定准确率高。另一种是从一到十一复制改出来的「野」课件标题就是一个普通文本框只能靠启发式取页面上边缘位置最靠上、且字号最大的那个文本框当标题。我一般的顺序是先试占位符命中就认一页里一个占位符都没有再退回启发式。这样在混合来源的课件集合上表现最省心。3.2 完整脚本与字段说明import json from pathlib import Path from pptx import Presentation from pptx.enum.shapes import MSO_SHAPE_TYPE from pptx.enum.shapes import PP_PLACEHOLDER def iter_shapes(shapes): for sh in shapes: if sh.shape_type MSO_SHAPE_TYPE.GROUP: yield from iter_shapes(sh.shapes) else: yield sh def text_of(sh): if sh.has_text_frame: return \n.join(p.text for p in sh.text_frame.paragraphs).strip() if sh.has_table: return \n.join(\t.join(c.text.strip() for c in r.cells) for r in sh.table.rows) return def dump(path): prs Presentation(path) pages [] for idx, slide in enumerate(prs.slides, start1): title, body , [] for sh in iter_shapes(slide.shapes): txt text_of(sh) if not txt: continue is_title sh.is_placeholder and sh.placeholder_format.type in ( PP_PLACEHOLDER.TITLE, PP_PLACEHOLDER.CENTER_TITLE) if is_title and not title: title txt.replace(\n, ) else: body.append(txt) notes slide.notes_slide.notes_text_frame.text.strip() if slide.has_notes_slide else pages.append({page: idx, title: title, body: body, notes: notes}) return {file: Path(path).name, slide_size: [prs.slide_width, prs.slide_height], pages: pages} doc dump(2022年安全生产月主题宣讲课件十二.pptx) Path(课件.json).write_text(json.dumps(doc, ensure_asciiFalse, indent2), encodingutf-8)字段设计说明如下这套结构是为后续比对和入库准备的字段类型说明filestring原文件名归档时用来追溯版本slide_size[int, int]EMU 宽高判断 16:9 还是 4:3pages[].pageint放映顺序从 1 开始与prs.slides一致pages[].titlestring首个标题占位符文本换行已压平pages[].bodystring[]除标题外的所有文本块保持框内换行pages[].notesstring讲稿无备注页时为空串而不是 null再看一层导出为 Markdown方便丢进内部 Wiki 做全文检索lines [f# {doc[file]}] for p in doc[pages]: lines.append(f\n## 第 {p[page]} 页 {p[title]}.rstrip()) lines.extend(p[body]) if p[notes]: lines.append(f**讲稿**{p[notes]}) Path(课件.md).write_text(\n\n.join(lines), encodingutf-8)3.3 抽出来的结果怎么自查代码跑完不报错不代表抽对了。三类问题几乎每批课件都会遇到空白页。title和body都为空但页面里其实是一张大图比如隐患照片墙。清洗时不能直接删页先看有没有PICTURE类型形状有就标记为「纯图页」保留。私有区乱码。抽出来是一串\uf0b7之类的字符这是 Wingdings、Webdings 这类符号字体的编码。ord(ch) 0xF000基本可以判定为符号字符处理成对应的项目符号或者直接丢掉。重复文本。同一个段落被拆进多个文本框或者标题在正文里又出现一次。用一条简单的巡检语句就能筛出来def audit(pages): for p in pages: if not p[title] and not p[body]: print(疑似空白页:, p[page]) if p[title] and any(p[title] in b for b in p[body]): print(标题在正文重复:, p[page]) if p[notes] and len(p[notes]) 5: print(讲稿过短可能是页码占位:, p[page]) audit(doc[pages])异常现象常见成因处理方式某页正文为空内容是图片或图表检查 PICTURE / CHART 形状标记而非删除文本含私有区字符符号字体按ord(ch) 0xF000过滤一段话被拆成多块手工回车换行按页面聚合后按空行重新分段标题识别错位无占位符版式退回位置加字号启发式4. 按模板批量生成下一版安全生产月宣讲课件4.1 模板工程化的三条硬规则第一条占位符写成{{key}}并且保证在同一个 run 里。pptx 的段落由 run 组成同一句话如果中间改过字体颜色就会裂成多个 run{{project_name}}可能被切成两半正则匹配不到。写模板时统一全选一段重新设格式能避免大部分问题。第二条脚本只改文本不动坐标。任何在代码里硬算left、top的操作都会在换模板后崩掉版式的事交给母版和版式页。第三条中文字体只用一个。一份课件里混了三种中文字体在没装对应字体的机器上渲染行高和换行全变页数可能多出一页。4.2 替换脚本与参数表import re from pptx import Presentation from pptx.util import Pt from pptx.enum.text import MSO_AUTO_SIZE TOKEN re.compile(r\{\{\s*([A-Za-z0-9_])\s*\}\}) def fill_runs(shape, data): 按 run 替换保留原有字体、颜色、加粗等格式 for para in shape.text_frame.paragraphs: for run in para.runs: if TOKEN.search(run.text): run.text TOKEN.sub(lambda m: str(data.get(m.group(1), m.group(0))), run.text) def set_body_style(shape, size18): tf shape.text_frame tf.word_wrap True tf.auto_size MSO_AUTO_SIZE.NONE # 关掉自动缩放避免打开时被重算 for para in tf.paragraphs: for run in para.runs: run.font.size Pt(size) prs Presentation(模板_安全生产月宣讲.pptx) data {org: XX 公司, month: 6 月, year: 2022, lead: 安全环保部} for slide in prs.slides: for sh in slide.shapes: if sh.has_text_frame: fill_runs(sh, data) prs.save(2022年安全生产月主题宣讲课件十三.pptx)关于{{key}}被拆分的情况加一个兜底如果整个段落文本里能匹配到{{就把段落的 runs 合并成一个再替换代价是这一段会丢掉局部格式但对模板来说是划算的。auto_size值得单独说一句。python-pptx 写进去的只是a:normAutofit标记真正的字号缩放系数由 PowerPoint 打开时重算脚本里读不到。所以别指望设了自动缩放就万事大吉观感要自己控。参数表如下正文框按这个配一般不会太难看出问题参数推荐值说明正文字号16–20 pt投影场景不小于 16会议室投屏 20 更稳妥word_wrapTrue关闭会导致长句横向溢出页面auto_sizeNONE由脚本按字数控制字号避免渲染差异行距1.2–1.5 倍中文段落 1.3 倍阅读体验较好每页正文≤ 180 字超过就该拆页投影后排看不清字号反推有个够用的估算中文字符按一个全角宽算每行可容纳字数约等于「文本框宽度pt÷ 字号pt」总行数约为「总字数 ÷ 每行字数」所需高度约为「行数 × 字号 × 1.4」。拿这个值和文本框高度比超了就降一档字号18 → 16 → 14 逐级试比打开文件肉眼调快得多。4.3 克隆页面时最容易丢的东西需要把某一页复制成 N 页比如每个车间一页隐患清单时常见写法是深拷贝spTree里的元素import copy def duplicate_slide(prs, index, blank_layout_index6): src prs.slides[index] dst prs.slides.add_slide(prs.slide_layouts[blank_layout_index]) for shp in src.shapes: dst.shapes._spTree.append(copy.deepcopy(shp._element)) return dst这段能跑通但只复制了形状本身图片、超链接、图表数据这些靠关系rels维系的东西不会跟着走页面上的图会变成红叉。稳妥做法是模板里预先放好足够多的空白页再逐页填内容而不是运行时克隆。真要克隆就得同时处理ppt/slides/_rels/下的关系文件和[Content_Types].xml工作量和直接做模板差不多。5. 交付前巡检转 PDF、查页数、看字体到底有没有嵌进去脚本生成完光靠 Python 打开是看不出问题的因为 python-pptx 不做渲染。最省事的验证是用 LibreOffice 无头模式转一份 PDF让真实的排版引擎跑一遍。# 转 PDF输出到 ./out soffice --headless --convert-to pdf --outdir ./out 2022年安全生产月主题宣讲课件十三.pptx # 页数、字体、内嵌情况 pdfinfo out/2022年安全生产月主题宣讲课件十三.pdf | grep Pages pdffonts out/2022年安全生产月主题宣讲课件十三.pdfpdfinfo里的 Pages 是你唯一能信的页数和len(prs.slides)对不上就说明有页面在渲染阶段被合并或丢弃通常指向被删掉的媒体关系文件。pdffonts的输出重点看emb那一列全是no就意味着换台机器打开会跑版。字体缺失在 Linux 服务器上尤其常见用fc-list :langzh确认中文字体装了没有没装的话转出来全是方块但脚本本身不会报任何错。巡检项命令通过标准页数一致pdfinfo ... | grep Pages与len(prs.slides)相等字体可用fc-list :langzh | wc -l大于 0且含模板指定字体字体内嵌pdffonts ...中文字体emb列为 yes文本可提取pdftotext ... - | head无大片方块和乱码体积异常ls -lh *.pptx与上一版相差不超过 20%体积突然涨一截八成是同一张图被重复嵌了多份。用sha1在包内去重能直接看出问题读所有ppt/media/*的字节算摘要重复的记录下来再看是哪几页引用了它们替换成同一份关系即可。这套巡检跑完再发出去比事后被人截图问「第三页字怎么糊了」省事得多。本文还有配套的精品资源点击获取