简介:这份《岩石和矿物》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_type是MSO_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.part | ppt/presentation.xml | 版面尺寸、幻灯片顺序 |
prs.slide_layouts | ppt/slideLayouts/slideLayoutN.xml | 标题框、正文框的位置定义 |
slide.slide_layout.slide_master | ppt/slideMasters/slideMaster1.xml | 主题色、主题字体 |
slide.shapes | ppt/slides/slideN.xml的p:spTree | 形状树,逐页内容的真正载体 |
shape.text_frame | a:txBody | 段落与 run(文字片段) |
shape.table | a: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都有Type和Target。Type以/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同时传width和height会强制拉伸,只传一个才等比;add_table的宽高是建议外框,最终列宽由columns[i].width决定,行高则是最小值;add_chart的categories决定条目的排列顺序,硬度这类有序数据一定按硬度升序排,否则图上读不出趋势。
3.3 数据驱动:把岩石矿物清单批量灌进占位符
课件的数据源用 CSV 最省事,一行一个条目,字段设计如下:
| 字段 | 示例 | 用途 |
|---|---|---|
name_cn | 石英 | 主标题 |
name_en | Quartz | 副标题,便于检索 |
category | 矿物 / 岩浆岩 / 沉积岩 / 变质岩 | 分组与配色 |
formula | SiO2 | 表格行,岩石类留空 |
mohs | 7 | 属性表与硬度图表 |
image | quartz.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:ea和a: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.25、p.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_row和horz_banding只是表格样式的开关,真正决定观感的还是列宽分配:属性名列宽给 1.6~2.0 英寸,取值列吃掉剩余宽度,化学式这类长字符串就不会挤成两行。
4.4 体积与打开报错
| 现象 | 大概率原因 | 处理 |
|---|---|---|
| 打开提示「需要修复」 | rels 指向的部件不存在 | 用 zipfile 逐个校验Target是否存在 |
| 中文全变宋体 | 只设了a:latin | 补a:ea、a:cs |
| 图片被压扁 | 同时传了width和height | 只给一个,或按比例算 |
| 表格末行被切 | 行高只是建议值,文本撑高后超出 | 减小字号或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显式指定填充色,而不是依赖主题的自动配色,否则同一批幻灯片里岩浆岩和沉积岩可能拿到相近的色块,投影上根本分不出来。
本文还有配套的精品资源,点击获取