分类: 办公自动化

Python 办公自动化#14自动生成 PowerPoint 报告!只改 Excel 里的数字,演示文稿就做好了 (python-pptx) 📊

本文译自韩语原文。

目录13 项

大家好!我是 Mojangcam 😎

上一篇我们讲了如何把 Excel 数据自动上传到 Google 表格(gspread)。收集、保存、共享数据,现在都可以交给 Python 了。

但公司的工作并不会到此为止。最终总得有人把这些数字搬进 PPT。 每个月都是同样的模板,只有数字在变的报告。我以前每到月底都要贴表格、重画图表,一弄就是两个小时。

今天我们就让 Python 替你把它做出来。只要换掉一个 Excel 文件,就会自动生成标题页 → 摘要 → 图表 → 详细表格共 4 页的报告。库只需要 python-pptx 一个!

📌 准备工作

  • Python 3.8 及以上 (python-pptx 1.0.2 的 Requires-Python 条件是 >=3.8)
  • python-pptx 1.0.2 — 用来创建和编辑 PPTX 文件的库
  • openpyxl 3.1.x — 用来读取作为原始数据的 Excel
  • PowerPoint 没有安装也没关系! python-pptx 直接处理文件格式,所以没有 Office 也能生成 .pptx。(用 Hancom Office、Google 幻灯片、LibreOffice 也都能打开)
终端
pip install python-pptx openpyxl

可以这样确认是否安装成功。

终端
python -c "import pptx; print(pptx.__version__)"
# 1.0.2

💡 包名是 python-pptx,但 import 的时候是 pptx。如果输入 pip install pptx,装上的会是一个完全不同的包,一定要注意!

📌 第 1 步 - 准备并读取原始 Excel

首先创建一个作为报告素材的 Excel。sales.xlsx 的第一个工作表里只要有这些内容就行。

团队 目标 业绩
销售一组 5000 5400
销售二组 4500 3900
线上组 3000 3600

用 Python 把它读进来。

Python
import openpyxl

def load_rows(xlsx_path):
    wb = openpyxl.load_workbook(xlsx_path, data_only=True)
    ws = wb.active
    rows = []
    for name, target, actual in ws.iter_rows(min_row=2, values_only=True):
        if name is None:
            continue
        rows.append({"name": str(name), "target": int(target), "actual": int(actual)})
    return rows

我们逐个来看 💡

  • data_only=True : 即使单元格里有公式,也会取到计算后的结果值。去掉它的话,读到的会是 =B2*1.1 这样的公式字符串本身。
  • ws.iter_rows(min_row=2) : 第 1 行是表头,所以从第 2 行开始读。
  • values_only=True : 返回的不是单元格对象,而是只有值的元组,可以直接解包使用。
  • if name is None: continue : 过滤掉 Excel 底部被一起读进来的空行。不加这一句就会报 int(None) 错误!

📌 第 2 步 - 理解幻灯片版式并制作标题页

python-pptx 里最容易搞混的就是版式编号。默认模板里有 11 种版式,但实际工作中常用的其实只有三种。

  • slide_layouts[0] : 标题幻灯片 (标题 + 副标题)
  • slide_layouts[1] : 标题和内容 (项目符号正文)
  • slide_layouts[5] : 仅标题 (直接放图表、表格时最方便)
Python
from pptx import Presentation
from pptx.util import Inches

prs = Presentation()
prs.slide_width = Inches(10)      # 4:3 比例
prs.slide_height = Inches(7.5)

slide = prs.slides.add_slide(prs.slide_layouts[0])
slide.shapes.title.text = "2026 年 9 月业绩报告"
slide.placeholders[1].text = "自动生成 · Mojangcam"

我们逐个来看 💡

  • Presentation() : 不带参数调用时,会从默认模板开始。如果有公司模板,像 Presentation("회사양식.pptx") 这样传进去,就能直接沿用那套设计。这可是真正实用的职场小技巧!
  • slide_width / slide_height : 默认是 4:3 (10×7.5 英寸)。想要 16:9 的话,改成 Inches(13.333) × Inches(7.5) 就行。
  • shapes.title : 获取版式里自带的标题位置(占位符)。
  • placeholders[1] : 0 号是标题,1 号是副标题(或正文)。编号因版式而异,搞不清的时候用 for ph in slide.placeholders: print(ph.placeholder_format.idx, ph.name) 打印出来看看就行。

📌 第 3 步 - 制作摘要·图表·表格幻灯片

摘要幻灯片 (项目符号)

Python
from pptx.dml.color import RGBColor

