刚开始用matplotlib画图,第一张图就满屏小方框,标题、坐标轴全是豆腐块一样的空框框,换成个折线图画中文字体,依旧原样。这个问题在中文开发者圈子里几乎算是入门必经关卡,项目标题看着简单——matplotlib无法显示中文,但真想彻底解决,光搜一个答案抄过来是不够的。因为同样的配置在某些电脑上好用,换台机器就失效,你在本地跑通了,放到服务器上又出问题。这篇文章我把原理、排查思路和一套能直接用的工程化方案都写清楚,无论你是刚接触Python可视化的新手,还是需要批量出图的报表开发者,按这个思路走都能把中文字体这件事一次搞定。
1. 为什么matplotlib会把中文显示成方框——先搞清楚它在挑什么字体
1.1 不是matplotlib不支持中文,而是默认字体不支持
很多人遇到这个问题,第一反应是“matplotlib不支持中文”。实际上matplotlib对字符本身没有任何限制,它本质上是把自己配置的字体丢给渲染引擎,让渲染引擎按照字体文件里的字形表去绘制字符。问题出在它默认配置的那套字体上。
matplotlib默认情况下用的是它内置的一套西文字体,这套字体只覆盖了拉丁字符、希腊字母和一部分数学符号,里面根本没有中文字形。当要绘制一个中文字符时,字库里找不到这个字符对应的字形,于是渲染引擎就画一个空方框出来占位,这就是“豆腐块”或“tofu”的来历。
很多人误以为matplotlib会像Word那样自动遍历系统中的字体去替补缺失的字形,实际上matplotlib的字体回退机制非常原始。它只会按你给定的家族名称列表去找第一个能用的字体,这个字体如果缺字符,它顶多提示一下缺字形警告,不会自动去其他字体里补。所以你必须明确告诉它“你去用某个包含中文字形的字体文件”。这就像你让一个只会英语的人去朗读中文,他不会自己翻字典,你得先把中文译本递到他手里。
1.2 什么时候最容易触发这个问题
从我的实践来看,最容易中招的几种场景非常有共性,你可以对号入座:
- 本地Windows或macOS电脑上开发,代码里没有任何字体配置,图一显示中文就变方框。
- 代码从别人那里复制过来,对方用的字体名称在你自己机器上不存在,比如别人配了Windows下的SimHei,你却是macOS。
- 部署到服务器或容器环境,系统是精简版Linux,中文字体压根没装。
- 用plt.savefig保存图片时正常,但在某些交互式窗口里显示正常,换个后端渲染又不正常。
- 同一个脚本,换台机器就出问题,因为两台机器的中文字体列表不一样。
除了场景,你还需要知道一点:matplotlib并不是“启动时”就去扫描一遍系统字体,它会把字体信息做成本地缓存。首次运行时会扫描系统字体目录并生成缓存文件,之后直接读缓存。系统新装了一个中文字体,但matplotlib缓存还没刷新,它依然不认识这个字体。这个缓存机制是后面很多玄学问题的根源,第4部分我会详细展开。
2. 从零到一:让matplotlib正确显示中文的三种姿势
2.1 先确认系统里有没有中文字体
在动手配置之前,第一步是确认字体真的存在。很多服务器默认不带中文字体,你配置写得再对也没用。
先看系统层面有没有中文字体。Windows下打开字体目录或者控制面板,macOS打开字体册,Linux下可以用命令:
fc-list :lang=zh这个命令会把系统中所有支持中文的字体列出来。如果你在Linux服务器上执行后发现输出为空,说明系统根本没装中文字体,那就得先安装。Debian系发行版装Noto CJK字体比较省心:
sudo apt install fonts-noto-cjkRed Hat系的用:
sudo yum install google-noto-sans-cjk-fonts装完后再执行一次fc-list :lang=zh确认。
接着再看matplotlib认识哪些字体。这里要理解一个关键差异:系统里装了字体,不代表matplotlib一定能用到它。matplotlib有自己扫描字体文件的逻辑和缓存,它会扫描系统的字体目录,但不是所有格式的字体它都支持,一般支持TTF和OTF,老旧的点阵字体格式可能识别不了。
在Python里用font_manager列出当前matplotlib已知的字体:
from matplotlib import font_manager font_list = font_manager.fontManager.ttflist for font in font_list: if 'CJK' in font.name or 'Hei' in font.name or 'YaHei' in font.name or 'Song' in font.name: print(font.name, font.fname)这个代码会输出字体名称和字体文件路径,非常直观。如果你发现系统有中文字体,但这段代码什么都没有输出,那就是matplotlib的字体缓存没有刷新。
2.2 姿势一:全局配置rcParams
最省事的办法是在脚本开头一次性设置全局字体参数。下面这套配置是目前中文社区流传最广、兼容性也最好的方案:
import matplotlib.pyplot as plt plt.rcParams['font.family'] = 'sans-serif' plt.rcParams['font.sans-serif'] = ['Noto Sans CJK SC', 'SimHei', 'Microsoft YaHei', 'WenQuanYi Micro Hei', 'Arial Unicode MS'] plt.rcParams['axes.unicode_minus'] = False这里每个参数都有讲究。
font.family指定的是字体家族类别,我这里写sans-serif,表示让matplotlib使用无衬线字体家族。font.sans-serif是一个候选列表,matplotlib会从左到右逐个检查,选中第一个在系统中真实存在的字体。所以这个列表的顺序很关键,你需要把你机器上最有把握的字体放在最前面,而不是随便抄别人的列表。
为什么强调这一点?因为不同系统上可用的中文字体不一样。Windows上优先SimHei或者Microsoft YaHei,macOS上Arial Unicode MS或者PingFang SC比较靠谱,Linux上Noto Sans CJK SC几乎是标配。把候选列表写全,代码拿到哪个系统上都能找到至少一个可用字体。
axes.unicode_minus设置为False,是用来修复负号显示问题的。设置完中文字体后,如果这个参数不改,坐标轴上的负号可能变成一个小方块,这是因为中文字体里通常没有数学减号对应的字形,matplotlib默认用它选中的字体去画负号,结果缺字形,显示成框。设为False后,matplotlib会用ASCII的连字符-来显示负号,问题立刻消失。
2.3 姿势二:局部指定FontProperties
全局配置适合脚本内所有图都要用中文字体的场景。但有些场景你不想全局生效,只想让某个图的标题用中文字体,或者想让标题和正文用不同的中文字体。这时局部指定FontProperties更合适。
from matplotlib.font_manager import FontProperties zh_font = FontProperties(fname='/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc') plt.title('季度销售额对比', fontproperties=zh_font, fontsize=14) plt.xlabel('月份', fontproperties=zh_font)这种方式的优势是稳定。你直接给定了字体文件的路径,不再依赖matplotlib去系统里“猜”。缺点是需要提前确认字体文件的绝对路径。前面讲过的font_manager列出字体时,每个字体条目里都有fname字段,那就是字体文件的绝对路径,你可以先跑一遍打印出来,然后填进代码里。
你的目标是做一张带中文的图,用fname方式指定是最不容易出错的,因为它完全绕过了字体名映射、候选列表、缓存刷新这一堆中间环节。字体文件路径就是那个文件,只要文件在,渲染就没问题。
2.4 姿势三:动态探测字体名称
第二种方式稳定但写死路径,一旦部署到另一台机器,路径可能就不同了。我比较推荐的做法是写一个动态探测函数,先扫描系统里有哪些可用的中文字体,再从中选择第一个可用的作为字体配置。
from matplotlib import font_manager def get_available_chinese_fonts(): available = set(f.name for f in font_manager.fontManager.ttflist) preferred = ['Noto Sans CJK SC', 'SimHei', 'Microsoft YaHei', 'Arial Unicode MS', 'PingFang SC'] for name in preferred: if name in available: return name return None这个函数返回的字体名可以直接用来配置rcParams或者FontProperties。它的好处是代码换环境不换内容,到任何机器上都会自动适应当前系统的可用字体。
3. 踩坑实录:为什么配置了字体还是显示成方框
3.1 字体缓存已经过期,matplotlib根本没查出新字体
我第一次在服务器上遇到诡异问题就是这样:系统里用fc-list能看到中文字体,但matplotlib画出来还是方框。排查了很久,最后发现是字体缓存的问题。
matplotlib首次扫描字体后会生成一个缓存文件,在Linux上通常在~/.cache/matplotlib目录,Windows上在用户目录下的.matplotlib目录里。这个缓存文件里记录了字体列表、字体名、样式等元信息。如果缓存是在你安装中文字体之前生成的,那安装字体之后,matplotlib依然拿着旧缓存,不会自动感知新字体。
解决办法是手动清理缓存:
import matplotlib import shutil cache_dir = matplotlib.get_cachedir() shutil.rmtree(cache_dir, ignore_errors=True)清理完缓存后,下次运行时matplotlib会重新扫描系统字体,新装的中文字体就能被识别了。如果你不想写代码,直接在命令行删除缓存目录也行。
3.2 font.family配置对了,但绘图顺序中有其他配置覆盖
全局配置写在脚本开头,按理说应该生效,但有时候在某些脚本中间用到了seaborn的样式设置,或者导入了某些自带样式配置的库,它会覆盖掉rcParams的字体设置。
seaborn有个set_style和set_theme,调用之后会把matplotlib的rcParams重置一部分。如果你在seaborn设置之后才配置中文字体,那没问题;但如果你先配置中文字体,后面又调用seaborn.set_style,那配置就被冲掉了。
这种配置覆盖问题最难排查,因为代码看起来每一行都没毛病,但组合起来就出错。我现在的习惯是:把所有字体配置集中放在绘图函数的统一入口处,并且放在所有样式设置之后,不做二次覆盖。
3.3 同一台机器上同时装了宋体和黑体,但matplotlib总选错
你指定了font.sans-serif列表为Microsoft YaHei加SimHei,按理说应该选中雅黑。但有时候matplotlib还是用了别的字体,或者图中有的地方显示宋体,有的地方显示黑体。这种问题往往是因为font.family设置的是serif,而你把中文字体加到了sans-serif列表里,macOS和Linux下matplotlib对serif家族有自己的默认列表,里面可能也包含一些支持中文的字体。
建议是把中文字体同时加到font.serif和font.sans-serif里,或者直接统一font.family,保证标题、正文、坐标轴都用同一个家族下的中文字体。还有一种情况是同一个字体家族里既有Regular又有点阵变体,matplotlib的匹配规则不是完全可预测的,你只能通过“显式指定字体文件路径”或者“缩小变量范围”来根治。
3.4 保存图片时字体没有嵌入,换个环境打开依然方框
有一种很隐蔽的情况:你在自己电脑上生成的图片看起来好好的,发给别人后对方打开图片,中文又变成方框。这种情况常见于保存PDF这类矢量格式时,如果字体没有嵌入PDF文件,别的机器上没有对应字体,打开PDF时就会用替代字体渲染。
解决思路是保存时指定字体嵌入。PDF后端配置如下:
import matplotlib matplotlib.rcParams['pdf.fonttype'] = 42这里的42表示TrueType字体嵌入,可以保留中文字形信息。默认的pdf.fonttype是3,它可能不嵌入子集字体,导致跨平台打开时丢失字形。这个参数还牵扯到后期编辑:pdf.fonttype=42生成的PDF文字仍然是可编辑文本,而不是转成图片,这对后续修改很有用。
PNG这类位图格式不会有这个问题,因为像素已经固定。要图片精度高,建议保存dpi设成300以上。
3.5 中文字体名字有兼容性差异
字体名是一个很微妙的坑。同一个字体,系统显示的名称和matplotlib识别的名称可能不同。例如Arial Unicode MS在macOS上系统里叫这个名,在Windows上叫Arial Unicode。另外有些字体的官方名称带后缀,比如Noto Sans CJK SC和Noto Sans CJK SC Regular,matplotlib列出来的名字就可能带上后缀,如果你只写了核心名称,可能匹配不上。
解决方法是不要凭记忆写字体名。先跑font_manager扫描,把输出结果直接复制到代码里。我就见过人在网上复制了一段配置,里面写了simhei,拼接全小写,结果matplotlib里字体名是SimHei,写完之后看着配置没问题,实际根本没匹配上。
4. 不同平台的差异化配置与整套可复用的工程模板
4.1 Windows / macOS / Linux三平台差异速查
三套系统的字体情况差异很大,我整理了简单的对照表,方便跨平台开发参考:
| 系统 | 常用中文字体 | 字体文件常见路径 | 是否需要额外安装 |
|---|---|---|---|
| Windows | Microsoft YaHei, SimHei, SimSun | C:\Windows\Fonts | 无需安装 |
| macOS | PingFang SC, Arial Unicode MS, Hiragino Sans GB | /System/Library/Fonts | 无需安装 |
| Linux | Noto Sans CJK SC, WenQuanYi Micro Hei | /usr/share/fonts | 可能需要安装 |
Linux下Noto Sans CJK SC是目前覆盖最全、最规范的中文字体,优先推荐。macOS的PingFang SC字体名在matplotlib里可能显示为PingFang SC,也可能显示为PingFang HK等变体,需要扫描确认。很多情况下,macOS最简单的方式是直接用Arial Unicode MS,这个字体对中文的覆盖率足够高,兼容性也稳定。
4.2 工程化模板:一个字体配置模块直接复用
接触到项目出图后,你会发现“一次性写死在脚本里”不够用。项目里可能有几十个绘图脚本,每个脚本都复制一份rcParams配置,一旦要调整字体,几十个文件都要改。更好的做法是把字体配置抽成一个公共模块,其他脚本统一调用。
下面是我常用的模板,你可以直接拿来改:
# font_config.py import matplotlib from matplotlib import font_manager import shutil def reset_font_cache(): cache_dir = matplotlib.get_cachedir() shutil.rmtree(cache_dir, ignore_errors=True) def get_first_available_font(preferred_fonts): available = set(f.name for f in font_manager.fontManager.ttflist) for name in preferred_fonts: if name in available: return name return None def setup_chinese_font(): preferred = [ 'Noto Sans CJK SC', 'Microsoft YaHei', 'SimHei', 'Arial Unicode MS', 'PingFang SC', 'WenQuanYi Micro Hei' ] font = get_first_available_font(preferred) if font is None: raise RuntimeError('No Chinese font available') matplotlib.rcParams['font.family'] = 'sans-serif' matplotlib.rcParams['font.sans-serif'] = [font] matplotlib.rcParams['axes.unicode_minus'] = False return font其他脚本引入后这样用:
from font_config import setup_chinese_font setup_chinese_font() import matplotlib.pyplot as plt plt.plot([1, 2, 3], [-1, -2, -3]) plt.title('中文标题测试') plt.show()这个模板有几个好处:自动适配系统、字体不存在时报错(避免你花两小时排查为什么没生效)、统一的修改入口。字体配置的逻辑被完全隔离出来了,业务绘图脚本里不用关心字体细节,你只管调用setup_chinese_font()就行。项目里如果多人协作,这个模块也降低了混乱的概率:不管谁换了机器,代码都能自动找到合适的字体。
4.3 容器与服务器环境的特别照顾
如果你的出图脚本部署在Docker容器里,那有一个很容易踩的坑:容器基于精简镜像,系统里默认没有中文字体,你apt install之后还得重启容器或者清理matplotlib缓存,否则配置依然无效。
在Dockerfile里安装字体比较标准的方式是:
RUN apt-get update && apt-get install -y fonts-noto-cjk && rm -rf /var/lib/apt/lists/*然后在启动脚本里加一步强制清理matplotlib缓存,或者直接在代码里检测到字体缺失时就清理缓存。容器里还有一个常见问题:环境变量HOME没有设置,matplotlib会把缓存写到无法访问的目录,导致扫描失败。这种情况下要显式设置一个可写的缓存目录:
import matplotlib matplotlib.get_cachedir = lambda: '/tmp/matplotlib_cache' import matplotlib.pyplot as plt这句话一定要放在import matplotlib.pyplot之前,不然matplotlib已经在导入阶段初始化了缓存路径。
5. 字体优先级选择与深层知识总结
5.1 中文字体选型:黑体、宋体不落错
配置中文显示时,你还要考虑目标图表的使用场景。是不是所有地方都适合用同一个字体?做数据可视化时,我一般是这样选的:
- 界面展示、屏幕阅读为主的图:优先选无衬线字体(黑体、雅黑、Noto Sans),显示清晰,没有笔画粗细差异过大的问题。
- 学术论文、正式报告的图:选宋体或者思源宋体,视觉风格稳重,在印刷场景下阅读体验好。
- 代码组合的图:优先微软雅黑或者Noto Sans CJK,因为字符宽度比较均匀,与英文字体的混排效果比宋体更自然。
在matplotlib里,如果你需要全局用宋体:
matplotlib.rcParams['font.sans-serif'] = ['SimSun'] matplotlib.rcParams['font.serif'] = ['SimSun'] matplotlib.rcParams['font.family'] = 'serif'有些情况下你可能同时用两种字体:标题用宋体加粗,正文用黑体。这是前面讲过的局部FontProperties方案的长处,同一张图里可以绑定不同字体文件。
5.2 不同字体文件的格式细节
中文字体文件体积普遍比西文字体大不少,一个字体文件动辄十几兆,甚至几十兆。这会影响matplotlib的初始化时间。如果你办公室电脑配置不高,第一次扫描字体时加载大字体文件可能会明显卡顿,这是正常现象。你可以在使用前把不必要的字体从系统字体目录临时移走,减少扫描量。
TTF、OTF、TTC这三种常见格式matplotlib基本都能识别。TTC是多个字体打包在一个文件里的集合格式,matplotlib读取时可以识别其中包含的多个字体名。但要注意,某些老版本的matplotlib对TTC的支持有缺陷,如果你发现某个字体明明安装好,扫描也扫到了,但渲染还是失败,你可以尝试只保留该字体的TTF或OTF版本。
5.3 matplotlib版本对字体支持的影响
不同版本的matplotlib在字体方面的表现差异明显。比较旧一些的版本在字体扫描、TTC支持、嵌入逻辑上都有不少bug;而新版本对字体的处理更规范,很多旧问题在新版本里已经不存在了。如果你遇到了无法解释的字体怪问题,先看一眼matplotlib版本。
python -c "import matplotlib; print(matplotlib.__version__)"如果版本比较旧,优先考虑升级:
pip install --upgrade matplotlib升级后记得清理一次字体缓存,这能在最大程度上排除缓存问题。当然,升级也可能引入新的API变化,比如rcParams里某些参数名变了,这时候以官方文档为准调整即可。
6. 最终使用的体会与建议
我在一个需要生成几十张中文图表的项目里把这套配置模板完整跑了一遍。最深的体会是:字体配置看似只是三行代码,但实际上它依赖系统字体、字体缓存、matplotlib版本、渲染后端、输出格式五个环节。每个环节都可能成为那个“导致方框”的变量。你单独解决任何一个环节的代码问题,都可能会因为下一个环节的原因再次出问题。
建议你从一开始就养成三个习惯:第一,统一把字体配置抽成独立模块,不要在每个脚本里写copy-paste的rcParams;第二,部署到新环境后,第一时间执行字体扫描+缓存清理,提前验证环境而不是等出图时才发现问题;第三,把字体配置加到项目的启动检查里,比如在初始化脚本里执行setup_chinese_font(),如果字体真缺失就立刻报个错,而不是让用户收到一张遍布方框的图之后才去排查。
还有一个很少被提到的小技巧:matplotlib其实提供了一个用于调试字体匹配的函数,在交互式环境里可以用它检查某个字符会被哪个字体渲染:
from matplotlib.font_manager import findfont from matplotlib import font_manager font_manager.findfont('Noto Sans CJK SC', fallback_to_default=False)这会返回实际匹配的字体文件路径。如果返回的路径不是你预期的字体,那就说明字体名匹配出现了偏差,赶紧去调整候选列表。这个检查方法在排查“明明设置了字体但显示不对”这类问题时特别实用。
下次再遇到matplotlib中文显示问题,先别急着复制网上的代码,按照文章里的顺序检查一下:系统有没有中文字体,matplotlib认不认识这个字体,缓存有没有刷新,配置有没有被覆盖,输出格式有没有正确嵌入。五步走下来,绝大多数方框问题都能定位。剩下的边缘情况,也多半逃不出这几个环节的组合变化。