在 Matplotlib 里处理中文坐标轴、标题字体和字号,这个问题几乎每周都有新人来问。多半是好不容易画好的图突然变成一排方块,或者坐标轴、标题、图例的字号大小怎么调都达不到效果,最后导出的图一眼看上去像外星文字。这篇文档把我自己折腾中文字体的完整思路、代码模板和踩过的坑都整理出来,适合刚接触 Matplotlib 画图的新手,也适合已经遇到中文乱码但一直没系统排查的读者。直接说结论,再讲为什么。
1. 先搞清楚问题:Matplotlib 为什么不显示中文
1.1 你看到的“方块字”到底是什么
这里要先搞清楚一个基本概念:Matplotlib 本质是“用字体来渲染文字”的绘图库。它本身不含中文字体,默认使用的字体族是 DejaVu Sans,这组字体是为拉丁字母、希腊字母和部分符号设计的,压根没有汉字字形。当你的代码写了ax.set_title("中文标题"),Matplotlib 在 DejaVu Sans 里找不到“中”这个字形,就只能在画布上打一个空方块占位,专业说法叫“缺字形”。有时候你看到的不是方块,而是一个个小方框再加一个问号,或者干脆某些字符直接消失,归根结底都是字形缺失造成的。
一个常见误区是认为乱码是因为“编码问题”或“字符集问题”。其实 Python 字符串里的中文编码通常没有问题,问题只出在渲染阶段没有可用的字体。验证方法很简单:把plt.rcParams['font.family']打印出来看一眼,默认值里必然没有任何中文字体;再把你系统里安装的中文字体名称打出来,你会发现 Matplotlib 压根没把对方列进可用的字体表里。只要解决了“字体缺失”这一环,中文乱码就解决了 90%。
1.2 字体配置机制:Matplotlib 是怎么查找字体的
要彻底理解解决方案,得先明白 Matplotlib 的字体查找机制。Matplotlib 在启动时会扫描系统安装的字体文件,生成一个字体管理器对象,里面维护了一张字体列表。渲染文字时,它会根据你指定的font.family去匹配字体族。如果family设置为sans-serif,它就遍历font.sans-serif这个列表,按顺序找到第一个存在的字体。中文字体比如黑体、微软雅黑、思源黑体,在字体分类里都属于无衬线字体,对应的就是这个sans-serif列表。所以当你把中文字体名字塞进font.sans-serif列表后,Matplotlib 在遇到汉字时就会尝试用这个字体渲染。
这里有个容易搞混的点:font.family可以是一个具体的字体名(比如"SimHei"),也可以是一个大类(serif、sans-serif、monospace、cursive、fantasy)。如果你直接用plt.rcParams['font.family'] = 'SimHei',表面看起来没问题,但是当某台机器没有安装 SimHei 时,Matplotlib 不会自动退回到别的中文字体。反过来,如果你保持font.family为sans-serif,而把多个中文字体按优先顺序写进font.sans-serif列表,一台机器上只要有任何一种列出的字体存在,就能正常显示。所以更稳健的做法是改font.sans-serif列表,而不是直接改font.family。
另外,Matplotlib 从比较新的版本开始支持“按字符回退”机制:遇到目标字体里没有的字符,会继续从你给出的后续字体列表中查找可用字形。这也是为什么人们倾向于在一个列表里写多个中文字体。你可以在代码里做一个快速实验:故意把font.sans-serif列表设成一个不存在的字体名开头,看看画出来的中文是不是变成方块,再对比把可用中文字体放在第一位的情况,一下就理解了。
2. 三种中文字体配置方案:从全局到局部
2.1 全局配置:用 rcParams 一次性解决
最适合绝大多数场景的方式,是在脚本或 Notebook 开头直接修改rcParams。rcParams是 Matplotlib 的全局运行时配置字典,相当于它对所有绘图对象的默认设置。只要在绘图之前完成修改,后面创建的每个 Figure、每个坐标轴都会继承这套默认配置。
import matplotlib.pyplot as plt # 全局设置中文字体,通常放在 import 之后第一件事 plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'Noto Sans CJK SC'] # 解决负号坐标轴显示异常的设置,务必加上 plt.rcParams['axes.unicode_minus'] = False # 全局统一设置字号:标题、坐标轴标签、刻度分别可设 plt.rcParams['axes.titlesize'] = 14 # 坐标轴标题(set_title)的默认字号 plt.rcParams['axes.labelsize'] = 12 # 坐标轴标签(set_xlabel/ylabel)的默认字号 plt.rcParams['xtick.labelsize'] = 10 # x 轴刻度标签默认字号 plt.rcParams['ytick.labelsize'] = 10 # y 轴刻度标签默认字号这段配置放在 Python 脚本开头即可,之后的每一张图都默认使用中文字体。模拟一下效果:某位开发者画一张学生成绩分布图,x 轴写班级名称,y 轴写平均分,标题写"期末成绩分布"——如果没有这段配置,图中这些位置全部是方块;加上配置后,所有中文标签立刻恢复正常。
有个细节值得说明:axes.unicode_minus = False到底在解决什么。默认情况下,Matplotlib 渲染坐标轴负号时用的是 ASCII 连字符-,而不是真正的数学减号−。中文字体往往不自带 ASCII 连字符,或者对其渲染位置处理得很差,导致负号变成方块。设置这个参数后,Matplotlib 会改用 Unicode 减号 U+2212,这个字符在中文字体里普遍存在,负号坐标轴就能正常显示。如果你画的是纯正数据、值域大概率有负数,这个参数请务必写上。
2.2 动态注册:用 font_manager 精确指向字体文件
如果你没有系统权限安装中文字体,或者你希望项目只依赖一个特定字体文件,可以使用matplotlib.font_manager动态添加字体。
import matplotlib.font_manager as fm import matplotlib.pyplot as plt # 直接指向 ttf 字体文件路径,注册到字体管理器 font_path = "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc" fm.fontManager.addfont(font_path) # 从字体文件中读取字体实际名称 font_name = fm.FontProperties(fname=font_path).get_name() print(f"注册成功,字体名称为:{font_name}") # 设置全局字体 plt.rcParams['font.family'] = font_name plt.rcParams['axes.unicode_minus'] = False这种方法最硬核的地方在于:它不依赖字体安装包、不依赖用户环境里恰好有现成字体,只要有字体文件,就能注册成功。放在服务器场景尤其好用。比如你在某台 Linux 服务器上跑数据脚本,既不方便安装图形界面相关组件,也不想动系统字体目录,就可以在项目目录下放一个webfonts/文件夹,里面放一个思源黑体的 ttf 或者 ttc 文件,启动脚本时自动注册。
注册完成后,FontProperties 对象的get_name()也很关键,因为字体文件名往往不等于字体内部名称。比如NotoSansCJK-Regular.ttc文件内部实际注册的字体名可能是 "Noto Sans CJK JP" “Noto Sans CJK SC” "Noto Sans CJK TC" 等多个变体。如果你忽略这一步,直接尝试plt.rcParams['font.family'] = 'Noto Sans CJK SC'而系统里没装同名字体,注册就白做了。先让 Matplotlib 从文件里读取真实名称,再把名称作为配置项,是最稳妥的做法。
2.3 局部覆盖:只对当前这张图生效
全局配置有一个缺点:如果某个项目里十几张图,大多数用默认的中文字体即可,但某一张图需要特殊的字体风格,或者你是在改写别人的代码,不方便动全局设置,这时可以用 FontProperties 局部指定。做法大致分两种:
import matplotlib.font_manager as fm import matplotlib.pyplot as plt # 方式一:针对某一个文本元素设置 fontproperties 参数 font_prop = fm.FontProperties(family='SimHei', size=15) fig, ax = plt.subplots() ax.plot([1, 2, 3], [-4, 5, -6]) ax.set_title("局部设置标题字体", fontproperties=font_prop) ax.set_xlabel("X 轴中文标签", fontproperties=font_prop)# 方式二:直接使用 fontdict 字典,指定字体属性和字号 font_dict = { 'family': 'SimHei', 'size': 15, 'weight': 'bold', 'color': 'black' } ax.set_xlabel("X 轴中文标签", fontdict=font_dict)你会发现fontproperties和fontdict是互相独立的两套传参方式:fontproperties接受一个 FontProperties 对象,fontdict接受一个普通字典。在实际项目里,我习惯把 FontProperties 对象预先构建好,一次定义、多处复用。比如某张图上标题性格强烈,想要特别粗黑的标题字体,而坐标轴标签用常规字体,那就创建两个 FontProperties 实例分别传参,比反复修改全局配置要清晰得多。
3. 实操演示:坐标轴、标题字体及字号的完整模板
3.1 最简可行的代码模板
把这篇文章最核心的代码模板放在这里。这段代码涵盖了大多数典型需求:中文标题、中文坐标轴、字体字号调整、解决负号问题。
import matplotlib.pyplot as plt import numpy as np # ========== 全局字体配置 ========== plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'Noto Sans CJK SC', 'DejaVu Sans'] plt.rcParams['axes.unicode_minus'] = False # ================================== # 生成模拟数据 x = np.linspace(0, 10, 100) y = x ** 2 - 8 * x + 12 # 一段抛物线,确保有正有负,用负值验证unicode_minus # 创建画布和坐标轴对象 fig, ax = plt.subplots() # 设置标题及字号:ax.set_title 和 plt.title 是同一场景的两套接口 ax.set_title("函数曲线 y = x² − 8x + 12", fontsize=16, fontweight='bold') ax.set_xlabel("自变量 x (单位:秒)", fontsize=13) ax.set_ylabel("因变量 y (单位:米)", fontsize=13) # 设置刻度的字号 ax.tick_params(axis='x', labelsize=11) ax.tick_params(axis='y', labelsize=11) ax.plot(x, y, lw=2, label="y=x²−8x+12") ax.legend(fontsize=11) plt.show()这组代码里的每个fontsize参数都是可以独立控制的。图的左上角标题用了 16 号加粗,x 轴标签和 y 轴标签用了 13 号,刻度用的是 11 号,图例也是 11 号。实际项目中你完全可以按图的大小调配:整张图尺寸比较大的时候,标题到 18 号没有问题;图比较小的时候,标题 12 号就够了,刻度 9 号也不会显得拥挤。这里不存在绝对正确的字号,只有相对平衡的搭配。
3.2 逐个拆解不同文字元素的设置方式
上面的模板有一个值得注意的细节:ax.set_title、ax.set_xlabel、ax.set_ylabel都是坐标轴对象的方法,而plt.title、plt.xlabel、plt.ylabel是 pyplot 接口的等价替代。它们最终操作的是当前活跃的坐标轴。我建议在你的代码里统一使用ax.xxx这套面向对象接口,尤其是使用subplots()生成多子图时更清晰。原因很简单:plt.xxx操作的是“当前坐标轴”,一旦代码里有多个子图或多次切换,你容易搞不清楚到底设置的是哪一张图;ax对象则直接、明确、不易出错。
对于刻度标签,可以用tick_params统一调整,也可以单独设置某个轴的刻度格式:
# 单独设置 x 轴刻度字号与旋转角度 ax.tick_params(axis='x', labelsize=11, rotation=15) # 单独设置 y 轴刻度字号 ax.tick_params(axis='y', labelsize=11)刻度还有个常见需求:当 x 轴是日期或长分类名时,刻度文字容易重叠,旋转 15 到 30 度通常就能解决。加了rotation之后,同时调大一点刻度字号也不会显得拥挤。还有坐标轴标签的间距,如果觉得“自变量 x”离坐标轴太近,可以加labelpad参数调间距,比如ax.set_xlabel("自变量 x", fontsize=13, labelpad=15)。
如果你需要把所有文字元素的字体、字号一次性统一,也可以把参数写进plt.rcParams作为全局默认值:
plt.rcParams['font.size'] = 12 # 全局默认字号,所有文字都会以此为基准 plt.rcParams['axes.titlesize'] = 16 plt.rcParams['axes.labelsize'] = 14 plt.rcParams['xtick.labelsize'] = 11 plt.rcParams['ytick.labelsize'] = 11 plt.rcParams['legend.fontsize'] = 11注意font.size是所有文字的基础字号。如果你先设置font.size=12,再单独设置axes.titlesize=16,是后者的值生效。建议只在项目方向比较混乱时使用这种全局方式,做演示或试验时逐参数设置更直观。
3.3 保存图片时的字体细节
图片在屏幕上显示正常,并不代表导出文件一定正常。这里要分两种输出格式说。
一是位图格式 PNG/JPG。保存时记得设置合适的dpi,分辨率太低会导致文字糊成一团。建议是:
fig.savefig("output_plot.png", dpi=300, bbox_inches='tight', facecolor='white')dpi=300对大多数出版和汇报场景都够用,bbox_inches='tight'会让图片边缘自动裁掉多余白边,如果是插在文档里显得更紧凑。
二是矢量图格式 PDF/SVG。矢量图保存的不再是逐像素点,而是路径和文字信息。别人拿到你的 PDF 文件后,如果本机没有对应的中文字体,可能仍然出现中文显示问题。有些高度定制化的场景会要求把字体嵌入 PDF 文件里。Matplotlib 默认会在保存 PDF 时尝试嵌入所需字体,所以你通常不用额外操作。但如果你发现生成的 PDF 在自己机器上正常、换一台机器就乱码,要先检查对方的 PDF 阅读器是否做了字体替换,再做进一步处理。一个偏保守的建议是:如果对格式兼容要求极高,可以固定使用一个常见字体,比如黑体或微软雅黑,并尽量使用系统自带版本,避免风格差异。
4. 高频踩坑记录与排查方法
4.1 明明设置了中文字体,为什么还是方块
这是遇到最多的场景:代码里已经写了plt.rcParams['font.sans-serif'] = ['SimHei'],但运行结果仍然显示方块。排查时建议按以下顺序走:
第一步,确认系统里真的有这个字体。在代码里打印所有可用字体列表:
from matplotlib import font_manager font_names = {f.name for f in font_manager.fontManager.ttflist} print([name for name in font_names if 'Hei' in name or 'CJK' in name or 'Yai' in name or 'PingFang' in name])如果打印结果为空,说明你没有安装 SimHei,配置自然无效。Windows 上一般存在 SimHei,Linux 和 macOS 则不一定。第二步,确认你把字体名写对了。比如微软雅黑在 Matplotlib 中字体名是 "Microsoft YaHei",思源黑体在系统里可能是 "Noto Sans CJK SC",误写成 "Source Han Sans SC" 也可能匹配不上。第三步,确认你的font.sans-serif里确实有中文字体,并且排在其他西文字体前面。如果font.sans-serif = ['DejaVu Sans', 'SimHei'],因为 DejaVu Sans 自身有拉丁字形,西文字符会正常用它渲染;但遇到中文字符时,旧版 Matplotlib 在第一个找不到字形时并不一定会自动尝试第二个字体,可能直接输出方块。把中文字体放第一位是更稳的选择。
4.2 负号显示成方框,别忘掉 unicode_minus
画函数图或任何包含负值数据时,坐标轴上会出现负号。默认情况下 Matplotlib 用 ASCII 连字符渲染负号,当你把字体换成中文字体后,有些中文字体对 ASCII 连字符的渲染并不理想,甚至缺失字形,于是负号变成了方框。解决方案就是那条配置:
plt.rcParams['axes.unicode_minus'] = False设置了之后,负号会使用 Unicode 减号 U+2212 渲染,这个字符绝大多数字体中都有。建议和字体设置写在一起,形成条件反射:只要设置了中文字体,就顺手把 unicode_minus 一起设置。我有一次调试了半小时才发现只是漏了这个参数,坐标轴负号全变成框,而中文标签看着却完全正常,很容易误入排查方向。
4.3 不同操作系统下的字体选择与路径差异
不同操作系统默认自带的中文字体差异很大。Windows 上常见的是 SimHei(黑体)、Microsoft YaHei(微软雅黑)、KaiTi(楷体)、SimSun(宋体)。macOS 上常见的是 PingFang SC(苹方)、STHeiti、Hiragino Sans GB。常见的 Linux 发行版一般不会预装中文字体,多数需要单独安装,常用的是 Noto Sans CJK SC(思源黑体简体中文版)、文泉驿正黑、文泉驿微米黑。
如果你的脚本会在多台机器上运行,最好的策略不是硬编码单一字体,而是把可用的中文字体列成一个备选列表:
import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = [ 'SimHei', 'Microsoft YaHei', 'PingFang SC', 'Noto Sans CJK SC', 'WenQuanYi Zen Hei', 'DejaVu Sans' ] plt.rcParams['axes.unicode_minus'] = FalseMatplotlib 在大多数系统上会按顺序找到第一个可用的字体,并将其用于渲染。列表中最后再放一个 DejaVu Sans,是为了保证英文和数字也能有良好的兜底表现,但这只对较新版本有效,旧版本对中文字符的回退行为差异较大。
如果你在改代码时发现自己使用的是一个受限的服务器环境,里面连最基本的fonttools都没有,还有一个小技巧:用fm.FontProperties(fname=...)从任意字体文件路径创建对象,不依赖系统字体表。这种做法在 Docker 镜像里尤其常见,把字体文件固定到镜像里,字体配置就有很强的确定性。
4.4 改完字体设置不生效?多半是字体缓存问题
Matplotlib 在第一次运行时,会遍历系统所有字体文件,生成缓存列表,通常存放在用户主目录下的.matplotlib文件夹里,比如 Linux 下是~/.matplotlib/fontlist-v330.json。如果你在 Matplotlib 已经运行过一次之后才安装新字体,那么即使字体文件存在于系统中,Matplotlib 也可能无法识别它的名字,因为缓存列表是在之前的扫描中生成的。
解决办法有两个。一是删除缓存文件后,重新运行代码,让 Matplotlib 重新扫描字体。二是直接用font_manager里的addfont把字体文件动态加入,不走缓存。
实际项目中,如果遇到改字体不生效且重启解释器也没用,我建议直接删除~/.matplotlib目录下的缓存文件,再重启 python 进程。注意不要删除整个.matplotlib目录,否则会连带删除你的绘图风格配置文件,比较麻烦。还有一点容易被忽略:修改rcParams的代码一定要在创建 Figure 之前执行。如果你在图已经画完之后才设置字体,已经渲染出来的文字不会自动更新,你可能会误以为“改了不生效”。
5. 进阶习惯:把字体配置封装成工具函数
5.1 统一入口:一个函数搞定字体、字号、负号
我在多个项目里养成了一个习惯:把中文字体配置封装成一个统一的函数,放在项目公共模块中。这样不仅整个项目里的画图脚本能复用同一套字体策略,你对字体的任何调整都只需要改动这一处。
import matplotlib.pyplot as plt def setup_chinese_font(titlesize=16, labelsize=14, ticksize=12): plt.rcParams['font.sans-serif'] = [ 'SimHei', 'Microsoft YaHei', 'PingFang SC', 'Noto Sans CJK SC', 'WenQuanYi Zen Hei', 'DejaVu Sans' ] plt.rcParams['axes.unicode_minus'] = False plt.rcParams['axes.titlesize'] = titlesize plt.rcParams['axes.labelsize'] = labelsize plt.rcParams['xtick.labelsize'] = ticksize plt.rcParams['ytick.labelsize'] = ticksize setup_chinese_font(titlesize=18, labelsize=15, ticksize=12)调用时只要传一次参数,后面所有图都按照统一标准渲染。如果某一张图想单独放大标题,用上一节提到的fontsize参数临时覆盖就行。这样兼顾了“全局统一”和“局部微调”。
这种封装还有一个附带好处:团队协作时,每个成员不必都知道必须设置哪些参数,只要调用这个函数就保证出一致风格的中文图。我第一次接手别人的项目代码时,发现每张图都是乱码,后来把所有脚本里零散的乱码处理抽成一个函数,整个项目的图表风格立刻统一了。这个动作看似琐碎,带来的维护收益却很值。
5.2 中文字体与英文数字的搭配
使用中文字体后,有一个容易被忽略的小问题:中文字体里的拉丁字母和数字的观感,通常和 DejaVu Sans 这类西文字体有明显差异。比如 SimHei 里的英文是黑体风格的,偏粗,和 DejaVu Sans 相比少了一些现代感。如果你的图中有大量英文单位、公式或数据标签,整体视觉风格可能不如只用西文字体舒服。
比较务实的做法:优先使用风格中性的中文字体。微软雅黑和思源黑体的拉丁字符设计比较现代,在混排场景下与英文配合良好,不会显得突兀。如果你的图中文很少、英文符号很多,也可以稍微调小中文字体的全局字号,因为中文在同等字号下看起来比英文大一些,视觉平衡会更好。
另外,在结合 Seaborn 使用时要多注意一点。Seaborn 的set_theme或set_style会覆盖一部分 Matplotlib 的 rcParams,包括字体设置。正确做法是先执行sns.set_theme(),再执行你的中文字体配置,或者更直接地,用sns.set(font='SimHei')单独指定字体。很多人在 Matplotlib 里设置中文字体没问题,一进入 Seaborn 又乱码,往往就是因为调用顺序颠倒了,主题设置把刚才的字体配置覆盖掉了。
5.3 动态检测字体并自动适配
如果想做得更智能一点,还可以写一个自动检测函数:运行的时候先扫描系统中存在哪些中文字体,再去从那几个已知候选中挑选一个可用的。这特别适合那种要在多种环境里跑的脚本,避免每次换一台机器就得改一次字体名。
from matplotlib import font_manager def pick_chinese_font(): candidates = [ 'SimHei', 'Microsoft YaHei', 'PingFang SC', 'Noto Sans CJK SC', 'WenQuanYi Zen Hei' ] installed = {f.name for f in font_manager.fontManager.ttflist} for name in candidates: if name in installed: return name return None这个函数返回一个可用的中文字体名称,拿到后可以直接写进 rcParams。如果返回None,说明环境里一个中文字体都没有,脚本再继续跑下去一定乱码。这时可以打印提示让用户安装字体,或者自动尝试从本地目录注册字体文件。这种主动检测的方式比硬编码一个字体名要靠谱得多,我强烈建议在跨平台项目中采用。
我把这个函数配合第 5.1 节的封装函数一起用,运行逻辑是:先检测可选中文字体,如果检测到就设入全局配置;如果没检测到,就打印一段可读性强的提示。这比让用户面对满屏方块自己去猜原因,体验好了不是一个层次。
6. 最后分享一点个人习惯
关于中文坐标轴和字体配置,我个人在实际操作中的体会是:解决问题的核心不是记住某一行代码,而是理解 Matplotlib 匹配字体的顺序。如果你的图形有自己的风格追求,建议每次拿到新的绘图需求,先确定整体视觉规范,再决定字体和字号参数放在哪里。多用rcParams做基础兜底,用局部fontdict或fontproperties做重点强调,代码会清晰很多。
最后再分享一个小技巧:如果你在使用 Jupyter Notebook 画图,发现中文字体设置后偶尔不生效,可以直接重启 Kernel 再执行一次;如果重启后仍然不行,检查一下是不是在调用%matplotlib inline之前就设置了rcParams。把参数放在import matplotlib.pyplot as plt之后、%matplotlib inline之前是最稳妥的顺序。还有,保存图片时尽量把字体相关的设置都在同一段代码里管理清楚,不要东放一行西放一行。这样你在几个月后回看脚本时,依旧能一眼看懂哪些图用的是什么字体策略,而不是对着满屏方块重新猜一遍。