适用版本:Python 3.10+ / pandas 2.0+ / matplotlib 3.x / xlwings 0.24.3+
依赖 :
pandas、matplotlib、xlwings平台:Windows(xlwings 需要本机安装 Microsoft Excel)
1. 为什么需要这套方案
做报表的朋友大概都有这种经历:业务方拿到 Excel,转头就问"能不能带个趋势图?"。行吧,画就画,可你用 Excel 自带图表辛辛苦苦做完,发过去第二天人又来了------数据更新了,图得重做。
手动截图、粘贴、对齐位置,一次两次还行,天天来谁受得了。
这篇讲一个我自己用了挺久的套路:用 Python 从 Excel 读数据,matplotlib 画图,然后直接把图片插回同一张 Excel 工作表里。脚本写好之后,数据一变重跑一下就行,全程不用手动碰。
2. 工具分工
| 环节 | 工具 | 理由 |
|---|---|---|
| 从 Excel 读数据 | xlwings | 文件可能有合并表头/格式,xlwings 读的是"所见即所得"的当前区域 |
| 计算与整理 | pandas | 转成 DataFrame 后排序、重采样都方便 |
| 画图 | matplotlib | 样式可控,能出高质量图 |
| 插回 Excel | xlwings sheet.pictures.add() |
唯一能把图片落到指定单元格位置的方案 |
3. 完整实现
直接上代码,注释写得很细,复制过去改改路径就能跑:
python
import matplotlib.pyplot as plt
import pandas as pd
import xlwings as xw
from pathlib import Path
DATA_DIR = Path(r"D:\MTCResearch\Python-excels\data")
SRC = DATA_DIR / "本月份上衣销量数据.xlsx"
# -------------------------------------------------------------
# 中文字体配置(Windows)------这两行千万别忘了!
# 不配置的话,所有中文都会渲染成"□□□"方框
# -------------------------------------------------------------
plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei"] # 前者优先,后者兜底
plt.rcParams["axes.unicode_minus"] = False # 解决负号显示为方块的问题
with xw.App(visible=False, add_book=False) as app:
wb = app.books.open(SRC)
sheet = wb.sheets[0]
# ---------------------------------------------------------
# 第一步:把 Excel 当前区域读成 DataFrame
# options(pd.DataFrame, ...) 的第一个参数指定"返回类型"
# expand="table" 从 A1 向下向右扩展到连续数据边界
#
# 注意:默认会把【第一列当作行索引】,本例第一列是日期,正好拿来做横轴;
# 若你的首列是普通数据(如"序号"),加 index=False 关掉这个行为
# ---------------------------------------------------------
df = sheet.range("A1").options(pd.DataFrame, expand="table").value
# 保险起见统一类型:Excel 存的数字常常被读成 float
df["销量"] = df["销量"].astype(float)
print(df.head())
# 类型 销量
# 日期
# 2021-07-01 上衣 691.0
# 2021-07-02 上衣 121.0
# 2021-07-03 上衣 118.0
# 2021-07-04 上衣 879.0
# 2021-07-05 上衣 470.0
# ---------------------------------------------------------
# 第二步:画图
# figsize 单位是英寸,(12, 6) 是 12:6 的宽屏比例
# dpi=100 → 输出 1200×600 像素
# ---------------------------------------------------------
fig, ax = plt.subplots(figsize=(12, 6), dpi=100)
ax.plot(df.index, df["销量"], marker="o", linewidth=1.5, color="#C00000")
ax.set_title("本月份上衣销量趋势", fontsize=14)
ax.set_xlabel("日期")
ax.set_ylabel("销量(件)")
ax.grid(alpha=0.3, linestyle="--")
fig.autofmt_xdate() # 日期标签自动倾斜,避免重叠
fig.tight_layout() # 自动裁剪白边
# ---------------------------------------------------------
# 第三步:插入 Excel
# name → 图片在 Excel 中的名称(相当于"图层名")
# update → True 表示已存在同名图片时【替换】,False 会叠加上去
# left/top → 以 E3 单元格的左上角为锚点(单位:磅 point)
# ---------------------------------------------------------
pic = sheet.pictures.add(
fig,
name="折线图",
update=True,
left=sheet.range("E3").left,
top=sheet.range("E3").top,
)
print(pic) # <Picture '折线图' in <Sheet [本月份上衣销量数据.xlsx]Sheet1>>
# ---------------------------------------------------------
# 第四步:释放资源
# 不 close 的话,matplotlib 会累计 figure 并在超过 20 个时告警
# ---------------------------------------------------------
plt.close(fig)
wb.save()
wb.close()
4. 6 个你很可能踩的坑
这些都是我实打实踩过的坑,列出来帮你省点时间。
4.1 中文乱码:这两行千万别忘了
python
plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei"] # 指定中文字体
plt.rcParams["axes.unicode_minus"] = False # 修复负号变方块
只写第一行不行------中文能显示,但坐标轴上的负号 - 会变成方块,因为默认字体里没有对应的字形。两行必须同时加。
各平台可用的中文字体:
| 平台 | 推荐字体名 |
|---|---|
| Windows | SimHei(黑体)、Microsoft YaHei(微软雅黑) |
| macOS | Arial Unicode MS、PingFang SC |
| Linux | 需手动安装 Noto Sans CJK SC,再 fc-list :lang=zh 查名称 |
4.2 options(pd.DataFrame) 默认吃掉第一列
python
df = sheet.range("A1").options(pd.DataFrame, expand="table").value
这行默认把第一列作为行索引。本例第一列是日期,正好当横轴用,感觉还挺方便。但如果你的第一列是普通数据(比如"序号"),数据就会被"吃掉"变成索引,人都懵了------怎么少了一列?
关掉它:
python
df = sheet.range("A1").options(pd.DataFrame, expand="table", index=False).value
对应的还有 header=False(不把第一行当列名),需要的时候也记得加。
4.3 重跑脚本,图越叠越多
pictures.add() 每调用一次就插入一张新图。如果你不带 update=True 就反复运行脚本,工作表上会叠着一堆一模一样的图,而且肉眼很难发现------因为它们完美重合了。
下次你手动拖动图片的时候,下面还藏着十张八张,那画面太美......
养成习惯: update=True 加上。
4.4 定位图片别写死像素
python
left=sheet.range("E3").left # E3 左边缘距工作表左边的距离(磅)
top=sheet.range("E3").top # E3 上边缘距工作表顶部的距离(磅)
用单元格坐标来定位,好处是插入列、调整行高之后,图片锚点会自动跟着走。
如果你写 left=300, top=50 这种魔法数字,哪天表格前面加了一列,图就飘到不知道哪儿去了。
需要缩放的话,再加 width / height(单位也是磅)或者 scale=0.8。
4.5 一定要 plt.close(fig),不然内存炸
matplotlib 的 figure 不会自动回收。你要是在循环里给几十个 sheet 各画一张图,又忘了 close(),第 21 张就会弹警告:
RuntimeWarning: More than 20 figures have been opened.
内存也会一路涨。画完即关,这是个好习惯。
4.6 不需要插回 Excel?直接存图片更省事
如果你的需求只是"出一张图",不需要嵌回 Excel,那直接 savefig 就完事儿了,也完全不依赖 Excel:
python
fig.savefig(DATA_DIR / "销量趋势.png", dpi=150, bbox_inches="tight")
bbox_inches="tight" 能帮你把多余的白边裁掉,出图更清爽。
5. 常见报错对照表
踩坑多了,整理了一份速查表,遇到问题先对照看看:
| 报错 / 现象 | 什么情况 | 怎么修 |
|---|---|---|
图上中文全是 □□□ 方块 |
没配置中文字体 | 加 font.sans-serif 那行 |
| 负号也是方块 | 缺 axes.unicode_minus=False |
补上,这两行要成双成对 |
| 每次运行图越来越多、越叠越厚 | update=False 或者没写 |
改成 update=True |
报 More than 20 figures 警告 |
没 plt.close() |
画完就关,别攒着 |
| 插进 Excel 的图是空白的 | 你先 close() 了再 add |
顺序反了,先 add 再 close |
| 第一列数据"消失"了 | options(pd.DataFrame) 默认吃首列作索引 |
加 index=False |
任务管理器里一堆 EXCEL.EXE 关不掉 |
没用 with / 没 quit() |
用 with xw.App() as app 包起来 |
| 日期横轴挤成一团看不清 | 标签太长了 | 加 fig.autofmt_xdate() 自动倾斜 |
6. 小结
这套"读 Excel → 画图 → 插回 Excel"的流程,核心就三点:
- 用 xlwings 读写 Excel,所见即所得,不破坏原表格式;
- 用 matplotlib 画图,样式可控、质量高;
- 用
update=True+ 单元格锚点定位,脚本可重复运行,数据更新后一键重出图。
把这套脚本固化下来,以后业务方再要趋势图,你只需要改改数据文件路径,跑一下,完事儿。