把一张手头的 Excel 表格丢进去,几分钟后拿到一张可以直接贴进周报的柱状图、折线图或交互式 HTML 图表——不需要记图表菜单在哪,也不需要手动拖数据区域。这篇教程带你搭一条可复用的流水线:让大模型把"业务需求 + 表结构"翻译成一份图表配置(JSON),再由本地脚本读取 Excel 渲染出图。
这样做比直接让模型输出绘图代码更稳:配置是纯数据,容易被校验、被修改、被批量复用;而生成的代码一旦列名写错,往往要来回调试好几轮。
这篇能做出什么
完成后你会拥有三个文件:
inspect.py:读取 Excel,打印列名、类型、缺失值,帮你确认"数据长什么样"render.py:读一份 JSON 配置 + 一个 Excel,输出 PNG 或交互式 HTMLprompt.md:一段固定提示词,把表结构和你的需求发给任意大模型,拿回合法配置
再配一个 run.sh 或一段 Python 批处理,就能对同一个 Excel 里的多个 sheet 一次性出图。
前置条件清单
- 一台能装 Python 的电脑(Windows / macOS / Linux 均可)
- Python 3 环境,版本以官方文档当前版本为准
- 一个能读取 Excel 的大模型对话入口或 API(可选,没有也能跑,配置手写即可)
- 待处理的 Excel 文件,
.xlsx格式
依赖清单:pandas、openpyxl、matplotlib、plotly。前三者负责读表和出静态图,plotly 负责交互式 HTML。
如果你要把数据发到云端模型,先做脱敏:删掉姓名、手机号、身份证号、客户编号这类列,或者只发列名和前 5 行样例,不发全量数据。
第一步:建目录、装依赖
```bash
mkdir -p ~/excel-chart/data ~/excel-chart/out
cd ~/excel-chart
python3 -m venv .venv
source .venv/bin/activate # Windows 用 .venv\Scripts\activate
pip install pandas openpyxl matplotlib plotly
```
把待处理的 Excel 放进 data/ 目录,比如 data/sales.xlsx。输出的图统一落在 out/。
第二步:先做数据体检
很多人出图失败,不是绘图代码的问题,而是 Excel 里的数字被当成了文本。先跑体检脚本看清事实。
```python
inspect.py
import sys
import pandas as pd
path = sys.argv[1] if len(sys.argv) > 1 else "data/sales.xlsx"
xl = pd.ExcelFile(path)
print("sheet 列表:", xl.sheet_names)
for name in xl.sheet_names:
df = xl.parse(name)
print(f"\n=== sheet: {name} | 行数 {len(df)} 列数 {len(df.columns)} ===")
print("列名:", list(df.columns))
print("类型:")
print(df.dtypes.to_string())
print("缺失值:")
print(df.isna().sum().to_string())
print("前 5 行:")
print(df.head().to_string())
```
运行:
```bash
python inspect.py data/sales.xlsx
```
重点看三件事:
1. 本该是数字的列(销售额、数量)类型是不是 int64 / float64。如果是 object,说明混了文本。
2. 日期列是不是 datetime64。如果是 object,排序会按字符串走,"10月"会排在"2月"前面。
3. 缺失值有多少。数值列有缺失,聚合结果会偏。
第三步:把脏数据修干净
针对体检发现的问题,在读取阶段加一层清洗。下面是一个通用清洗函数,按需取用。
```python
clean.py
import pandas as pd
def to_number(s: pd.Series) -> pd.Series:
"""处理 '1,234'、'¥1200'、'12.5%' 这类文本数字"""
if s.dtype.kind in "if":
return s
t = s.astype(str).str.strip()
if t.str.endswith("%").any():
return pd.to_numeric(t.str.rstrip("%"), errors="coerce") / 100
t = t.str.replace(r"[,\s¥$¥元]", "", regex=True)
return pd.to_numeric(t, errors="coerce")
def load(path: str, sheet: str) -> pd.DataFrame:
df = pd.read_excel(path, sheet_name=sheet)
df.columns = [str(c).strip() for c in df.columns]
合并单元格读出来是 NaN,向下填充
for col in df.columns:
if df[col].dtype == object and df[col].isna().any():
df[col] = df[col].ffill()
日期列:列名里带"日期/时间/月/日"的尝试转换
for col in df.columns:
if any(k in col for k in ("日期", "时间", "date", "Date", "月份")):
df[col] = pd.to_datetime(df[col], errors="coerce")
文本列里的数字
for col in df.columns:
if df[col].dtype == object and col not in ("部门", "产品", "地区"):
df[col] = to_number(df[col]) if df[col].astype(str).str.contains(r"\d").any() else df[col]
return df
```
最后那个循环是个启发式,实际用的时候建议把要转数字的列名显式写出来,避免误伤。比如:
```python
for col in ["销售额", "数量", "单价"]:
df[col] = to_number(df[col])
```
第四步:定义图表配置格式
不要让模型直接写 matplotlib 代码,让它填这张"表单":
```json
{
"chart": "bar",
"x": "月份",
"y": ["销售额"],
"agg": "sum",
"sort": "desc",
"top": 10,
"title": "各月销售额排名"
}
```
字段含义:
chart:bar柱状、line折线、pie饼图、scatter散点x:横轴列名y:数值列名数组,可以有多个agg:聚合方式,sum/mean/max/min/null(不聚合)sort:desc/asc/nulltop:只取前 N 项,null表示全部title:图标题
保存为 spec/bar.json。
第五步:写渲染器
这个脚本读取配置和 Excel,做列名校验、聚合、排序,然后出图。
```python
render.py
import json
import sys
import pandas as pd
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from clean import load
中文字体:按系统已有字体排优先级
plt.rcParams["font.sans-serif"] = [
"Noto Sans CJK SC", "Source Han Sans SC", "PingFang SC",
"Microsoft YaHei", "SimHei", "Arial Unicode MS",
]
plt.rcParams["axes.unicode_minus"] = False
def validate(spec: dict, df: pd.DataFrame) -> None:
need = [spec["x"]] + list(spec["y"])
missing = [c for c in need if c not in df.columns]
if missing:
raise ValueError(
f"表里找不到这些列: {missing}\n实际列名: {list(df.columns)}"
)
def prepare(spec: dict, df: pd.DataFrame) -> pd.DataFrame:
x = spec["x"]
ys = list(spec["y"])
data = df[[x] + ys].copy().dropna(subset=[x])
for c in ys:
data[c] = pd.to_numeric(data[c], errors="coerce")
data[c] = data[c].fillna(0)
if spec.get("agg"):
data = data.groupby(x, as_index=False)[ys].agg(spec["agg"])
if spec.get("sort") == "desc":
data = data.sort_values(ys[0], ascending=False)
elif spec.get("sort") == "asc":
data = data.sort_values(ys[0], ascending=True)
if spec.get("top"):
data = data.head(int(spec["top"]))
return data
def draw(spec: dict, data: pd.DataFrame, out: str) -> None:
kind = spec.get("chart", "bar")
x, ys = spec["x"], list(spec["y"])
fig, ax = plt.subplots(figsize=(10, 5))
if kind == "bar":
data.plot.bar(x=x, y=ys, ax=ax)
elif kind == "line":
data.plot.line(x=x, y=ys, ax=ax, marker="o")
elif kind == "scatter":
data.plot.scatter(x=x, y=ys[0], ax=ax)
elif kind == "pie":
ax.pie(data[ys[0]], labels=data[x].astype(str), autopct="%1.1f%%",
startangle=90)
else:
raise ValueError(f"不支持的图表类型: {kind}")
ax.set_title(spec.get("title", ""))
if kind != "pie":
ax.set_xlabel(x)
ax.tick_params(axis="x", rotation=45)
ax.grid(axis="y", alpha=0.3)
fig.tight_layout()
fig.savefig(out, dpi=150)
plt.close(fig)
print("已输出:", out)
if __name__ == "__main__":
spec_path, excel_path, sheet, out = sys.argv[1:5]
with open(spec_path, encoding="utf-8") as f:
spec = json.load(f)
df = load(excel_path, sheet)
validate(spec, df)
draw(spec, prepare(spec, df), out)
```
运行:
```bash
python render.py spec/bar.json data/sales.xlsx Sheet1 out/bar.png
```
第六步:让大模型产出配置
这一步是"AI"真正发挥作用的地方。把下面这段提示词连同表结构一起发出去。
```text
你是数据可视化助手。根据表结构和需求,输出一个 JSON 对象。
只输出 JSON,不要解释,不要 markdown 代码块。
字段说明:
chart: bar | line | pie | scatter
x: 横轴列名,必须来自给定列名
y: 数值列名数组,必须来自给定列名
agg: sum | mean | max | min | null
sort: desc | asc | null
top: 整数或 null
title: 中文标题
列名:[把 inspect.py 打印出的列名粘到这里]
每列含义:[一句话说明,可省略]
数据样例:
[粘前 5 行]
需求:看各月份的销售额,从高到低排,只看前 8 个月。
```
把返回的 JSON 存成文件,跑 render.py。如果报"表里找不到这些列",说明模型编了列名,把错误信息连同实际列名一起发回去让它重写,通常一次就能改对。
对同一张表,你可以为不同需求各存一份配置:spec/monthly.json、spec/by_region.json、spec/trend.json,之后改需求只改 JSON,不动代码。
第七步:换成交互式 HTML
要给同事看、想鼠标悬停显示数值,把 draw 换成 plotly 版本即可。
```python
render_html.py
import json
import sys
import pandas as pd
import plotly.express as px
from clean import load
from render import prepare, validate
spec_path, excel_path, sheet, out = sys.argv[1:5]
spec = json.load(open(spec_path, encoding="utf-8"))
df = load(excel_path, sheet)
validate(spec, df)
data = prepare(spec, df)
kind = spec.get("chart", "bar")
fn = {"bar": px.bar, "line": px.line, "scatter": px.scatter, "pie": px.pie}[kind]
if kind == "pie":
fig = fn(data, names=spec["x"], values=spec["y"][0], title=spec.get("title", ""))
else:
fig = fn(data, x=spec["x"], y=spec["y"], title=spec.get("title", ""))
include_plotlyjs=True 会把 JS 内嵌,离线也能打开,文件会大一些
fig.write_html(out, include_plotlyjs=True)
print("已输出:", out)
```
第八步:批量处理多个 sheet
```python
batch.py
import json
import pandas as pd
from clean import load
from render import validate, prepare, draw
excel_path = "data/sales.xlsx"
specs = {
"Sheet1": "spec/monthly.json",
"Sheet2": "spec/by_region.json",
}
for sheet, spec_path in specs.items():
spec = json.load(open(spec_path, encoding="utf-8"))
df = load(excel_path, sheet)
validate(spec, df)
draw(spec, prepare(spec, df), f"out/{sheet}.png")
```
常见坑与排错
中文变成方框。 系统缺中文字体。Linux 上装 fonts-noto-cjk(包名以发行版仓库为准);macOS 用 PingFang SC;Windows 用 Microsoft YaHei。装完删掉 matplotlib 字体缓存再跑,缓存目录用 matplotlib.get_cachedir() 查。
数字列被读成文本。 典型是单元格里有千分位逗号、货币符号、全角空格。用第三步的 to_number 处理。注意 pd.to_numeric(..., errors="coerce") 会把转不了的值变成 NaN,转换后统计一下 NaN 数量,别有数据被悄悄丢掉。
日期排序不对。 月份列如果是"1月、2月…10月"这种中文,按字符串排会乱。要么转成 datetime,要么加一列数字月份做排序键,出图时用数字列当 x、显示标签另设。
饼图扇区太多。 超过 7~8 类基本看不清。做法是把小类合并成"其他",或者干脆换成横向柱状图,可读性通常更好。
双轴单位不一致。 一个"销售额(元)"一个"转化率(%)"放在同一根 Y 轴上,数值差几个数量级,图形会被压平。要么拆成两张图,要么在配置里支持次坐标轴,要么先各自归一化。
列名前后有空格或换行。 读表后统一 df.columns = [str(c).strip() for c in df.columns],否则模型给的"销售额"和表里的"销售额 "匹配不上。
合并单元格。 读进 pandas 后只有首行有值,其余是 NaN。用 ffill() 向下填充,但只对分类列做,数值列别乱填。
.xls 老格式打不开。 openpyxl 只处理 .xlsx。老格式需要额外依赖,且支持范围有限,建议先用 Excel 另存为 .xlsx。具体支持情况以各库官方文档为准。
模型给的配置跑不通。 把 validate 抛出的错误原文发给模型,再附上真实列名列表,让它重出配置。这个回路比让它"再想想"有效得多。
下一步建议
配置格式跑通之后,有几个方向可以继续加:
- 定时出图:用系统定时任务(cron 或 Windows 任务计划程序)每天跑一次
batch.py,把图推到共享目录或聊天群机器人,周报里的图自动就有了。 - 做成面板:用 Streamlit 包一层,上传 Excel、选图表类型、点按钮出图。适合给不写代码的同事用。
- 配置进版本库:把
spec/*.json提交到 Git,改配色、改排序都有记录,出了问题能回滚。 - 接文件读取能力:部分助手支持直接读取上传的表格文件,可以先让它出配置、再本地渲染,省掉手工粘贴表结构的步骤。
- 加一层异常提示:在
prepare里判断某列缺失比例、数值波动是否超出历史区间,出图时顺手在标题旁标注"本期数据缺失 12%",比一张漂亮但失真的图更有用。
整套流程的核心思路只有一句:把模型的输出限制在一份小小的、可以被程序校验的结构化配置里。模型负责理解你的意图,脚本负责把事做对。
