目标:掌握在 Hugo 博客中使用 matplotlib 预生成 SVG 矢量图的完整工作流,包括配置、中文渲染、配色方案、批量生成脚本和常见问题排查。
前置要求:基本的 Python 和 matplotlib 使用经验,了解 Hugo 静态站点的基本结构。
在撰写技术博客时,公式、表格和文字往往不足以表达所有信息——函数图像、架构示意图、对比图表等可视化内容能大幅提升可读性。本文总结了在 Hugo + KaTeX 环境下,使用 matplotlib 生成 SVG 图片并嵌入 Markdown 的完整实践经验。
1. 为什么选择 matplotlib + SVG
1.1 为什么不用 LaTeX 原生绘图
KaTeX 是 Hugo 中最常用的数学公式渲染方案,但它只支持公式渲染,不支持 TikZ、PGFPlots 等 LaTeX 绘图包。想在 Hugo 中直接用 LaTeX 画函数图像是不可行的。
1.2 为什么选择 matplotlib
matplotlib 是 Python 生态中最成熟的绑图库,优势在于:
- 函数绘图精确:支持 NumPy 数学表达式,可以精确绘制任意函数。
- 输出格式丰富:支持 SVG、PNG、PDF 等多种格式。
- 可编程:用脚本批量生成,修改方便,版本可控。
1.3 为什么选择 SVG 格式
| 特性 | SVG | PNG |
|---|---|---|
| 矢量/栅格 | 矢量(任意缩放不失真) | 栅格(放大后模糊) |
| 文件大小 | 通常更小(20~60KB) | 同等质量下更大 |
| 渲染清晰度 | 任意分辨率下都清晰 | 取决于 DPI 设置 |
| 文本可选中 | 是(SEO 友好) | 否 |
| 浏览器兼容 | 所有现代浏览器 | 所有浏览器 |
结论:对于函数图像、流程图、概念示意图等,SVG 是首选格式。只有截图、照片等像素图像才需要 PNG。
2. 基础配置
2.1 最小可用脚本
import numpy as np
import matplotlib
matplotlib.use('Agg') # 必须在 import pyplot 之前调用,使用非交互式后端
import matplotlib.pyplot as plt
# 创建画布
fig, ax = plt.subplots()
# 绑图
x = np.linspace(-3, 3, 200)
y = x ** 2
ax.plot(x, y, color='#2563eb', linewidth=2)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_title('y = x²')
# 保存为 SVG
fig.savefig('output.svg', format='svg', bbox_inches='tight')
plt.close(fig)
关键点:matplotlib.use('Agg') 必须在 import matplotlib.pyplot 之前调用,否则在无 GUI 环境(如 CI/CD、远程服务器)中会报错。
2.2 全局样式设置
在脚本开头统一设置样式,避免每张图单独配置:
plt.rcParams.update({
'font.size': 12,
'axes.titlesize': 14,
'axes.labelsize': 12,
'figure.facecolor': 'white', # 透明背景在暗色主题下不可见
'axes.facecolor': '#fafafa', # 浅灰色坐标区背景
'axes.grid': True,
'grid.alpha': 0.3,
'lines.linewidth': 2,
'figure.figsize': (8, 5), # 默认画布大小
'savefig.dpi': 150, # PNG 的 DPI(SVG 忽略此参数)
'savefig.bbox': 'tight', # 自动裁剪多余空白
'savefig.pad_inches': 0.2, # 保留少量边距
})
3. 中文支持——最常见的坑
3.1 问题现象
matplotlib 默认字体不包含中文字形,图表中的中文会显示为方块(□)或报错:
Font 'default' does not have a glyph for '\u5347' [U+5347], substituting with a dummy symbol.
3.2 解决方案:指定中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'WenQuanYi Micro Hei',
'Noto Sans CJK SC', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示为方块的问题
字体优先级:系统会按列表顺序查找第一个可用字体。
| 操作系统 | 推荐字体 |
|---|---|
| Windows | SimHei、Microsoft YaHei |
| macOS | PingFang SC、Heiti SC |
| Linux | WenQuanYi Micro Hei、Noto Sans CJK SC |
3.3 更稳妥的方案:避免中文
如果图表用于英文博客或不需要中文,最稳妥的做法是直接用英文标注,完全避免字体问题:
# 推荐:用英文标注
ax.set_title('FFN Structure: Expand -> Activate -> Compress')
ax.annotate('$d_{model} \\to d_{ff}$ (expand)', ...)
如果必须用中文,可以将中文放在 Markdown 正文中,图表本身只用英文——这是我在系列博客中采用的策略。
4. 配色方案
4.1 推荐配色表
定义一套统一的配色字典,在所有图表中复用:
COLORS = {
'blue': '#2563eb', # 主色:线条、主要数据
'red': '#dc2626', # 警告/对比:异常值、错误梯度
'green': '#16a34a', # 正面/正确:正确答案、推荐方案
'orange': '#ea580c', # 次要强调
'purple': '#9333ea', # 第三类数据
'cyan': '#0891b2', # 第四类数据
'pink': '#db2777', # 第五类数据
}
4.2 配色原则
- 不超过 5~6 种颜色:超过后区分度下降。
- 红绿对比:用于"错误 vs 正确"的语义对比。
- 蓝橙对比:用于两个方案的对比。
- 透明度(alpha):用于重叠区域或背景填充,避免遮挡。
5. 常用图表类型与模板
5.1 函数曲线对比图
fig, ax = plt.subplots()
x = np.linspace(-3, 3, 200)
ax.plot(x, x**2, color=COLORS['blue'], label='MSE')
ax.plot(x, np.abs(x), color=COLORS['red'], label='MAE')
ax.set_xlabel('$y - \\hat{y}$')
ax.set_ylabel('Loss')
ax.set_title('MSE vs MAE')
ax.legend()
ax.axhline(0, color='gray', linestyle='--', alpha=0.5)
fig.savefig('mse_vs_mae.svg', format='svg')
plt.close(fig)
5.2 热力图(注意力矩阵)
fig, ax = plt.subplots(figsize=(6, 5))
data = np.random.rand(8, 8)
im = ax.imshow(data, cmap='Blues', aspect='equal')
ax.set_xticks(range(8))
ax.set_yticklabels([f't{i+1}' for i in range(8)])
for i in range(8):
for j in range(8):
ax.text(j, i, f'{data[i,j]:.2f}', ha='center', va='center', fontsize=8)
fig.colorbar(im)
fig.savefig('heatmap.svg', format='svg')
plt.close(fig)
5.3 柱状图
fig, ax = plt.subplots()
formats = ['FP32', 'FP16', 'INT8', 'INT4']
sizes = [28, 14, 7, 3.5]
bars = ax.bar(formats, sizes, color=[COLORS['blue'], COLORS['green'],
COLORS['orange'], COLORS['red']], alpha=0.7)
for bar, size in zip(bars, sizes):
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.3,
f'{size} GB', ha='center', fontsize=10)
ax.set_ylabel('Model Size (GB)')
fig.savefig('bar_chart.svg', format='svg')
plt.close(fig)
5.4 流程图(矩形+箭头)
fig, ax = plt.subplots(figsize=(10, 4))
boxes = [
(1, 'Input', '#2563eb'),
(3, 'Process', '#16a34a'),
(5, 'Output', '#9333ea'),
]
for x, label, color in boxes:
ax.add_patch(plt.Rectangle((x-0.7, 0.2), 1.4, 0.6,
facecolor=color, alpha=0.3, edgecolor=color, linewidth=2))
ax.text(x, 0.5, label, ha='center', va='center', fontsize=11, fontweight='bold')
for i in range(len(boxes)-1):
ax.annotate('', xy=(boxes[i+1][0]-0.7, 0.5), xytext=(boxes[i][0]+0.7, 0.5),
arrowprops=dict(arrowstyle='->', lw=2, color='gray'))
ax.set_xlim(0, 6.5)
ax.axis('off')
ax.set_title('Pipeline', fontsize=14)
fig.savefig('pipeline.svg', format='svg')
plt.close(fig)
6. 在 Hugo 中集成
6.1 目录结构
static/
img/
article-name/ # 按文章名归档
chart1.svg
chart2.svg
content/
posts/
tech/
article.md # 引用图片
6.2 Markdown 引用

