如果你画图的时候遇到过标题变成一个个小方框、中文直接消失、或者同一张图里中文和英文挤在一起怎么都对不齐,那这篇文章大概率能省你半天时间。我最早是在Ubuntu服务器上跑数据可视化踩到这个问题,后来换到Windows笔记本又踩了一遍,而且两次的报错和表现还不完全一样。更气人的是,同一个脚本在A机器上正常,拷到B机器上中文就全变成方框,调了半天也不知道到底是代码问题还是系统问题。
matplotlib里的中英文字体混排,说白了就是一个“字体匹配”的问题,但牵扯到系统字体库、matplotlib自身缓存、字体族优先级、后端渲染方式好几个层面。只看网上一两句话的解决办法,经常只能救一时。这篇我把自己在Ubuntu和Windows两边反复调通的配置方案、排查流程和几个哭笑不得的坑一次性讲清楚,代码直接能抄,里面的原理也尽量用人话说明白。
1. 这个问题到底出在哪:先看懂matplotlib的字体机制
1.1 场景还原:你遇到的是哪一种“不正常显示”
同样一段画图代码,不同人遇到的现象其实不太一样。先对号入座,方便后面精准处理。
- 中文完全不显示,图上该出文字的地方空白一片
- 中文显示成一个个方框或者“豆腐块”,英文和数字正常
- 中文能显示,但中文和英文的字体风格不一致,混排时看着很突兀
- 中文显示出来了,但是负号变成横杠、省略号变成奇怪的符号
- 报错信息里出现类似
findfont: Font family 'SimHei' not found的警告 - 图在本地显示正常,保存成图片后中文变方框,或者反过来
- 同一个脚本在Windows正常,在Ubuntu上就乱,或者换一台电脑就出问题
大部分人遇到的是第二种和第五种。但我想先说一句:这些现象背后经常不只是“缺一个中文字体”这么简单。matplotlib找字体有一套自己的逻辑,如果不理解这套逻辑,你就算装了字体也可能还是不行。
1.2 根因一:字体搜索顺序和缺字体
matplotlib画任何文字,都需要把字符映射到一个具体的字体文件。它不是一个字符一个字符去系统里现找,而是给每个字体文件建立索引、读取字体元信息,然后按字体族(font family)和字形(glyph)去匹配。
在默认情况下,matplotlib根据font.family参数决定用哪一类字体。比如font.family: sans-serif,那么它会去font.sans-serif这个候选列表里依次查找。如果某个字体文件里根本没有这个字形,它也会继续往后找。问题就出在这里:
- Ubuntu默认不一定装了中文字体,尤其精简版服务器,通常只有DejaVu Sans、Liberation Sans这类西文字体
- Windows自带的微软雅黑、SimHei是有的,但matplotlib未必把它们排在候选列表前面
- 有些字体文件是中文名,matplotlib的字体索引抓不到,或者排序靠后
你可能会想:“我系统里明明能用中文输入法打字,为什么matplotlib找不到?”因为操作系统在显示文字时有一套复杂的字体回退机制,浏览器、文本编辑器会替你选中后备字体;而matplotlib默认不做完整的回退,它更依赖你显式指定名字或者路径。
1.3 根因二:中文字体存在但不被识别
这个坑在Ubuntu上特别常遇。你可能会执行fc-list :lang=zh看到系统里确实有中文字体,但matplotlib照样报找不到。原因在于matplotlib维护的是自己的字体列表,而且第一次启动时会扫描字体目录并生成缓存。
如果字体是在matplotlib缓存建立之后才安装的,或者你装的位置不在matplotlib扫描范围内,那它根本不知道有这个字体。更隐蔽的情况是,某些中文字体文件没有正确声明family name,或者声明里的名称和实际文件名对不上,导致你按文件名去设置时依然无效。
我自己就在Ubuntu上遇到过:下载了一个NotoSansCJK-Regular.ttc,放在了/usr/share/fonts/truetype/noto/,fc-list能看到,但matplotlib就是提示找不到。后来用matplotlib.font_manager.FontProperties(fname='/路径/NotoSansCJK-Regular.ttc')直接指定文件路径才解决。
1.4 根因三:混排中最容易忽略的英文字体和标点
很多人觉得只要中文字体能显示,中英混排就没问题了。但实际上如果中文字体里没有包含合适的英文字形,matplotlib会用中文字体去渲染所有字符,结果就是英文和数字变得很怪:间隔不均匀、字重不对劲、大写字母和中文高度不协调。反过来,如果你全图把字体设为某个英文字体,中文又变成方框。
中英混排的真正需求是:中文部分用中文字体,英文和数字部分用一套好看的西文字体,标点符号也要选择合适的处理方式。matplotlib默认不区分这些,它只会按照你给定的字体族去渲染所有文本。要做到真正漂亮的混排,需要一点“分别控制”的技巧,这部分我放到第3节详细讲。
还有一种情况:Windows下用SimHei之后,负号-会显示成一个很短的横杠,在坐标轴上特别明显。原因是SimHei的负号字形不是标准的数学负号(U+2212),而matplotlib默认用的ASCII减号在部分字体里渲染不好看。这个问题通过设置axes.unicode_minus: False可以缓解,但并不完美。
2. 一套能打的配置方案:全平台字体设置模板
2.1 最省心的方式:用font_manager直接指定字体路径
如果你不想被系统字体搜索顺序绑架,最省心的方式就是绕开它,直接告诉matplotlib“用这个文件”。这种方式在Windows和Ubuntu上通用,可移植性也强,适合那种把字体文件随代码一起放进项目里的场景。
方案模板如下,建议放在脚本最前面:
import matplotlib import matplotlib.pyplot as plt from matplotlib import font_manager # 直接指定中文字体文件路径,绕开系统搜索 zh_font_path = "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc" # Ubuntu示例 # 如果是Windows,可以换成: # zh_font_path = "C:/Windows/Fonts/msyh.ttc" zh_font = font_manager.FontProperties(fname=zh_font_path) # 注册进全局字体管理,按名称引用 font_manager.fontManager.addfont(zh_font_path) # 查看该字体注册后的内部名称 prop = font_manager.FontProperties(fname=zh_font_path) print(prop.get_name()) # 全局设置 plt.rcParams['font.family'] = 'sans-serif' plt.rcParams['font.sans-serif'] = ['Noto Sans CJK SC', 'Microsoft YaHei', 'DejaVu Sans'] plt.rcParams['axes.unicode_minus'] = False这里有一个关键点:addfont()之后,字体并不一定立刻就能用plt.rcParams['font.sans-serif']里填的名字找到。你需要用FontProperties.get_name()拿到真实的字体族名,有些ttc文件的get_name()出来可能很长,比如Noto Sans CJK SC,有些还会带版本后缀。建议把打印出来的名字填进font.sans-serif列表里,而不是凭感觉猜。
如果嫌路径硬编码麻烦,也可以把字体文件放在项目下的fonts/目录,然后动态拼接路径:
import os base_dir = os.path.dirname(os.path.abspath(__file__)) zh_font_path = os.path.join(base_dir, "fonts", "NotoSansCJK-Regular.ttc")这样做的好处是,代码换到哪台机器都不依赖系统装没装字体,打包给别人跑也省心。缺点是用ttc字体文件时,matplotlib的光标定位和子字体解析偶尔会有小问题,但绝大多数场景下都正常。
2.2 Windows下的中英字体匹配
Windows下的通用做法比较直接,因为系统自带的字体足够多。最稳的组合是:
- 中文用
Microsoft YaHei(微软雅黑)或SimHei(黑体) - 英文、数字用
Times New Roman或Arial - 如果希望整体更接近出版风格,中文用
SimSun(宋体),英文用Times New Roman
具体设置可以这样:
import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'Arial'] plt.rcParams['font.family'] = 'sans-serif' plt.rcParams['axes.unicode_minus'] = False这里把微软雅黑放第一位,通常能覆盖大多数中英文和符号。但要注意,matplotlib的font.sans-serif列表并不是“主字体找中文、有缺字再找英文字体”的完美回退机制,它是按优先级顺序去找第一个能显示对应字符的字体。同一个文本字符串里如果既有中文又有英文,它不会自动拆给两个字体分别渲染。
更可控的做法是分别创建两个FontProperties对象,然后在text、legend、set_title等地方分别为英文部分和中文部分指定。实战技巧在第3节讲。
2.3 Ubuntu下的字体安装与指定
Ubuntu下先看系统里有哪些中文字体:
fc-list :lang=zh如果输出为空,说明需要安装。常见做法:
sudo apt install fonts-noto-cjk # 或者 sudo apt install fonts-wqy-zenhei sudo apt install fonts-wqy-microhei安装后重新生成matplotlib字体缓存。最彻底的方式是删掉缓存目录让它重建。matplotlib的缓存位置一般在~/.cache/matplotlib或~/.matplotlib,具体看版本。可以这样:
rm -rf ~/.cache/matplotlib然后启动Python,打印一下matplotlib能找到的含中文关键字的字体:
from matplotlib import font_manager for f in font_manager.fontManager.ttflist: if 'Noto' in f.name or 'CJK' in f.name or 'WenQuanYi' in f.name: print(f.name, f.fname)如果这一步能看到字体名,再往rcParams里填就有意义了。
Ubuntu还有一种情况:你用的是非root用户,但字体装在/usr/share/fonts/,权限没问题;可如果你装在/root/.fonts/下,另一个用户自然看不到。再或者你用的conda环境里的matplotlib,它不一定读取系统所有字体目录,有些版本还会优先读环境内的字体目录。遇到“明明系统有字但matplotlib找不到”,不要急着重装,先执行上面的打印命令看matplotlib自己能看到什么。
2.4 让设置“一次生效”的三个细节
很多人改完rcParams还是老样子,通常不是代码问题,而是下面三个细节没注意。
细节一:plt.rcParams的修改必须在创建figure之前。如果你已经plt.figure()或者画了点什么,再改rcParams,只对后续新建的figure有效,已经创建的figure不会自动更新。最常见的错误是:
plt.figure() plt.rcParams['font.sans-serif'] = ['Microsoft YaHei'] # 晚了 plt.title('中文')细节二:改了字体设置后,如果只是重新运行脚本,之前的matplotlib缓存可能还在起作用。尤其你刚才还是用addfont指定过同名但不同文件的字体,最好先清一下缓存,避免新旧字体信息交叉污染。
细节三:如果是在Jupyter Notebook里试验,需要重启内核或者重新加载matplotlib,否则fontManager里的字体列表仍然停留在第一次导入时的状态。我在Notebook里踩过很多次,改完字体不显示,重启内核马上就好了。建议把字体配置代码独立成一个font_config.py,每个notebook开头统一导入,改一处全局生效。
3. 中英混排的实战技巧:字体族、回退和手动标注
3.1 字体族(font.family)与回退机制
matplotlib的字体选择涉及几个层级:font.family可以是serif、sans-serif、monospace等通用族,也可以是具体字体名。当你设成一个具体字体名,比如plt.rcParams['font.family'] = 'Noto Sans CJK SC',那所有文本都尝试用它。当设成sans-serif,它会去读font.sans-serif列表。
matplotlib在渲染时,对每个fixed字符串,会从字体列表中找到一个“能覆盖所有字符”的字体。理论上如果第一个字体缺字,它应该尝试第二个,但实际处理并不总是符合预期,尤其是缺字时可能会静默渲染成空或者方框,而不是自动切换。这也就解释了为什么很多人把中文字体放在列表第一位后,英文也跟着用中文字体渲染,因为第一个字体“能覆盖”这些英文字符——它能覆盖,不代表它适合。
要想真正按语言分别渲染,就得用更细的控制方法。
3.2 用FontProperties分离控制中文和英文
我的常用做法是,保留全局设置给默认元素,然后在关键文本上分别传入中英文字体属性。比如:
from matplotlib import font_manager zh_font = font_manager.FontProperties(fname="C:/Windows/Fonts/msyh.ttc") en_font = font_manager.FontProperties(family="Times New Roman") plt.title("销量趋势图 Sales Trend", fontproperties=zh_font) plt.xlabel("季度 Quarter", fontproperties=zh_font) plt.ylabel("销售额 Revenue (万元)", fontproperties=zh_font)但这样会带来一个问题:混排字符串里的英文部分同样会被zh_font渲染。如果一定要让中英各用各的字体,可以把文本拆分,分别用text()手动排位置,但这样很麻烦。实际项目里我更推荐另一个思路:选择一个中文字体本身就包含较好西文字形的字体,然后全局统一用这个字体。
比如思源黑体(Noto Sans CJK SC)的英文字形和数字设计得相当均衡,用它来渲染中英混排,视觉上完全能接受。微软雅黑的中英混排也不错。也就是说,与其强行拆分字体,不如选一个“中英通吃且不难看”的字体作为全图默认。这才是工程上最简单的混排方案。
如果你实在想严格区分中英文,还有一个办法:把中文和英文放到不同的Text对象里,人工控制坐标。这个只适合某些图表信息量不大、需要精细排版的情况。
3.3 负号、特殊符号和单位怎么处理
中文字体通常不含标准数学符号U+2212,而坐标轴的刻度负号默认用的是U+2212。当你把默认字体设为中文字体时,部分中文字体因为缺这个字形,会把负号渲染成一个框或者消失。所以必须加上:
plt.rcParams['axes.unicode_minus'] = FalseFalse的意思是用ASCII的连字符减号代替数学负号。虽然不够标准,但至少不会出现方框。如果你对负号样式有要求,比如希望更短的标准负号,可以考虑在字体文件里补齐字符,或者改用某些同时包含数学负号的字体。实际画图场景里,绝大多数人设置成False就足够了。
其他易出问题的符号包括:
- 中文引号、省略号,某些西文字体里没有
- 度数符号
°、正负号±、乘号× - 货币符号
¥、€ - 上标、下标,比如
m²、CO₂
这些符号最好也用一个覆盖面广的字体。我一般优先选择 Noto Sans CJK 或者微软雅黑,因为它们覆盖的CJK和符号区域很全。如果图表里出现一个类似fi的连字符号缺失,那更建议直接替换成普通fi两个字符,别跟字体较劲。
3.4 图例、标题和坐标轴标注的最佳实践
图例里的中英混排比较隐蔽。legend()默认会从plt.rcParams取字体,所以全局设置好之后,图例通常跟着变。但如果你在某个地方单独传了prop=FontProperties(...)给legend,其他地方又没传,风格就会不一致。
我的统一习惯是:
- 在脚本最前面定义好一组
FontProperties对象,比如zh_font、en_font - 全局rcParams设为
font.family = 'sans-serif',font.sans-serif列表第一位放中文字体 - 标题用
ax.set_title(..., fontproperties=zh_font)单独指定 - 坐标轴标签用
ax.set_xlabel(..., fontproperties=zh_font) - 图例用
ax.legend(prop=zh_font) - 刻度文字一般用全局设置
这样能保证绝大多数元素风格一致,同时需要重点强调时又能单独控制。要注意legend的prop参数名是prop,不是fontproperties,写错了会报类型错误。
如果要往图里加注释annotate,同样有fontproperties参数。坐标轴刻度标签默认会用font.family,如果你想单独改刻度标签字体,需要操作ax.xaxis.label或者用ax.tick_params(labelsize=...),但字体风格一般跟全局保持一致就好,不需要特别区分。
4. 高频报错与排查实录
4.1 “findfont: Font family ‘...‘ not found”该怎么查
这是最常见的警告,说明rcParams里写的字体名在matplotlib的字体列表里找不到。常见原因有三个:
- 字体名字拼写不对,比如把
Microsoft YaHei写成MicrosoftYaHei或msyh(微软雅黑的字体族名是Microsoft YaHei) - 系统里确实没有这个字体,但Windows下很多用户以为有
- matplotlib缓存里没有这个字体,需要清缓存重建
排查步骤很简单:
from matplotlib import font_manager # 列出所有已注册的字体名 names = sorted(set(f.name for f in font_manager.fontManager.ttflist)) # 用关键字过滤 matched = [n for n in names if 'YaHei' in n or 'Sim' in n or 'Noto' in n] print(matched)如果匹配不到,说明matplotlib确实不知道这个字体。如果匹配到了,再看你rcParams里写的名字是不是完全一致,注意空格和大小写。
还有一种比较坑的情况:同一个字体族名在ttc文件里可能有多个子字体,比如Noto Sans CJK SC和Noto Sans CJK TC是两个不同名字,你装了含全集的ttc,matplotlib注册的可能只有某一个名字。所以一定要以打印出来的名字为准。
4.2 渲染出来全是方框/豆腐块的处理
方框出现,说明字体文件被找到了,但对应字形不存在,或者字体文件本身加载不完整。处理思路:
- 确认你设置的字体确实能显示中文。用下面这段快速验证:
from matplotlib import font_manager from matplotlib.ft2font import FT2Font font_path = "C:/Windows/Fonts/msyh.ttc" font = FT2Font(font_path) print(font.get_sfnt()) cmap = font.get_charmap() print(0x4E2D in cmap) # 检查“中”字U+4E2D是否存在如果返回False,说明这个字体文件里没有这个汉字。ttc文件里可能有多个子字体,需要指定索引加载。matplotlib的FontProperties不太容易直接控制ttc的subfont index,这种情况下最好换一个单独的ttf/otf文件。
- 查看matplotlib实际渲染文本时用的字体路径:
from matplotlib.textpath import TextToPath path = TextToPath() print(path._get_font(None, 'sans-serif', ['Microsoft YaHei'], '中'))这个方法可以打印出最终选择的字体文件路径。如果路径里是个不认识的西文字体,那说明你的候选列表里中文优先级不够,或者根本没匹配上。
- 还有一种情况是图形后端问题,比如某些Linux下用PGF或PDF后端,字体嵌入规则不一样。如果你保存PDF时中文变方框,可以尝试换一个后端,或者在保存参数里加
bbox_inches='tight'试试。
4.3 缓存导致的“改了什么都没用”
matplotlib字体索引是有缓存的,目的是加速启动。但副作用就是你装了新字体之后,它可能还在用旧索引。具体表现是:清理过字体文件、往项目里加了新字体、修改了字体名称,但不管怎么重启脚本都没变化。
这时候需要强制清理缓存。matplotlib版本不同,缓存位置也不一样,可以用代码定位:
import matplotlib print(matplotlib.get_cachedir())得到目录后,直接删掉这个目录下的所有文件(或者整个目录),下次运行会重新扫描并建立缓存。如果不想手动删,也可以这样:
import matplotlib matplotlib.font_manager._rebuild()但注意,_rebuild是私有方法,不同版本效果可能不同。更稳妥的还是手动删缓存目录。
在Jupyter里改完字体不生效,还有一个容易忽略的点:matplotlib已经导入,fontManager已经初始化。即使删了缓存,当前内核里的fontManager仍然是旧的。需要在删除缓存后重启内核重新导入,或者在不重启的情况下手动重新实例化:
from matplotlib import font_manager font_manager.fontManager = font_manager.FontManager()很多开发者用这招在notebook里热更新字体。
4.4 后端不一致:Agg、TkAgg、Cairo的问题
matplotlib有多个后端,比如Agg(无界面渲染)、TkAgg、QtAgg、Cairo、PDF等。字体渲染在实际显示和保存时可能走不同的代码路径。最常见的异常是:
- 屏幕上显示正常,保存成图片后中文乱掉
- 通过SSH在服务器上用Agg保存图片正常,但用某些GUI后端在本地预览时出问题
- 使用
pgf后中文完全不行,或者使用usetex=True时中文字体不工作
解决办法是先锁定一个后端,再排查其它变量。比如在脚本开头加:
import matplotlib matplotlib.use('Agg')如果你需要交互式窗口预览,可以用TkAgg或QtAgg。注意别混用。在Jupyter里一般用%matplotlib inline或notebook,它也是一个后端选择。
如果你使用plt.savefig时报错说字体找不到,但plt.show()没问题,看看是不是因为保存时dpi或者format的影响。另外,保存PDF时,字体必须能够嵌入到PDF文件中,有些中文字体因为授权或编码原因嵌入失败。这时可以考虑保存为高分辨率PNG,或者使用pdf.fonttype: 42之类的参数调整嵌入方式。
5. 几个容易踩的坑和我最后想说的话
5.1 同一台机器上用了不同Python环境
我遇到过一个项目是在conda环境里跑,系统里面明明装了中文字体,但conda环境里的matplotlib死活不认。后来发现,conda安装的matplotlib可能会自带一份字体缓存,而且它搜索字体的路径里不包含系统的/usr/share/fonts。解决办法还是那句话:用font_manager.addfont显式注册字体文件路径,或者把字体复制到matplotlib搜索目录里。
5.2 别用“把中文字体改成英文名”这种偏方
网上流传一种办法:复制中文字体文件,把文件名改成英文,然后指定这个英文名。说实话这种办法只对部分场景有效,而且绕了远路。因为matplotlib读取的是字体文件内部的family name,不是文件名。你改文件名、复制文件,都不影响内部name。很多时候你改了之后同样报错,反而让自己更困惑。
5.3 混排时不是设置越多越好
有些人会把font.family、font.serif、font.sans-serif全填一遍,加一堆自己都记不住的字体名。这样反而容易触发各种奇奇怪怪的警告。我的建议是最简配置三行起步:
plt.rcParams['font.family'] = 'sans-serif' plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'Noto Sans CJK SC', 'DejaVu Sans'] plt.rcParams['axes.unicode_minus'] = False先让图表正常,再去追求字体风格。如果还想要英文字体特殊化,再按第3节的方法做局部指定。
5.4 做一个一劳永逸的字体配置模块
我最后在项目里是这样做的:把字体配置单独写成模块font_config.py,内容类似:
import os import matplotlib.pyplot as plt from matplotlib import font_manager CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) LOCAL_FONT_DIR = os.path.join(CURRENT_DIR, "fonts") def setup_font(zh_name="NotoSansCJKsc-Regular.otf", en_name="Times New Roman"): zh_path = os.path.join(LOCAL_FONT_DIR, zh_name) if os.path.exists(zh_path): font_manager.fontManager.addfont(zh_path) prop = font_manager.FontProperties(fname=zh_path) zh_family = prop.get_name() else: zh_family = "Microsoft YaHei" # 备用 plt.rcParams['font.family'] = 'sans-serif' plt.rcParams['font.sans-serif'] = [zh_family, en_name, "DejaVu Sans"] plt.rcParams['axes.unicode_minus'] = False return font_manager.FontProperties(family=zh_family)每个脚本开头只要:
from font_config import setup_font zh_font = setup_font()后续画图时如果用得上,就把zh_font传给个别元素。整套配置不再依赖具体系统的预装字体,项目拷到哪里都能跑出一致效果。
5.5 如果还是不行,试试直接用matplotlib的字体管理器看输出
排查到最后,我建议你写一个最小复现脚本,不要带数据集,不要带复杂绘图,只画一个标题、一个x轴标签,里面同时包含中文和英文,然后用不同配置去试。大多数问题都能在一分钟内定位。有人会问:“为什么我按照别人成功的配置还不行?”因为别人的系统情况、matplotlib版本、后端、缓存残留都不一样。最小复现能排除大部分干扰项。
还有一个比较实用的经验:别一上来就更新matplotlib版本。有些老版本虽然字体匹配逻辑不够好,但至少稳定;新版本改了默认字体回退策略之后,反而可能让你原来靠巧合能显示的图变样。如果某个版本下字体工作正常,尽量不要为了升级而升级。
最后分享一个我自己的习惯:每次写完绘图脚本,我会特意把输出图片的字体检查一下,看中英文和负号是否都正常,并把字体版本、操作系统、matplotlib版本一起记在注释里。这样过几个月翻出来用,不会莫名其妙又踩一遍字体坑。matplotlib中英混排的字体问题说到底不是玄学,只是它默认依赖外部字体、又不做自动回退,搞清楚它的字体注册和匹配流程,就能让它听你的话。