news 2026/10/20 6:17:50

Python自动化生成PPT:从零搭建ppt-master的架构设计与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python自动化生成PPT:从零搭建ppt-master的架构设计与避坑指南

1. 从零搭建一个PPT自动化生成工具,我为什么选了这条最难的路

做技术分享这些年,被问得最多的问题之一就是:“有没有什么办法能让我不用打开PPT软件,就把演示文稿批量生成出来?”问这话的人,有做数据周报的运营,有需要给几十个客户分别出方案的项目经理,也有带学生做课题汇报的高校老师。他们的共同痛点是:内容框架是固定的,数据是动态的,但每次都要手动复制粘贴、调格式、换图表,一套流程走下来,半天时间就没了。

ppt-master这个项目,就是冲着这个场景去的。它的核心目标很明确:把PPT从“手工艺术品”变成“可编程的输出物”。你给它一份结构化数据,它给你一份可以直接打开演示的.pptx文件。听起来像是一个模板引擎该干的事,但真正动手之后你会发现,PPT的自动化远不是“填字游戏”那么简单——它涉及版式继承、母版逻辑、占位符映射、图表嵌入、字体回退等一系列让人头皮发麻的细节。

我最初的想法很天真:找一个现成的库,调几个API,把文字塞进去就完事了。结果第一版跑出来,文字全部堆在左上角,图片比例失调,表格边框粗得像用记号笔画的。那一刻我才意识到,PPT自动化的核心难点不在于“生成文件”,而在于“生成一个看起来像人做的文件”。这中间的差距,就是ppt-master要填的坑。

这篇文章适合两类人看:一类是正在寻找PPT批量生成方案的技术人员,另一类是对文档自动化感兴趣、想了解底层逻辑的开发者。我会把整个项目的设计思路、技术选型、核心实现和踩过的坑,按照实际开发顺序拆开来讲。不堆砌术语,不搞理论空转,每一段都对应着真实写过的代码和调过的参数。

2. 技术选型:为什么最终锁定了python-pptx而不是其他方案

2.1 先搞清楚PPT文件到底是什么

在选工具之前,得先明白你要操作的对象是什么。.pptx文件本质上是一个ZIP压缩包,里面装着一堆XML文件和资源目录。你把它后缀改成.zip解压,会看到ppt/slides/下面是一张张幻灯片的XML,ppt/media/里是图片和视频,ppt/slideLayouts/和ppt/slideMasters/定义了版式和母版。每一张幻灯片的内容,都是通过XML标签描述的形状树。

这意味着,理论上你可以完全不用任何库,直接用zipfile和xml.etree来操作PPT。我试过这条路,写了大概两百行代码之后放弃了。原因很简单:PPT的XML命名空间极其繁琐,一个简单的文本框涉及a:、p:、r:三套命名空间,形状的坐标和尺寸用的是EMU单位(1厘米约等于360000 EMU),手动计算和拼接字符串的出错率高得离谱。更别提还要处理关系映射、内容类型声明这些隐藏文件。

所以,找一个成熟的库来封装这些底层细节,是理性的选择。

2.2 几个候选方案的横向对比

当时我评估了四个方案,用表格列一下核心差异:

方案语言优势致命短板
python-pptxPythonAPI直观,社区活跃,支持读写对复杂图表和动画支持弱
Apache POIJava功能全面,企业级验证学习曲线陡,依赖重
Open XML SDKC#微软官方,最贴近底层只支持Windows生态,门槛高
直接操作XML任意完全可控开发成本极高,维护噩梦

Apache POI的XSLF模块确实强大,但它的API设计偏向Java企业开发习惯,一个简单的“设置文本框内容”要写好几行。Open XML SDK是最权威的,但绑定.NET生态,对于我这种主力用Python的人来说不划算。直接操作XML的方案,我在2.1节已经用亲身经历否定了。

最终选择python-pptx,核心原因是它的抽象层级刚刚好:它把幻灯片、形状、文本框、表格、图表都封装成了Python对象,你可以用slide.shapes.add_textbox()这样的方式创建元素,用text_frame.text设置内容,用shape.left和shape.top定位。同时,它保留了访问底层XML的入口(shape._element),当高层API不够用时,你可以直接操作XML节点。这种“高层易用、底层可达”的设计,在实际开发中非常关键。

