python-pptx 实现《岩石和矿物》PPT 批量生成与 OOXML 校验
2026/9/20 16:37:04 网站建设 项目流程

简介:这份《岩石和矿物》PPT课件面向小学科学或初学地球科学知识的教师与学生,围绕岩石的观察、描述与分类展开系统讲解。内容从岩石成因入手,梳理岩浆岩、沉积岩与变质岩三大类别,并借助看、摸、闻、敲击、刻划、称重等观察方法,引导学习者识别页岩、砂岩、石灰岩、砾岩、大理岩、花岗岩、板岩等常见岩石的颜色、颗粒、层理与硬度特征,同时延伸至化石与陨石的识别要点。资源包内仅含1个pptx文件,体积约2.71MB,可用于课堂演示、备课讲解与自主复习。课件还通过滴稀盐酸冒气泡等实验现象,帮助判断岩石是否含碳酸钙,并说明砂岩、石灰岩、花岗岩在建筑与工业中的常见用途。目前已有109人学习,适合需要搭建岩石单元教学框架、补充课件的教师及自主学习者。

1. 一份《岩石和矿物.pptx》背后,其实是一套可编程的图文排版问题

同事把一份《岩石和矿物.pptx》丢过来:两百多页,每页一张标本照片、一张属性表、一段成因说明,要求把硬度列统一改成莫氏硬度,再补一列化学式。手改到第三十页就知道这事不能靠鼠标。这类课件的本质是高度重复的图文排版——版式固定、字段固定、图片按命名规则落位。把它当成数据驱动的文档生成问题,python-pptx 配一张 CSV 就能批量产出,改配色、换字号都不用重排。下面按解析、生成、排错、校验的顺序走一遍,适合要接手教学资源包或企业培训课件的工程师,也适合被临时拉来做文档自动化的后端。

2. 解析《岩石和矿物.pptx》:从幻灯片树到 rId 引用链

要批改一份课件,第一步不是写生成脚本,而是先把它读明白。同样一页「石英」,标题框、正文框、图片框在 XML 里是三个互不相干的节点,只有走通对象模型和关系链,才知道该改哪一个。

2.1 打开文件之后:Presentation 到 Shape 的对象链

python-pptx 的入口是Presentation,它把 OOXML 包读进内存后暴露成树状对象。写一段「体检脚本」,先把每页的骨架打出来:

from pptx import Presentation from pptx.util import Emu prs = Presentation("岩石和矿物.pptx") print("页数", len(prs.slides)) print("版面", Emu(prs.slide_width).inches, "x", Emu(prs.slide_height).inches) for i, slide in enumerate(prs.slides, 1): print(f"[{i}] layout={slide.slide_layout.name}") for shape in slide.shapes: idx = shape.placeholder_format.idx if shape.is_placeholder else None txt = "" if shape.has_text_frame: # 表格、图片没有 text_frame txt = shape.text_frame.text[:20].replace("\n", " ") print(" ", shape.shape_type, shape.name, "idx=", idx, "|", txt)

prs.slides是可迭代的幻灯片序列,顺序就是放映顺序;slide.slide_layout指回该页使用的版式;shape.is_placeholder用来区分「占位符」和「自由画的文本框」,前者能跟着版式自动对齐,后者一旦生成就固定坐标。shape.shape_typeMSO_SHAPE_TYPE枚举,常见的13是图片、19是表格、3是图表、14是占位符。

尺寸单位是 EMU,不是像素:1 英寸 = 914400 EMU,1 厘米 = 360000 EMU,1 磅 = 12700 EMU。16:9 的版面通常是 12192000 × 6858000 EMU,也就是 13.333 × 7.5 英寸。

Python 对象对应 XML 部件在《岩石和矿物》里的用途
prs.partppt/presentation.xml版面尺寸、幻灯片顺序
prs.slide_layoutsppt/slideLayouts/slideLayoutN.xml标题框、正文框的位置定义
slide.slide_layout.slide_masterppt/slideMasters/slideMaster1.xml主题色、主题字体
slide.shapesppt/slides/slideN.xmlp:spTree形状树,逐页内容的真正载体
shape.text_framea:txBody段落与 run(文字片段)
shape.tablea:tbl行列单元格与填充色

把这层对应关系记住,后面调字体、调行高,就知道该往哪个节点上找。

2.2 沿 rId 找到图片与图表的真实数据

课件里最容易出问题的不是文字,而是图片比例和被压成一团的图表。图片对象本身不带二进制,它只持有一个关系 ID(rId),真正的字节在ppt/media/下。python-pptx 做了封装,直接读属性即可:

