Matplotlib中文乱码彻底解决:字体原理与三种修复方法

发布时间:2026/10/12 10:19:22
Matplotlib中文乱码彻底解决:字体原理与三种修复方法 做数据分析的人十有八九都被Matplotlib的中文乱码折磨过一次。辛辛苦苦画出来的图标题、坐标轴标签、图例只要带上中文就集体变成一个个小方框有些场景下甚至连方框都不给直接显示成空白。我第一次遇到这个问题的时候第一反应是编码不对于是把文件头改成utf-8又给源码加了一堆编码声明折腾了半天毫无起色。后来才搞明白这种事和编码没有半点关系真正的根源在Matplotlib的字体体系上——默认情况下它能用来渲染文字的字体列表里根本没有中文字体。这篇文章把我踩过的坑和最终解决的完整路径整理出来包含原理、排查步骤、三种修法和几个必然遇到的连带问题。不管你是刚入门的新手还是在服务器上部署绘图脚本的老手照着操作都能彻底解决中文显示问题。1. 乱码根因Matplotlib的字体回退机制1.1 乱码的三种典型形态我见过不少朋友发图求助乱码的形态其实不止一种。最常见的是豆腐块也就是一个个空心矩形第二种是直接空白文字区域什么都没有第三种是乱码字符比如出现一些莫名其妙的繁体字、生僻字或者特殊的符号。前两种是Matplotlib找不到能渲染中文字符的字形最后一种往往是字体本身包含的字形和你想表达的字不匹配或者系统字体被某些工具污染了。不同形态对应的问题不完全一样但绝大多数情况都是前两种。判断方法也很简单看英文和数字能不能正常显示。在同一张图里如果英文、数字都正常只有中文出问题那就基本可以断定是字体缺字而不是编码问题。这个结论非常重要它可以帮你省掉大量无意义的排查时间。1.2 默认字体列表为什么容不下中文Matplotlib渲染文本时会在一组候选字体中逐个查找看哪个字体包含当前字符对应的字形。这个候选字体列表存在配置文件里默认是DejaVu Sans这类西文字体外加一些类似Bitstream Vera Sans、Arial的备选。问题在于这些字体设计的初衷是覆盖拉丁字母、希腊字母、数字和常用符号它们的字形表里根本没有汉字所在的Unicode区间。我最初以为Matplotlib会自动调用操作系统里已安装的中文字体实测下来完全不是这样。Matplotlib启动时会扫描一部分系统字体目录并把扫描到的字体登记到内部的fontManager里但登记归登记它不会自作主张把中文字体加进默认的候选列表。也就是说哪怕你Windows系统里装了微软雅黑macOS里装了苹方Matplotlib默认也不会用它们去渲染中文。它只会沿着默认列表走一遍所有字体都渲染不了汉字最后丢给你一个方框。1.3 一个核心结论改字体而不是改编码我后来在某高校的Python课上帮一个A同学排查问题他也是死活画不出中文代码里文件头和打印输出都用了utf-8甚至把操作系统区域设置都换了还是满屏方框。我让他直接在脚本里加一行指定字体问题当场消失。这件事给了我一个很深的印象Python 3的字符串本身已经是Unicode数据从文件读进来、写出去都不会丢真正卡住的是最后一步——渲染引擎能不能找到对应的字形。所以结论很简单解决中文乱码核心是让Matplotlib使用一个包含中文字形的字体而不是去折腾编码。看清这一点之后后面的所有操作都是围绕如何把合适的中文字体交给Matplotlib展开的。2. 动手前先确认系统里到底有哪些中文字体2.1 查看系统字体能不能反手帮你不要一上来就改配置先看看操作系统里有没有可用的中文字体。Windows系统一般自带黑体、微软雅黑、宋体macOS自带苹方、华文黑体、Hiragino Sans GB很多Linux发行版自带文泉驿正黑也有一些装了思源黑体。需要特别注意的是字体文件存在不代表Matplotlib认得但系统这个层面连文件都没有后面就白忙活了。查看方式各系统不太一样。Windows可以在设置里看字体也可以直接打开C:\Windows\Fonts目录macOS可以用字体册应用或者去/System/Library/Fonts、~/Library/Fonts目录看Linux可以在终端执行fc-list :langzh这个命令会列出所有支持中文的字体。我个人最推荐在Linux和macOS上用fc-list信息非常直接。Windows上也可以用PowerShell跑一段命令但不如直接看Fonts目录直观。2.2 检查Matplotlib已识别的字体系统层面有字体之后第二步是确认Matplotlib到底识别了哪些。用一段很短的代码就能查from matplotlib import font_manager fonts {f.name for f in font_manager.fontManager.ttflist} cn_keywords [Hei, YaHei, PingFang, WenQuanYi, Noto Sans CJK, Source Han] matched [f for f in sorted(fonts) if any(k in f for k in cn_keywords)] print(matched)如果输出是空列表说明Matplotlib没有扫描到任何中文字体。这时候要检查字体目录是否在扫描范围内或者干脆跳过这一步直接采用后面我讲的指定字体路径方法。如果输出里有一些名称比如SimHei、Microsoft YaHei、PingFang SC那说明字体文件已经登记在案接下来只需要在配置里把它加入候选列表即可。我习惯把这一步叫作字体体检。它虽然不能直接解决问题但能让你少走很多弯路。有一次我在一台刚装的Linux服务器上排查问题系统里明明有文泉驿字体但Matplotlib就是不用执行上面的代码才发现ttflist里根本没有它。后来查明是字体文件权限不对Matplotlib进程读不到。2.3 五分钟复现一次乱码建议你先把问题复现出来后面改完配置可以对比效果。一段最简代码就行import matplotlib.pyplot as plt plt.plot([1, 2, 3], [1, 4, 9]) plt.title(中文标题测试) plt.xlabel(横轴名称) plt.ylabel(纵轴名称) plt.show()正常运行后如果标题和坐标轴文字出现方框说明问题复现成功。把这段代码留好后面每一步配置改完都跑一次能非常直观地验证是否生效。我强烈建议不要只改配置凭感觉判断一定要用真实图片确认因为有时候配置改错位置或者被其他设置覆盖你以为解决了下一张图又原形毕露。3. 按场景选择的三种修法3.1 临时调试最稳的方式指定字体路径如果你不想动全局配置只是临时画一张图我推荐直接指定字体文件路径。这种做法的好处是彻底绕开Matplotlib的字体扫描机制管它能不能被识别你告诉渲染引擎就用这个文件。import matplotlib from matplotlib import font_manager font_path C:/Windows/Fonts/simhei.ttf # Windows 黑体 font_manager.fontManager.addfont(font_path) font_name font_manager.FontProperties(fnamefont_path).get_name() print(font_name) matplotlib.rcParams[font.sans-serif] [font_name, DejaVu Sans] matplotlib.rcParams[axes.unicode_minus] Falseaddfont是Matplotlib较新版本提供的接口作用是把字体文件动态注册进fontManager。注册完之后用FontProperties从同一个文件取出来font_name这个名字才是Matplotlib内部认识的名字。有人喜欢直接写SimHei之类的中文名但如果你不确定字体文件对应的内部名是什么用get_name()是最保险的。macOS上比较常写的路径是/System/Library/Fonts/PingFang.ttc注册后拿到的名字通常是PingFang SC。Linux上如果有Noto Sans CJK路径一般在/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc。这种方式把字体文件当作一次性原料注册进当前进程一旦脚本重启它又会被遗忘。所以适合快速验证、临时绘图不适合长期项目。3.2 日常全局切换rcParams配置用得最多的其实是rcParams这种配置方式。它不需要每次重新注册字体文件前提是目标字体已经被Matplotlib扫描到。你只要在脚本开头把所有需要的中文字体名称写进候选列表import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, PingFang SC, WenQuanYi Zen Hei, DejaVu Sans] plt.rcParams[axes.unicode_minus] False这段配置的意思是当需要渲染sans-serif字体时按顺序在候选字体里找先试黑体不行试微软雅黑再不行试苹方直到找到能显示当前字符的字体为止。我这么组合的原因很现实同一份脚本可能要跨Windows、macOS、Linux跑不同系统可用的中文字体不一样。把多套系统的字体都塞进去哪个系统能匹配就用哪个灵活性最高。使用rcParams的前提是上面字体体检那一步确认字体已经被识别。如果出现findfont: Font family xx not found的警告说明候选字体列表中有一个或多个字体名不在fontManager的登记表里这时候就要回到3.1把对应字体文件addfont进去或者换一个能被识别的字体名。3.3 一劳永逸修改matplotlibrc配置文件rcParams的方案每写一个脚本都要复制粘贴一遍虽然简单但还是不够省心。更推荐的做法是直接修改Matplotlib的配置文件matplotlibrc让所有脚本启动时自动加载中文字体配置。先定位配置文件位置import matplotlib print(matplotlib.matplotlib_fname()) print(matplotlib.get_configdir())第一个输出是配置文件路径第二个是配置目录。找到文件后编辑它找到下面这几行#font.family: sans-serif #font.sans-serif: DejaVu Sans, Bitstream Vera Sans, Lucida Grande, Verdana, Geneva, Lucida, Arial, Helvetica, Avant Garde, sans-serif #axes.unicode_minus: True把注释去掉改成font.family: sans-serif font.sans-serif: SimHei, Microsoft YaHei, PingFang SC, WenQuanYi Zen Hei, DejaVu Sans axes.unicode_minus: False保存之后重启Python内核再运行绘图脚本配置就会全局生效。这样以后新建的脚本不需要再写任何rcParams直接画图就是中文。我通常会把SimHei、Microsoft YaHei、PingFang SC、WenQuanYi Zen Hei这组字体名固定写在前面再兜底一个DejaVu Sans保证任何系统上都能找到可用字体。有一点必须提醒matplotlibrc的解析是按块来的如果你在脚本里用plt.rcParams覆盖了配置以脚本里的为准。而且matplotlibrc只改配置文件还不够如果Matplotlib的字体缓存里没有目标字体的记录配置了字体名也没用。所以最稳的配合方式还是先addfont注册再改rcParams或matplotlibrc。4. 两个容易被忽略的连带坑4.1 负号变方块axes.unicode_minus只解决了中文还不够我发现很多人紧接着会遇到第二个问题坐标轴上的负号变成方块。这是Matplotlib的一个历史设计。默认情况下Matplotlib渲染负号用的是Unicode字符集里的真正的负号U2212而不是普通键盘上的减号连字符U002D。DejaVu Sans这套默认字体恰好包含了这个字符所以默认状态下负号显示正常。问题在于一旦你换成SimHei或微软雅黑这些中文字体的字形表里不一定包含U2212于是原本正常的负号反而变方块。解决方案就是前面反复出现的那一行plt.rcParams[axes.unicode_minus] False它的作用很简单告诉Matplotlib用ASCII的连字符号来渲染负号不碰Unicode负号。这行配置我建议和字体配置永远绑定在一起不管在文件里还是代码里写成一对。我最开始不懂这个原理中文字体换好后负号又出问题还以为字体配置有冲突白折腾了很久。4.2 字体缓存不刷新Matplotlib内部有一层字体缓存机制它会把扫描到的字体文件信息保存到本地缓存目录用来加速下次启动。这个机制本身不复杂但有个烦人的副作用当你往系统里新装了一个中文字体或者手动更新了字体文件Matplotlib可能还拿着旧的缓存信息导致新字体怎么都不生效。遇到这种情况先找到缓存目录import matplotlib print(matplotlib.get_cachedir())然后把这个目录下的文件删掉重启Python内核Matplotlib会重新扫描字体目录并重建缓存。不同版本的操作系统上缓存位置略有差异有的在用户目录下的.matplotlib有的在.cache/matplotlib以get_cachedir()的输出为准。删除缓存不影响任何数据最多让Matplotlib启动时慢几秒因为要重新扫描一次。我个人的习惯是新增字体、修改字体文件、或者跨系统搬运项目时先清一次缓存。这个小操作能排除掉很多莫名其妙的问题尤其是那种照着配置写了但就是不生效的情况。4.3 字体名声明的顺序陷阱font.sans-serif是一个列表顺序是有讲究的。Matplotlib会从前往后逐个尝试第一个能渲染目标字符的字体就被选中。所以原则上要把最想要的字体放前面兜底字体放最后。但这里有一个我踩过的坑如果某个字体被安装了但实际能渲染的字符集很有限你把它放在列表最前面结果不仅是中文连部分英文都显示异常。这是因为Matplotlib认为这个字体已经有字形了不会继续往后找。比如把某个特殊符号字体放在SimHei前面图中普通的英文字母就会用它来渲染风格看起来非常奇怪。所以列表的排列原则应该是中文字体优先但不要选那些字符覆盖范围过窄的字体顶在最前面。SimHei和微软雅黑这类覆盖拉丁、中文、常用标点的字体放在第一位基本是安全的。另一个细节是font.family和font.sans-serif的关系。默认font.family是sans-serifMatplotlib在渲染时会根据family去查找对应的serif或者sans-serif列表。如果你直接把某个中文字体名写进font.family相当于把整条链路换了很多渲染配置会受影响。所以我通常只改font.sans-serif保持font.family不变这样只会替换sans-serif候选字体影响范围更可控。5. 不同环境的字体清单与服务器部署5.1 三个常用系统的中文字体对照不同操作系统的字体名称和文件路径差异很大我整理了一个简单的对照表方便你在不同环境里快速查找。系统字体名Matplotlib内部名典型文件路径备注WindowsSimHeiC:\Windows\Fonts\simhei.ttf黑体系统自带最常用WindowsMicrosoft YaHeiC:\Windows\Fonts\msyh.ttc微软雅黑现代感强WindowsSimSunC:\Windows\Fonts\simsun.ttc宋体适合正式文档macOSPingFang SC/System/Library/Fonts/PingFang.ttc苹方macOS 10.11macOSHiragino Sans GB/System/Library/Fonts/Hiragino Sans GB.ttc冬青黑体macOSSTHeiti/System/Library/Fonts/STHeiti Light.ttc华文黑体LinuxWenQuanYi Zen Hei/usr/share/fonts/wenquanyi/wqy-zenhei/wqy-zenhei.ttc文泉驿正黑LinuxNoto Sans CJK SC/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc思源黑体出现ttc后缀的文件时要注意ttc是字体集合文件里面可能包含多个字重或多个子字体。addfont之后用FontProperties(fnamepath).get_name()取到的名字才是这个集合在Matplotlib里登记的名字。不同文件对应的内部名可能和你直觉里想的字体名不一样务必用代码确认。5.2 服务器上没装字体的处理服务器环境是另一个高发区。很多生产环境里的绘图脚本跑在Docker容器或者精简版的Linux服务器上这类环境往往什么都没装fc-list执行结果几乎是空的。这种情况不要慌思路很简单没有字体就手动放一个进去。你可以从开源渠道下载一个中文字体文件比如Noto Sans CJK的otf或ttf放到服务器上一个固定目录比如/opt/fonts/。然后在绘图脚本里用addfont注册import matplotlib from matplotlib import font_manager font_manager.fontManager.addfont(/opt/fonts/NotoSansCJK-Regular.ttc) prop font_manager.FontProperties(fname/opt/fonts/NotoSansCJK-Regular.ttc) name prop.get_name() matplotlib.rcParams[font.sans-serif] [name, DejaVu Sans] matplotlib.rcParams[axes.unicode_minus] False我一般不放系统目录而是放在项目或用户目录里。这样做的原因有两个第一普通用户可能没有写入系统字体目录的权限第二把字体文件跟着项目走迁移环境时不容易丢。如果你用Docker直接在构建镜像时把字体文件COPY进去就行这样每次启动镜像都自带中文字体不需要额外处理。5.3 一套可以复用的配置函数为了避免每次写脚本都重复字体配置我把这部分封装成了一个函数放到一个公共工具文件里哪个项目用到就直接调用import matplotlib from matplotlib import font_manager def setup_cn_font(preferred_fontsNone): font_paths [ C:/Windows/Fonts/simhei.ttf, /System/Library/Fonts/PingFang.ttc, /usr/share/fonts/wenquanyi/wqy-zenhei/wqy-zenhei.ttc, /usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc, ] for path in font_paths: try: font_manager.fontManager.addfont(path) except FileNotFoundError: continue fallback [DejaVu Sans] candidates preferred_fonts or [SimHei, Microsoft YaHei, PingFang SC, WenQuanYi Zen Hei, Noto Sans CJK JP] matplotlib.rcParams[font.sans-serif] candidates fallback matplotlib.rcParams[axes.unicode_minus] False这段代码的思路是先把多个常见系统里的中文字体文件路径全都尝试注册一遍能注册哪个算哪个然后把字体名候选列表写进rcParams最后统一关闭unicode负号。在Windows上它会注册SimHei在macOS上注册PingFang SC在Linux上注册文泉驿或Noto一套代码横跨三个平台。函数里用了try-except跳过不存在的路径不会因为找不到文件而报错。6. 从报错到验证完整排障链路6.1 读懂常见的三条警告排查过程中Matplotlib会输出一些日志很多人看到英文警告就紧张其实它们含义相当明确。第一条是findfont: Font family SimHei not found意思是候选列表里写了SimHei但fontManager的登记表中找不到它。遇到这个警告说明字体文件没有被扫描或注册解决方式是先执行addfont或者检查系统字体目录里有没有SimHei。第二条是findfont: Glyph missing from current font这是指当前选中的字体里缺少某个字符对应的字形。更常见的表现就是方框、空白或问号。中文出现这个问题说明你选的字体本身不支持中文或者字体覆盖范围不够需要换一个更完整的中文字体。第三条是Matplotlib is building the font cache using fc-list这类日志一般出现在你删除缓存或首次启动时。它不是错误只是提示Matplotlib正在重建字体缓存耐心等它跑完就行。如果这步执行很久还没停可能是字体目录里的文件太多或者有损坏的字体文件需要检查一下字体目录。6.2 一张图结束乱码问题把上面的方法全部串起来我给你一个我实测过多次的完整示例直接复制就能用import matplotlib import matplotlib.pyplot as plt from matplotlib import font_manager font_manager.fontManager.addfont(C:/Windows/Fonts/simhei.ttf) matplotlib.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, DejaVu Sans] matplotlib.rcParams[axes.unicode_minus] False fig, ax plt.subplots(figsize(8, 4.5)) x [1, 2, 3, 4, 5] y1 [2.5, 3.6, 4.1, 3.8, 5.2] y2 [-1.2, -2.1, -1.8, -3.0, -2.6] ax.plot(x, y1, markero, label营业数据) ax.plot(x, y2, markers, label变化幅度) ax.set_title(2025年第一季度数据对比) ax.set_xlabel(月份) ax.set_ylabel(金额万元) ax.legend() ax.grid(axisy, linestyle--, alpha0.6) plt.show()这个例子里既有中文标题、中文标签也有带负号的数据一次跑通可以同时验证字体和负号两个问题。如果你换到别的系统把addfont那行的路径改成对应环境下的中文字体文件即可。6.3 我总结的排查口诀最后把这套排查逻辑压缩成几句口诀方便你日后快速定位先看系统字体有没有再查Matplotlib的ttflist认不认。临时解决用addfont指定路径长期解决改matplotlibrc。负号和字体一起配unicode_minus关了它。有了新字体不生效删掉缓存再重启。最后拿张真实图做回归确认中文和负号都正常。这套流程我前前后后用了很多次从Windows笔记本换到macOS再把绘图脚本部署到Linux服务器基本没有失手过。中文乱码这个事不复杂但涉及的环节不少——系统字体、Matplotlib扫描机制、字体名匹配、缓存策略、负号处理——每个环节都可能卡一下。把这些环节都理清楚之后它就再也不是问题。最实用的经验是永远用指定路径注册 候选字体列表 关闭unicode负号这三件套几乎能挡掉所有后续的坑。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询