2.3 版本选择和依赖管理

python-pptx的版本迭代不算快,但稳定性很好。我锁定的是0.6.21版本,这个版本对Python 3.8到3.11都兼容,而且修复了早期版本中表格样式丢失的问题。安装很简单:

pip install python-pptx==0.6.21

但这里有个坑要注意:python-pptx依赖Pillow来处理图片,而Pillow的版本如果太新,在某些Linux发行版上会因为缺少系统级图像库而安装失败。我的做法是在requirements.txt里同时锁定Pillow==9.5.0,这个版本在主流系统上的预编译轮子最全,不需要额外装libjpeg之类的系统包。

另外,如果你需要生成图表,python-pptx内置的图表功能比较基础,只支持柱状图、折线图、饼图等几种类型,而且样式定制能力有限。我的方案是:用matplotlib先生成图表图片,再作为图片插入PPT。这样虽然牺牲了图表的可编辑性,但换来了无限的样式自由度。这个取舍在后面讲图表章节时会详细展开。

3. 核心架构设计:把PPT拆成可组装的积木

3.1 三层结构:数据层、模板层、渲染层

ppt-master的架构不是一上来就定好的,而是随着功能增加逐步演化出来的。最初只是一个函数,接收一个字典,返回一个PPT文件。后来需求变复杂了:有的页面要放表格,有的要放图表,有的要分栏,有的要加页眉页脚。如果继续在一个函数里写if-else,代码会变成一团乱麻。

于是我把整个流程拆成了三层:

  • 数据层:负责接收和校验输入数据。输入可以是JSON、YAML、CSV,甚至是从数据库查出来的结果集。这一层的任务是把各种来源的数据统一成内部的标准字典结构。
  • 模板层:定义PPT的“骨架”。一个模板描述了有哪些页面、每页用什么版式、每个占位符对应数据中的哪个字段。模板本身也是用Python字典或JSON描述的,不涉及任何PPT底层操作。
  • 渲染层:真正调用python-pptx干活的部分。它读取模板定义,从数据层取数据,然后逐页、逐形状地生成PPT。

这三层之间通过明确的接口通信。数据层输出dict,模板层输出Template对象,渲染层输出Presentation对象。任何一层出问题,都可以单独调试,不会牵连其他层。

3.2 模板描述语言的设计取舍

模板层的关键问题是:用什么格式来描述一个PPT模板?我考虑过三种方案:

第一种是直接用python-pptx打开一个现成的.pptx文件作为模板,然后替换其中的占位符。这是很多人的第一反应,也是最直观的做法。但它的缺点是:模板的制作和修改必须依赖PPT软件,无法用代码版本控制,而且占位符的命名和映射关系容易混乱。

第二种是设计一套DSL(领域特定语言),用类似YAML的语法描述页面结构。这种方案灵活度高,但学习成本也高,而且需要自己写解析器。

第三种是用Python原生数据结构(字典和列表)来描述模板。开发者直接写Python代码来定义模板,不需要学新语法,而且可以利用Python的表达式做动态计算。

我最终选了第三种。原因很实际:ppt-master的目标用户是开发者,不是设计师。开发者更愿意写Python字典,而不是学一套新的模板语法。而且Python字典天然支持嵌套和条件逻辑,描述能力足够强。

一个典型的模板定义长这样:

template = { "slides": [ { "layout": "title", "fields": { "title": "{{project_name}}", "subtitle": "生成日期:{{date}}" } }, { "layout": "content", "fields": { "title": "数据概览", "body": "{{summary_text}}" } } ] }

这里的{{project_name}}是占位符,渲染层会用实际数据替换它。这种设计的好处是模板和数据完全解耦,同一套模板可以复用于不同的数据集。

3.3 版式映射:如何让代码理解“标题页”和“内容页”

PPT的版式(Layout)是母版(Master)的子集,每个版式定义了占位符的位置和类型。python-pptx允许你通过索引或名称来访问版式。但不同PPT软件生成的母版,版式名称可能不一样。比如有的叫“Title Slide”,有的叫“标题幻灯片”。

我的做法是在模板层做一个版式映射表,把逻辑名称映射到实际的版式索引:

LAYOUT_MAP = { "title": 0, "content": 1, "two_column": 3, "blank": 6 }

