先问一个扎心的问题:你在matplotlib里满怀信心地写完标题,运行,弹出来一张图,标题和坐标轴上的中文全部变成小方框。你上网搜了一圈,答案都是同样的三行配置,照着抄完,有的图好了,有的图还是老样子,甚至原本好好的负号也跟着变成了方块。是不是很熟悉?
这个坑我从第一次用matplotlib画中文图表起就反复踩。一开始只知道复制配置代码,直到后来换了几台机器、换了几套操作系统,才真正把matplotlib的字体机制搞明白。说白了,绝大多数中文乱码问题的根因就一句话:matplotlib内置的默认字体DejaVu Sans压根没有中文字形,遇到中文时它不报错、不警告,只是画一个占位的方框,然后就没有然后了。
这篇文章不是单纯甩给你一份配置,而是把字体设置这件事拆开讲清楚:全局配置是怎么起作用的;坐标轴刻度、坐标轴标签、标题这些不同的文字分别由哪些参数控制;跨平台怎么保证不崩;最后给出一份可以直接抄的完整模板。内容侧重实操,每一步都可以直接复制验证,适合刚接触matplotlib的初学者,也适合被字体问题反复折磨、想彻底搞明白机制的老手。
1. 乱码不是玄学:matplotlib找不到中文字体的那一刻
1.1 方框背后的字体渲染流程
很多人以为乱码是"编码问题",折腾utf-8、gbk,其实跟编码没什么关系。matplotlib拿到一个字符串后,会把它交给渲染引擎,渲染引擎再去当前选中的字体文件里查询每一个字符对应的字形(glyph)。如果这个字体里恰好没有中文字形,系统并不会崩溃,而是用一个特殊的占位字形来顶替——就是你在图里看到的空心小方框。
这里的关键在于"当前选中的字体"。matplotlib默认的字体族是sans-serif,而sans-serif默认匹配到的具体字体是DejaVu Sans。DejaVu Sans是一套非常完善的西文字体,覆盖了拉丁字母、希腊字母、西里尔字母,但是中文的GB2312/GBK字符集它一个都没有。于是默认状态下,画中文就必然出方框,这不是bug,是字体缺失。
打个比方:你让一位只认识英文字母的排版师傅去排一段中文,他排不出来,只能在每个不认识的字上画个方块标记。matplotlib里干的正是这件事。
1.2 快速确认当前缺失的中文字体
在动手配置之前,建议先摸清楚自己系统里到底有哪些中文字体。matplotlib自身维护了一个字体缓存列表,通过font_manager可以查看:
from matplotlib import font_manager # 列出所有已经注册到matplotlib的字体名称 all_fonts = sorted({f.name for f in font_manager.fontManager.ttflist}) # 过滤出常见中文字体相关的关键字 keywords = ['Hei', 'Song', 'Kai', 'YaHei', 'PingFang', 'Noto', 'WenQuanYi', 'Source Han'] zh_fonts = [name for name in all_fonts if any(k in name for k in keywords)] print(zh_fonts)运行这段代码,如果输出是空列表,说明系统里虽然有中文字体,但matplotlib没有把它们加载进来。这种情况在Linux服务器上很常见,在后面第5节我会专门讲。如果输出里有SimHei、Microsoft YaHei、PingFang SC这类名字,说明字体文件已经被matplotlib识别了,接下来只要在配置里指明用哪一个即可。
1.3 为什么"改了配置却没生效"
网上最常见的答案是这样:
plt.rcParams['font.sans-serif'] = ['SimHei']很多人抄了这行代码,发现还是方框,就开始怀疑人生。其实排查起来很简单,就三个原因:
第一,配置代码位置不对。rcParams是全局运行时配置,必须在绘制任何图形之前设置。如果你已经把图画好了再设置,当前图形已经进入渲染流程,自然不生效。正确做法是放在脚本顶部、import matplotlib.pyplot as plt之后。
第二,字体名字写错了。SimHei是字体家族名,不是你能在系统字体管理里看到的中文名"黑体",也不是文件名simhei.ttf。Microsoft YaHei、SimSun、KaiTi这些都是matplotlib在加载字体时记录的内部名称,写错一个字符都不会生效。
第三,用了样式表覆盖。如果你调用了plt.style.use('某个样式'),而该样式文件中恰好重新定义了font.sans-serif,那么你在样式调用之前写的rcParams会被覆盖掉。建议把字体配置放在plt.style.use()之后,或者直接在样式表里修改。
2. 全局配置:让所有图画一次就告别中文乱码
2.1 rcParams是matplotlib的全局遥控器
matplotlib把几乎所有可调参数都放在了一个叫rcParams的全局字典里,相当于整个库的默认设置仓库。只要修改了这里的值,之后创建的每一个坐标轴、每一张图都会继承这些默认值。字体相关的核心参数有这么几个:
font.family:字体族,默认是sans-serif;font.sans-serif:无衬线字体候选列表,默认以DejaVu Sans开头;font.serif:衬线字体候选列表;axes.unicode_minus:坐标轴刻度上负号是否使用Unicode的减号,默认是True。
matplotlib选择字体的逻辑是:先看font.family指定了哪个字体族,然后从对应的候选列表里按顺序找第一个可用的字体。所以我们通常只需要修改font.sans-serif这个列表,把中文字体放在最前面,DejaVu Sans的优先级就自动被顶下去了。
2.2 中文字体怎么选:每种字体的性格不同
不同中文字体在图表里的表现差异很明显,不是随便选一个就能满足所有场景。我自己常用的几个:
| 字体家族名 | 中文名 | 适用场景 | 注意事项 |
|---|---|---|---|
| SimHei | 黑体 | 数据图表、演示投屏 | 笔画粗细均匀,小尺寸下清晰 |
| Microsoft YaHei | 微软雅黑 | 屏幕阅读、导出网页图片 | 现代感强,字面大,适合看细节 |
| SimSun | 宋体 | 学术论文、正式文档配图 | 衬线笔画在低分辨率下有轻微发虚 |
| KaiTi | 楷体 | 少量标注、偏文艺风格 | 大段文字用楷体反而影响阅读 |
| Noto Sans CJK SC | 思源黑体 | Linux服务器、跨平台部署 | 开源字体,覆盖全,适合离线打包 |
我的经验是:通用性的数据图表优先用SimHei或Microsoft YaHei;如果是给论文配图,用SimSun和衬线风格的坐标轴更协调。跨平台部署时,建议在候选列表里多写几个字体名,让matplotlib按顺序自动寻找第一个存在的:
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'PingFang SC', 'Noto Sans CJK SC', 'WenQuanYi Micro Hei']这样在Windows、macOS、Linux上运行同一套代码时,只要系统装了其中任何一款中文字体,就不会出现方框。
2.3 全局配置的标准模板
下面这份配置我几乎每个项目都直接在文件开头复制一遍,够用且稳定:
import matplotlib.pyplot as plt # 中文字体候选列表,按优先级排列 plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'PingFang SC', 'Noto Sans CJK SC'] # 解决负号显示为方块的问题 plt.rcParams['axes.unicode_minus'] = False # 顺手统一全局文字大小,后面可以单独覆盖 plt.rcParams['axes.labelsize'] = 14 # x轴和y轴标签字号 plt.rcParams['xtick.labelsize'] = 12 # x轴刻度数字字号 plt.rcParams['ytick.labelsize'] = 12 # y轴刻度数字字号 plt.rcParams['axes.titlesize'] = 16 # 坐标轴标题字号设置完之后,直接画一张最简单的图验证:
import numpy as np x = np.linspace(0, 10, 100) y = np.sin(x) plt.plot(x, y, label='正弦曲线') plt.xlabel('横轴:时间(s)') plt.ylabel('纵轴:振幅') plt.title('中文标题验证') plt.legend() plt.show()如果能看到正常的"正弦曲线""横轴:时间(s)",说明全局配置生效了,后面所有的图都会继承。
2.4 axes.unicode_minus这个参数为什么必须设
很多教程把axes.unicode_minus = False一笔带过,但它其实是一个独立的坑。matplotlib默认情况下,坐标轴上的负号会使用Unicode字符集里的"减号"(U+2212),而不是ASCII的连字符-。DejaVu Sans里有这个字形,所以默认状态下负号显示正常。一旦你把字体切换成了中文字体,诡异的事情就来了:有些中文字体(比如早期版本的某些思源黑体字重)根本没有U+2212这个字形,于是每个负号都变成了小方框。
设置成False之后,matplotlib会改用ASCII连字符来渲染负号,几乎所有字体都内置ASCII连字符,一下就清净了。哪怕你的图里暂时没有负数,我也建议无条件保留这一行,否则将来数据里出现负值,又要排查半天。
3. 坐标轴刻度与标签:区分"刻度数字"和"轴名称"两套体系
3.1 刻度数字由xtick.labelsize和ytick.labelsize控制
坐标轴上密密麻麻的数字,在matplotlib里叫"刻度标签"(tick labels)。控制它们字体和大小的参数是独立于其他文字的。全局配置里我们已经设置了:
plt.rcParams['xtick.labelsize'] = 12 plt.rcParams['ytick.labelsize'] = 12这两个参数只作用于刻度数字,不会影响坐标轴名称,也不会影响标题。如果想要更精细地控制,可以用tick_params针对单个坐标轴单独设置:
fig, ax = plt.subplots(figsize=(8, 5)) ax.plot(x, y) # 只调整x轴刻度:字号11,旋转30度避免日期重叠 ax.tick_params(axis='x', labelsize=11, rotation=30) # 只调整y轴刻度:字号13,加粗 ax.tick_params(axis='y', labelsize=13, width=2)这里有一个很多人忽略的点:tick_params里的labelsize只改字号,不改字体家族。如果不小心在某个局部代码里写了ax.set_xticklabels(..., fontfamily='DejaVu Sans'),那这一组刻度标签会强制使用默认字体,中文还是会变方框。所以局部覆盖时要记得连fontfamily一起带上。
3.2 坐标轴名称:set_xlabel和set_ylabel的完整参数
坐标轴名称(也就是"横轴:时间""纵轴:振幅")和刻度数字是两个独立的对象,通过set_xlabel/set_ylabel控制。它们有几个常用参数:
ax.set_xlabel('日期', fontsize=14, fontfamily='SimHei', fontweight='bold', color='#333333', labelpad=10) ax.set_ylabel('访问量(人次)', fontsize=13, fontfamily='Microsoft YaHei', labelpad=6)fontsize:字号,可以是整数或字符串如'large'、'x-large';fontfamily:字体家族名;fontweight:字重,'bold'加粗;color:颜色;labelpad:标签与坐标轴之间的距离,单位是像素,数值越大离轴越远。
labelpad是个容易被忽略的细节。当我用figsize比较大的图时,默认的轴向间距往往偏小,标签贴得离刻度数字很近,观感很挤。手动加上labelpad是最直接的调整手段。
3.3 单独修改已经创建好的坐标轴标签
有时候图已经画完了,又不想重新跑到set_xlabel那里改,可以直接通过ax.xaxis拿到标签对象再修改:
label_obj = ax.xaxis.get_label() label_obj.set_fontsize(15) label_obj.set_fontfamily('SimHei') label_obj.set_color('#cc4444')这种写法适合在函数式绘图流程里动态调整,比如你在一个循环里生成了多个子图,想统一改其中几个子图的轴标签风格。get_label()返回的是一个Text对象,Text对象的set_*方法相当丰富,几乎所有文字属性都能改。
3.4 实例:一张带中文坐标轴的折线图
把上面的知识组合起来,做一张数据图表:
import matplotlib.pyplot as plt import numpy as np # 全局配置 plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei'] plt.rcParams['axes.unicode_minus'] = False np.random.seed(42) days = np.arange(1, 31) visits = 800 + 50 * np.sin(days / 5) + np.random.normal(0, 30, 30) fig, ax = plt.subplots(figsize=(10, 5)) ax.plot(days, visits, color='#2e7d32', linewidth=2, marker='o', markersize=4) # 轴标签 ax.set_xlabel('日期(天)', fontsize=14, fontfamily='Microsoft YaHei', labelpad=8) ax.set_ylabel('访问量(人次)', fontsize=14, fontfamily='Microsoft YaHei', labelpad=8) # 刻度 ax.tick_params(axis='x', labelsize=11) ax.tick_params(axis='y', labelsize=11) ax.grid(axis='y', linestyle='--', alpha=0.4) plt.tight_layout() plt.show()这张图的刻度数字、轴标签都启用了中文字体,字号也分开了:刻度数字11,轴标签14。视觉上既有层次,又不会出现方框。
4. 标题字体与大小:主标题、副标题、图例文字各有各的入口
4.1 set_title里的字体参数:不只是中文
标题文字的字体设置和轴标签非常相似,核心方法是ax.set_title(),它支持的字体参数比轴标签还多几个:
ax.set_title( '近30天站点访问趋势', fontsize=16, fontfamily='Microsoft YaHei', fontweight='bold', color='#222222', pad=14, loc='center' )pad:标题到坐标轴的距离,值越大标题离图越远。默认值在figsize拉大后会显得标题贴着图顶,手动加pad是常规操作。loc:'left'、'center'、'right',控制标题在坐标轴内的水平位置。
如果一张图里既有总体说明又有具体指标,我会把图分成两层:整张图的fig.suptitle()放总标题,ax.set_title()放子图标题。两层互不干扰,各自设置字号字体:
fig, ax = plt.subplots(figsize=(9, 5)) fig.suptitle('2025年各区域访问趋势总览', fontsize=18, fontfamily='Microsoft YaHei', fontweight='bold') ax.set_title('华东区域细分', fontsize=14, fontfamily='SimHei')这种"总标题大、子标题小"的结构在报告里非常实用,一眼就能区分层级。
4.2 图例中文乱码的独特性
图例是另一个容易踩坑的地方。全局配置生效后,图例默认会继承全局字体,所以正常情况下不会出问题。但一旦你在某个局部代码里单独创建了一个legend,或者给图例传了prop参数,就可能会把字体重新带到默认值上:
# 错误的例子:prop里只写size,不写family ax.legend(prop={'size': 12}) # 正确的例子:连同字体族一起指定 ax.legend(prop={'family': 'Microsoft YaHei', 'size': 12})prop字典只写了size时,family会使用默认值DejaVu Sans。所以如果你在图里发现"图例文字是方框,其他中文都正常",九成就是prop里没写family。更稳妥的做法是用FontProperties对象:
from matplotlib.font_manager import FontProperties legend_prop = FontProperties(family='Microsoft YaHei', size=12) ax.legend(prop=legend_prop)4.3 图中任意文字:ax.text和annotate的字体控制
图表里偶尔还要往图中添加标注文字,用的是ax.text或者ax.annotate。它们同样有独立的字体参数:
ax.text( 15, 850, '峰值出现在月中', fontsize=12, fontfamily='SimHei', color='#c62828', ha='center' ) ax.annotate( '最低点', xy=(8, 760), xytext=(3, 720), fontsize=12, fontfamily='KaiTi', arrowprops=dict(arrowstyle='->', color='#555555') )ax.text的fontsize、fontfamily都是独立的,不设置就继承全局。我的习惯是:全局配置只管通用部分,凡是需要特殊强调的标注,都显式写上fontfamily,避免被谁悄悄覆盖。
5. 跨平台字体差异:Windows能跑,换台机器就崩的终极解法
5.1 为什么同样的代码在Windows正常、在Linux崩
很多项目的开发环境是Windows本机,画出来的图一切正常;代码推到Linux服务器上跑批任务,输出的图全是方框。原因非常朴素:matplotlib只能加载它认识的、系统中存在的字体文件。Windows系统预装了SimHei、Microsoft YaHei,Linux往往没有;macOS又用另一套字体。你用plt.rcParams['font.sans-serif'] = ['Microsoft YaHei'],在Linux上font_manager根本找不到这个字体文件,于是候选列表为空,又回退到DejaVu Sans,中文全变方框。
这不是代码逻辑问题,而是"字体注册表"不同。解决方向有两个:要么让系统之间拥有相同的字体,要么把字体文件随代码一起带着走。
5.2 一步定位操作系统里可用的中文字体
在Linux服务器上,最快的方法是先用系统命令看看有哪些中文字体:
fc-list :lang=zh输出会列出字体文件路径和字体名,比如:
/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc: Noto Sans CJK SC,Noto Sans CJK TC然后用matplotlib确认它是否被注册:
from matplotlib import font_manager target = 'Noto Sans CJK SC' fonts = [f for f in font_manager.fontManager.ttflist if f.name == target] print(fonts)如果ttflist里查不到,说明matplotlib的字体缓存还没刷新。这时候可以强制重载缓存,或者使用后面第5.3节的addfont直接注册。
macOS上的常见中文字体名称是PingFang SC、STHeiti、Songti SC,Windows常见的是SimHei、Microsoft YaHei、SimSun。我把三种系统常见的字体放一起做成候选列表,基本可以覆盖绝大多数场景:
plt.rcParams['font.sans-serif'] = [ 'PingFang SC', 'Microsoft YaHei', 'SimHei', 'Noto Sans CJK SC', 'WenQuanYi Micro Hei' ]matplotlib会自动从左到右找第一个实际存在的字体。
5.3 手动注册字体文件:不依赖系统安装的解法
最稳的跨平台方案是:把字体文件(.ttf或.otf)直接放到项目目录里,运行前用font_manager.addfont()注册。这样不管目标机器系统里装了什么,都不影响图表输出。
from matplotlib import font_manager # 注册项目自带的字体文件 font_manager.fontManager.addfont('/path/to/project/fonts/SourceHanSansCN-Regular.otf') # 注册之后要重新读取字体名称 font_name = font_manager.FontProperties(fname='/path/to/project/fonts/SourceHanSansCN-Regular.otf').get_name() print(font_name) # 通常是 'Source Han Sans CN' plt.rcParams['font.sans-serif'] = [font_name] plt.rcParams['axes.unicode_minus'] = False这里有一个很重要的细节:addfont()之后不能想当然写一个中文字体名让它匹配,必须用FontProperties(fname=...).get_name()去读它实际登记的家族名。不同来源的字体文件内部名称可能和你预期的不一样,比如思源黑体的Regular字重通常登记为Source Han Sans CN,但个别版本可能是Source Han Sans SC。读出来最保险。
另外,官方文档说addfont对.ttf、.otf支持最稳,对.ttc(字体集合)的支持在部分3.x版本里可能不完整。我在一个内部系统里就遇到过simhei.ttc注册成功但渲染时仍显示方块的情况,后来换成了单独拆出的.ttf文件才解决。如果你手里只有.ttc文件,优先找一下有没有同名的.ttf替代品。
5.4 企业内部离线环境的字体打包建议
在无法联网安装字体包的内网环境里,我通常这样做:
- 在开发机上找到一款最常用的开源中文字体,比如思源黑体或Noto Sans CJK;
- 把它放进项目的
fonts/目录,同时提交到版本库; - 项目入口处写一个统一的初始化模块,比如
setup_chinese_font.py,在里面完成addfont和rcParams设置; - 所有绘图脚本只需要
import setup_chinese_font,不再重复配置字体。
这样即使换到一台完全干净的新服务器,只要代码能拉到,图表文字就一定正常。这个方法我用了很久,基本杜绝了"换台机器图就崩"的尴尬。
6. 实战模板与速查:一套配置走天下
6.1 可直接复制的完整绘图模板
把前面所有的点整合成一个文件,是我在实际项目中一直沿用的骨架:
"""中文matplotlib绘图模板""" import matplotlib.pyplot as plt import numpy as np # ---------- 1. 全局中文配置 ---------- plt.rcParams['font.sans-serif'] = [ 'Microsoft YaHei', # Windows 'SimHei', # Windows 'PingFang SC', # macOS 'Noto Sans CJK SC', # Linux ] plt.rcParams['axes.unicode_minus'] = False plt.rcParams['axes.labelsize'] = 14 plt.rcParams['xtick.labelsize'] = 12 plt.rcParams['ytick.labelsize'] = 12 plt.rcParams['axes.titlesize'] = 16 # ---------- 2. 数据区 ---------- x = np.linspace(0, 10, 200) y = 2 * np.sin(x) - 0.5 # ---------- 3. 绘图区 ---------- fig, ax = plt.subplots(figsize=(9, 5)) ax.plot(x, y, linewidth=2, label='正弦信号') # 标题与轴标签 ax.set_title('信号强度随时间的变化', fontsize=17, fontweight='bold', pad=12) ax.set_xlabel('时间 (s)', labelpad=6) ax.set_ylabel('幅值', labelpad=6) # 刻度 ax.tick_params(axis='x', labelsize=12) ax.tick_params(axis='y', labelsize=12) ax.tick_params(axis='both', length=5, width=1.2) # 图例 ax.legend(prop={'family': 'Microsoft YaHei', 'size': 11}) # 网格 ax.grid(axis='y', linestyle='--', alpha=0.3) plt.tight_layout() plt.savefig('output_chinese_chart.png', dpi=200, bbox_inches='tight') plt.show()第bbox_inches='tight'参数在savefig里值得注意,它会把画布裁切到和图形内容刚好贴合,避免标题、轴标签被切掉。
6.2 常见错误速查表
| 现象 | 根本原因 | 解决办法 |
|---|---|---|
| 标题、坐标轴全部是方框 | 全局字体没配置,或配置了但字体不存在 | 设置font.sans-serif候选列表,确认列表中至少有一个字体存在于当前系统 |
| 负号显示为方框 | axes.unicode_minus仍为True,中文字体没有U+2212字形 | 设置plt.rcParams['axes.unicode_minus'] = False |
| 图例文字是方框,其他文字正常 | legend的prop参数覆盖了family,或没指定family | 用prop={'family': 'Microsoft YaHei', 'size': 12}或FontProperties |
| 刻度的中文正常,但轴标签不生效 | set_xlabel里的fontfamily写错了字体名 | 在代码里打印font_manager里实际的字体名,再填进去 |
| Windows正常,Linux崩 | 两套系统的中文字体集合不同 | 用字体候选列表多平台兼容,或项目内自带字体文件addfont |
addfont注册了ttc,渲染还是方框 | 某些matplotlib版本对ttc支持不完整 | 换成.ttf/.otf文件重新注册 |
| 字号改了没变化 | rcParams设置在绘图之后,或style.use覆盖了配置 | 把字体配置放在绘图前、style.use之后 |
6.3 我踩过坑之后的一些体会
字体问题最磨人的地方在于:出错的时候matplotlib不吭声,不报警,出图照样很"成功",只有人眼盯着屏幕才能发现全是方框。所以我后来的习惯是,换一台新环境第一件事就跑一次第1.2节里的字体检测代码,确认ttflist里有中文字体再开始画图,省得把乱码问题带到后面的分析流程里埋雷。
还有一点,项目里图特别多的时候,不要每张图都单独写一遍字体设置。写一个统一的配置模块,把rcParams集中管理起来。哪天换了字体,只改一处,所有图一起变。比起到处复制代码、改漏一处然后排查半天,这个成本低太多。
如果你现在也被中文乱码折磨,建议直接照着第6.1节的模板跑一遍,然后把第6.2节的表格收藏起来,下次碰到对应现象直接对症下药。这个坑填平之后,matplotlib的中文绘图基本就是一片坦途了。