def add_summary_slide(prs, rows):
    slide = prs.slides.add_slide(prs.slide_layouts[1])
    slide.shapes.title.text = "摘要"
    body = slide.placeholders[1].text_frame

    total_t = sum(r["target"] for r in rows)
    total_a = sum(r["actual"] for r in rows)
    body.text = f"总目标 {total_t:,} 万韩元 / 业绩 {total_a:,} 万韩元"

    for r in rows:
        rate = r["actual"] / r["target"] * 100
        p = body.add_paragraph()
        p.text = f'{r["name"]} : 达成率 {rate:.1f}%'
        p.level = 1
        if rate < 100:
            p.runs[0].font.color.rgb = RGBColor(0xC0, 0x00, 0x00)
  • text_frame.text = ... : 这是填充第一个段落的代码。从第二行开始要用 add_paragraph()。
  • p.level = 1 : 缩进级别。0 是最上层,数字越大缩进越深。
  • p.runs[0].font.color.rgb : 只把没达到目标的团队用红字标出来。用 p.text 赋值时只会生成一个 run,所以取 runs[0] 就行。

图表幻灯片 (真正可编辑的 PPT 图表!)

Python
from pptx.chart.data import CategoryChartData
from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION

def add_chart_slide(prs, rows):
    slide = prs.slides.add_slide(prs.slide_layouts[5])
    slide.shapes.title.text = "各团队目标与业绩对比"

    data = CategoryChartData()
    data.categories = [r["name"] for r in rows]
    data.add_series("目标", [r["target"] for r in rows])
    data.add_series("业绩", [r["actual"] for r in rows])

    graphic_frame = slide.shapes.add_chart(
        XL_CHART_TYPE.COLUMN_CLUSTERED,
        Inches(0.8), Inches(1.8), Inches(8.4), Inches(4.5),
        data,
    )
    chart = graphic_frame.chart
    chart.has_legend = True
    chart.legend.position = XL_LEGEND_POSITION.BOTTOM
    chart.legend.include_in_layout = False

这里就是 python-pptx 最大的优点。插入的不是图片,而是 PowerPoint 原生图表。演示前领导说一句"帮我改一下这个数字",在 PPT 里就能直接编辑!🙌

  • XL_CHART_TYPE.COLUMN_CLUSTERED : 簇状柱形图。折线图是 LINE,饼图是 PIE,条形图是 BAR_CLUSTERED。
  • add_chart(类型, left, top, width, height, data) : 位置和大小全部用 Inches() 或 Cm() 传入。以幻灯片左上角为 (0, 0)。
  • legend.include_in_layout = False : 防止图例挤进绘图区让图表变窄。不加这句的话,图表会被压得很奇怪。

表格幻灯片

Python
from pptx.util import Pt

def add_table_slide(prs, rows):
    slide = prs.slides.add_slide(prs.slide_layouts[5])
    slide.shapes.title.text = "详细表格"

    table = slide.shapes.add_table(
        len(rows) + 1, 4, Inches(0.8), Inches(1.8), Inches(8.4), Inches(0.8)
    ).table

    for col, head in enumerate(["团队", "目标", "业绩", "达成率"]):
        table.cell(0, col).text = head

    for i, r in enumerate(rows, start=1):
        rate = r["actual"] / r["target"] * 100
        table.cell(i, 0).text = r["name"]
        table.cell(i, 1).text = f'{r["target"]:,}'
        table.cell(i, 2).text = f'{r["actual"]:,}'
        table.cell(i, 3).text = f"{rate:.1f}%"
  • add_table(行数, 列数, left, top, width, height) : 因为要多加一行表头,所以是 len(rows) + 1。
  • 最后一定要加上 .table! add_table() 返回的是形状(GraphicFrame),要操作单元格就需要它里面的 .table。因为漏了这个而看到 AttributeError 的人真的很多 😅
  • height 是最小高度,行数变多时 PowerPoint 会自动把它撑高。

📌 完整代码 (直接复制可用)

Python
# python-pptx 1.0.2 / openpyxl 3.1.x / Python 3.8+
from pathlib import Path

import openpyxl
from pptx import Presentation
from pptx.chart.data import CategoryChartData
from pptx.dml.color import RGBColor
from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION
from pptx.util import Inches, Pt

XLSX_PATH = Path("sales.xlsx")
OUT_PATH = Path("output/2026_09_report.pptx")
POINT_COLOR = RGBColor(0x2E, 0x5B, 0xFF)

def load_rows(xlsx_path):
    wb = openpyxl.load_workbook(xlsx_path, data_only=True)
    ws = wb.active
    rows = []
    for name, target, actual in ws.iter_rows(min_row=2, values_only=True):
        if name is None:
            continue
        rows.append({"name": str(name), "target": int(target), "actual": int(actual)})
    return rows

def add_title_slide(prs, title, subtitle):
    slide = prs.slides.add_slide(prs.slide_layouts[0])
    slide.shapes.title.text = title
    slide.placeholders[1].text = subtitle
    return slide