这个映射表是在初始化时动态生成的:打开模板PPT,遍历所有版式,根据版式名称中的关键词来匹配。如果匹配不到,就回退到默认索引。这样即使换了模板文件,只要版式命名大致规范,代码就能自动适配。

注意:python-pptx的版式索引是从0开始的,但母版中的版式顺序不一定和PPT软件中显示的顺序一致。最稳妥的方式是用prs.slide_layouts遍历并打印每个版式的名称,确认后再写映射。

4. 占位符替换:看似简单,实则暗坑最多的环节

4.1 文本替换的基本逻辑和边界情况

占位符替换是PPT自动化的核心操作,也是最容易出问题的地方。基本逻辑很简单:遍历幻灯片中的所有形状,如果是文本框,就读取其中的文字,找到{{...}}模式,用数据中的对应值替换。

但实际写起来,边界情况多到让人崩溃。我列一下遇到过的典型问题:

  • 占位符跨段落:PPT中的一个文本框可以包含多个段落,每个段落又可以包含多个文本运行(Run)。如果{{name}}被拆分到两个Run里,简单的字符串替换就会失效。
  • 占位符在表格中:表格单元格里的文字也需要替换,但表格的遍历方式和普通形状不同。
  • 占位符在组合形状中:组合形状(GroupShape)内部的子形状需要递归遍历。
  • 占位符在图表标题或数据标签中:图表的文字替换需要访问图表对象,而不是形状的文本框。

我的解决方案是写一个通用的遍历函数,递归处理所有可能包含文字的对象:

def replace_text_in_shape(shape, data): if shape.has_text_frame: for paragraph in shape.text_frame.paragraphs: for run in paragraph.runs: for key, value in data.items(): placeholder = "{{" + key + "}}" if placeholder in run.text: run.text = run.text.replace(placeholder, str(value)) if shape.shape_type == MSO_SHAPE_TYPE.GROUP: for sub_shape in shape.shapes: replace_text_in_shape(sub_shape, data) if shape.has_table: for row in shape.table.rows: for cell in row.cells: replace_text_in_shape(cell, data)

这段代码的关键点是:替换操作在Run级别进行,而不是在段落或文本框级别。因为只有Run级别才能保留字体、颜色、加粗等格式信息。如果你直接设置text_frame.text,所有格式都会丢失,文字会变成默认样式。

4.2 格式保留:为什么不能直接改text_frame.text

很多人第一次用python-pptx时,会这样写:

shape.text_frame.text = "新的内容"

这行代码确实能替换文字,但它会把整个文本框的格式重置为默认值。原来设置的字体、字号、颜色、对齐方式全部丢失。如果你的模板精心设计了标题的字体和颜色,这一行代码就全毁了。

正确的做法是操作Run对象。一个段落(Paragraph)包含多个Run,每个Run是一段连续的文字,拥有独立的格式。当你替换文字时,应该尽量在原有的Run内部替换,而不是新建Run。如果占位符跨越多个Run,就需要先把这些Run的文字合并到一个Run中,再执行替换。

我写了一个辅助函数来处理跨Run的占位符:

def merge_runs_and_replace(paragraph, data): full_text = "".join(run.text for run in paragraph.runs) for key, value in data.items(): placeholder = "{{" + key + "}}" if placeholder in full_text: full_text = full_text.replace(placeholder, str(value)) # 保留第一个Run的格式,删除其余Run if paragraph.runs: first_run = paragraph.runs[0] first_run.text = full_text for run in paragraph.runs[1:]: run._element.getparent().remove(run._element)

这个函数的逻辑是:先把段落中所有Run的文字拼起来,在完整文本上做替换,然后把结果写回第一个Run,删除多余的Run。这样既完成了替换,又保留了第一个Run的格式。

4.3 数据校验:在渲染之前把错误拦住

占位符替换最怕的情况是:数据里缺少某个字段,渲染出来的PPT里还留着{{name}}这样的原始占位符。这种PPT发给客户,场面会非常尴尬。

我的做法是在渲染之前加一道校验:扫描模板中所有的占位符,和输入数据的键做比对。如果发现模板中有占位符在数据中找不到对应值,就抛出异常并列出缺失的字段名。这样可以在生成PPT之前就发现问题,而不是等到打开文件才发现。

