AI 做 PPT 都绕不开它,python-pptx 却连删一张幻灯片都没做成 API
- python-pptx 为 AI 生成 PPT 服务提供底层支持,核心基于 xmlchemy 机制。
- python-pptx 缺乏删除幻灯片 API,但实现仅需两行代码。
- python-pptx 通过 OPC 格式规范操作 PPTX 文件,实现高效操作。
- xmlchemy 将 OOXML Schema 编译成 Python 类,确保操作合规。
- python-pptx 依赖 XlsxWriter 处理图表数据,实现图表与 Excel 数据同步。
你大概用过某种 AI 生成 PPT 的服务,丢一段需求进去,几分钟拿到一份排版齐整的 deck。这类工具前台五花八门,后台却高度一致,最后一公里几乎都是同一个 Python 库,python-pptx。
它 2012 年出现在 GitHub 上,3.5K Star,MIT 协议,纯 Python 实现,不装 PowerPoint 也能跑,macOS 和 Linux 都行。批量生成汇报材料、做语料抽取,全是它的地盘。
说真的,第一次读它源码之前,我以为这库就是一层薄薄的文件读写。读完才发现,它其实是一台「把 OOXML Schema 编译成 Python 类」的小型编译器,库名里的 pptx 只是产品,真正的工程核心在那套叫 xmlchemy 的机制上。
更有意思的是另一件事。这个库连「删除一张幻灯片」都没有官方 API,对应的 issue 从 2015 年挂到现在,11 年了。而我自己动手验证后发现,实现它只需要两行代码。
这两件事放在一起,才是这个项目完整的模样。
先把 .pptx 拆开
很多人对 PowerPoint 文件的想象还停留在「二进制大文件」。其实 2007 之后,.pptx 就是一个 ZIP 包,里面装着一堆 XML、图片和关系文件。
我在本地做了个实验,用 python-pptx 建 3 张幻灯片,保存后 unzip 看内容。
ppt/slides/slide1.xml 923 字节
ppt/slides/_rels/slide1.xml.rels
ppt/slides/slide2.xml 923 字节
ppt/slides/_rels/slide2.xml.rels
ppt/slides/slide3.xml 923 字节
ppt/slides/_rels/slide3.xml.rels
[Content_Types].xml
_rels/.rels
每张幻灯片是一个独立的 XML part,part 之间不靠路径互相引用,靠的是 _rels 目录下的关系文件。slide1.xml.rels 里记录着这张幻灯片连接到哪个版式、引用了哪些图片,每条连接有个 rId。整个演示文稿就是一张以 presentation.xml 为根的关系图。
这个格式规范叫 OPC,Open Packaging Conventions,ECMA 376 的一部分。Word 的 .docx、Excel 的 .xlsx 用的是同一套。理解了这一点,python-pptx 的代码结构就好懂了,它从头到尾都在做一件事,把这张关系图变成可以安全操作的 Python 对象。
四层结构,各管一段
src/pptx/ 的目录按职责切得非常干净。
最底层是 opc/,管物理包的读写。opc/serialized.py 里的 PackageWriter 负责 ZIP 落盘,写入顺序是固定的,先写 [Content_Types].xml,再写包级关系 /_rels/.rels,然后逐个写 part 的 blob 和各自的关系文件。读的时候 PackageReader 甚至兼容目录形式,一个解压过的文件夹也能当包打开。
往上一层是 parts/,每种 part 一个类,SlidePart、ImagePart、ChartPart 各自持有自己的 XML 元素和关系。
再往上是 oxml/,直接面对 OOXML 的 XML 词汇表,CT_Slide 对应 p:sld 元素,CT_Tbl 对应 a:tbl。这是全库最厚的一层,也是 xmlchemy 机制所在。
最外层 api.py、presentation.py、slide.py、shapes/ 提供人话 API,你在文档里看到的 Presentation()、slides.add_slide() 都在这一层,本体全是轻量代理,逻辑都下沉到了下面三层。
有个细节我特别喜欢。Presentation() 不带参数时加载的「空白演示」,不是程序拼出来的 XML,而是 api.py 的 _default_pptx_path() 指向的一个真实的 default.pptx 文件,就躺在包目录的 templates 下。空白文档本身也是用 PowerPoint 做出来的,库只管往这个底子上加东西。这个决定很省心,默认文档永远和真实 PowerPoint 行为一致。
xmlchemy,把 Schema 写成类声明
到核心了。操作 OOXML 最痛的问题不是「能不能改」,是「改了之后 PowerPoint 还认不认」。
OOXML 的 XML Schema 对子元素顺序有严格规定。一个 p:sld 元素下面,p:cSld 必须在前,p:clrMapOvr 在后,p:timing 更靠后。你要往一张幻灯片里插一个 p:timing,插错位置,PowerPoint 打开就是修复提示。手写 lxml 代码处理这些顺序,每个调用点都是坑。
python-pptx 的解法在 oxml/xmlchemy.py,717 行,把 Schema 声明直接写成 Python 类属性。
class CT_Slide(_BaseSlideElement):
cSld = OneAndOnlyOne("p:cSld")
clrMapOvr = ZeroOrOne("p:clrMapOvr", successors=("p:transition", "p:timing"))
transition = ZeroOrOne("p:transition", successors=("p:timing",))
timing = ZeroOrOne("p:timing", successors=())
这段代码在声明什么?p:sld 下有且只有一个 p:cSld,可选一个 p:clrMapOvr,它的后继元素是 p:transition 和 p:timing,以此类推。注意 successors 参数,它把 Schema 的顺序规则也带上了。
魔法在 MetaOxmlElement 这个元类里。类定义完成时,元类扫描所有类属性,遇到 ZeroOrOne、OneAndOnlyOne、OptionalAttribute 这类描述符,就调用它的 populate_class_members,往类上动态生成一整套方法。声明一个 timing = ZeroOrOne("p:timing", successors=()),这个类就自动有了 get_or_add_timing()、_add_timing()、_remove_timing()、_insert_timing() 四个方法,插入位置由 successors 序列算出来,永远合规。
这是声明式的胜利。Schema 里一条规则,代码里一行声明,顺序、基数、类型转换全从这一行生出来。全库 CT_ 开头的类上百个,都靠这套机制撑着。
另一半魔法在 oxml/__init__.py。文件开头创建了 lxml 的 ElementNamespaceClassLookup,挂到解析器上,然后一百多行 register_element_cls("c:catAx", CT_CatAx) 这样的注册调用。效果是,XML 字符串解析进来的一瞬间,<c:catAx> 元素就已经是 CT_CatAx 实例,带着全部定制方法。不需要任何「先解析再遍历转换」的步骤,解析即实例化。
lxml 的这个扩展点很少被用得这么彻底。你在高层 API 里拿到的每个对象,往下摸两层就是这些定制元素,改属性就是改 XML,中间没有翻译层。
关系图模型的红利
理解了「关系图」这个底座,很多设计就顺理成章了。
图片去重。package.py 的 _ImageParts 在插入图片前先算 SHA1,_find_by_sha1() 扫一遍现有的 ImagePart,哈希命中就直接复用。我也验证了一把,同一张 213 字节的 PNG 往幻灯片里插 3 次,保存后 ZIP 里只有一份 ppt/media/image1.png,3 个形状的 r:embed 全指向同一个 rId。OPC 本来就允许多条关系指向同一个 part,库只是把这个格式特性用足了。
孤儿回收。保存时 PackageWriter 只写从关系图可达的 part,没有任何显式删除逻辑,不可达的自然就丢了。这个设计直接解释了下面这件事。
两行代码的删除,十一年的 issue
现在说那个删除幻灯片的事。
Slides 类的完整方法清单是 add_slide、get、index,加上 __getitem__、__iter__、__len__。没有 delete,没有 duplicate。issue #67 2015 年 1 月开贴,「feature: delete a slide」,37 条评论。作者 scanny 在 2015 年 10 月给过答复,意思是你自己撤掉 add_slide 做的事就行,从演示文稿元素里移除幻灯片引用,删掉连接幻灯片的关系。点赞最高的 #132,请求复制幻灯片,90 条评论,同样悬着。
维护者给的两步,我完整跑了一遍。
xml_slides = prs.slides._sldIdLst
slides = list(xml_slides)
prs.part.drop_rel(slides[1].rId) # 撤掉关系
xml_slides.remove(slides[1]) # 撤掉 sldIdLst 条目
prs.save("/tmp/out.pptx")
保存后解包对比,原始包里 slide1、slide2、slide3 三个 XML 齐全,新包里 slide2.xml 直接消失了。没有任何显式删除文件的代码,上一节说的孤儿回收机制替你把垃圾收走了。重开文件,两张幻灯片,标题都对。
你看,技术上是通的,两行核心调用,加保存一共四行。可这个 API 就是没进主库。
为什么?我倾向于认为不是难,是没人。看贡献数据,scanny 一人 463 次贡献,第二名 11 次,bus factor 实打实的 1。这个人同时还是 python-docx 的第一贡献者,193 次对第二名的 23 次。Word 和 PowerPoint 两个最主流的 Python Office 库,都压在同一个人肩上。他的精力优先给了架构和正确性,API 覆盖率这种活,一拖就是十一年。
时间线上还有更直白的信号。0.6.18 停在 2019 年 5 月,1.0.0 到 2024 年 8 月才来,五年跨度,CHANGELOG 里 1.0 的主体变更居然是「Add type annotations」。1.0.2 之后,主仓库最后一次 push 停在 2024 年 8 月 7 日,到今天两年多没有动静。库本身是 Production/Stable 没错,64 个单元测试文件加 54 个 Gherkin 特性文件护着,质量过硬,但你如果打算在 2026 年的新项目里押注它,得知道自己在押一个静态维护的东西。
类似的缺口不止一处。高赞 open issue 里,「应用表格样式」「表格单元格边框」「多轴图表」排成一排,全是同一类故事,XML 层都支持,人话 API 没铺到。用这个库的正确姿势是接受它只铺了主路,长尾需求直接下到 oxml 层自己操作 XML,前面说的 xmlchemy 机制就是为此准备的,这也是它和纯封装库最大的不同。
图表为什么要带一个 Excel
一个容易被忽略的细节,python-pptx 的依赖只有 4 个,Pillow、lxml、typing_extensions,和一个 XlsxWriter。
为什么做 PPT 要带 Excel 库?因为 PowerPoint 的图表对象不存数据,只存引用。你在一个柱状图上看到的数字,实际躺在一份嵌入的 xlsx 工作簿里。chart/xlsx.py 里 _BaseWorkbookWriter.xlsx_blob() 用 XlsxWriter 把你传入的 ChartData 写成真实的工作簿,作为 ChartPart 嵌进包里。双击图表,「编辑数据」打开的 Excel 就是它。
「文件格式代理」的思路,这里又用了一次。python-pptx 从不重新发明 Office 的任何机制,Office 怎么存,它就怎么存。类库小,产物逼真,PowerPoint 打开毫无违和。
和其他路线比一比
程序化生成 PPT,可选的路不止一条。
| 方案 | 依赖 | 跨平台 | 成本 |
|---|---|---|---|
| python-pptx | 纯 Python,无需 Office | ✅ | 免费开源 |
| PowerPoint COM (win32com) | Windows + 已安装 Office | ❌ | Office 授权 |
| Aspose.Slides | 无 | ✅ | 商业授权,四位数美元起 |
| Apache POI (Java) | JVM | ✅ | 免费但 PPT 支持弱 |
| LibreOffice UNO | LibreOffice 全量安装 | ✅ | 免费但重 |
COM 自动化功能最全,毕竟驱动的是真 PowerPoint,但服务器场景直接出局。Aspose 是商业库里做得最全的,预算够可以省心。POI 的 XSLF 模块对 PPT 的支持深度和 python-pptx 差着一个量级。LibreOffice UNO 能做的事很多,代价是每台机器都要跑一个 headless LibreOffice。
python-pptx 卡的位置很准,不完整但够用的免费纯软件方案,加上一层随时可以下钻的 XML 底座。生态也验证了这点,Python 圈里它没有同量量的对手,AI 生成 PPT 的工具链几乎没得选。
它教给我们的东西
拆完这个库,我觉得最有迁移价值的是一个可以命名的模式,Schema 编译层。
面对一个带严格规范的复杂格式,别急着写胶水代码,先花力气把规范本身翻译成类型系统,Schema 的每条约束对应一层声明,元类或者等价机制把声明展开成安全的方法。这一层投入是一次性的,之后所有功能开发都站在类型安全的地板上,而不是每次都手搓字符串和顺序。xmlchemy 总共 717 行,换来的是全库上百个元素类的秩序。任何一个要对接复杂协议或格式的项目,都可以掂量一下这个买卖。
选型结论也给一个。要在服务端批量生成或改写 PPTX,python-pptx 依然是默认答案,免费、行为稳定,长尾功能下钻 XML 就有。如果你需要的功能恰好在它没铺的地方,比如重度表格样式、复杂图表组合,要么接受 XML 层的手工活,要么直接看商业方案,别指望它在短期内补上,两年静默加单人维护,这个预期得摆在前头。
至于那个 11 年的 issue,倒也不必太苛责。一个人把两个亿级下载量的库护到 Production/Stable,历史的账已经算不清是库欠用户一个 API,还是用户欠作者一句谢谢。

评论互动