1. 为什么我决定自己动手做一个 PPT 生成工具
做技术分享、写项目复盘、给团队做内训,绕不开的一件事就是做 PPT。我粗略算过,过去一年我至少做了四十多份演示文稿,平均每份从梳理大纲到排版收尾要花三到五个小时。最让人抓狂的不是内容本身,而是那些重复劳动:调字号、对齐文本框、统一配色、给每页加页码、把同一套版式复制到十几个页面里再逐个改。内容我早就想清楚了,时间全耗在“搬砖”上。
市面上现成的方案我也试过不少。在线模板站点的模板确实好看,但改起来束手束脚,稍微动一下版式就全乱了;一些 AI 生成工具能一句话出稿,可出来的东西要么排版稀碎,要么内容空洞得像套话合集,改的成本比自己写还高。我真正想要的东西很朴素:给我一个结构化的内容源,帮我自动套上一套干净、统一、专业的版式,生成一个能直接用的 PPT 文件。内容我来把控,排版交给工具。
这就是ppt-master这个项目诞生的背景。它不是要取代人做设计,而是把“内容”和“排版”这两件事解耦——你只管把每一页要讲什么写清楚,工具负责把它变成一份拿得出手的演示文稿。它适合所有被 PPT 排版折磨过的技术人、产品经理、讲师和学生,尤其适合那些内容已经想明白、只差一个体面外壳的场景。下面我把整个项目的设计思路、核心实现、踩过的坑和实操方法完整拆一遍,你可以直接照着复现一套属于自己的版本。
2. 整体设计思路与方案选型
2.1 核心需求拆解:到底要解决什么问题
在动手之前,我先把需求拆成了几条硬指标,这几条决定了后面所有的技术选型。
第一,输入要足够简单。我不想为了生成一份 PPT 去学一套新的标记语法,最好是我平时写文档用的 Markdown 就能直接喂进去。Markdown 天然有层级结构,标题、列表、引用、代码块,这些恰好对应 PPT 里的标题页、要点页、强调页和代码展示页。
第二,输出必须是真正的 PPT 文件,而不是图片或者网页。原因很现实:交付场景里,别人要的是.pptx,要能二次编辑、能插动画、能改母版。生成一堆 PNG 图片拼起来的“伪 PPT”,在真实工作流里是没法用的。
第三,版式要统一且可换。同一份文稿里所有页面的字体、配色、间距必须一致,同时我希望换一套主题时,整份文稿的风格能整体切换,而不是一页页去改。
第四,要能处理中文排版。这一点被很多人忽略。中文字体和西文字体的行高、字重、标点处理完全不同,很多工具默认按西文逻辑排版,中文出来要么挤成一团,要么行距大得离谱。
把这四条摆在一起,方案的大方向其实就清晰了:用 Markdown 做输入层,用一套模板引擎做渲染层,最终输出标准的 pptx 文件。
2.2 技术选型:为什么是 python-pptx 而不是别的
生成 pptx 的路子有好几条,我逐一对比过。
| 方案 | 优点 | 致命缺点 | 是否采用 |
|---|---|---|---|
| 直接操作 OOXML | 完全可控 | 学习成本极高,XML 结构复杂易错 | 否 |
| 调用办公软件接口 | 渲染效果最真 | 依赖本地安装,无法在服务器批量跑 | 否 |
| 网页转 pptx | 排版灵活 | 转换损失大,元素变成图片不可编辑 | 否 |
| python-pptx | 纯 Python,跨平台,元素可编辑 | 高级排版需自己封装 | 是 |
最终选python-pptx,核心理由是它生成的是原生可编辑元素——文本框是真文本框,形状是真形状,别人拿到文件后能直接改字、换色、调位置。这一点在交付场景里是刚需。它的短板是“高级排版能力弱”,比如没有现成的自动布局、没有主题系统,但这些恰好是我想自己封装的部分,因为只有自己封装才能精确控制中文排版。
提示:选型时不要只看“能不能生成”,要看“生成的东西别人能不能接着用”。可编辑性在协作场景里的权重,往往比生成速度更高。
2.3 架构分层:三层结构各司其职
整个项目我分成了三层,层与层之间通过明确的数据结构通信,这样任何一层想换实现都不影响其他层。
- 解析层:把 Markdown 文本解析成一份“页面清单”。每个页面是一个字典,包含页面类型(封面、目录、要点、代码、结尾)、标题、正文要点列表、备注等字段。这一层只关心“讲什么”,完全不碰样式。
- 样式层:定义一套主题,包含配色方案、字体族、各级字号、行距、页边距、装饰元素规则。样式层是一组配置,换主题就是换这组配置。
- 渲染层:拿着页面清单和主题配置,逐页调用
python-pptx的 API 把内容画到幻灯片上。这一层只关心“怎么画”,不关心内容从哪来。
这种分层的最大好处是可测试。解析层可以单独用一堆 Markdown 样例跑单元测试,渲染层可以用固定的页面清单反复调样式,互不干扰。我踩过的一个坑就是早期把解析和渲染揉在一起,改一个字号要重新跑整个流程,调试效率极低。
3. 核心细节解析与实操要点
3.1 Markdown 到页面清单的映射规则
解析层是整个项目的入口,规则设计得好不好,直接决定用起来顺不顺手。我定的映射规则是这样的:
- 一级标题
#作为封面页的标题,紧随其后的第一段作为副标题。 - 二级标题
##开启一个新的内容页,标题就是这一页的页标题。 - 二级标题下的无序列表
-作为这一页的要点条目,每个条目一行。 - 三级标题
###在内容页内作为小节标题,用于把一页里的要点分组。 - 引用块
>单独渲染成强调页,用于放金句、结论或警示。 - 代码块 ``` 渲染成代码页,保留等宽字体和语法高亮的基础配色。
- 水平分割线
---强制分页,给作者精确控制页面边界的能力。
这套规则的好处是符合直觉。你平时怎么写技术文档,就怎么组织 PPT 内容,不需要额外记忆一套语法。我实测下来,把一篇现成的技术博客改造成 PPT 大纲,基本只需要删掉冗余段落、把长句拆成要点,十分钟就能搞定。
注意:解析层一定要处理“空页”和“超长页”。一个二级标题下面如果没有任何要点,应该跳过而不是生成一张空白页;一页要点超过七条时,应该自动拆成两页,否则字会小到看不清。
3.2 中文排版的关键参数怎么定
中文排版是这个项目里最费心思的部分,也是很多同类工具做不好的地方。我调了很久才找到一组比较舒服的参数,这里把计算逻辑说清楚。
字号。PPT 的默认画布是 16:9,宽度 13.333 英寸,换算成厘米约 33.87 厘米。正文要点字号我定在 20pt,页标题 32pt,封面主标题 44pt。为什么是这几个数?因为投影场景下,最后一排观众到屏幕的距离通常是屏幕高度的 4 到 6 倍,20pt 的字在这个距离下刚好能看清,再小就吃力了。页标题比正文大 1.6 倍,是为了建立清晰的视觉层级。
行距。中文的行距不能照搬西文的 1.15 倍,因为汉字是方块字,字面率高,行距太小会显得密不透风。我实测 1.4 到 1.5 倍行距对中文最舒服,最终定在 1.45。段间距设为行距的 0.6 倍,让要点之间既有区分又不至于太散。
页边距。左右边距各留 1.2 英寸,上下边距各留 0.8 英寸。左右留宽一点是因为投影时屏幕边缘容易有裁切,上下留窄一点是为了给内容更多纵向空间。这个比例是我对比了十几份专业模板后总结出来的。
字体。正文用思源黑体或微软雅黑这类无衬线字体,标题可以用思源宋体增加正式感。关键是中英文字体要分别指定,否则英文和数字会跟着中文字体走,显得很别扭。python-pptx里通过设置run.font.name只能设一个,要分别设置中西文需要操作底层的 XML,这一点后面会讲。
3.3 主题系统的设计:换肤不改内容
主题系统是我最满意的设计之一。一个主题就是一份 Python 字典,结构大致如下:
THEME = { "name": "深蓝商务", "colors": { "background": "FFFFFF", "title": "1F3864", "body": "333333", "accent": "2E75B6", "code_bg": "F2F2F2", }, "fonts": { "title_cn": "思源宋体", "title_en": "Arial", "body_cn": "思源黑体", "body_en": "Arial", "code": "Consolas", }, "sizes": { "cover_title": 44, "page_title": 32, "body": 20, "code": 16, }, "spacing": { "line": 1.45, "margin_x": 1.2, "margin_y": 0.8, }, }渲染层所有取色、取字体、取字号的地方都从这个字典里读,绝不硬编码。这样我想换风格时,只需要写一份新的主题字典,整份文稿立刻改头换面。我目前维护了四套主题:深蓝商务、极简黑白、暖色学术、暗色科技,覆盖了大部分使用场景。
提示:主题字典里一定要给每个颜色和字体留一个“兜底值”。如果某个字体在目标机器上没装,
python-pptx不会报错,但打开时会静默替换成默认字体,导致排版错乱。稳妥的做法是优先选系统自带字体,或者在生成后手动检查一遍。
4. 实操过程与核心环节实现
4.1 环境准备与依赖安装
先把环境搭起来。我用的 Python 版本是 3.10,python-pptx对 3.8 以上都支持。依赖很少,核心就一个库,再加一个 Markdown 解析库。
pip install python-pptx markdown-it-pymarkdown-it-py比标准库的markdown更适合做结构化解析,因为它输出的 token 流层级清晰,方便我按标题切分页面。装完之后建一个项目目录,结构建议这样组织:
ppt-master/ ├── main.py # 入口,串联三层 ├── parser.py # 解析层 ├── renderer.py # 渲染层 ├── themes.py # 主题配置 └── samples/ # 示例 Markdown这种拆分不是强迫症,而是为了后面调试方便。我早期把所有代码塞在一个文件里,改一处要滚动几百行,效率极低。
4.2 解析层的实现:把 Markdown 切成页面
解析层的核心逻辑是遍历 token 流,遇到二级标题就开一个新页面,遇到列表项就往当前页面加要点。关键代码如下:
from markdown_it import MarkdownIt def parse_markdown(text): md = MarkdownIt() tokens = md.parse(text) pages = [] current = None for tok in tokens: if tok.type == "heading_open" and tok.tag == "h2": current = {"type": "content", "title": "", "points": []} pages.append(current) elif tok.type == "inline" and current is not None: if not current["title"]: current["title"] = tok.content else: current["points"].append(tok.content) return pages这段代码是简化版,实际实现里还要处理封面、代码块、引用块和分页符。这里有个细节值得说:标题和正文的区分不能只看 token 类型,还要看它前面最近的一个 heading_open 是几级。我一开始没注意这点,导致三级标题被误当成正文要点,排版全乱了。
解析完成后,我会做一次“页面体检”:检查每页要点数量是否超过阈值、标题是否为空、有没有孤立的代码块。体检不通过的页面会打印警告,提醒作者调整内容。这一步看似多余,但能挡掉大部分“生成出来才发现排版崩了”的情况。
4.3 渲染层的实现:从页面清单到幻灯片
渲染层是工作量最大的部分。核心流程是:新建演示文稿、设置画布尺寸、逐页创建幻灯片、往每页填内容。先看骨架:
from pptx import Presentation from pptx.util import Inches, Pt def render(pages, theme, output_path): prs = Presentation() prs.slide_width = Inches(13.333) prs.slide_height = Inches(7.5) blank_layout = prs.slide_layouts[6] for page in pages: slide = prs.slides.add_slide(blank_layout) if page["type"] == "content": draw_content_page(slide, page, theme) elif page["type"] == "cover": draw_cover_page(slide, page, theme) prs.save(output_path)这里有个关键选择:用空白版式(layout 6)而不是内置的标题+内容版式。内置版式虽然省事,但它的占位符位置、字体、颜色都受母版控制,改起来很麻烦。用空白版式意味着所有元素都要自己画,但换来的是完全的控制权。对于追求统一排版的场景,这个取舍是值得的。
draw_content_page函数负责画一页内容页,逻辑是:先画页标题,再画一条装饰线,然后逐条画要点。要点文本的绘制要注意几个细节:
def add_textbox(slide, text, left, top, width, height, theme, size_key): box = slide.shapes.add_textbox(left, top, width, height) tf = box.text_frame tf.word_wrap = True p = tf.paragraphs[0] p.line_spacing = theme["spacing"]["line"] run = p.add_run() run.text = text run.font.size = Pt(theme["sizes"][size_key]) run.font.color.rgb = RGBColor.from_string(theme["colors"]["body"]) set_font(run, theme["fonts"]["body_cn"], theme["fonts"]["body_en"]) return boxword_wrap = True必须开,否则长文本会溢出文本框。line_spacing用主题里的值,保证全篇一致。set_font是我自己封装的函数,用来分别设置中西文字体,下面单独讲。
4.4 中西文字体分别设置的底层操作
python-pptx的run.font.name只能设置一个字体名,它对应的是 OOXML 里的latin字体。中文字体对应的是ea(East Asian)字体。要分别设置,得直接操作 XML:
from pptx.oxml.ns import qn def set_font(run, cn_font, en_font): run.font.name = en_font 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', cn_font)这段代码是踩坑踩出来的。一开始我只设了font.name,结果中文全部回退到默认字体,英文倒是正常。查了半天 OOXML 规范才明白,中文字体走的是a:ea节点。加上这段之后,中文才真正用上我指定的字体。这个坑非常隐蔽,因为程序不报错,只有打开文件才看得出来。
注意:
a:ea节点必须放在a:latin之后,顺序错了某些版本的办公软件会忽略它。稳妥做法是先设font.name(它会自动创建a:latin),再追加a:ea。
4.5 代码页的语法高亮处理
代码页是技术分享里绕不开的。python-pptx本身不支持语法高亮,我的做法是在解析阶段就把代码按 token 染色,渲染时按颜色分段写入。具体是用pygments把代码切成带颜色标记的片段,然后每个片段作为一个 run 加到段落里。
from pygments import lex from pygments.lexers import get_lexer_by_name from pygments.token import Token def render_code(slide, code, lang, theme): lexer = get_lexer_by_name(lang) box = slide.shapes.add_textbox(...) tf = box.text_frame p = tf.paragraphs[0] for token_type, value in lex(code, lexer): run = p.add_run() run.text = value run.font.name = theme["fonts"]["code"] run.font.size = Pt(theme["sizes"]["code"]) run.font.color.rgb = token_color(token_type, theme)token_color函数把 pygments 的 token 类型映射到主题配色。这样代码页既有高亮,颜色又和整体主题协调。实测下来,一页放 15 行以内的代码最舒服,超过就拆页。
5. 常见问题与排查技巧实录
5.1 生成的文件打开后字体全变了
这是最高频的问题,几乎每个用python-pptx的人都会遇到。原因通常是目标机器没装你指定的字体,办公软件静默替换成了默认字体。排查思路是:先确认字体名拼写正确,再确认目标机器是否真的装了这款字体。
解决办法有两个。一是只用系统自带字体,比如中文用微软雅黑、英文用 Arial,兼容性最好。二是嵌入字体,但python-pptx不支持嵌入,需要生成后用办公软件手动操作。我的建议是前者,牺牲一点设计感换稳定性,在交付场景里更划算。
5.2 文本框里的文字溢出或换行位置奇怪
文字溢出一般是文本框高度不够或者没开自动换行。检查word_wrap是否为True,以及文本框高度是否留足了行数乘以行距的空间。换行位置奇怪通常是中西文混排导致的,因为办公软件默认按西文规则断行,中文标点可能被挤到行首。这个问题没有完美的程序化解法,我的经验是在要点文本里手动控制长度,每条要点尽量不超过 30 个汉字,从源头避免换行问题。
5.3 要点太多导致一页挤爆
解析层的“页面体检”就是干这个的。我设的阈值是每页最多 7 条要点,超过就自动拆页。拆页的逻辑是:前 7 条留原页,剩下的开一个新页,标题加“(续)”。这个策略在实测中很实用,既不会让单页过挤,也不会让内容莫名其妙消失。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方向 | 解决方式 |
|---|---|---|---|
| 字体全变 | 目标机缺字体 | 换系统自带字体 | 用微软雅黑/Arial |
| 文字溢出 | 未开自动换行 | 检查 word_wrap | 设为 True 并留足高度 |
| 中文回退 | 未设 ea 字体 | 检查 XML 节点 | 手动追加 a:ea |
| 页面空白 | 解析出空页 | 检查体检日志 | 跳过无要点页面 |
| 代码无高亮 | 未走 pygments | 检查渲染分支 | 按 token 分段染色 |
| 换主题无效 | 硬编码样式 | 全局搜索颜色值 | 全部改读主题字典 |
5.5 几个我踩过的独家坑
第一个坑是幻灯片尺寸单位。python-pptx里所有位置和尺寸都用 EMU(English Metric Unit),1 英寸等于 914400 EMU。我一开始直接传像素值,结果所有元素都小得看不见。后来统一用Inches()和Pt()包装,才正常。
第二个坑是段落和 run 的关系。一个段落可以有多个 run,每个 run 可以有自己的字体和颜色。我早期把整段文本塞进一个 run,导致没法做局部加粗和染色。后来改成按需拆分 run,灵活性大增。
第三个坑是保存路径。如果输出目录不存在,prs.save()会直接抛异常。加一句os.makedirs(os.path.dirname(path), exist_ok=True)就能避免,但这个小细节坑了我不少时间。
6. 内容组织与写作层面的经验
工具只是手段,真正决定一份 PPT 好坏的还是内容组织。用ppt-master的过程中,我总结出几条和工具配合的写作经验,分享出来。
一页一个核心观点。这是最重要的一条。很多人做 PPT 喜欢把一页塞满,结果观众不知道重点在哪。我的做法是每个二级标题只讲一件事,要点控制在 3 到 5 条,每条不超过两行。工具会自动帮你排版,但内容的取舍只能靠人。
要点用短句,不用长句。PPT 是给人看的,不是给人读的。要点写成“解析层负责切分页面”比“解析层的主要职责是将输入的 Markdown 文本按照标题层级切分成一个个独立的页面对象”要好得多。短句在投影上更易读,也更容易被记住。
善用引用块做强调。我在解析规则里把引用块单独渲染成强调页,就是鼓励作者用它来放结论和金句。一份 PPT 里有两三处这样的强调页,节奏感会好很多。
代码页要克制。技术分享里代码很重要,但一页放太多代码就是灾难。我的经验是每页不超过 15 行,只放最核心的片段,完整代码放到附录或者用链接给出。
备注栏别浪费。python-pptx支持给每页加备注,我把它用来放讲稿要点。这样演示时开演讲者视图,备注只有自己看得到,观众看到的还是干净的页面。
7. 后续可以怎么扩展
这套东西跑通之后,我陆续加了一些扩展,用起来更顺手。一个是批量生成,把多份 Markdown 一次性转成 pptx,适合做系列课程。另一个是模板导入,读取一份现成的 pptx 作为母版,提取它的配色和字体,套到新内容上。还有一个想法是接入图表生成,把 Markdown 里的表格自动转成幻灯片里的图表,这个还在试验阶段。
如果你也想动手做一套,我的建议是先跑通最小闭环:一份最简单的 Markdown,生成一份只有标题和要点的 pptx,能打开、能看,就算成功。然后再逐步加封面、加代码页、加主题系统。不要一上来就追求功能齐全,那样很容易卡在某个细节上半途而废。我在实际开发中的体会是,这类工具的价值不在于功能多,而在于它能不能真正融进你的日常工作流——当你下次做 PPT 时下意识地先写 Markdown 再生成,这个工具就算成了。