def validate_data(template, data): required_keys = extract_placeholders(template) missing = required_keys - set(data.keys()) if missing: raise ValueError(f"数据中缺少以下字段:{missing}")

extract_placeholders函数会遍历模板定义,用正则表达式提取所有{{...}}中的键名。这一步虽然简单,但能省掉大量事后排查的时间。

5. 图表和表格的自动化生成:从“能看”到“好看”的距离

5.1 为什么放弃python-pptx原生图表

python-pptx提供了add_chart方法,可以直接在幻灯片中创建柱状图、折线图、饼图等。我一开始很兴奋,觉得这下省事了。但实际用下来,发现几个硬伤:

第一,图表类型有限。它只支持XL_CHART_TYPE枚举中定义的几种类型,像桑基图、热力图、雷达图这些稍微复杂一点的图表,完全无法生成。

第二,样式定制困难。虽然可以设置图表的标题、图例、数据标签,但要对单个数据点设置不同的颜色,或者添加自定义的参考线,API就力不从心了。

第三,数据更新麻烦。原生图表的数据是嵌入在PPT中的,如果你想更新数据,需要重新操作图表对象,而不是简单地替换一个图片文件。

所以我的最终方案是:用matplotlib或plotly生成图表图片,保存为PNG,然后用add_picture插入PPT。这样做的好处是图表样式完全由代码控制,想怎么画就怎么画。代价是图表在PPT中不可编辑,但对于自动化生成的场景来说,这个代价完全可以接受。

5.2 图表尺寸和分辨率的计算

插入图片时,最容易出问题的是尺寸和分辨率。python-pptx的add_picture方法接受width和height参数,单位是EMU。如果你不指定,它会按图片的原始像素尺寸和DPI来换算。

这里有一个计算公式:EMU = 像素数 / DPI * 914400。比如一张 1920x1080 像素的图片,DPI是96,那么它的宽度是 1920/96*914400 = 18288000 EMU,换算成厘米大约是50.8厘米。这远远超出了一张A4纸的宽度。

所以,插入图片时一定要显式指定尺寸。我的做法是先在模板中定义好图片占位区域的大小(用厘米或英寸),然后在插入时换算成EMU:

from pptx.util import Cm def add_chart_image(slide, image_path, left_cm, top_cm, width_cm): slide.shapes.add_picture( image_path, Cm(left_cm), Cm(top_cm), width=Cm(width_cm) )

高度不指定,python-pptx会自动按比例缩放。这样图表就不会变形。

另外,matplotlib保存图片时,建议把DPI设置为200以上,这样在PPT中放大时不会模糊。但DPI太高会导致文件体积膨胀,200是一个比较平衡的值。

5.3 表格样式的精细控制

python-pptx的表格功能比图表完善一些,支持设置单元格文字、背景色、边框、合并等。但它的API设计有点反直觉:表格样式是通过table.style属性设置的,而边框需要操作XML。

我封装了一个set_cell_style函数,用来统一设置单元格的字体、对齐和背景色:

def set_cell_style(cell, text, font_size=12, bold=False, bg_color=None, align=PP_ALIGN.LEFT): cell.text = str(text) for paragraph in cell.text_frame.paragraphs: paragraph.alignment = align for run in paragraph.runs: run.font.size = Pt(font_size) run.font.bold = bold if bg_color: cell.fill.solid() cell.fill.fore_color.rgb = RGBColor(*bg_color)

这个函数在实际使用中非常高频。每次生成表格时,先创建表格对象,然后遍历每个单元格调用这个函数。虽然代码量不大,但省去了大量重复的样式设置代码。

提示:python-pptx的表格默认会应用一个内置样式,如果你不想要这个样式,可以把table.style设置为None,然后手动设置每个单元格的格式。但要注意,设置为None后,表格边框会消失,需要自己通过XML添加边框。

6. 批量生成中的性能优化和文件管理

6.1 一次生成一百份PPT的耗时分析

ppt-master的一个典型使用场景是批量生成:比如给一百个客户分别生成一份方案PPT。如果每份PPT需要2秒,一百份就是200秒,还能接受。但如果每份需要10秒,那就是十几分钟,体验就很差了。

