ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python自动化更新电力市场PPT:python-pptx与XML模板替换实战

Python自动化更新电力市场PPT:python-pptx与XML模板替换实战 简介这是一份聚焦电力市场管理与改革的PPT教学课件面向电力系统从业人员、经管类学生以及关注中国电改的读者。内容从2000年美国加州电力危机切入先讲清“市场”概念与健康市场的特征再过渡到电力市场定义、基本原则和六大基本特征随后以加州为样本复盘其从垂直垄断到引入竞争、再到危机爆发的全过程重点梳理批发电价飞涨、轮流断电、公用事业公司破产等事件背后的原因并结合各国市场化实践落点在中国电力市场改革的必要性与目标。全篇以案例为线索涉及电价形成机制、发电容量短缺、天然气价格冲击、监管错配等关键议题适合用于高校课堂、企业内训或个人系统自学。资源为单个pptx文件大小4.29MB页面编排突出条理性重点内容以图示和要点方式呈现便于直接演示或二次修改。目前已有67人学习下载适合需要快速建立电力市场分析框架的入门读者。1. 打开这份 pptx先别急着做版式很多做电力市场运营或售电业务的人每个月都要在“电力市场管理.pptx”上花掉大半天从交易中心导出的结算单、负荷预测报表、现货价格曲线一个一个复制粘贴到 PPT 里调字体、对齐、改数字最后再检查一遍有没有哪页的数据还停留在上个月。这套流程最大的问题不是慢而是错。手工更新意味着每一个数字都经过一次人眼核对而人眼在连续看十页表格之后基本已经失效了。如果换一个思路把“电力市场管理.pptx”理解成一个数据库查询结果的呈现层而不是一份“画出来的文档”整个维护方式就会彻底改变。这个文件的本质是一个 OOXML 压缩包内部是一堆 XML 文件页面上每一个文本框、表格、图表都有对应的 XML 节点。这意味着你可以用代码直接定位到某一页的某一行某个数字替换它保存然后得到一个版式完全不变、数据已经刷新的新文件。整个过程不需要打开 PowerPoint也不需要 VBA 宏更不需要人工复制。适合读这篇文章的人是负责电力市场月报、周报或结算分析的一线工程师被 PowerPoint 手工更新折磨过的业务分析师以及想在现有 PPT 基础上做自动化输出但又不想推翻重来的开发者。下面从 pptx 文件格式的底层结构开始逐步讲清楚如何把这个文件变成一套可维护的模板系统。2. 理解 pptx 的 XML 结构它本来就该被代码操作2.1 PPT 不是一张画布而是一个 zip 包把“电力市场管理.pptx”后缀改成 .zip解压之后你会看到ppt/slides/slide1.xml这样的文件这才是每一页幻灯片的真实内容。文本、表格、图形都记录在这些 XML 里版式由ppt/slides/_rels/slide1.xml.rels这样的关系文件指明图片放在ppt/media/主题样式在ppt/theme/theme1.xml。这个结构决定了三件事第一pptx 文件本身就是为程序处理而设计的只是 PowerPoint 图形界面把它包装成了“画图”的感觉第二你要修改的实际上是 XML 节点这就让正则匹配、XPath 定位、DOM 操作这些常规编程手段全部适用第三文件能不能打开、能不能兼容 WPS取决于你修改后 XML 是否仍然合法这一点后面会重点讲。2.2 页面级的数据存在哪里在slide1.xml里一个文本框大致长这样p:sp p:nvSpPr p:cNvPr id4 nameTextBox 3/ /p:nvSpPr p:spPr/ p:txBody a:p a:r a:t2024年12月结算电量/a:t /a:r /a:p /p:txBody /p:sp注意几个关键节点名称p:txBody是文本框主体a:p是段落a:r是文本片段a:t是真正的字符串内容。理解这一层之后“改一个数字”在代码里的本质就是找到特定的a:t节点替换它的textContent。但要小心另外一层关系也藏在这个 XML 里文本可能分散在多个a:r中。PowerPoint 在保存时会自动把一句话拆成多个文本片段甚至拆成一个字一个片段。如果直接在这个粒度上盲目替换你会遇到“明明文本存在但替换不成功”的问题。实际工程里我会把a:p级别的完整文本拼出来匹配成功后再决定如何写回。2.3 图表和表格另有一套机制如果是图表数据实际存放在ppt/charts/chart1.xml里而图表在页面上只是一个嵌入视图如果是表格数据还在a:tbl的全部a:tc单元格中。这两类在“电力市场管理”场景里出现频率极高——现货价格曲线几乎必然是图表结算结果几乎必然是表格。所以动手之前先做一次排查明确你这个 pptx 里哪些是文本框、哪些是表格、哪些是图表这决定了后面用哪种方式注入数据。排查方法在下面给出。3. 用 python-pptx 做工程化改造的最小路径3.1 先盘点现有文件的内容分布不推荐直接上手改文件。正确的第一步是先写一段只读脚本把每一页的文字、表格行列数、图表标题全部列出来。这样能确认“电力市场管理.pptx”里到底哪些页面是数据驱动的哪些页面是纯说明性的。from pptx import Presentation from pptx.util import Emu prs Presentation(电力市场管理.pptx) for slide_index, slide in enumerate(prs.slides, start1): print(f--- 第 {slide_index} 页 ---) for shape in slide.shapes: # shape_type 为 19 时是表格为 3 时是图表其他多为文本框 print(f形状类型: {shape.shape_type}, 名称: {shape.name}) if shape.has_text_frame: for para in shape.text_frame.paragraphs: text .join(run.text for run in para.runs) if text.strip(): print(f 文本: {text.strip()[:50]}) if shape.has_table: tbl shape.table print(f 表格: {len(tbl.rows)} 行 x {len(tbl.columns)} 列) if shape.has_chart: print(f 图表类型: {shape.chart.chart_type})这段脚本有三个输出维度值得关注。shape.shape_type虽然名字叫类型但它返回的是枚举值19 表示表格、3 表示图表其余数字对应不同图形实际排查中形态很多最好按名称过滤而不是按类型猜。has_text_frame和has_table是不同的属性一个图形要么有文本框要么有表格但两者都有或都没有的情况也常见代码里两个判断都写互不干扰。shape.chart.chart_type返回图表类型这个信息在后面更新图表数据时能帮助你确认不是柱状图被替换成了折线图而不知道。跑完这个脚本你手头就有了一个“文件内容地图”。我在实际操作中发现超过一半的“电力市场管理”类 PPT 存在同样的问题表格的标题行其实是普通文本框画的图表下方还有一个数据来源说明的文本框这些文本框里的内容不会自动跟着图表更新。所以这份地图不只是排查用它直接决定你要给哪些形状做“命名改造”。3.2 给关键元素命名自动化改造的第一步python-pptx 允许你读取形状名称但更关键的是你可以在模板里提前把名字改成程序能识别的标签例如settle_total、spot_price_chart、load_forecast_table。这相当于在前端给 DOM 元素加id后续所有定位都基于名称而不是基于页码和坐标。for slide_index, slide in enumerate(prs.slides, start1): for shape in slide.shapes: print(slide_index, shape.shape_id, shape.name)输出中的shape_id是文件内部的唯一标识name是显示名称。在 PowerPoint 的“选择窗格”功能里可以直接双击改名这是最省力的姿势。如果你不想手动打开 PPT 改也可以写代码批量重命名from pptx import Presentation prs Presentation(电力市场管理.pptx) rename_map { (3, 文本框 5): settle_total, (5, 图表 1): spot_price_chart, } for slide_index, slide in enumerate(prs.slides, start1): for shape in slide.shapes: key (slide_index, shape.name) if key in rename_map: shape.name rename_map[key] prs.save(电力市场管理_renamed.pptx)讲一下为什么只认名字不认位置PPT 里任何一次手动拖动、增加页码、调整版式坐标全部会变但在 XML 里形状 ID 通常稳定。可如果你在页面里复制粘贴了一个文本框shape_id会变成新的原 ID 就失效了。相比之下名称是“人可读 机器可读”的双重标识讨论模板时大家都能看懂“结算总额”指哪个框写到脚本里也能直接对应。这个步骤虽然琐碎但它是后续所有自动化工作的地基不做的话每次改版都要重新对坐标做了之后换人维护也能接手。3.3 用命名占位符实现文本替换这是最推荐的做法在模板里直接把文本写成{{settle_total}}、{{spot_price_date}}这样的占位符。替换时只要找所有形状里的文本节点匹配占位符格式再写回真实数据。import re from pptx import Presentation PLACEHOLDER_PATTERN re.compile(r\{\{(\w)\}\}) def render_text_frame(shape, data_map): if not shape.has_text_frame: return False changed False for para in shape.text_frame.paragraphs: # 将段落所有 run 拼接成完整文本再匹配 full_text .join(run.text for run in para.runs) match PLACEHOLDER_PATTERN.search(full_text) if match: key match.group(1) if key in data_map: # 因为 run 可能被拆分最简单做法是清空所有 run只留第一个 for run in para.runs[1:]: run.text para.runs[0].text full_text.replace(f{{{{{key}}}}}, str(data_map[key])) changed True return changed prs Presentation(电力市场管理_renamed.pptx) data { settle_total: 123456, spot_price_date: 2025年1月, } for slide in prs.slides: for shape in slide.shapes: render_text_frame(shape, data) prs.save(电力市场管理_2025年1月.pptx)这里处理了一个常见坑文本被拆分成多个run直接设置para.runs[0].text只改第一段后面几段会作为残留文本叠在原位。解决办法是先清空除第一个 run 外的所有run.text再用完成替换后的完整文本写回第一个 run。这样格式仍然由第一个 run 的样式属性控制因为没动它的字体、字号、颜色属性。实际在写这种 render 脚本时我通常还会加一个report列表记录哪一页、哪个形状成功匹配了哪个占位符。这样如果模板里写错了占位符名比如文本里打成了{settle_total}少了花括号输出报告里能一眼看到没有命中的形状避免事后打开 PPT 才发现漏了一页。3.4 表格填充按行插入还是整体重写表格在“电力市场管理”里通常是月度结算表或者日前中标结果表特点是列固定、行数变化并且表头可能有合并单元格。python-pptx 里操作表格支持读取和写入单元格但对合并单元格的读取限制较多所以先判断你的表格表头是否合并过。from pptx import Presentation from pptx.util import Inches prs Presentation(电力市场管理_renamed.pptx) def fill_table(shape, data_rows): if not shape.has_table: return tbl shape.table # 先计算需要增加的行数 current_rows len(tbl.rows) needed_rows len(data_rows) 1 # 1 为表头行 if needed_rows current_rows: for _ in range(needed_rows - current_rows): tbl.rows.add_row() # 写入表头 headers [机组, 结算电量(MWh), 结算金额(元)] for col_idx, header in enumerate(headers): tbl.cell(0, col_idx).text header # 写入数据行 for row_idx, row_data in enumerate(data_rows, start1): for col_idx, value in enumerate(row_data): tbl.cell(row_idx, col_idx).text str(value) rows_data [ [#1机组, 125.4, 12345], [#2机组, 98.2, 9876], ] for slide in prs.slides: for shape in slide.shapes: if shape.name settle_table: fill_table(shape, rows_data) prs.save(电力市场管理_2025年1月.pptx)tbl.rows.add_row()是在表格末尾追加行新行的行高默认继承表格样式。如果你要插入到中间位置python-pptx 没有直接提供 insert常见做法是先复制一个 XML 节点再插入但通常“电力市场管理”场景都是整表重写追加行即可满足。写入单元格时用.text 会清掉原有的段落和 run然后以默认格式写入新文本。这个行为是破坏性的如果单元格原来设置了加粗、居中替换后格式就丢了。所以要么在模板里先做好单元格格式写入后统一再设一遍 alignment要么用tf cell.text_frame后手动控制 paragraph 和 run 的格式。对表格类页面我会在写入循环里同步设置cell.vertical_anchor和段落对齐方式牺牲一点代码量换回稳定的排版。4. 处理复杂模式图表数据、嵌入对象和图片更新4.1 图表数据更新改 XML 还是用 ChartData电力市场分析报告里图表是最容易出问题的部分。pptx 里的图表类型分成两类一类是 PowerPoint 内嵌的图表数据作为chart1.xml的一部分存在python-pptx 可以通过chart.replace_data()方法替换整个数据源另一类是从 Excel 复制过来的 OLE 嵌入对象数据源在ppt/embeddings/Microsoft_Excel_工作表.xlsx里python-pptx 原生 API 基本碰不到。对于内嵌图表replace_data是已经封装好的完整方案from pptx import Presentation from pptx.chart.data import CategoryChartData prs Presentation(电力市场管理_2025年1月.pptx) chart_data CategoryChartData() chart_data.categories [1月, 2月, 3月, 4月] chart_data.add_series(日前均价, (325.5, 356.2, 341.8, 389.6)) chart_data.add_series(实时均价, (312.4, 344.9, 385.3, 402.5)) for slide in prs.slides: for shape in slide.shapes: if shape.has_chart and shape.name spot_price_chart: shape.chart.replace_data(chart_data) prs.save(电力市场管理_2025年1月.pptx)这个方法的核心好处是图表类型、样式、坐标轴格式、图例位置全部保留只替换序列的数值。CategoryChartData对应分类轴的类别数据add_series对应每个数据系列。需要注意的是原图表如果有额外的表格数据列比如“同比涨幅”列而图表本身没展示它replace_data会把这些列清掉因为它是整体重建了数据缓存。所以用之前最好先确认图表里是否只包含展示所需的数据如果内部数据还有辅助统计列就得改用直接操作chart1.xml的方式。4.2 嵌入 Excel 对象不得不改 OLE 包遇到 OLE 嵌入对象时python-pptx 的shape.has_chart通常是 False。这种情况下必须先修改 PPT 内部的 OLE 数据才能让呈现到页面上的图形跟着变。读取和替换 OLE 内容的核心步骤是从 PPT 的嵌入关系文件里找到xlsx的路径把xlsx解出来用 openpyxl 修改它再塞回 pptx 包重新打包时注意压缩格式要兼容 Office。实际工程里我会尽量避免使用 OLE 嵌入图表换用原生图表。因为 OLE 链路太长而且在 WPS 和 Office 之间来回打开容易触发重新链接弹窗。如果文件是历史遗留已经用了 OLE我的处理方案是保留原始嵌入数据不动在模板改版时把 OLE 图表整个删掉换成内嵌图表重新做一次。一次性投入半小时换后续每月十分钟的稳定自动化划算。4.3 图片更新替换 media 目录里的文件电力市场相关的图通常包括负荷曲线截图、出清结果热力图、结算单扫描件。这些图片通常都是直接在 PPT 里插入的对应ppt/media/image1.png这种命名。用 Python 直接改图片文件是可行的import zipfile import shutil def replace_media(pptx_path, output_path, media_name, new_image_path): # 读取原文件全部内容 with zipfile.ZipFile(pptx_path, r) as zin: items {name: zin.read(name) for name in zin.namelist()} with open(new_image_path, rb) as f: items[fppt/media/{media_name}] f.read() with zipfile.ZipFile(output_path, w, zipfile.ZIP_DEFLATED) as zout: for name, data in items.items(): zout.writestr(name, data)但这里有个容易踩的坑ppt/media/里文件名带序号而这个序号在实际文件里不一定按插入顺序排。改之前必须先从关系文件slideN.xml.rels中找到这张图片在哪个关系rId下、对应哪个 media 文件名否则你会替换掉一张完全不相干的图片。稳妥的做法是按前面的“内容地图”脚本输出每一页所有媒体文件的对应关系。4.4 页面复制批量生成月度报告的核心如果“电力市场管理.pptx”已经是一个月一份的模式模板里有一页“现货价格走势”是固定的但每个月你要生成新的 PPT 或新增一页复制页面就不可避免。python-pptx 官方没有提供duplicate_slide方法实现方法要在 XML 层做import copy from pptx import Presentation def duplicate_slide(prs, slide_index, new_slide_indexNone): source prs.slides[slide_index] blank_layout prs.slide_layouts[6] dest prs.slides.add_slide(blank_layout) for shape in source.shapes: el copy.deepcopy(shape._element) dest.shapes._spTree.append(el) # 处理 slide 的 rels图片、图表等关联资源 for rel in source.part.rels.values(): if image in rel.reltype or chart in rel.reltype: dest.part.rels.get_or_add(rel.rId, rel.reltype, rel.target_part) return dest prs Presentation(电力市场管理.pptx) new_page duplicate_slide(prs, 3) prs.save(电力市场管理_含复制页.pptx)copy.deepcopy复制的是形状的 XML 元素这一步没问题。真正容易坏的是关系资源如果原页引用了图片或者图表仅深拷贝形状 XML 会得到一堆指向不存在关系的无效引用。所以上面代码后半段额外把图片和图表的关系也复制到新页的关系列表里。复制完成后新页的位置排在最后一般不是你想要的位置。要调整顺序需要操作prs.slides._sldIdLst里的元素重新排列。调整顺序的复杂度不高但很繁琐常见的做法是按 XML 节点索引操作。5. 跑数据接入构建可重复的“模板数据”管线5.1 从交易系统 API 直接拉数据电力市场数据接口形态各异有的是 HTTP API 返回 JSON有的需要从 FTP 下载 CSV。这里的关键点不是某一种接口的写法而是建立一个固定的导入层——所有外部数据统一转成结构化 Python 对象再交给 PPT 生成逻辑使用。import requests import json def fetch_settlement_data(date_str, api_url, token): headers {Authorization: fBearer {token}} params {date: date_str} resp requests.get(f{api_url}/settlement, headersheaders, paramsparams, timeout10) resp.raise_for_status() data resp.json() # 接口返回的字段名可能不长这样按实际响应结构调整 return { settle_total: data[summary][total_amount], months: [item[month] for item in data[daily]], prices: [item[avg_price] for item in data[daily]], rows: [ [item[unit], item[energy], item[amount]] for item in data[units] ], }raise_for_status()在 HTTP 状态码不是 2xx 时直接抛异常这样脚本失败时你知道是数据源出问题而不是 PPT 模板格式问题。timeout10也很重要省调或交易中心的内网接口偶尔会卡住没有超时可能让整个批量任务挂起。5.2 读取 Excel 报表作为数据源更多时候数据不是从 API 拿到而是业务部门发来一个 Excel。这种场景用 openpyxl 读取即可但同样要注意别人发的 Excel 往往有合并单元格、空行、单位错误。写一个防御性的读取函数比事后对账省事得多。from openpyxl import load_workbook def load_excel_data(path, sheet_name, header_row1): wb load_workbook(path, data_onlyTrue) ws wb[sheet_name] rows [] for row in ws.iter_rows(min_rowheader_row 1, values_onlyTrue): if row[0] is None: continue # 跳过全空行 rows.append(list(row)) return rowsdata_onlyTrue一定要加它读取单元格里公式的“缓存值”而不是公式本身。没有这个参数你读到的是SUM(C2:C6)还得自己再求一遍值。5.3 用 config 文件管理模板路径与数据关联当模板里占位符多了硬编码就变得不可维护。我常年在系统里放一份config.yaml记录这个月要生成哪些文件、对应哪个模板、页面顺序怎么排。python 读取 YAML 天然方便也方便非开发同事看懂和修改。template: 电力市场管理_template.pptx output: 电力市场管理_2025年02月.pptx data: settlement_file: /data/settlement/2025-02.xlsx spot_api_url: http://10.20.30.40:8080/api/spot replacements: settle_total: {{settle_total}} spot_price_chart: {{spot_chart_data}}这份配置文件把模板设计占位符和实际数据源路径和 API解耦开下个月换数据源路径时只需要改 YAML不需要碰 Python 代码。5.4 批量生成全套月度材料把上面所有步骤串起来形成一条完整流水线def main(): prs Presentation(电力市场管理_模板.pptx) # 从外部数据源加载数据 settle_data load_excel_data(/data/settlement/2025-02.xlsx, 结算明细) api_data fetch_settlement_data(2025-02, http://10.20.30.40:8080/api, tokenos.environ[API_TOKEN]) data_map { settle_total: api_data[settle_total], report_date: 2025年2月, } for slide in prs.slides: for shape in slide.shapes: render_text_frame(shape, data_map) if shape.name settle_table: fill_table(shape, settle_data[1:]) # 第一行是表头 if shape.name spot_price_chart: replace_chart_data(shape, api_data) output f电力市场管理_{api_data[report_date]}.pptx prs.save(output)这段代码省略了一些细节比如replace_chart_data函数的完整实现以及os.environ的导入但整体结构就是真实项目里的常规形态。它把问题从“做 PPT”变成“跑一个脚本”原来可能耗费半天的工作现在只需要确认数据源没问题脚本执行完检查输出文件页数是否正确。6. 验证与运行时检查最容易被忽视的一环很多人以为脚本跑通了输出文件能打开就万事大吉。但真正上生产环境千万不能只依赖“能打开”这一个信号。有一个很基础却极为有效的习惯用 LibreOffice 命令行把生成的 PPT 转成 PDF然后比对页数。这个操作能快速暴露页面丢失、复制失败等问题而这些问题在 Windows 上手动打开 PPT 时往往会因为缓存被掩盖。具体做法是在生成脚本的最后加一步soffice --headless --convert-to pdf 电力市场管理_2025年02月.pptx转换完成后统计 PDF 页数和模板页数做对比。数字对不上说明复制页面或删除页面时 XML 出了问题。不要小看这一步它能在你写完脚本、准备交给业务使用之前拦截掉最多的低级错误。另外两个运行时的检查要点。第一个检查图表数据是否更新输出 PPT 的 XML 里chart1.xml的c:numCache部分存放的是缓存数值另一处c:numRef可能关联到 Excel 外部数据源。如果外部数据源还在PowerPoint 打开时会弹“是否更新链接”这个弹窗在自动化流程里会卡死整个生成过程。用zipfile模块读一下输出 PPT 里的chart.xml确认c:externalData不存在或者至少知道它在哪个页面。第二个检查图片引用是否有效前面提到复制页面后关系可能丢失这种情况打开 PPT 时图片直接显示红叉。用一个简短的脚本检查slideN.xml.rels里每个Target是否指向存在的媒体文件比人工翻页可靠。最后一个技巧是在模板里把“数据源说明”这类文本也做成占位符。比如“数据来源省电力交易中心”这样一行数据接入时从接口响应里取一个字段填进去。这样生成的每一份 PPT 都自带数据来源和获取时间归档时非常容易查证也方便事后追溯某份报告里的数字是不是来源于某个特定版本的结算单。这份追溯能力在电力市场这种数据口径经常调整的领域价值远高于自动化本身。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进