如果你在 Windows 上用 Python 画过图表大概率撞见过这个名场面坐标轴标题写着中文画出来却是一排整整齐齐的小方框控制台里还飘着一行黄色警告大意是Font family Microsoft YaHei not found。我第一次遇到这个问题时以为代码写错了排查半天才发现根子不在代码逻辑而在字体匹配。Windows 下的 Matplotlib 中文显示问题看起来是一个小配置实际牵扯到字体注册、字体缓存、rcParams 优先级和不同后端的渲染差异不把这条链路理清今天调好了明天换个环境又得重来。这篇文章不讲虚的直接把我在 Windows 上从“满屏豆腐块”到“中文完美渲染”的排查过程和解决方案完整写出来。内容包括乱码和警告分别代表什么、怎么用代码找出系统里真正可用的中文字体、两条主流配置路线的选择、我踩过的几个高频翻车点、字体缓存的诡异行为以及最后一套可以直接抄进项目的中文绘图基线配置。无论你是刚入门的 Python 脚本选手还是在团队里负责出报表和论文图的老手照着这篇文章的步骤走一遍基本能一次解决。1. 乱码根源Matplotlib 的字体匹配机制与系统字体的“失联”很多人第一次遇到中文乱码时第一反应是“代码里加个编码声明”或者“把中文字符串换成英文”然后发现治标不治本。要彻底解决得先搞清楚 Matplotlib 在渲染文本时到底做了什么。Matplotlib 渲染文字并不像 Word 那样直接调用操作系统的文本渲染引擎它自己做了一层字体管理和字形匹配。绘图时任何文本对象标题、坐标轴刻度、图例、注释都会被交给字体管理器处理。字体管理器会按照你指定的字体列表从它自己的字体注册表里逐个查找匹配项找到后通过 FreeType 等库读取字形数据并渲染到画布上。问题就出在这个字体注册表上它不读 Windows 的字体设置也不会自动把系统中已安装的字体全部加载进来只在初始化时扫描固定的字体目录建立一份内部清单。Windows 的中文字体大多数放在C:\Windows\Fonts其中不少是.ttc集合文件早期版本的 Matplotlib 对.ttc的支持并不完整扫描时容易漏掉或者只注册了集合里的某一个字体面。这就导致你明明在系统里装了微软雅黑Matplotlib 却完全不知道它的存在最终在字体表里找不到任何能显示中文的字体只能退回默认的 DejaVu Sans 家族。DejaVu Sans 是一套非常优秀的开源无衬线字体但它的字符集主要覆盖拉丁字母、希腊字母、西里尔字母等并不包含中日韩统一表意文字CJK。当 Matplotlib 拿着中文字符去 DejaVu Sans 里找字形时找不到对应字形就会画出一个空心的矩形框来占位。这个矩形框在图形界面里显示为一个小方框在保存的图片里也一样。这就是中文变成“豆腐块”的直接原因。1.1 方框与警告两个症状对应的两种失败乱码呈现的方式不同背后的失败环节也不一样排查方向必须区分清楚。第一种情况是只有方框、没有警告。这时候字体管理器其实正常工作也找到了你指定的字体但该字体的字符集不包含中文。最典型的例子是你手动设置了font.family为Arial或Times New Roman。系统里有这个字体Matplotlib 也成功匹配到了它但 Arial 没有中文字形于是所有中文都变成了方框。因为字体本身“存在且匹配成功”所以不会有not found警告问题却依旧存在。第二种情况是既有方框又有findfont: Font family not found警告。这说明字体管理器在你指定的字体列表里完全没找到匹配的字体比如你把字体名写成了中文“微软雅黑”或者在代码里填了.ttc文件的完整路径但没注册。这时 Matplotlib 会直接跳过这个字体名继续尝试列表里的下一个候选如果所有候选都不存在最终落到默认字体上。默认字体没有中文字形于是方框和警告一起出现。还有一种容易混淆的情况字体文件存在但 Matplotlib 的缓存里没有它。比如你刚用安装包装了一款新字体没有重建 Matplotlib 的字体缓存就去运行脚本字体管理器读到的还是旧清单这时候也会报找不到。很多人装了字体之后仍然报错多半是这个原因后面我会单独用一章讲字体缓存。1.2 为什么 Windows 比 Linux 和 macOS 更容易踩坑在 Windows 上装 Python 环境官方发行版和 Anaconda 的分化让字体情况更加复杂。官方 Python 发行版只负责解释器和标准库Matplotlib 通过 pip 安装后它的字体扫描逻辑需要自己去识别 Windows 字体目录。Windows 字体目录里的中文字体以.ttc为主老版本 Matplotlib 对.ttc的解析能力弱容易漏掉部分字体面。Anaconda 环境虽然自带了 conda 的字体配置但 conda 内部的fontconfig配置默认偏向 Linux 风格对 Windows 字体目录的接入同样不完美有时候你单独给 conda 环境装了字体包它反而优先读取 conda 目录下的字体而忽略系统字体。对比来看Linux 上的很多教程会顺带安装fonts-noto-cjk之类的包装完 Matplotlib 自动就能找到中文字体macOS 的字体目录结构相对简单常见中文字体如 PingFang、STHeiti 的名字也比较固定。Windows 则因为中文字体种类多、命名乱、.ttc集合文件占比高成为三个平台里最容易在 Matplotlib 中文字体上翻车的一个。但反过来说只要理解了字体注册和缓存机制Windows 下的解决方案反而最灵活因为它系统内置的中文字体足够多完全不需要额外下载字体包。2. 找到系统里真正能用的中文字体从字体清单到真机验证解决乱码的第一步不是去网上随便搜一段代码抄而是先搞清楚这台 Windows 机器上Matplotlib 到底能看到哪些字体这一步非常关键因为很多人知道要把font.sans-serif设成中文字体却不知道该填哪个名字于是把中文显示名“微软雅黑”直接写进代码自然得不到预期效果。在 Matplotlib 的字体体系里字体名称分几种有字体家族名称比如 Microsoft YaHei、SimHei、SimSun、有字体文件名称比如 msyh.ttc、simhei.ttf、还有系统显示名称。rcParams里需要填的是字体家族名称而不是文件名或中文显示名。想拿到准确清单最直接的办法是遍历font_manager的字体注册表。from matplotlib import font_manager names sorted({f.name for f in font_manager.fontManager.ttflist}) for name in names: print(name)运行这段代码终端会输出一大串字体名比如DejaVu Sans、DejaVu Serif、Microsoft YaHei、Microsoft YaHei UI、SimHei、SimSun、KaiTi等。看到SimHei和SimSun就说明这台机器上黑体和宋体已经被 Matplotlib 正常扫描到了可以直接用于后续配置。2.1 用 FontManager 列出已注册字体清单ttflist是fontManager里的核心属性它是一个列表每一项对应一个已注册的字体文件及其字体面信息。你可以把它理解成 Matplotlib 自己的“字体电话本”Matplotlib 运行时只认这个电话本不认系统的字体设置。列出清单后还需要注意一个问题ttflist里出现某个字体名不代表该字体的符号覆盖范围足够画中文。比如DejaVu Sans也在清单里但它没有 CJK 字符。所以筛选中文字体时更可靠的做法是针对性地检测候选字体的字符覆盖范围但 Matplotlib 没有直接提供“是否包含某字符”的公开 API下面这个做法最实用。2.2 真机渲染验证让候选字体逐个画一段中文与其分析字符集不如直接让候选字体“跑一遍”用真实渲染结果来判断。写一个循环给每个候选字体生成一张小图图上画几个中文词保存成 PNG 后用图片浏览器看一眼。代码大致如下import matplotlib.pyplot as plt candidates [Microsoft YaHei, SimHei, SimSun, KaiTi] for font_name in candidates: plt.rcParams[font.sans-serif] [font_name] plt.rcParams[axes.unicode_minus] False fig, ax plt.subplots(figsize(4, 2)) ax.text(0.5, 0.5, 中文测试字体渲染正常, hacenter, fontsize14) ax.set_title(font_name) fig.savefig(f{font_name}.png, dpi100) plt.close(fig)保存出来的图片里如果标题和文字都正常显示说明该字体可用如果出现方框或空白立刻排除。这个方法看似笨但非常有效尤其是在拿不准某个字体名称时一次验证就能获得准确结论。还有一个实用技巧当你手头有一个.ttc或.ttf文件但不知道它在 Matplotlib 里的字体家族名时可以直接用FontProperties读取。from matplotlib import font_manager fp font_manager.FontProperties(fnamerC:\Windows\Fonts\msyh.ttc) print(fp.get_name())输出结果是Microsoft YaHei这样就能把字体文件和可配置的字体名对应起来。我在处理一些老项目时经常靠这个方式确认字体名避免写错。3. 两条配置路线快速修改 rcParams 与永久改写 matplotlibrc在 Windows 下把中文字体配好有两条路线一是在代码里动态修改rcParams二是修改 Matplotlib 的配置文件matplotlibrc。两条路线没有绝对的好坏取决于你的使用场景。我自己的习惯是写一次性脚本或调试阶段用rcParams长期维护的项目或者要交付给团队统一使用直接改配置文件。3.1 快速做法脚本内动态配置这种方案适合在脚本开头就设置好几分钟内看到效果。最精简的配置是两行import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [Microsoft YaHei, SimHei, SimSun] plt.rcParams[axes.unicode_minus] False第一行设置字体列表。有人会问为什么是font.sans-serif而不是font.family因为font.family在 Matplotlib 里是一个大类别的设置通常指定serif、sans-serif、monospace等类型而真正控制无衬线字体具体用哪一款的是font.sans-serif这个列表。更通俗地说font.family决定“我要用哪一类”font.sans-serif决定“这一类里具体用哪一款”。设置成列表而不是单个字符串是为了让 Matplotlib 能按顺序尝试第一个找不到就找第二个类似一个兜底链。我把微软雅黑放第一位黑体和宋体作为后备哪怕微软雅黑在某台机器上没被扫描到后面的 SimHei 也能顶上。第二行的axes.unicode_minus设置为False是解决坐标轴负号显示为方框的必选项这一行几乎每个做中文图表的人都踩过坑后面会有专门的小节展开讲。这种动态配置的优点是即时生效、不污染全局环境缺点是每个脚本都要写一遍一旦某个脚本忘了写图表又变回满屏方框。所以我在团队里要求所有绘图脚本的入口处必须封装一个公共配置模块把这套代码放进去统一调用。3.2 永久做法修改全局配置文件 matplotlibrc如果你受够了每个脚本重复写配置直接把方案固化到 Matplotlib 的配置文件里让所有脚本默认带上中文字体设置。先找到配置目录import matplotlib print(matplotlib.get_configdir())在 Windows 上通常是C:\Users\用户名\.matplotlib也可能是C:\Users\用户名\AppData\Local\matplotlib。进入目录后用文本编辑器打开matplotlibrc找到或新增以下几个配置项font.family: sans-serif font.sans-serif: Microsoft YaHei, SimHei, SimSun, DejaVu Sans axes.unicode_minus: False保存后重启 Python 环境。从此以后这台机器上所有使用 Matplotlib 的脚本默认就会带上中文字体配置不需要在代码里重复设置。需要注意的是font.sans-serif这一行如果原来已经存在不要简单地在后面追加因为旧配置里可能带着一长串 DejaVu 字体建议把中文字体放到列表前几位Matplotlib 按顺序匹配时会优先命中中文字体。永久配置的优点很明显一次配置所有脚本都受益。缺点也同样明显——配置文件是跟着你的用户目录走的换一台机器、换一个用户账号配置就失效了。如果项目需要多人协作新成员拉完代码在自己的机器上直接跑依然会遇到中文乱码。所以要结合场景来选择方案不能无脑改全局文件。另外还有一个环境变量值得了解MPLCONFIGDIR可以指定 Matplotlib 使用另一个配置目录。在搭建多环境隔离的测试任务时我会专门给某个任务设置独立的配置目录避免影响全局配置。4. 高频翻车点负号、字体覆盖、非 GUI 环境与版本差异配置代码就那几行很多人照着写却仍然显示不正常原因是几个非常隐蔽的细节没处理好。我在项目里排查过大量类似问题下面这几个坑是最常见的。4.1 负号变方框unicode_minus 的坑你可能会发现设置完中文字体后中文标签正常了但坐标轴上的负号变成了细长的方框。这其实是另一个独立的问题Matplotlib 默认使用 Unicode 的 U2212 减号来渲染数学负号而不是普通的 ASCII 连字符-。很多中文字体尤其是微软雅黑和宋体并没有包含 U2212 这个字形于是渲染时就出现了方框。解决办法就是plt.rcParams[axes.unicode_minus] False。设置之后Matplotlib 遇到负号会直接用 ASCII 的-虽然字形不如 U2212 美观但绝大多数中文字体都带这个字符显示一定正常。这个小问题的坑在于字体设置正确的人看不出它和中文乱码之间的关联往往会以为是字体没配好反复调整字体列表但徒劳无功。所以我的习惯是只要涉及中文图表unicode_minus和font.sans-serif永远成对出现。4.2 字体名写对了图还是乱码优先级与覆盖问题配置了font.sans-serif却仍然乱码就要检查是不是有其他代码在之后重新修改了 rcParams。典型场景是使用了带主题功能的第三方库比如 seaborn。sns.set_theme()或者sns.set_style()在执行过程中会覆盖一部分 Matplotlib 的 rcParams其中就可能包括font.family和font.sans-serif导致你先前好不容易配好的中文字体被重置。解决方式很简单把中文字体配置放在第三方库主题设置之后而不是之前。如果你在一个脚本里既调用 seaborn 又设置中文字体顺序应该是import seaborn as sns import matplotlib.pyplot as plt sns.set_theme(stylewhitegrid) plt.rcParams[font.sans-serif] [Microsoft YaHei, SimHei, SimSun] plt.rcParams[axes.unicode_minus] False另一个常见的覆盖场景是reset_defaults()。有些人在 Jupyter Notebook 里调试时会调用matplotlib.rcParams.update(plt.rcParamsDefault)或matplotlib.rcdefaults()来恢复默认配置结果把中文字体也一起重置了。Jupyter 的单元格执行顺序还会造成非直观的影响先执行了某个单元格设置好中文字体之后又执行了包含 reset 的单元格再画图时中文字体就丢了。排查这种问题建议每次绘图前都确认一下当前的font.sans-serif到底是什么。4.3 纯脚本保存与 GUI 窗口渲染不一致Windows 下常见的 Matplotlib 后端包括TkAgg、QtAgg等画图时会在桌面弹出一个交互窗口。有些同学发现弹出的窗口里中文正常但savefig保存出来的图片里中文变成方框或者反过来保存的图片正常GUI 窗口显示异常。这种不一致是后端处理和字体解析方式不同导致的。在纯脚本环境或服务器环境里没有显示器可用必须使用非交互式后端Agg此时没有 GUI 窗口savefig是唯一的输出通道。建议在脚本开头显式指定import matplotlib matplotlib.use(Agg) import matplotlib.pyplot as plt这行要在导入pyplot之前执行否则不起作用。指定Agg后所有绘图都以文件输出为准字体配置的行为也更稳定。如果脚本既要在有显示器的机器上交互查看又要在服务器上定时运行可以在代码里根据是否处于交互环境做分支判断但更稳妥的做法是统一使用Agg输出图片后用图片浏览器查看结果。4.4 不同版本和发行版的差异Matplotlib 对.ttc字体集合的支持是在 3.2 左右大幅改进的。如果你还在用 3.1 及更早的版本系统里的微软雅黑很可能不会被完整注册你设置了Microsoft YaHei却还是提示找不到。遇到这种情况先升级 Matplotlib 到当前稳定版很多时候乱码问题直接消失。conda 环境又另有一套体系。conda 安装的 Matplotlib 可能链接到 conda 自带的fontconfig它会优先扫描 conda 环境目录下的字体而不是 Windows 系统字体目录。如果你在 conda 环境里画中文用font_manager列出的字体清单会发现SimHei、SimSun可能根本不在里面。解决方式有两种一是直接把 Windows 字体文件复制到 conda 环境的字体目录并重建缓存二是用代码显式指定字体文件路径并注册后面讲兜底函数时会给出详细代码。5. 字体缓存的两难为什么删掉字体后仍提示找不到字体缓存是我排查中文乱码时花时间最多的地方也是很多人反复栽跟头的隐藏雷区。这个缓存机制本来是提升性能用的但在某些操作顺序下会变成麻烦制造者。5.1 缓存的构建机制Matplotlib 的font_manager在第一次被使用时会扫描一组字体目录包括系统字体目录、用户字体目录等然后把扫描到的字体信息保存到一个 JSON 格式的缓存文件里。这个文件的位置通过get_cachedir()获取Windows 下通常是C:\Users\用户名\AppData\Local\Matplotlib文件名类似fontlist-v330.json。之后每次启动脚本只要缓存文件存在且没有过期font_manager就直接读取缓存不再重新扫描全部字体目录。这样做的原因是字体文件数量多、解析慢每次启动都全量扫描会拖慢性能。问题就出在这个“缓存优先”的机制上。如果你在系统里新安装了字体或者删除了某个字体文件Matplotlib 不一定能感知到变化。它会继续使用旧的缓存文件直到缓存失效或被手动清理。5.2 缓存滞后带来的怪异行为有一种我亲身踩过的坑是这样的某个字体文件损坏导致画图时弹出异常提示于是我手动把该字体从C:\Windows\Fonts里删掉了重新运行代码发现font_manager.fontManager.ttflist里居然还列出了这个字体。这就是缓存在起作用——缓存里的记录并没有因为磁盘上的文件被删除而自动消失。反过来也成立你刚安装了一款新字体希望立刻在 Matplotlib 里使用但ttflist里找不到它因为缓存的构建时间早于字体安装时间。所以遇到“字体明明装了但 Matplotlib 不认”或“字体明明删了但 Matplotlib 还在用”的情况第一步不是怀疑配置写错而是清理字体缓存。5.3 三种可选的缓存清理方法最直接的方法是手动删除缓存文件。在资源管理器里打开matplotlib.get_cachedir()返回的目录删除所有fontlist-*.json文件下次运行脚本时 Matplotlib 会自动重新扫描字体目录并重建缓存。为了省事也可以直接把整个缓存目录内容清空效果相同。用代码清理可以写成一个安全的小段import matplotlib from pathlib import Path cache_dir Path(matplotlib.get_cachedir()) for f in cache_dir.glob(fontlist-*.json): f.unlink()还有更省事但不太建议的方式设置环境变量MPLCONFIGDIR指向一个全新的空目录相当于给 Matplotlib 一个全新环境它首次运行时会重新扫描所有字体。这个方法适合多用户隔离场景但不适合日常使用因为新目录里没有缓存也没有配置首次运行反而会变慢。需要注意清理缓存时要确保没有其他 Matplotlib 进程正在运行否则缓存文件被占用删除可能失败。我在处理 CI 任务里的乱码问题时每次都会在安装字体后加一步缓存清理指令再跑绘图用例。顺便说一句Windows 系统本身还有一个服务叫 FontCache它维护系统层面的字体缓存有时删除字体后系统的字体列表仍显示旧状态。如果你的 Python 环境读取系统字体的方式绕过了 Matplotlib 而直接走 Windows API也可能受到系统 FontCache 的影响。这时候去控制面板的字体设置里刷新一下或者重启相关进程会让系统层级的字体状态恢复一致。6. 把解决方案固化下来一套可直接复用的中文绘图基线配置讲完原理和坑位最后给你一套我目前在项目中实际使用的中文绘图基线配置。它解决两个痛点一是不同机器字体清单可能不同配置不能写成死值二是如果系统里实在没有可用中文字体还能用手动注册字体文件的方式兜底。6.1 一个兜底函数先匹配已注册字体再手动注册字体文件下面的函数放在公共模块里任何人都可以直接调用不依赖本机具体字体情况import os import matplotlib.pyplot as plt from matplotlib import font_manager def setup_chinese_font(candidatesNone): if candidates is None: candidates [Microsoft YaHei, SimHei, SimSun, KaiTi, Noto Sans CJK SC] available {f.name for f in font_manager.fontManager.ttflist} chosen next((c for c in candidates if c in available), None) if chosen is not None: plt.rcParams[font.sans-serif] [chosen] list(plt.rcParams[font.sans-serif]) plt.rcParams[axes.unicode_minus] False return chosen # 手动注册字体文件作为兜底 fallback_paths [ rC:\Windows\Fonts\msyh.ttc, rC:\Windows\Fonts\simhei.ttf, rC:\Windows\Fonts\simsun.ttc, rC:\Windows\Fonts\simkai.ttf, ] for font_path in fallback_paths: if os.path.exists(font_path): font_manager.fontManager.addfont(font_path) prop font_manager.FontProperties(fnamefont_path) plt.rcParams[font.sans-serif] [prop.get_name()] list(plt.rcParams[font.sans-serif]) plt.rcParams[axes.unicode_minus] False return prop.get_name() print(Warning: no suitable CJK font found) return None逻辑很直白先查已注册字体清单命中一个中文字体名就用它如果清单里一个中文字体都没有就回退到常见 Windows 字体路径用addfont手动把字体文件注册进 Matplotlib再用FontProperties(fname...)拿到准确的字体家族名。这种兜底思路比直接写死某个字体名要可靠得多因为不同 Windows 版本自带的字体集合不完全一样某些精简版系统可能连宋体都没有。6.2 脱离本机字体捆绑分发字体文件当脚本要交给别人运行或者要部署到 CI、Docker、远程服务器时你的 Windows 本机配置统统不算数因为目标环境完全未知。最稳健的做法是把一款开源中文字体文件放进项目目录启动时直接注册。推荐使用思源黑体的单字重版本开源许可允许再分发文件大小可以在几 MB 到十几 MB 之间权衡选择。from matplotlib import font_manager font_manager.fontManager.addfont(fonts/NotoSansSC-Regular.otf) prop font_manager.FontProperties(fnamefonts/NotoSansSC-Regular.otf) import matplotlib.pyplot as plt plt.rcParams[font.family] prop.get_name() plt.rcParams[axes.unicode_minus] False要注意设置font.family时不能用字体文件名必须用prop.get_name()返回的字体家族名否则还是匹配不上。这一套方案唯一的代价是项目体积多出几 MB 字体文件换来的好处是所有运行环境渲染效果完全一致不会被 Windows 版本差异、系统字体缓存或 conda 字体配置干扰。6.3 保存矢量格式与交付检查清单配置只是第一步最终交付的图表格式同样会影响中文显示效果。我的建议是需要嵌入文档的场景优先导出 PDF 或 SVG需要直接放进 PPT 或 Word 的可以导出 EMF网页和快速预览用 PNG。一个简单的对比表格可以参考格式适用场景中文渲染要点PNG网页、快速查看分辨率固定放大可能发虚PDF论文、打印矢量效果好需确认对方环境有该字体SVG网页、后续编辑矢量效果但部分浏览器字体缺失时会显示异常EMFWord/PPTWindows 下嵌入稳定适合办公文档交付前的检查清单我一般会逐项过一遍设置字体后中文字符在标题、坐标轴标签、图例中是否都正常。设置axes.unicode_minusFalse后坐标轴负号是否显示正常。在干净环境里重新运行一次脚本确认是否还依赖本机的字体缓存。导出 PDF 或 SVG 后用其他设备打开确认字体没有被替换。把这些检查项固化到团队的项目文档里能避免很多“我这边是好的”式的沟通问题。最后分享一个我个人的习惯第一次配好中文字体之后我不会马上关掉脚本而是顺手画一张包含中文标题、中文刻度和负号的测试图保存成 PNG 和 PDF 各一份。PNG 用来快速看效果PDF 用来验证矢量输出。这样一套组合拳下来基本可以确认这台机器的中文渲染链路是完全通的。把这个测试流程写进团队的初始化验证脚本里后续每个人换新机器时跑一遍有问题的机器一眼就能看出来不用反复折腾。