ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

python-pptx 实现《岩石和矿物》PPT 批量生成与 OOXML 校验

python-pptx 实现《岩石和矿物》PPT 批量生成与 OOXML 校验 简介这份《岩石和矿物》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 EMU1 厘米 360000 EMU1 磅 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.xml的p:spTree形状树逐页内容的真正载体shape.text_framea:txBody段落与 run文字片段shape.tablea:tbl行列单元格与填充色把这层对应关系记住后面调字体、调行高就知道该往哪个节点上找。2.2 沿 rId 找到图片与图表的真实数据课件里最容易出问题的不是文字而是图片比例和被压成一团的图表。图片对象本身不带二进制它只持有一个关系 IDrId真正的字节在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), widthInches(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_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), widthInches(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, encodingutf-8-sig, newline) as f: for row in csv.DictReader(f): fill_slide(prs.slides.add_slide(title_only), row) prs.save(岩石和矿物-自动生成.pptx)encodingutf-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, latinArial, 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/topPx(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.62.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_pages48) print(bad[:10], 共, len(bad), 处问题)这种回读校验比「人肉翻页」可靠因为它检查的是形状树本身不受渲染差异影响。把expect_pages换成一份从 CSV 统计出来的预期值页数漂移当天就能发现。5.2 导出位图巡检与两层解耦结构没问题不代表排版没问题投影比例、字体替换这些只有渲染出来才看得见。无桌面环境用命令行转换soffice -env:UserInstallationfile:///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显式指定填充色而不是依赖主题的自动配色否则同一批幻灯片里岩浆岩和沉积岩可能拿到相近的色块投影上根本分不出来。本文还有配套的精品资源点击获取
返回列表