for i, slide in enumerate(prs.slides, 1): for shape in slide.shapes: if shape.shape_type == 13: # PICTURE img = shape.image r_shape = shape.width / shape.height r_px = img.size[0] / img.size[1] flag = "失真" if abs(r_shape - r_px) > 0.02 else "正常" print(i, shape.name, img.ext, img.size, img.dpi, flag)

img.size是像素元组(宽, 高)img.dpi(水平, 垂直)。把形状的宽高比和像素宽高比一比,就能批量揪出「被拉扁的岩石薄片照片」——这种图在投影上看不出来,导出 PDF 时特别明显。基准 96 dpi 的图在 4 英寸宽的框里,需要 384 像素以上才不糊。

图表要拿数据得绕一下,因为值缓存在图表 XML 里,源工作簿是另一个部件:

from pptx.oxml.ns import qn for shape in slide.shapes: if shape.has_chart: chart = shape.chart print(chart.chart_type, [s.name for s in chart.plots[0].series]) ext = shape._element.find(".//" + qn("c:externalData")) if ext is not None: rId = ext.get(qn("r:id")) xlsx_part = chart.part.related_part(rId) # 内嵌工作簿 print("源文件", xlsx_part.partname)

related_part(rId)是通用的关系解析入口,图片、内嵌表格、版式都能用它取到目标部件。

2.3 用 zipfile 直接看 OOXML 包结构

所有pptx都是 zip 包,遇到 python-pptx 没暴露的字段,直接开箱看最快:

python -c " import zipfile with zipfile.ZipFile('岩石和矿物.pptx') as z: names = z.namelist() print(len(names), '个部件') print([n for n in names if n.startswith('ppt/media/')][:5]) print(z.read('ppt/slides/_rels/slide1.xml.rels').decode()) "

slide1.xml.rels里每一行Relationship都有TypeTargetType/image结尾指向媒体文件,以/slideLayout结尾指向版式,以/chart结尾指向图表,以/package结尾就是内嵌的 xlsx。Target是相对路径,所以ppt/slides/slide1.xml引用的图片会写成../media/image3.png

提示:同一张花岗岩照片往往被几十页共用,改图要动的是ppt/media下那一个部件,而不是逐页替换。先看 rels 再动手,能省掉大量重复写入。

3. 用 python-pptx 生成《岩石和矿物》课件的最小可跑脚本

读通结构之后,生成反而是直路。核心思路只有一句:版面交给模板,内容交给数据,脚本只负责把两者拼起来。

3.1 版式锁定:用 layout 名称而不是索引定位

prs = Presentation("模板.pptx") # 想从零开始就写 Presentation() layouts = {l.name: l for l in prs.slide_layouts} title_only = layouts.get("Title Only") or prs.slide_layouts[5] blank = layouts.get("Blank") or prs.slide_layouts[6]

中文版 PowerPoint 的版式名是本地化的,叫「仅标题」「空白」,同一份文件换到英文环境就取不到。稳妥写法是「名称做主键、索引兜底」:常用默认模板里 0 是标题幻灯片、1 是标题和内容、5 是仅标题、6 是空白。用自己准备的模板文件能彻底绕开这个问题,也顺便把主题色和字体一起锁死。

3.2 文本、表格、图片、图表四件套的最小写入代码

from pptx.util import Inches, Pt from pptx.enum.text import PP_ALIGN, MSO_ANCHOR from pptx.chart.data import CategoryChartData from pptx.enum.chart import XL_CHART_TYPE slide = prs.slides.add_slide(title_only) slide.shapes.title.text = "石英 Quartz" # 图片:只给宽度,让高度按原比例算,避免拉伸 pic = slide.shapes.add_picture("assets/quartz.jpg", Inches(0.6), Inches(1.6), width=Inches(4.2)) # 表格:行列数 + 初始外框,实际渲染以列宽行高为准 tbl = slide.shapes.add_table(5, 2, Inches(5.2), Inches(1.6), Inches(7.2), Inches(2.4)).table tbl.cell(0, 0).text, tbl.cell(0, 1).text = "属性", "取值" tbl.columns[0].width = Inches(1.8) tbl.rows[0].height = Inches(0.4) # 图表:条形图比柱状图更适合排硬度这种有序数据 cd = CategoryChartData() cd.categories = ["滑石", "方解石", "长石", "石英", "金刚石"] cd.add_series("莫氏硬度", (1, 3, 6, 7, 10)) gf = slide.shapes.add_chart(XL_CHART_TYPE.BAR_CLUSTERED, Inches(0.6), Inches(4.4), Inches(6.0), Inches(2.4), cd) gf.chart.has_legend = False gf.chart.value_axis.maximum_scale = 10

