拒绝“豆腐块”:Docker 容器化部署中的 Matplotlib 中文显示

在 Python 数据可视化的世界里,Matplotlib 无疑是王者。然而,当将那个在本地跑得完美的脚本打包进 Docker 容器并部署到服务器上时,往往会遭遇当头一棒:生成的图表中,原本优雅的中文标题变成了满屏的方框(俗称“豆腐块”)。

现在从一段生产环境的代码出发,探讨为什么 Docker 会“吃掉”字体,以及一种简单粗暴却极度有效的解决方案——本地字体映射

1. 案发现场:代码解析

让我们看看 utils/create_pie.py 中的关键片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import os
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
from config import setting

# 关键点 1:明确指定字体文件的绝对路径
font_path = os.path.join(setting.FONT_DIR, 'SourceHanSerifSC-Regular.otf')

# 关键点 2:创建一个 FontProperties 对象
font = FontProperties(fname=font_path, size=35)

def create_pie_chart_jpg(labels, sizes, output_file="pie_chart.jpg", title="饼图"):
plt.figure(figsize=(10, 8))
# 关键点 3:在所有涉及文字的地方强制指定 fontproperties
plt.pie(sizes, labels=labels, ..., textprops={'fontsize': 35, 'fontproperties': font})
plt.title(title, fontsize=35, fontweight='bold', fontproperties=font)
# ...

这段代码没有依赖系统默认字体,而是显式地加载了一个本地的 SourceHanSerifSC-Regular.otf(思源宋体)文件。为什么要这么做?

2. 幕后黑手:为什么 Docker 里没有中文字体?

为了追求轻量化,绝大多数 Docker 基础镜像(如 python:3.9-slim, alpine 等)都是经过极度精简的。

  • 缺失的字体库:Linux 发行版默认通常只包含基础的英文字体,几乎不会预装庞大的 CJK(中日韩)字体包。
  • Matplotlib 的寻找机制:当你在代码里写 plt.title("你好") 时,Matplotlib 会去系统字体目录扫描。如果找不到支持中文的字体,它就会退回到默认字体(通常不支持中文),最终显示为方框 □□

3. 常见(但不仅限于)的“坑”方案

在网上搜索解决方案,你可能会看到以下建议,但它们在容器化场景下都有缺陷:

  • 方案 A:安装系统字体包
    • 做法:在 Dockerfile 里写 RUN apt-get install -y fonts-noto-cjk
    • 缺点:镜像体积瞬间膨胀几百 MB;构建时间变长;不同 Linux 发行版包名不一致。
  • 方案 B:修改 Matplotlib 配置文件
    • 做法:修改 matplotlibrc 文件,指定默认字体。
    • 缺点:配置繁琐;Matplotlib 经常重建字体缓存(fontList.json),导致配置不生效。

4. 最佳实践:本地字体映射(The “Just Works” Approach)

正如上述代码演示的,将字体文件作为项目的一部分(或通过 Volume 挂载),是解决此问题的最佳方案之一。

核心原理

我们不再请求操作系统“给我一个中文字体”,而是直接告诉 Matplotlib:“用我手里这个文件来渲染文字”。

这种方案的巨大优势

  1. 环境一致性(Write Once, Run Anywhere)
    无论你的代码是跑在 macOS 开发机、Ubuntu 服务器,还是基于 Alpine Linux 的 Docker 容器里,生成的图片像素级完全一致。因为字体文件是同一个,渲染引擎是同一个。

  2. 零系统依赖
    你的 Docker 镜像不再需要安装任何额外的字体库(fontconfig, ttf-mscorefonts-installer 等)。这让你的镜像保持极度精简(Slim)。

  3. 完全可控的视觉效果
    设计师要求用“思源宋体”?没问题,直接把 .otf 文件放进去。你不需要担心服务器上装的是“文泉驿微米黑”还是“谷歌 Noto”,从而导致布局跑偏或风格不符。

  4. 部署解耦
    如果通过 Docker Volume 映射字体目录(如 -v /host/fonts:/app/fonts),你甚至可以在不重新构建镜像的情况下,随时更换图表使用的字体。

5. 总结

在 Docker 化部署 Python 数据应用时,不要信任环境

显式地管理你的静态资源(包括字体),并在代码中通过 FontProperties(fname=path) 硬指定,是一种“防御性编程”的体现。它虽然多写了两行代码,却能帮你规避掉 99% 的跨平台渲染灾难。