我做过一次性能剖析,发现耗时主要分布在三个环节:

  • 打开模板文件:每次生成都重新打开模板PPT,这个操作涉及磁盘IO和XML解析,大约占30%的时间。
  • 图片插入:如果PPT中包含大量图片,图片的读取和编码会占用大量时间。
  • 保存文件:保存PPT时,python-pptx需要重新打包所有XML和资源,文件越大越慢。

针对第一个问题,我的优化方案是:只打开一次模板,然后通过深拷贝的方式复制模板对象。python-pptx没有直接提供深拷贝API,但可以用copy.deepcopy来复制Presentation对象。实测下来,深拷贝比重新打开文件快3到5倍。

import copy template_prs = Presentation("template.pptx") for data in data_list: prs = copy.deepcopy(template_prs) render(prs, data) prs.save(f"output_{data['id']}.pptx")

针对图片插入,我的做法是预先加载所有图片到内存,避免重复读取磁盘。如果图片在不同PPT中重复使用,可以用一个字典缓存Image对象。

6.2 文件命名和目录组织

批量生成时,文件命名是个容易被忽视的问题。如果直接用客户名称做文件名,可能会遇到特殊字符(如斜杠、冒号)导致保存失败。我的做法是对文件名做一次清洗:

import re def safe_filename(name): return re.sub(r'[\\/:*?"<>|]', '_', name)

这个正则替换了Windows和Linux下常见的非法文件名字符。另外,建议在文件名中加入时间戳或序号,避免重复生成时覆盖旧文件。

目录结构方面,我习惯按批次组织:

output/ 2024-01-15_batch01/ client_001.pptx client_002.pptx ...

这样每次生成的结果都在独立目录中,方便追溯和清理。

6.3 异常处理和日志记录

批量生成最怕的是:跑到第87份时程序崩溃,前面86份白做了。所以异常处理必须到位。我的做法是在循环内部用try-except包裹每个生成任务,记录失败的任务和原因,但不中断整个循环:

failed = [] for data in data_list: try: generate_ppt(data) except Exception as e: failed.append({"id": data["id"], "error": str(e)}) continue

循环结束后,统一输出失败列表。这样即使有个别数据有问题,也不会影响其他PPT的生成。日志方面,我用Python内置的logging模块,把关键步骤和耗时记录下来,方便后续优化。

7. 实际使用中积累的经验和避坑清单

7.1 字体问题:为什么你的PPT换台电脑就变样

这是PPT自动化中最隐蔽的坑。你在代码中设置了“微软雅黑”,生成PPT的电脑上也有这个字体,一切正常。但把PPT发给客户后,客户电脑上没有这个字体,PPT软件会自动回退到默认字体,排版可能完全乱掉。

解决方案有两个:一是嵌入字体,但python-pptx不支持这个功能,需要手动操作XML;二是使用通用字体,比如“Arial”或“Calibri”,这些字体在绝大多数系统上都有。我的建议是:如果PPT只在内部使用,可以用特定字体;如果要对外发送,尽量用通用字体,或者在PPT中附带字体说明。

7.2 图片路径:相对路径和绝对路径的坑

add_picture方法接受文件路径。如果你用相对路径,要注意当前工作目录是什么。在脚本中运行时,工作目录可能是脚本所在目录,也可能是调用目录。最稳妥的方式是用绝对路径:

import os image_path = os.path.abspath("charts/chart_01.png")

另外,如果图片文件不存在,add_picture会抛出FileNotFoundError。建议在插入之前先检查文件是否存在,给出更友好的错误提示。

7.3 模板文件的兼容性

不同版本的PPT软件保存的.pptx文件,内部结构可能有细微差异。我遇到过用旧版软件保存的模板,在python-pptx中打开时版式索引错乱的情况。建议模板文件用较新版本的PPT软件保存,并且在代码中不要硬编码版式索引,而是通过版式名称来查找。

7.4 内存占用:生成大文件时的注意事项

如果PPT中包含大量高分辨率图片,生成过程中内存占用会飙升。我遇到过生成一份200页的PPT时,内存占用超过2GB的情况。优化方法是:及时释放不再使用的对象,避免在内存中同时保留多份PPT对象。如果确实需要处理大文件,可以考虑分批次生成,然后合并。