参数上有三个坑:add_picture同时传widthheight会强制拉伸,只传一个才等比;add_table的宽高是建议外框,最终列宽由columns[i].width决定,行高则是最小值;add_chartcategories决定条目的排列顺序,硬度这类有序数据一定按硬度升序排,否则图上读不出趋势。

3.3 数据驱动:把岩石矿物清单批量灌进占位符

课件的数据源用 CSV 最省事,一行一个条目,字段设计如下:

字段示例用途
name_cn石英主标题
name_enQuartz副标题,便于检索
category矿物 / 岩浆岩 / 沉积岩 / 变质岩分组与配色
formulaSiO2表格行,岩石类留空
mohs7属性表与硬度图表
imagequartz.jpg图片文件名,约定放assets/
note六方柱状晶体,贝壳状断口成因与鉴定要点
import csv from pathlib import Path ASSETS = Path("assets") def fill_slide(slide, row): slide.shapes.title.text = f"{row['name_cn']} {row['name_en']}" ph = slide.placeholders.get(1) # 正文占位符,可能不存在 if ph is not None: ph.text_frame.text = row["note"] src = ASSETS / row["image"] if src.exists(): slide.shapes.add_picture(str(src), Inches(0.6), Inches(1.6), width=Inches(4.2)) else: # 缺图不中断,留可见占位 box = slide.shapes.add_textbox(Inches(0.6), Inches(1.6), Inches(4.2), Inches(0.6)) box.text_frame.text = f"缺图:{row['image']}" with open("minerals.csv", encoding="utf-8-sig", newline="") as f: for row in csv.DictReader(f): fill_slide(prs.slides.add_slide(title_only), row) prs.save("岩石和矿物-自动生成.pptx")

encoding="utf-8-sig"是为了吃掉 Excel 导出 CSV 时带的 BOM,否则第一个字段名会变成\ufeffname_cn,后面取值全取不到。slide.placeholders.get(1)[1]安全,某些版式根本没有 idx 为 1 的占位符。缺图时写占位文本而不是抛异常,两百页的批量任务才不会因为一张图断在半路。

注意:shapes.add_picture对同一份图片二进制会复用同一个媒体部件,但如果你先把图片统一转码成 JPEG,每个文件都会变成独立部件,包体积会明显变大。要么统一转码后再去重,要么保持原文件不变。

4. 版式与内容排错:OOXML 层面的常见翻车点

脚本跑通不等于能交差。中文字体、单位换算、表格行高这三处,是《岩石和矿物》这类图文课件最集中的翻车现场。

4.1 中文字体不生效:a:latin 与 a:ea 的差别

run.font.name = "微软雅黑"只写了 DrawingML 里的a:latin,中文字符会回退到主题的东亚字体,页面上就是「英文雅黑、中文宋体」的割裂效果。要真正换掉中文,得把a:eaa:cs一起写上:

from pptx.oxml.ns import qn def set_run_font(run, latin="Arial", ea="微软雅黑"): run.font.name = latin rPr = run.font._rPr # 访问 run.font 时已确保 rPr 存在 for tag, face in (("a:latin", latin), ("a:ea", ea), ("a:cs", ea)): el = rPr.find(qn(tag)) if el is None: el = rPr.makeelement(qn(tag), {}) rPr.append(el) el.set("typeface", face)

三个子元素缺一不可:a:latin管西文,a:ea管中日韩,a:cs管复杂文种。段落级别再补上p.line_spacing = 1.25p.space_after = Pt(6),行距松紧就稳定了。想把字体一次性改到全篇,得动主题字体,python-pptx 没有公开接口,常规做法是准备一份已设好主题字体的模板文件,所有页面从它派生。

4.2 图片 DPI 与版面比例:EMU 换算别靠猜

尺寸单位混用是另一个高频问题。有人把像素值直接当 EMU 传进去,元素就飞到版面外了:

单位折合 EMU常见误用
Inches(1)914400稳妥,推荐
Cm(1)360000与 Pt 混算
Pt(1)12700字号单位,误用在left/top
Px(1)9525按 96 dpi 假设,导出后尺寸对不上

图片进框建议写个等比缩放函数,尤其薄片显微照片这种长宽比极端的图:

def fit_into(pic, box_w, box_h): r = min(box_w / pic.width, box_h / pic.height, 1.0) # 只缩不放 w, h = int(pic.width * r), int(pic.height * r) pic.width, pic.height = w, h pic.left = int(pic.left + (box_w - w) / 2) # 框内居中 pic.top = int(pic.top + (box_h - h) / 2)

