有没有遇到过这种场景:Python 代码跑得顺顺利利,数据算得也没问题,但plt.title()一执行,出来的图标题和坐标轴标签全变成一个个空心方块,有些环境里直接是一串乱码——明明数据没问题,图却没法看。这个问题我在实操中碰到的频率非常高,几乎每带一个新人就会重现一次,可以说“Matplotlib 中文显示”是纯数据裸奔阶段最容易劝退人的一道坎。
这篇文章就从我这些年调试 Matplotlib 的经验出发,把中文显示失败的原因、不同系统下的解决方案、字体缓存清理、负号方块这些连带问题一次讲清楚。无论是刚装好 Python 想画第一张图的新手,还是被中文乱码折腾到头秃的资深工程师,都能在这里找到对应的解法。当然,文章最后还会分享一个我自己反复使用的“字体体检函数”,帮你随时确认环境是否正常。
1. 先认清问题:中文乱码到底长什么样,为什么会出现
1.1 三种典型故障现象
中文显示异常通常不是一种表现,而是分好几种,我见过的至少有三类:
第一种是空心方框,这也是最常见的情况。图里的中文变成了一个个小方框,中文本身“消失”了。这种问题本质不是“不能显示中文”,而是渲染引擎找不到支持中文的字体,用默认字体顶替,结果默认字体里没有中文字形,只能渲染出占位符。
第二种是乱码,英文数字正常,中文变成䏿之类不可读内容。这种一般就不是字体的问题了,而是文件编码问题——脚本文件保存成 UTF-8,但运行环境读取时用了别的方式;或者数据源本身编码混乱。处理思路和字体问题不一样,要优先排查编码。
第三种是虚化模糊或者出现细线、重影。这种情况在交互式后端下偶尔会遇到,一般和字体渲染路径有关,尤其是多个字体混用、或者桌面对字体的抗锯齿处理冲突。这个稍后在字体配置方式里会一并展开。
我见过不少同学一看到方框就上网搜“怎么让 Matplotlib 显示中文”,然后照着代码片段一顿贴,还是有各种怪问题。这里我想先把底层原理讲清楚,你理解了以后就不会被各种网上“玄学配置”带偏。
1.2 核心原理:字体家族、默认字体与渲染引擎
Matplotlib 是一个用 Python 写的绘图库,但它底层渲染文字时并不是“想用什么字体就用什么字体”。它有自己的一套字体管理机制,大致流程是:代码里给rcParams['font.family']设置一个字体家族名称(或者通过fontproperties指定字体路径),然后 Matplotlib 调用底层的 FreeType 库去系统字体目录里查找匹配的字体文件,找到后解析字形轮廓,再渲染到画布上。
问题出在默认配置上。Matplotlib 的默认字体家族是DejaVu Sans,这套字体是 Matplotlib 自带的,跟着安装包一起进来。它的优点是任何环境都能用、没有版权问题,缺点也很明显——它根本不包含中文字形。所以当你要画中文标签时,Matplotlib 内部找不到能渲染中文的字体时,它不会报错,而是“静默降级”,用一个空字形占位,最终画布上就是一排排空心方框。
我用一个生活化类比来解释:你想打印一张带中文的纸,但打印机里只有英文铅字,没有汉字铅字。打印机的策略不是停机报错,而是把缺的字用空白方块代替,继续把纸张吐出来。这个“代替”的动作就是 Matplotlib 默认的降级机制。
所以,解决中文显示问题的核心路径非常清晰:要么让 Matplotlib 能“看到”系统里有中文字形,你告诉它用哪个字体;要么直接把某个中文字体文件指定给它。舍此无他,网上那些 JavaScript、前端相关的配置方式在这里完全失效,因为问题出在 Python 进程内部的字体列表,而不是 CSS 或浏览器设置。
这个认知非常重要,哪怕你换了系统、换了环境,只要沿着“字体能不能被找到 → 字体列表里有没有中文 → 字体名称是否正确”这条线排查,问题基本都能解决。
2. 方案一:全局配置,一劳永逸
全局配置是我最推荐的方式,尤其适合日常写脚本、跑实验、做批量报表的场景。配置一次,所有脚本都能用到中文,不需要每张图都带着字体参数。
2.1 先查清楚系统里有哪些中文字体
配置的第一步,不是直接写rcParams,而是先看看当前系统里有哪些中文字体,字体在 Matplotlib 里叫什么名字。这一步很多人会跳过,结果经常出现“我都设置了SimHei,为什么还是方块”的情况——因为你的系统里根本没有SimHei,或者字体名不叫这个。
用下面的代码可以列出 Matplotlib 当前能识别到的所有字体:
from matplotlib import font_manager # 获取所有已缓存的字体文件 fonts = font_manager.fontManager.ttflist # 筛选名字里带常见中文字体关键词的 for f in fonts: if any(k in f.name for k in ['Hei', 'Song', 'YaHei', 'Kai', 'WenQuanYi', 'Noto', 'PingFang', 'Ming']): print(f.name, '--', f.fname)不同系统预装的中文字体不一样,我列一个常见对照表,方便快速定位:
| 系统 | 常见中文字体 | 字体文件路径(举例) | Matplotlib 名称 |
|---|---|---|---|
| Windows | 微软雅黑 | C:\Windows\Fonts\msyh.ttc | Microsoft YaHei |
| Windows | 宋体 | C:\Windows\Fonts\simsun.ttc | SimSun |
| Windows | 楷体 | C:\Windows\Fonts\simkai.ttf | KaiTi |
| Linux/Ubuntu | 文泉驿正黑 | /usr/share/fonts/truetype/wqy/wqy-zenhei.ttc | WenQuanYi Zen Hei |
| Linux/Ubuntu | 文泉驿微米黑 | /usr/share/fonts/truetype/wqy/wqy-microhei.ttc | WenQuanYi Micro Hei |
| Linux/Ubuntu | Noto Sans CJK SC | /usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc | Noto Sans CJK SC |
| macOS | 苹方 | /System/Library/Fonts/PingFang.ttc | PingFang SC |
| macOS | 华文黑体 | /System/Library/Fonts/STHeiti Medium.ttc | STHeiti |
如果你的列表里一条结果都没有,那就说明当前环境中缺少中文字体,需要先安装一台中文字体,然后再继续。这种情况最常发生在 Linux 服务器或者精简版 Docker 容器里,具体安装方法我在2.3节讲。
2.2 通过 rcParams 设置全局字体
确认系统里有中文字体后,直接设置全局配置即可。我通常把它放在脚本文件的最开头,跟 import 放在一起:
import matplotlib.pyplot as plt plt.rcParams['font.family'] = ['WenQuanYi Micro Hei', 'Microsoft YaHei', 'SimHei', 'Noto Sans CJK SC']这里我用的不是单个字符串,而是一个列表。这个列表是一个“字体回退链”:第一个字体找不到时,Matplotlib 会按顺序尝试后面的字体。这样既能在 Linux 上用文泉驿,又能在 Windows 上自动切到微软雅黑,脚本跨平台复用更友好。
配置后可以快速画一张带中文的图验证:
import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['WenQuanYi Micro Hei', 'Microsoft YaHei'] plt.plot([1, 2, 3], [1, 4, 9]) plt.title("中文标题测试") plt.xlabel("横轴") plt.ylabel("纵轴") plt.savefig("test_cn.png", dpi=150)注意:行业内常用写法是设置
font.sans-serif,因为 Matplotlib 的默认font.family是sans-serif这一组。直接设置font.family也可以,但如果配置了font.sans-serif而font.family又恰好是别的值(比如serif),你会看到配置不生效。最省心的做法是两个都写:plt.rcParams['font.family'] = 'sans-serif',同时plt.rcParams['font.sans-serif'] = [...]。我实测过,显式把font.family设为sans-serif后,兼容性最好。
2.3 Linux 服务器没中文字体?离线安装也很简单
很多朋友是在 Ubuntu 或者 CentOS 服务器上画图,尤其跑定时任务是纯命令行环境,没装图形桌面,中文字体往往一个都没有。这时候先不要想着去设置 Matplotlib,得先把字体文件装进去。
有两种方式:
方式一:在线安装(如果有正规软件源)
# Ubuntu / Debian sudo apt install fonts-noto-cjk # CentOS / RHEL / Fedora sudo yum install google-noto-sans-cjk-fonts装完不需要重启 Python,只要重新执行字体列表查询代码即可。
方式二:离线安装
服务器没有外网时,我一般从自己电脑上把字体文件拷贝过去。通用性最好的是思源黑体(Noto Sans CJK)的 TTF/OTC 文件,或者 Windows 的微软雅黑。Windows 字体文件路径在上面的表格里,直接把msyh.ttc拷到服务器上,放到用户字体目录:
mkdir -p ~/.local/share/fonts cp msyh.ttc ~/.local/share/fonts/ fc-cache -f ~/.local/share/fonts放到这个目录的好处是用户级生效,不需要 root 权限,也不影响系统公共目录。之后重新打开 Python,用font_manager重新检查一下,或者重建一次字体缓存,就能发现新字体了。
如果你连fc-cache工具都没有,也不用慌。Matplotlib 不依赖fc-cache,它自己会在启动时扫描字体目录。只是扫描的是它配置好的目录列表,不一定包含~/.local/share/fonts。这时候更保险的做法是把字体文件放到 Python 安装目录下 Matplotlib 自带的字体目录里,或者直接用font_manager.addfont()动态添加,这个在3.2节里有详细用法。
2.4 配置文件方式:适合固定环境反复部署
如果团队里有很多人、很多机器都用同一套环境,另一种全局配置方式是把参数写进 Matplotlib 的配置文件,而不是每个脚本都写一遍 rcParams。
先获知配置文件位置:
import matplotlib print(matplotlib.get_configdir())该目录下有一个matplotlibrc文件,我一般把下面这三行写在文件末尾:
font.family: sans-serif font.sans-serif: WenQuanYi Micro Hei, Microsoft YaHei, SimHei, Noto Sans CJK SC axes.unicode_minus: False配置文件方式的好处是大批量部署时可以直接用 Ansible 之类的工具批量派发,也可以把文件放到代码仓库里统一管理。缺点是一旦有人改乱了这个文件,整个环境的所有脚本都会受影响。所以我个人只推荐在固定环境、多人协作时用配置文件方案,单人临时环境里直接在脚本开头写 rcParams 反而更好维护。
3. 方案二:局部指定字体,不留全局副作用
全局配置好用,但有一个真实的问题:如果你在同一个进程里同时做不同的图,有的图想用衬线中文字体,有的想用黑体中文字体,全局改来改去会互相干扰。这时候就需要局部字体方案。
3.1 用 fontproperties 直接指定字体文件
最彻底、最可控的方法是直接给FontProperties传入字体文件路径,不经过名称匹配的流程:
from matplotlib.font_manager import FontProperties font_path = "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc" font_prop = FontProperties(fname=font_path) plt.plot([1, 2, 3], [1, 4, 9]) plt.title("局部字体测试", fontproperties=font_prop) plt.xlabel("横轴", fontproperties=font_prop) plt.ylabel("纵轴", fontproperties=font_prop) plt.show()这种方式的底层逻辑是直接告诉 Matplotlib:“别查字体列表了,直接用这个文件里的字形数据。” 它绕开了rcParams的名称映射,所以不管系统里有没有安装这个字体、Matplotlib 有没有扫描到这个文件,都能正常工作。只要字体文件本身能读、能被 FreeType 解析,就能渲染出中文字符。
实际生产中,这个方案非常适合那些“数据文件、字体文件、脚本文件”一起打包分发的场景。比如我给一些客户做报表脚本时,就会把一个开源字体文件放在脚本同级的fonts/目录里,用os.path拼接路径再传入 FontProperties,这样换服务器时不用重新安装字体,挂载目录就能跑。
不过要注意:如果每一条文本都写fontproperties=font_prop,代码会变得非常啰嗦。我在工程里通常定义一个辅助函数:
import os from matplotlib.font_manager import FontProperties _BASE_DIR = os.path.dirname(os.path.abspath(__file__)) _FONT_PATH = os.path.join(_BASE_DIR, "fonts", "wqy-microhei.ttc") def get_cn_font(size=12): return FontProperties(fname=_FONT_PATH, size=size)然后用的时候统一fontproperties=get_cn_font(),既干净又能全局维护。
3.2 用 rc_context 做局部临时生效
如果你只是想在某一段代码块里临时使用某个中文字体,不改变全局设置,可以使用rc_context上下文管理器:
import matplotlib.pyplot as plt with plt.rc_context({'font.sans-serif': ['WenQuanYi Micro Hei'], 'axes.unicode_minus': False}): fig, ax = plt.subplots() ax.plot([1, 2, 3], [4, 5, 6]) ax.set_title("临时中文标题") fig.savefig("temp_cn.png")这个模式我在写多语言图表时会刻意使用。比如一个自动化报告里面,中文版本用rc_context设置中文字体,英文版本不设置,两个版本之间互不影响。它的源码逻辑就是“进上下文时临时修改 rcParams,退出时自动还原”,非常可靠。
3.3 动态注册新字体 addfont 的高效用法
如果你的字体文件不是系统标准目录下,又不想放到matplotlibrc里,还有一个动态注册的办法,用font_manager.addfont或者fontManager.addfont:
from matplotlib import font_manager font_manager.fontManager.addfont('/path/to/custom_font.ttf') prop = font_manager.FontProperties(fname='/path/to/custom_font.ttf')调用之后,Matplotlib 会把该字体纳入内存中的字体列表,后续就可以通过prop局部使用,也可以从ttflist里查到它。我在给用户做 Python 打包应用(用 PyInstaller 打包成 exe)时经常用这个 API,因为打包后的程序无法依赖用户系统里是否装了中文字体,干脆把开源字体打进包内,运行时动态注册。这种灵活度是上面几种方式里最高的。
4. 避坑与排查:字体缓存、负号方块、多后端问题
很多人照着网上的配置写完还是失败,我遇到的情况里九成出在这几个地方:字体缓存没刷新、忽略了负号问题、后端不同导致表现不同、或者字体名称写得和系统实际名称不一致。这一节把常见的坑一一拆开。
4.1 字体缓存:为什么装了字体还是找不到
前面提到从系统安装新字体后,Matplotlib 未必马上能发现它。这是因为 Matplotlib 有一个字体缓存机制,它会扫描系统字体目录并把结果写入缓存文件,下一次启动时直接读缓存,以此加快启动速度。
这个缓存在不同系统下位置不一样,一般可以通过下面的代码查出确切位置:
import matplotlib print(matplotlib.get_cachedir())里面会有一个fonts相关文件,有时候是fontlist-*.json。如果系统里已经装了字体,但font_manager.fontManager.ttflist里始终查不到,最简单的解决办法是把这个缓存目录里的字体缓存文件删除,然后重新运行 Python:
rm -rf ~/.cache/matplotlib/fontlist-*.json重新进入 Python 时,Matplotlib 会重新扫描字体目录、重新建立缓存。这个过程通常几秒到十几秒,取决于系统字体总量。实测下来,这一步能救回大概一半的“装完还是不能用”案例。
提示:Docker 容器里面尤其容易踩这个坑。很多人
apt install fonts-noto-cjk装完字体后,发现 Matplotlib 照样方块。原因就是容器里已经有一次 Matplotlib 启动并生成了缓存,而缓存生成时字体还没装。清理缓存重新起一个 Python 进程,问题立刻消失。
4.2 负号变成方块,和中文是同一个坑的亲戚
设置好中文字体后,很多人会紧接着发现图表坐标轴的负号“-1”变成了一个小方块,或者显示成方框。这是 Matplotlib 的另一个经典坑:默认情况下,axes.unicode_minus为True,意味着 Matplotlib 使用 Unicode 的减号字符(U+2212)来渲染坐标轴负号。这个字符在 DejaVu Sans 里没问题,但很多中文字体里根本没有对应字形,于是照样显示成方框。
解决办法是在配置里关掉它:
plt.rcParams['axes.unicode_minus'] = False关掉之后 Matplotlib 会用普通的 ASCII 连字符-代替 Unicode 减号,虽然位置略微有些不同,但显示上没有任何问题。我每次配置中文字体时都会顺手加上这一行,因为它和中文字体问题几乎是绑定出现的,不处理就会留下一个“看起来很奇怪但又不确定哪里有问题”的尾部 bug。
4.3 不同后端的渲染差异,以及 savefig 与 show 的差异
另一个容易被忽略的问题是:同样一段代码,在 Jupyter Notebook 里能正常显示中文,到脚本里用plt.show()弹窗却有问题,或者反向的也有。这和 Matplotlib 使用的“后端”有关。
Matplotlib 有多个渲染后端:Agg(纯图形输出,不弹窗)、TkAgg(Tk 窗口)、QtAgg(Qt 窗口)、WebAgg(浏览器显示)等。不同后端调用的是不同的 GUI 工具库,这些库的中文字体渲染路径有细微差别。尤其是 Linux 桌面环境里,TkAgg和QtAgg使用的系统字体解析器不同,容易出现某个后端正常、另一个后端乱码的怪现象。
遇到这种情况时,先别怀疑字体配置,用下面的代码确认当前用的是哪个后端:
import matplotlib print(matplotlib.get_backend())如果弹窗后端显示异常,可以尝试切换到 Agg 然后保存图片查看:
import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt当然,如果你用 Jupyter Notebook,中间的展示是inline后端,本质也是 Agg 渲染后嵌入网页,所以保存图片和 Notebook 里显示不一致的现象相对少见。真正容易出问题的是桌面弹窗场景。
此外,我也遇到过同一个脚本plt.show()正常,但plt.savefig()出来的图片字体不对。这是因为show()和savefig()在字体渲染上使用了不同的字体解析路径,严格来说这是 Matplotlib 历史遗留的 bug 之一,但通过统一设置font.family和font.sans-serif通常能规避。如果还不行,就在savefig时显式传入fontproperties或使用上面的局部方案。
4.4 常见问题速查表
我把这些年在线下交流中常遇到的排查场景整理成一张速查表,方便直接对照定位:
| 现象 | 根因 | 解决动作 |
|---|---|---|
| 图中中文全是空心方框 | 当前字体列表里没有中文字体 | 安装中文字体,或通过 rcParams 指定中文字体名称 |
| 已经指定字体名称,还是方框 | 字体名称与实际名称不符 | 用font_manager.fontManager.ttflist查询真实字体名 |
| 新装了系统字体,重启仍找不到 | Matplotlib 字体缓存未更新 | 删除get_cachedir()下的fontlist-*.json后重启 |
| 有中文了,但负号变成方块 | axes.unicode_minus为 True | 设置plt.rcParams['axes.unicode_minus'] = False |
| Jupyter 正常,弹窗或保存异常 | 后端渲染路径差异 | 统一用 Agg 输出,或检查 GUI 工具包 |
| 中文显示模糊/线条不清 | 使用了 ttc 字体文件且渲染引擎有兼容问题 | 换成单字重的 TTF 文件,或换 Noto 系列字体 |
| 脚本读取 CSV 后中文全乱码 | 文件编码与数据读取编码不一致 | 用encoding='utf-8'或encoding='gbk'显式指定源文件编码 |
5. 延伸思考:从 Matplotlib 看“中文字体渲染”的通用逻辑
这个话题虽然以 Matplotlib 为主题,但我在平时折腾 GUI、嵌入式屏幕、甚至是 LCD 显示屏时,发现背后的字体渲染逻辑惊人地一致。看到热搜词里也有很多人搜 LCD 屏显示中文、LVGL 显示中文、Figma 显示中文,我觉得有必要把这个通用逻辑点透。
5.1 字体问题本质是“字形库 + 渲染引擎”的问题
无论是 Matplotlib 在电脑上画图、LVGL 在单片机上驱动 TFT LCD 屏、还是某个 Web 页面里的字体图标,中文字体能否正常显示都取决于两个条件:第一,系统或应用有没有拿到包含中文字形的字体文件;第二,渲染引擎有没有能力解析并按规则排版这些字形。
拿嵌入式屏幕场景举例,LVGL 中通常要做“字体转换”,把系统里的 TTF 转成 LVGL 自有的字体格式,再在运行时加载。整个过程和 Matplotlib 内部的 FreeType 解析如出一辙。只是 Matplotlib 因为运行在 PC 的完整操作系统之上,省去了“转换字体格式”这一步,但这并不意味着它不需要字体文件。
所以当你在任何平台遇到中文方块时,第一反应不应该是“这个工具是不是有 bug”,而是“它是否被提供了中文字形”。这个习惯能让你少走很多弯路。
5.2 在线工具和离线环境的差异
有些工具(比如 HTML 页面)显示中文依赖用户本机安装的字体,这种场景在浏览器里通常没什么问题,因为主流操作系统标配中文字体。但 Figma 一类的在线协作工具,它在网页里渲染文本时使用的是浏览器+服务端推送的字体的组合。如果设计文件里指定的某个中文字体在本地没有,浏览器会用默认字体回退,于是会出现不希望的“字体替换”现象。这又是另一个层面的字体问题,但依然符合“字体回退”的通用机制——和 Matplotlib 的font.sans-serif列表回退原理完全一样。
至于 LCD 屏显示中文,很多人第一反应是要不要“嵌字库”,其实也是基于同样的逻辑:你要把一个“认识中文字形”的字库文件放进去,然后调用字形 ID 来取模。Matplotlib 横向对比这些不同场景后,你会发现核心排查思路是可以复用的。这也解释了为什么“Matplotlib 中文显示”这个话题的各类搜索中总会捎带上这么多其它工具的显示问题——本质上大家卡的是同一个知识点。
5.3 关于“微信界面中文虚化模糊”这类问题的不同之处
搜这个词的人,和搜 Matplotlib 中文问题的人群重合度很高,但这两类问题的性质完全不同。微信界面中文虚化模糊通常是桌面环境的字体渲染配置问题,属于 GUI 应用的情景,和 Matplotlib 内部的字体字形缺失完全是两回事。Windows 上出现字体虚化,一般可以通过系统显示的 ClearType 调校来解决;Linux 桌面则要检查字体抗锯齿和 hinting 配置。
我在排查问题时常跟朋友说:先判断这是“找不到字形”还是“渲染效果差”。找不到字形,是硬故障,必须提供字体文件;渲染效果差,是软故障,要调渲染参数。这个判断适用于 Matplotlib、LCD 屏幕、Web 页面、桌面应用等等几乎所有图形界面场景。
6. 我的常用工具箱:字体体检与一键修复
最后分享一个我实际项目中一直用的“字体体检函数”。每次在新环境里跑数据可视化任务之前,我都会先执行一遍,确认中文字体是否就绪,避免画出一堆方块图之后才发现问题。
import matplotlib.pyplot as plt from matplotlib import font_manager def font_health_check(): fonts = set(f.name for f in font_manager.fontManager.ttflist) cn_fonts = [f for f in fonts if any( k in f for k in ['Hei', 'Song', 'Kai', 'YaHei', 'WenQuanYi', 'Noto Sans CJK', 'PingFang'] )] if cn_fonts: print("检测到中文字体:", cn_fonts) plt.rcParams['font.sans-serif'] = cn_fonts + plt.rcParams['font.sans-serif'] plt.rcParams['font.family'] = 'sans-serif' plt.rcParams['axes.unicode_minus'] = False print("已自动应用中文字体配置") else: print("警告:未检测到中文字体,请先安装或指定字体文件") return cn_fonts font_health_check()这个函数会自动检测环境里的中文字体并设置 rcParams,省去手动维护字体名称列表的麻烦。需要注意的是,cn_fonts的顺序取决于ttflist返回的顺序,如果你对字体优先级有明确要求,不建议依赖自动检测,还是手动指定列表更可控。
如果你遇到的是较为少见的字体问题,一个比较不容易被搜到的技巧是:把 Matplotlib 升级到最新版。很多字体渲染相关的 bug 都是在新版本中修的。我在几个月前就遇到过font_manager解析某些新版 TTC 字体失败的问题,升级 Matplotlib 后直接消失。遇到稀奇古怪的字体 bug 时,pip install -U matplotlib是一个值得优先尝试的选项。
根据我个人的经验,中文显示这类问题最怕的就是“不动脑子照搬配置”。每次看到网上的解决方案,先想一下它的配置最终改变的是字体列表、字体文件、还是渲染开关,一旦这样思考问题,绝大多数坑都能自己避开。希望这篇文章能帮你把 Matplotlib 的中文显示问题彻底解决,更希望你在以后遇到别的“中文方块”时,能快速联想到今晚看过的这套“字形库 + 渲染引擎”的通用逻辑,不再被各种平台的差异绕得团团转。