8. 这个项目后续还能怎么扩展

ppt-master目前实现的是“数据到PPT”的单向生成。在实际使用中,我发现还有一些值得探索的方向。

第一个方向是反向解析:从现有的PPT中提取文字和图片,转换成结构化数据。这个功能可以用于PPT内容的批量检索和迁移。python-pptx本身支持读取PPT,所以技术上可行,难点在于如何识别哪些内容是“有效内容”,哪些是装饰性元素。

第二个方向是模板的可视化编辑。目前模板是用Python字典定义的,对非开发者不够友好。可以做一个简单的Web界面,让用户上传PPT模板,在界面上拖拽占位符,然后导出模板定义文件。这样设计师和开发者可以分工协作。

第三个方向是集成到CI/CD流程中。比如每次代码提交后,自动生成项目进度PPT,发送给相关人员。这需要把ppt-master包装成一个命令行工具或API服务,目前我已经在本地做了原型,用argparse接收参数,用flask提供HTTP接口。

这些扩展方向我都在逐步尝试,但核心的生成逻辑已经稳定了。如果你也在做类似的事情,建议先把“生成一份正确的PPT”这个目标做到极致,再考虑扩展。因为PPT自动化的坑,大部分都藏在细节里,只有把基础打牢,后面的扩展才不会翻车。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 6:17:42

diagram-design:从语义模型到视觉规范,构建可维护的图表设计系统

1. 从一张草图到一套系统&#xff1a;diagram-design 到底在解决什么问题第一次听到 “diagram-design” 这个词&#xff0c;很多人会下意识觉得它只是“画图”的另一种说法。但真正在项目里被图表折磨过的人知道&#xff0c;画图只是冰山一角&#xff0c;水面下藏着的是一整套…

作者头像 李华
网站建设 2026/10/20 6:16:32

围棋新手教学第三天手记:从棋形引导到教学相长

1. 第三天的现场&#xff1a;从“我自己会下”到“能让别人学会”晚上九点&#xff0c;教室里还剩两桌棋没下完。我蹲在棋盘旁边&#xff0c;看着A同学一手“长”落下去&#xff0c;把另一边的弱子彻底晾在风口上&#xff0c;憋了两秒钟&#xff0c;还是没忍住开口&#xff1a;…

作者头像 李华
网站建设 2026/10/20 6:16:14

35+程序员转型大模型:真实薪资、可行路径与破局关键

35程序员转型&#xff1a;大模型岗位的真实薪水、可行路径与破局关键这两年每次和大厂的老同事聊起大模型&#xff0c;几乎都会碰到同一个句式&#xff1a;“35还转得动吗&#xff1f;”说实话&#xff0c;我见过不少35岁以上的人在这种焦虑里反复内耗&#xff0c;也有人已经默…

作者头像 李华
网站建设 2026/10/20 6:15:50

artcraft手作指南:小批量高质感产品的工序化解决方案

1. 从“artcraft”这个名字说起&#xff1a;它到底想解决什么问题第一次看到“artcraft”这个标题&#xff0c;我脑子里蹦出来的第一反应是——这大概率不是一个单纯的艺术项目&#xff0c;也不是一个纯粹的手工教程。把“art”和“craft”拼在一起&#xff0c;本身就带着一种“…

作者头像 李华
网站建设 2026/10/20 6:15:48

超链接XSS攻防:从href伪协议到浏览器解析绕过实战

1. 从href说起&#xff1a;超链接XSS的核心机制做Web安全的人都有个共识&#xff0c;XSS的payload入口看似千变万化&#xff0c;但真正绕来绕去&#xff0c;很多攻防点都收缩到一个非常朴素的问题上——开发者敢不敢信任用户输入的一段URL。早些年我在给某电商系统做渗透测试的…

作者头像 李华
网站建设 2026/10/20 6:15:46

用Python打造本地PPT生成工具:从YAML到演示文稿的自动化实践

1. 为什么我要自己动手做一个PPT生成工具先说说我做这个项目的起因。我在一家中型公司做技术方案和项目汇报相关的工作&#xff0c;平均每周要产出三到五份PPT&#xff0c;多的时候一天就要赶两份。最开始我也是老老实实打开软件&#xff0c;一张一张地排版、调字体、对齐图标&…

作者头像 李华