- 路径以
/img/开头(对应static/img/) - 必须填写 alt 文本(SEO + 无障碍)
- Hugo 构建时自动将
static/img/复制到public/img/
6.3 批量生成脚本模板
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import os
os.makedirs('static/img/article-name', exist_ok=True)
# 统一样式
plt.rcParams.update({
'font.size': 12, 'axes.titlesize': 14,
'figure.facecolor': 'white', 'axes.facecolor': '#fafafa',
'axes.grid': True, 'grid.alpha': 0.3,
'lines.linewidth': 2, 'figure.figsize': (8, 5),
'savefig.dpi': 150, 'savefig.bbox': 'tight',
})
COLORS = {'blue':'#2563eb','red':'#dc2626','green':'#16a34a',
'orange':'#ea580c','purple':'#9333ea'}
def save(fig, name):
fig.savefig(f'static/img/article-name/{name}.svg', format='svg')
plt.close(fig)
print(f' Saved {name}.svg')
# 生成每张图
fig, ax = plt.subplots()
# ... 绑图代码 ...
save(fig, 'chart1')
print('All done!')
7. 常见踩坑与解决方案
7.1 中文显示为方块
原因:默认字体无中文字形。
解决:设置 plt.rcParams['font.sans-serif'] 或改用英文标注。
7.2 SVG 中文字被 Hugo HTML 预处理器干扰
现象:SVG 中的 <text> 标签被 Hugo 的 Goldmark 解析器误处理。
解决:避免在 SVG 中使用 <、> 等特殊字符作为标签文本。matplotlib 生成的 SVG 通常不会有此问题,但手动编辑 SVG 时需注意。
7.3 负号显示为方块
原因:某些中文字体不包含 Unicode 负号(U+2212)。
解决:plt.rcParams['axes.unicode_minus'] = False(改用 ASCII 减号)。
7.4 SVG 文件过大
原因:数据点过多(如 10000 个点的散点图)。
解决:降采样数据,或改用 rasterized=True 参数将密集元素栅格化嵌入 SVG。
7.5 图片在深色主题下不可见
原因:默认白色线条在深色背景上不可见。
解决:设置 figure.facecolor='white' 确保图片自带白色背景,不依赖页面主题。
7.6 plt.close() 忘记调用
后果:批量生成时内存泄漏,脚本越来越慢。
解决:养成 savefig() 后立即 plt.close(fig) 的习惯。
8. 与 KaTeX 公式的配合技巧
8.1 图表中使用 LaTeX 数学符号
matplotlib 原生支持 LaTeX 数学符号,用 $...$ 包裹即可:
ax.set_xlabel('$y - \\hat{y}$')
ax.set_title('Huber Loss: $\delta = 1.0$')
ax.annotate('$d_{model} \\to d_{ff}$', (1, 0.5))
注意:在 Python 字符串中需要双重转义 \\(\hat 写成 \\hat)。
8.2 图表标注与文章公式呼应
图表中的变量名应与文章正文中的 KaTeX 公式保持一致。例如文章中写 $d_{\text{model}}$,图表中也用 $d_{model}$,让读者能对应起来。
9. 总结:最佳实践清单
| 实践 | 建议 |
|---|---|
| 输出格式 | SVG 首选(矢量、小文件、清晰) |
| 中文处理 | 优先用英文标注,必要时指定中文字体 |
| 配色 | 定义统一配色字典,不超过 5~6 种颜色 |
| 画布背景 | 设置白色背景,不依赖页面主题 |
| 样式 | 使用 rcParams 全局配置,保持一致性 |
| 资源管理 | savefig() 后立即 plt.close(fig) |
| 存放路径 | static/img/<文章名>/,按文章归档 |
| 批量生成 | 编写独立脚本,版本控制,可重复执行 |
| MathJax/KaTeX 协作 | 图表中用 $...$ 数学符号,与正文公式保持一致 |
相关文章:本博客系列中大量使用了 matplotlib 生成 SVG 图片的技术,详见 Loss Function 合集、大语言模型训练机制全解·第一篇 等文章。