分辨率上,投影和 PDF 输出按 150 dpi 准备就够,也就是 4 英寸宽的图长边 600 像素起步;要做印刷再上 300 dpi。原图动辄 4000 像素的照片,压到 1600 像素长边能省掉大半体积。

4.3 表格行高、文本溢出与自动缩放

rows[i].height只是最小高度,PowerPoint 会按文本自动撑高,所以「最后一行被切掉」几乎都发生在文字变长之后。要么控制字号和行距,要么给表格预留 15% 的纵向余量。单元格里的对齐和边距也得显式设:

tbl.first_row = True # 首行加粗底纹 tbl.horz_banding = False # 关掉隔行底色,矿物表观感更干净 for r in range(len(tbl.rows)): for c in range(len(tbl.columns)): cell = tbl.cell(r, c) cell.vertical_anchor = MSO_ANCHOR.MIDDLE cell.margin_left = cell.margin_right = Inches(0.08) for p in cell.text_frame.paragraphs: p.line_spacing = 1.1 for run in p.runs: run.font.size = Pt(12)

first_rowhorz_banding只是表格样式的开关,真正决定观感的还是列宽分配:属性名列宽给 1.6~2.0 英寸,取值列吃掉剩余宽度,化学式这类长字符串就不会挤成两行。

4.4 体积与打开报错

现象大概率原因处理
打开提示「需要修复」rels 指向的部件不存在用 zipfile 逐个校验Target是否存在
中文全变宋体只设了a:latina:eaa:cs
图片被压扁同时传了widthheight只给一个,或按比例算
表格末行被切行高只是建议值,文本撑高后超出减小字号或line_spacing,留余量
文件 200 MB 以上原图未重采样统一转 JPEG,长边压到 1600 像素
图表不显示数据内嵌工作簿部件缺失检查c:externalData的 rId 能否解析

「需要修复」绝大多数来自手工改 XML:改了[Content_Types].xml却漏掉新扩展名声明,或者删了媒体文件却没同步删 rels 里的关系项。改包之前先复制一份,改完用python -m zipfile -t检验压缩包完整性,再拿 PowerPoint 打开确认。

5. 进阶:把《岩石和矿物.pptx》做成可验证、可复用的产出

生成脚本能跑,只解决了「一次」。真正省事的是让产物自带校验,并且把数据层和表现层彻底分开——同一套数据既能出课件,也能出讲解稿或题库。

5.1 断言式校验:逐页检查标题、图片与表格

save()之后立刻回读文件,用断言把结构问题挡在交付之前:

from pptx import Presentation def check(path, expect_pages): prs = Presentation(path) if len(prs.slides) != expect_pages: raise AssertionError(f"页数 {len(prs.slides)},预期 {expect_pages}") problems = [] for i, slide in enumerate(prs.slides, 1): title = slide.shapes.title if title is None or not title.text.strip(): problems.append((i, "缺标题")) if not any(s.shape_type == 13 for s in slide.shapes): problems.append((i, "无图片")) for s in slide.shapes: if s.has_table and len(s.table.rows) < 5: problems.append((i, "表格行数不足")) return problems bad = check("岩石和矿物-自动生成.pptx", expect_pages=48) print(bad[:10], "共", len(bad), "处问题")

这种回读校验比「人肉翻页」可靠,因为它检查的是形状树本身,不受渲染差异影响。把expect_pages换成一份从 CSV 统计出来的预期值,页数漂移当天就能发现。

5.2 导出位图巡检与两层解耦

结构没问题不代表排版没问题,投影比例、字体替换这些只有渲染出来才看得见。无桌面环境用命令行转换:

soffice -env:UserInstallation=file:///tmp/lo_profile \ --headless --convert-to pdf --outdir build \ "/abs/path/岩石和矿物-自动生成.pptx" pdftoppm -jpeg -r 100 -f 1 -l 12 build/岩石和矿物-自动生成.pdf build/page

-env:UserInstallation指向一个临时目录,避免和已有进程抢用户配置;pdftoppm-r控制分辨率,100 足够看清表格是否溢出。前 12 页抽出来拼成一张联络表,一眼扫完就能定位版式问题。

5.3 数据层与表现层分离

minerals.csv里的字段当成契约:category决定用哪套配色,mohs决定图表数据,image决定配图。表现层只提供两个入口函数——「生成属性表」和「生成硬度图」,页面的组装逻辑完全不碰数据字段名。这样换一批岩石数据、换一个模板文件、把输出改成 PDF,改的都是同一处。真正的技巧在于给category建一张配色映射表,用RGBColor显式指定填充色,而不是依赖主题的自动配色,否则同一批幻灯片里岩浆岩和沉积岩可能拿到相近的色块,投影上根本分不出来。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询