如果你也被周报、月报、季度复盘这些演示文稿折磨过,那你大概能理解我为什么写了 ppt-master 这个工具。它做的事情听起来很简单:给一份 Markdown 写的大纲和一份 YAML 配置,就能批量生成结构完整、自带图表和表格的 .pptx 文件。没有花哨的界面,只有一条命令。项目是从我自己每周都要交汇报的痛点里长出来的,后来慢慢沉淀成了内容解析、样式渲染、插件扩展三条核心链路。这篇内容把它的设计思路、关键实现和踩过的坑都整理出来,给同样需要跟PPT搏斗的你一个可复制的参考。
1. 为什么我会写一个叫 ppt-master 的工具
1.1 每周都要被PPT绑架一次
在大部分公司里,PPT早就不是单纯的展示工具,而是工作产出的容器。我最多的一周要同时维护三份演示文稿:一份部门周报、一份项目周会同步、一份跨团队方案宣讲。每份的页数都在十页以上,内容结构大同小异,但每份都要重新调一次版式。最崩溃的一次,领导在周五下午四点提了个调整意见,说把整份周报的结论从第二页挪到第一页,同时把后面所有的标题层级顺延。听起来是很小的改动,但我手动把十三个页面的标题、正文、图表位置全部拖了一遍,折腾了将近两个小时才把格式修正。改完我就意识到,这件事一定有更聪明的做法。
当时身边同事的做法也差不多:先打开一个旧文件,另存为,然后逐页删掉旧内容再填新内容。运气好时旧文件刚好能用,运气不好时想删的文本框找不到,想对齐的图形怎么都对不齐。真正让人崩溃的不是写内容,而是每次都要重新和排版工具搏斗。于是我开始琢磨,能不能把“做PPT”这个过程抽象成一条流水线:内容归内容,样式归样式,数据归数据,我只需要改该改的地方,其他交给程序去跑。
1.2 现成方案缺的是什么
我认真比对过当时能找到的方案。在线模板库的问题在于“换皮等于重做”,内容稍微一变,原来的对齐关系就全乱了;AI生成PPT的方案那时候还比较粗糙,生成的页面逻辑是模板化的,想插入业务数据图表基本得靠手改;直接用母版批量调样式虽然能管住配色和字体,但母版本身维护成本很高,而且每个团队拿到的源文件都要重新挂一遍母版。所有这些方案都有一个共同问题:内容、样式、数据全搅在一起。内容改动会牵动样式,样式调整会破坏内容位置,数据更新又得手动重放图表。我要找的不是一个更漂亮的模板,而是一条可编程、可复用的生产链路。
我也试过用几款主流的办公脚本去自动化操作演示软件。脚本确实能减少一些重复点击,但每次运行都要依赖桌面上已经打开的进程,稍微弹个弹窗或网络抖动就中断了,根本扛不住批量生成。更麻烦的是,脚本录制的动作和具体界面绑定得太死,换个版本或者换台电脑,坐标偏移一点点就可能点错地方。所以从一开始,我就倾向于用一个无界面的、直接操作文件格式的底层库来做渲染,而不是让程序去假装人工操作。
1.3 目标:把它当成一条“生产流水线”
所以我给这个工具定的目标不是“帮你做一份好看的PPT”,而是“把PPT生产拆成上游、中游、下游三段可控的流程”。上游由人负责,只管把逻辑写清楚;中游是解析和装配,把Markdown大纲、配置文件、外部数据统一成一份中间文档对象;下游是渲染器,把中间文档对象映射到演示文稿里的每一页、每一个元素。这个架构的好处是每一层都可以独立替换。比如今天不想用Markdown了,可以再加一个JSON适配器;明天想换一套新的视觉规范,只需要新增一个主题配置。项目名里的 master,说的就是这种掌控感。
后来我意识到,这个名字也暗示了一个心态上的转变:我不再是PPT的“奴隶”,而是PPT流程的“控制者”。遇到需求变更,第一反应不是烦躁,而是去看看是改内容、改配置、还是加一个插件就能解决。这种掌控感,才是这个项目带给我最大的回报。
2. 核心设计:先让内容和样式彻底分家
2.1 输入格式:Markdown 负责结构,YAML 负责样式
我花了很长时间才确定输入格式。最后保留下来的方案非常克制:Markdown 只描述内容结构,YAML 只描述视觉样式。内容文件里看不到任何颜色或字号,样式文件里看不到任何正文文字。举个例子,下面是一份最简单的季度产品复盘大纲:
# 季度产品复盘 ## 核心结论 - DAU 环比提升 12% - 新增功能次日留存率 18% - 下一季度主攻方向 ## 业务数据 <!-- data_source: metrics.csv --> | 月份 | DAU | 增长率 | | 1月 | 10200 | - | | 2月 | 11400 | 11.8% | | 3月 | 12600 | 10.5% | ## 风险与对策 1. 新用户引导路径太长 2. 数据看板入口太深 3. 对策:在下季度改版中重新设计首次启动流程这份 Markdown 里,#对应封面页,##对应章节页,无序列表、有序列表和表格分别对应要点页、流程页和数据页。写的人只需要保证层级正确,完全不需要知道这些结构最后会渲染成什么样。样式侧则全部收进 config.yaml:
theme: primary_color: "1F4E79" secondary_color: "E8A33D" background: "F7F9FC" font_zh: "思源黑体" font_en: "Inter" layout: cover: "Title Slide" section: "Section Header" content: "Title and Content" table: "Title and Content" options: title_size: 26 body_size: 16 table_size: 14 legend_size: 12 logo: "assets/logo.png"写内容的人不需要打开配置文件,写配置的人也不需要碰内容文件。这两者通过一套固定的字段名做契约,只要字段存在,就能生成。这样做最大的好处是,审美统一和内容生产被彻底解耦:内容生产可以并行,视觉迭代也可以随时独立进行。
2.2 解析引擎如何把大纲变成中间文档对象
解析引擎要做的事情其实不复杂:读入Markdown,按行判断每个节点的类型,然后生成一棵文档对象树。我定义了一个简化的中间结构:
class SlideNode: def __init__(self, level, title): self.level = level self.title = title self.blocks = [] # 文本、表格、图片、图表 self.children = [] def add_block(self, block): self.blocks.append(block)解析器按顺序遍历 Markdown 的 AST。遇到标题就切换当前节点;遇到列表项、表格、引用块就当作当前节点的 block;遇到特殊注释比如<!-- data_source: metrics.csv -->就把它标记为数据源引用,后续渲染时由数据源插件去拉取数据。这一步最关键的是不要过早地做样式相关判断。解析器只负责把结构问清楚,至于标题用多大字号、表格用什么颜色,那是渲染器的事。这样当用户想调整视觉风格时,解析器一行都不用改。
实际写解析器时,我建议直接用现成的 Markdown 解析库,而不是自己写正则去匹配标题和列表。自己写正则看起来简单,但遇到嵌套列表、引用块、代码块时很容易漏判,而且 Markdown 本身有很多边缘语法,处理起来非常折腾。用成熟的 AST 库,能拿到每个元素的类型和层级,解析逻辑就变成了一次简单的树遍历。
2.3 为什么最终没有选 JSON 或 Excel 作为主输入
做技术选型的时候,JSON 是我的第一反应,结构化程度高,程序处理起来几乎零成本。但真正给非技术背景的同事试用之后发现,JSON 满屏的大括号和缩进对他们来说就是天书,连漏写一个逗号都能让他们崩溃。Excel 更适合承载扁平的数据列表,但一份 PPT 是一棵有层级的树,硬塞进二维表格里会损失父子关系。Markdown 的优势在于它是目前人类可读性最好的纯文本结构化格式,几乎所有能打字的地方都能写,而且表格、列表、标题都足够表达一份汇报的逻辑结构。
| 输入方案 | 优点 | 缺点 |
|---|---|---|
| Markdown | 人类可读性好、结构自然、学习成本低 | 不适合表达复杂嵌套关系 |
| JSON | 程序处理方便、严格结构化 | 手动编辑门槛高、容易语法错误 |
| Excel/CSV | 数据驱动能力强、适合报表 | 表达树状结构非常别扭 |
所以我最终定下 Markdown 负责内容、YAML 负责样式,数据密集型场景再让 Excel/CSV 作为数据源插件进来,各司其职。这个选择可能不够“极客”,但它在团队协作中的接受度高得多,工具最终能落地,往往取决于这些非技术细节。
3. 关键技术点:python-pptx 布局引擎与样式系统
3.1 从大纲节点到幻灯片的基础映射
渲染器的入口是遍历文档对象树,对每个 SlideNode 找到对应的 layout 名称,然后调用 python-pptx 在演示文稿里新建幻灯片。这里有一个需要注意的设计:不要直接在渲染代码里硬编码 layout 名称,而是通过配置文件的 layout 字段来做映射。我封装了一个查找函数:
from pptx import Presentation from pptx.util import Inches, Pt def find_layout(prs, layout_name): for layout in prs.slide_layouts: if layout.name == layout_name: return layout raise KeyError(f"layout not found: {layout_name}") def build_slide(prs, layout_name, title, blocks): slide = prs.slides.add_slide(find_layout(prs, layout_name)) if slide.shapes.title: slide.shapes.title.text = title for block in blocks: render_block(slide, block) return slide为什么要通过 layout.name 查找而不是直接用索引?因为不同版本的模板文件里,slide_layouts 的顺序可能不一样,硬编码索引会在一换模板就崩。用名称匹配虽然有点笨,但兼容性最好。渲染每一个 block 时,再根据 block.type 分发到不同的处理函数。
一个容易被忽略的细节是:直接用slide.shapes.title.text赋值时,如果标题文字太多,会被截断或者溢出。我的处理是,在赋值前先根据标题长度做一次字号缩减,必要时拆成两行。毕竟 PPT 的标题栏通常就是一行,塞太多字只会让版面变得拥挤。
3.2 文本、图片、表格和图表四大元件的封装
文本是最常用的元件。我会把一段文字按段落拆开,每个段落单独设置行距和对齐,而不是把所有文字塞进同一个 run。这样做的好处是,后续如果用户在软件里手工调整某一行,不会影响其他段落的格式。封装成函数就是下面这样:
from pptx.dml.color import RGBColor def add_textbox(slide, left, top, width, height, content, font_size=16, color="333333"): box = slide.shapes.add_textbox(Inches(left), Inches(top), Inches(width), Inches(height)) tf = box.text_frame tf.word_wrap = True first = True for line in content["paragraphs"]: paragraph = tf.paragraphs[0] if first else tf.add_paragraph() first = False paragraph.alignment = line.get("align", "left") run = paragraph.add_run() run.text = line["text"] run.font.size = Pt(font_size) run.font.color.rgb = RGBColor.from_string(color) set_east_asian_font(run, config_font_zh) return box图片的封装要注意比例。很多人直接用 width 和 height 两个参数,结果图片被拉伸得面目全非。我建议只指定宽度,高度按图片原始比例自动计算:
from PIL import Image def add_image(slide, img_path, left, top, width_inches): with Image.open(img_path) as img: w, h = img.size height_inches = width_inches * h / w slide.shapes.add_picture( img_path, Inches(left), Inches(top), width=Inches(width_inches), height=Inches(height_inches) )表格和图表则稍微绕一点。表格需要先 add_table 拿到 TableShape,再逐格填数据;图表需要用 CategoryChartData 构造成类目数据,然后 add_chart 指定图表类型。这里不推荐直接操作 xml,python-pptx 封装好的接口已经够用了。写表格时,我还会把表头行单独加粗、换底色,这样即使数据再多,读者也能一眼看到结构。
3.3 样式继承与覆盖的正确姿势
用 python-pptx 做渲染,最大的认知障碍是样式系统的继承关系。它有点像写网页时遇到的样式层叠问题:主题定义了默认字体,母版可能定义了占位符样式,布局可能又改了一次,最终落到具体 run 上的属性才是 PowerPoint 真正显示的。我的经验是:不要依赖任何默认值,所有关键属性都显式赋值到 run 上。也就是说,每次写入文字后,都重新设置字体、字号、颜色、粗体、斜体;每个表格单元格也都会设置边框和底色。虽然多写了几行代码,但生成的 PPT 在任何机器上打开,效果都是一样的。
为此我封装了一个apply_run_style的函数,在渲染管线里统一调用。函数里只做一件事:把配置里的设计令牌(颜色、字号、字体)映射成 python-pptx 的对象属性。所有版式都不能绕过这个函数直接设置样式,否则就会失去统一入口。这样即使以后要支持暗色主题,也只需要换一个配置,所有插件都会自动跟着变。
提示:不管你的下游渲染器是什么,都建议把“样式”抽象成设计令牌,而不是散落在各个代码片段里的魔法数字。否则后期改视觉规范,等于把所有逻辑重新翻一遍。
4. 踩坑实录:字体、尺寸、母版和图表
4.1 中文字体不生效
第一个让我排查了整个下午的坑是中文字体。python-pptx 的 font.name 对应的是 PowerPoint 里西文字体(latin typeface),中文字体存在 rPr 的 ea 节点里。如果你只设了 font.name,在中文环境里大概率还是宋体,到了不同系统上简直像开盲盒。我后来用了一个专门的处理函数:
from pptx.oxml.ns import qn def set_east_asian_font(run, font_name): run.font.name = font_name rPr = run._r.get_or_add_rPr() ea = rPr.find(qn('a:ea')) if ea is None: ea = rPr.makeelement(qn('a:ea'), {}) rPr.append(ea) ea.set('typeface', font_name)这段代码的作用是同时设置西文字体和中文字体,避免PPT在打开时因为找不到字体而回退到默认值。如果你用 WPS 预览,有时还会遇到字体名称冲突的情况,比如“微软雅黑”在不同版本里表现不一样。我现在的配置里统一使用开源字体,比如思源黑体,部署到哪台机器都不会缺字体。这个问题在自动化生成 PPT 时特别隐蔽,因为本地跑出来的效果可能完全正常,换一台机器就变了。
4.2 文本框高度不会自动扩展
第二个坑是文本框的高度不会自适应。PPT 的文本框一旦设定了固定高度,文字超出部分要么被裁掉,要么溢出到页面外,取决于文本框是否开启自动缩放。我的处理方案是在写入文案之前做一个粗略估算:先用宽度和字体字号算出单行能容纳多少个字,然后根据文本总字数估算行数,再乘以行高系数 1.3,得出需要的高度。如果估算高度超过配置的默认高度,就按估算值扩展;如果超过页面可用高度,就用更小的字号重新估一次。这个算法肯定没法跟精确排版引擎比,但能覆盖 95% 的汇报场景。
后来我还加了一个更稳妥的兜底:在渲染结束后用 python-pptx 重新读取每个文本框的 current_size 属性,凡是超出页面边界的都打日志。自动化工具最怕“看起来生成了,内容却被切没了”,有一道自动检查,比最后人工一页页翻要省心得多。
4.3 母版占位符的幽灵样式
第三个坑比较隐蔽。我最初用空白版式创建页面,然后再自己加文本框,结果发现某些页面会突然多出阴影、字体也变来变去。后来才意识到,这是母版里的占位符在起作用。PowerPoint 的母版、版式、占位符之间是继承关系,即便你在版式上没有放置占位符,母版里的样式也可能被带出来。解决思路是:每张幻灯片只依赖布局自带的标题占位符,正文内容全部用独立文本框绘制,并且每次绘制后都显式覆盖字体、颜色、对齐等属性。简单说,我信任主题提供位置,不信任它提供样式。
这个坑很难从文档里看出来,因为很多模板文件是从网上下载的,母版里藏着什么样式只有打开 XML 才能看到。后来我写了一个模板体检脚本,会把模板里所有母版和版式的名称、占位符列表、字体设置全部列出来,换任何模板先跑一遍体检,避免踩到隐藏的雷。
4.4 图表数据的缓存与刷新
第四个坑和图表有关。用 python-pptx 添加图表后,如果再想更新数据,直接修改原来的序列是不会生效的,因为图表的缓存数据还在 ppt 包里。正确做法是创建一个新的 CategoryChartData 对象,调用 chart.replace_data(new_data) 替换。如果连替换都失效,就干脆删除旧图表重新添加。这个坑在批量生成周报时特别容易出现:同样一个图表模板,这周的数据和上周不同,如果代码里没有替换干净,数据看板上就会显示上一期的数字。我最终在渲染层做了强制刷新:所有图表数据都通过 data_source 插件读取,每次生成都构造新图表对象,彻底绕开缓存问题。
如果你需要生成带多系列的图表,还要注意系列名称在 replace_data 之后是否会被重置。我的做法是每次构造 CategoryChartData 时都把系列名称从配置里读出来,不依赖上一次的历史状态。
4.5 体积失控:图片压缩与资源管理
最后一个容易被忽略的问题是体积。十几页的PPT塞几张高清图就能轻松超过50MB,不仅发送困难,打开也卡。我在管线里加了一个资源处理环节:图片超过 2MB 就自动压缩到 1920 宽度,质量调到 80%;背景图单独做一次 Base64 缓存,避免重复加载。经过处理,同样的30页汇报从 48MB 降到了 4.8MB,观感几乎无差别。如果你也用自动化工具生成PPT,建议在资源写入前先掂量一下这张图是否真的需要这么清晰。
音频和视频资源也是同理。如果PPT里要嵌入视频,我一般不建议直接打进 pptx 包,因为体积会成倍增长,而且不同平台的播放兼容性很差。更好的做法是把视频放在相对路径里,PPT里只保留一个跳转链接或者封面图。对于汇报场景,稳定性比炫技重要得多。
5. 插件机制:从单人脚本变成多人可用的工具
5.1 布局插件的注册表模式
当工具从“只给自己用”变成“给团队用”之后,最大的变化是版式需求开始爆炸。有人要多图对比页,有人要时间轴页,有人要数据大屏页。如果在主流程里用 if-else 堆这些版式,迟早变成一座屎山。我采用的方案是注册表模式:每个版式是一个独立的类,实现统一的 render 接口,然后通过装饰器注册到全局字典里。渲染器根据节点类型去字典里找对应的实现,找不到就抛出一个明确的错误。
LAYOUT_REGISTRY = {} def register_layout(name): def decorator(cls): LAYOUT_REGISTRY[name] = cls() return cls return decorator class LayoutPlugin: def render(self, ctx, node): raise NotImplementedError @register_layout("two_column") class TwoColumnLayout(LayoutPlugin): def render(self, ctx, node): ...这样新增一个版式,只是新增一个文件、一个装饰器,零侵入。主流程完全不需要知道新版式的内部实现。对我来说,这个模式最有价值的地方是隔离了风险:某个插件写错了,最多影响它自己,不会把整个生成流程带崩。
5.2 数据源插件:接入真实业务数据
第二个扩展点是数据源。PPT 里最需要自动化的是那些每周都要刷新的图表。我定义了 DataSource 接口:
class DataSource: def load(self, ref): """ref 是 Markdown 注释里的引用名,比如 metrics.csv""" raise NotImplementedError class CsvDataSource(DataSource): def load(self, ref): return read_csv_to_table(ref)写内容的人只需要在 Markdown 里写<!-- data_source: metrics.csv -->,渲染器就会去数据源插件里查找。CSV 是内建默认,Excel、远程接口、数据库查询都可以通过插件扩展。这样做的好处是内容文件保持了干净,业务数据从哪里来、怎么聚合,完全由配置决定。
我后来还加了一个“数据预览”命令,可以在不生成PPT的情况下,把 Markdown 引用的所有数据源读一遍,输出成一个摘要报告。这样写内容的人可以快速确认:数据文件路径有没有写错、列名有没有变动、数字条数够不够。这一步看起来小,却帮团队减少了很多因为数据文件表头改动而导致的返工。
5.3 插件如何被发现和加载
为了让非技术同事也能加插件,我选择了“约定优于配置”:插件放在 plugins/ 目录下,文件名以 _plugin 结尾,启动时用 importlib 自动扫描加载。这样一来,任何人往目录里丢一个文件,就能在团队里生效。加载时我会做一个简单校验:检查类是否实现了必需的接口,避免因为某个插件级别错误导致整个工具启动失败。
扫描路径的优先级也值得设计。我的做法是:项目根目录下的 plugins/ 优先级最高,其次是用户配置目录里的,最后才是内置的。这样每个团队可以有自己的私有插件,同时不会污染公共的内置逻辑。加载过程会打印详细的日志,哪个插件成功了、哪个失败了、失败原因是什么,一目了然。
5.4 给插件做自动化测试
插件一多,回归测试就变得很重要。我给每个 layout 插件准备了一个基准模板,测试时用它生成一份 3 页的演示文稿,然后断言文本、图片、表格的数量和位置坐标在允许误差内。数据源插件则统一用 mock 数据做输入,不访问真实业务库。这套测试跑一次不到十秒,但每次改动主流程时,至少能保证已经存在的版式不会突然崩掉。作为一个个人工具,能有这样的稳定性,已经超出预期了。
在做测试的过程中,我还发现了一个通用规律:几乎所有布局类 bug 都出在“坐标计算”上,尤其是百分比换算到英寸的时候。后来我把所有坐标计算都收敛到一个工具模块里,统一用 16:9 画布宽高作为基准,插件里不允许直接写死像素值。这样测试用例只需要验证标准宽度下的几个阈值,就能覆盖大多数异常。
6. 让 ppt-master 真正进入日常工作流
6.1 CLI 参数的取舍
工具落地最重要的环节是把使用成本降到最低。我设计的 CLI 只有两个核心参数:输入文件和输出路径。配置文件可以省略,省略时自动加载当前目录下的 ppt_master.yaml,如果也没有就用内置默认配置。所有其他参数都通过命令行覆盖,但不会新增配置文件中不存在的字段,避免语义分裂。
ppt-master build outline.md --output "./build/report.pptx" ppt-master build outline.md --config theme_dark.yaml --dry-run--dry-run是我非常喜欢的一个参数:它不会生成实际文件,只输出一份渲染计划,包括每一页用哪个布局、哪些数据源、哪些图片会被嵌入。这样在批量修改内容之后,可以先快速检查一遍计划,再决定要不要真正运行。这个习惯帮我省掉了很多次无意义的完整构建。
6.2 和 Git 及自动化流程的集成
我目前的使用方式是把大纲、配置和脚本放进同一个 Git 仓库。每次修改内容后,在提交前跑一遍构建命令,生成的 .pptx 会作为产物放在 build 目录。团队里如果有 CI 环境,还可以写一个很小的定时任务:每周五下午从某个目录读取最新的 Markdown,自动构建并上传到分享链接。这会带来一个新的好处:PPT 的每次改动都有了版本记录,不会再出现“最终版 v3 最终版 v4 最终版又改”这种混乱。
加上版本控制后,我甚至可以画出一条“内容变更历史”。比如说某个月的周报里提到的数据口径调整了,我能从 Git 历史里精准定位是哪一次提交改的,而不是靠某个同事的模糊记忆。对经常做长期汇报项目的人来说,这一点非常实用。
6.3 兼容性和安全红线
生成的 .pptx 在主流演示软件里打开基本没有问题,但 WPS 对图表字体的渲染偶尔会有偏差。我的建议是:重大项目生成后用目标软件做一次冒烟测试,重点检查图表和表格页。另外,工具里涉及业务数据时,不要把敏感信息写进配置或大纲文件;数据读取接口要设置超时和脱敏逻辑,日志只记录文件名不记录内容。这些红线看起来简单,真正出问题时才明白多重要。
还有一点容易被忽略:如果你把 .pptx 文件或者 Markdown 大纲放进 Git 仓库,一定要确认仓库的访问权限不会泄露业务数据。我见过有人把一个包含客户明细的 CSV 直接提交到了公开仓库,等到发现时已经晚了。所以工具层面我加了忽略规则,默认把所有 .csv 和包含 key 的配置文件排除在产物目录之外,避免误提交。
6.4 一个完整的最小示例
最后给一个可以直接上手的最小示例。目录结构如下:
project/ ├── outline.md ├── config.yaml └── build/outline.md:
# 项目周报 ## 本周进展 - 完成登录模块重构 - 修复三个线上问题 - 性能测试通过 ## 下周计划 1. 灰度发布方案 2. 监控告警完善config.yaml:
theme: primary_color: "2E75B6" font_zh: "思源黑体" font_en: "Inter" layout: cover: "Title Slide" content: "Title and Content"执行ppt-master build outline.md --config config.yaml --output build/weekly.pptx,就能得到一个四页的演示文稿:封面、本周进展一页、下周计划一页。整个过程不到三秒,文件可以在任何装有演示软件的机器上编辑。
完成后可以用 python-pptx 打开这个文件做个自检,确认标题和正文都在:
from pptx import Presentation prs = Presentation("build/weekly.pptx") for i, slide in enumerate(prs.slides, 1): print(f"第 {i} 页标题:{slide.shapes.title.text}")这样一个小循环,就能防止“文件生成了但内容为空”的低级错误。我在开发阶段几乎每次构建后都会跑一遍,后来才慢慢改成在自动化测试里做。
回过头看,我花在 ppt-master 上的时间其实不少,但每一分钟都值回来了。最大的体会是:做这类工具不要一上来就想做成万能的,先把最折磨自己的那个流程跑通,再慢慢往外扩。现在我已经把周报、月报、甚至跨团队汇报都迁移到这条流水线上,看图、改字、换模板都变成改配置或改 Markdown。对了,如果你也想自己搓一个,建议先从“分开内容和样式”这个原则开始,剩下的坑,等踩到了再一个一个填。