def add_summary_slide(prs, rows):
    slide = prs.slides.add_slide(prs.slide_layouts[1])
    slide.shapes.title.text = "摘要"
    body = slide.placeholders[1].text_frame
    total_t = sum(r["target"] for r in rows)
    total_a = sum(r["actual"] for r in rows)
    body.text = f"总目标 {total_t:,} 万韩元 / 业绩 {total_a:,} 万韩元"
    for r in rows:
        rate = r["actual"] / r["target"] * 100
        p = body.add_paragraph()
        p.text = f'{r["name"]} : 达成率 {rate:.1f}%'
        p.level = 1
        if rate < 100:
            p.runs[0].font.color.rgb = RGBColor(0xC0, 0x00, 0x00)
    return slide

def add_chart_slide(prs, rows):
    slide = prs.slides.add_slide(prs.slide_layouts[5])
    slide.shapes.title.text = "各团队目标与业绩对比"

    data = CategoryChartData()
    data.categories = [r["name"] for r in rows]
    data.add_series("目标", [r["target"] for r in rows])
    data.add_series("业绩", [r["actual"] for r in rows])

    graphic_frame = slide.shapes.add_chart(
        XL_CHART_TYPE.COLUMN_CLUSTERED,
        Inches(0.8), Inches(1.8), Inches(8.4), Inches(4.5),
        data,
    )
    chart = graphic_frame.chart
    chart.has_legend = True
    chart.legend.position = XL_LEGEND_POSITION.BOTTOM
    chart.legend.include_in_layout = False
    chart.plots[0].series[1].format.fill.solid()
    chart.plots[0].series[1].format.fill.fore_color.rgb = POINT_COLOR
    return slide

def add_table_slide(prs, rows):
    slide = prs.slides.add_slide(prs.slide_layouts[5])
    slide.shapes.title.text = "详细表格"

    table = slide.shapes.add_table(
        len(rows) + 1, 4, Inches(0.8), Inches(1.8), Inches(8.4), Inches(0.8)
    ).table
    for col, head in enumerate(["团队", "目标", "业绩", "达成率"]):
        table.cell(0, col).text = head
    for i, r in enumerate(rows, start=1):
        rate = r["actual"] / r["target"] * 100
        table.cell(i, 0).text = r["name"]
        table.cell(i, 1).text = f'{r["target"]:,}'
        table.cell(i, 2).text = f'{r["actual"]:,}'
        table.cell(i, 3).text = f"{rate:.1f}%"
    for row in table.rows:
        for cell in row.cells:
            for p in cell.text_frame.paragraphs:
                for run in p.runs:
                    run.font.size = Pt(14)
    return slide

def main():
    rows = load_rows(XLSX_PATH)
    prs = Presentation()
    prs.slide_width = Inches(10)      # 16:9 用 Inches(13.333)
    prs.slide_height = Inches(7.5)

    add_title_slide(prs, "2026 年 9 月业绩报告", "自动生成 · Mojangcam")
    add_summary_slide(prs, rows)
    add_chart_slide(prs, rows)
    add_table_slide(prs, rows)

    OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
    prs.save(OUT_PATH)
    print(f"완료: {OUT_PATH} (슬라이드 {len(prs.slides)}장)")

if __name__ == "__main__":
    main()

📌 运行结果

终端
python make_report.py
终端
완료: output/2026_09_report.pptx (슬라이드 4장)

打开 output 文件夹里生成的文件,就能看到按标题 → 摘要 → 图表 → 表格的顺序做好的 4 页幻灯片。点一下图表,再点"编辑数据",就会弹出 Excel 窗口。这就是真正 PPT 图表的证据 😎

那下个月呢?只要改一下 sales.xlsx 里的数字再运行一次就搞定了。两个小时的活儿变成了 2 秒。

📌 附赠 - 直接套用公司模板 💡

想连设计也统一的话,直接打开公司模板来用就行。

Python
prs = Presentation("회사양식.pptx")

# 先确认这个模板里有哪些版式!
for i, layout in enumerate(prs.slide_layouts):
    print(i, layout.name)

每个模板的版式顺序都不一样,所以先把编号打印出来,再挑出想要的版式索引来用比较稳妥。既可以保留已有的幻灯片,在后面接着添加,也可以事先做一个空白模板文件(只有母版的 pptx)来用,这也是个好办法。

📌 常见错误

1. ModuleNotFoundError: No module named 'pptx'

  • 原因:用 pip install pptx 装错了包,或者装到了别的虚拟环境里
  • 解决:用 pip install python-pptx 重新安装。如果怀疑安装位置不对,像 python -m pip install python-pptx 这样用运行脚本的同一个 Python 解释器来安装最保险。

2. AttributeError: 'GraphicFrame' object has no attribute 'cell'

  • 原因:add_table() 后面没有加 .table
  • 解决:一直写到 shapes.add_table(...).table 为止。

3. KeyError: 'no placeholder on this slide with idx == 1'

  • 原因:这个版式里没有 1 号占位符。尤其是 slide_layouts[5](仅标题)里没有正文位置
  • 解决:用 for ph in slide.placeholders: print(ph.placeholder_format.idx, ph.name) 确认实际编号,如果需要正文就用 slide_layouts[1]。

4. PermissionError: [Errno 13] Permission denied: '...pptx'

  • 原因:要生成的 PPT 文件还在 PowerPoint 里打开着,就运行了脚本
  • 解决:关掉文件再运行。实际工作中最好像 2026_09_report_20260920.pptx 这样在文件名里加上日期,避免覆盖。

5. 韩文等文字显示成方块(□□□)

  • 原因:不是代码的问题,而是用来演示的电脑上没有对应的字体
  • 解决:像 run.font.name = "맑은 고딕" (Malgun Gothic) 这样,指定任何电脑上都有的默认字体。特殊字体不会跟着文件一起走。

📌 总结

今天我们用 python-pptx 自动生成了 Excel 数据 → 4 页 PPT 报告。总结一下 💬

  • 从 Presentation() 开始,只要掌握 slide_layouts[0/1/5] 这三个,大部分情况都能搞定
  • 图表用 CategoryChartData + add_chart() → 不是图片,而是真正的 PPT 图表
  • 表格用 add_table(...).table,别忘了末尾的 .table
  • 公司模板用 Presentation("양식.pptx") 直接复用

再接上前几篇,就是一套完整的流程了。用爬虫和表格收集数据 → 用今天学的内容生成 PPT → 用邮件自动发送发给领导,就变成了一条完全不用人手参与的流水线!

下一篇会讲把生成的报告转换成 PDF 并分发的方法,再下一篇会把目前学到的全部组合起来,用 🏆 最终项目 - 每天早上自动送达的工作报告系统为这个系列收尾。敬请期待 🙌

有问题欢迎在评论区留言 💬 下一篇见!

📚 Python 办公自动化系列

  • #10 - 自动生成 Word 文档 (python-docx) → 查看
  • #11 - 个性化群发邮件自动发送 (smtplib) → 查看
  • #12 - 对接 Google 表格 (gspread) → 查看
  • #13 - 搭建日志·重试·失败通知体系 (logging·tenacity)
  • #14 - 自动生成 PowerPoint 报告 (python-pptx) ← 就是本文!
  • #15 - PDF 转换·分发 (即将推出)

连载 · 共 14 篇中的第 14 篇

Python 办公自动化

  1. #1엑셀 자동화 입문 (openpyxl)! (韩语)
  2. #2웹 크롤링 입문 (requests + BeautifulSoup)! (韩语)
  3. #3파일 정리 자동화 (이름 일괄 변경)! (韩语)
  4. #4디스코드 웹훅(Webhook)으로 알림 보내기 (requests)! (韩语)
  5. #5로컬 LLM(LM Studio)으로 고객 문의 자동 분류·요약하기! (무료 AI) (韩语)
  6. #6예약 실행 자동화! 매일 정해진 시간에 스크립트 돌리기 (schedule·APScheduler) (韩语)
  7. #7동적 페이지·로그인 자동화! requests로 안 되는 사이트 긁기 (Selenium) (韩语)
  8. #8PDF 자동 처리! 여러 파일 병합·분할부터 비밀번호까지 (pypdf) 📄 (韩语)
  9. #9마우스·키보드 GUI 자동화! 클릭·타이핑을 파이썬이 대신하게 하기 (PyAutoGUI) (韩语)
  10. #10워드(Word) 문서 자동 생성! 엑셀 명단으로 계약서·안내문 100장 뽑기 (python-docx) 📄 (韩语)
  11. #11개인화 대량 메일 자동 발송! 명단별 첨부파일·발송 로그·재시도까지 (smtplib 실전편) 📬 (韩语)
  12. #12구글 시트 연동하기! 팀원과 같이 보는 자동 시트 (gspread) 📗 (韩语)
  13. #13파이썬 자동화 로그·재시도·디스코드 실패 알림 만들기 - 조용히 멈추는 스크립트 잡기 (韩语)
  14. #14自动生成 PowerPoint 报告!只改 Excel 里的数字,演示文稿就做好了 (python-pptx) 📊当前文章
显示全部 14 篇收起

作者

Mojangcam

亲手写代码、跑服务器、解决卡住的报错,把一路学到的东西记录下来的开发与 IT 实战博客