ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Matplotlib 中文显示全攻略:从字体原理到乱码解决与缓存清理

Matplotlib 中文显示全攻略:从字体原理到乱码解决与缓存清理 有没有遇到过这种场景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.ttcMicrosoft YaHeiWindows宋体C:\Windows\Fonts\simsun.ttcSimSunWindows楷体C:\Windows\Fonts\simkai.ttfKaiTiLinux/Ubuntu文泉驿正黑/usr/share/fonts/truetype/wqy/wqy-zenhei.ttcWenQuanYi Zen HeiLinux/Ubuntu文泉驿微米黑/usr/share/fonts/truetype/wqy/wqy-microhei.ttcWenQuanYi Micro HeiLinux/UbuntuNoto Sans CJK SC/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttcNoto Sans CJK SCmacOS苹方/System/Library/Fonts/PingFang.ttcPingFang SCmacOS华文黑体/System/Library/Fonts/STHeiti Medium.ttcSTHeiti如果你的列表里一条结果都没有那就说明当前环境中缺少中文字体需要先安装一台中文字体然后再继续。这种情况最常发生在 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, dpi150)注意行业内常用写法是设置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(fnamefont_path) plt.plot([1, 2, 3], [1, 4, 9]) plt.title(局部字体测试, fontpropertiesfont_prop) plt.xlabel(横轴, fontpropertiesfont_prop) plt.ylabel(纵轴, fontpropertiesfont_prop) plt.show()这种方式的底层逻辑是直接告诉 Matplotlib“别查字体列表了直接用这个文件里的字形数据。” 它绕开了rcParams的名称映射所以不管系统里有没有安装这个字体、Matplotlib 有没有扫描到这个文件都能正常工作。只要字体文件本身能读、能被 FreeType 解析就能渲染出中文字符。实际生产中这个方案非常适合那些“数据文件、字体文件、脚本文件”一起打包分发的场景。比如我给一些客户做报表脚本时就会把一个开源字体文件放在脚本同级的fonts/目录里用os.path拼接路径再传入 FontProperties这样换服务器时不用重新安装字体挂载目录就能跑。不过要注意如果每一条文本都写fontpropertiesfont_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(size12): return FontProperties(fname_FONT_PATH, sizesize)然后用的时候统一fontpropertiesget_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.addfontfrom 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里始终查不到最简单的解决办法是把这个缓存目录里的字体缓存文件删除然后重新运行 Pythonrm -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 的减号字符U2212来渲染坐标轴负号。这个字符在 DejaVu Sans 里没问题但很多中文字体里根本没有对应字形于是照样显示成方框。解决办法是在配置里关掉它plt.rcParams[axes.unicode_minus] False关掉之后 Matplotlib 会用普通的 ASCII 连字符-代替 Unicode 减号虽然位置略微有些不同但显示上没有任何问题。我每次配置中文字体时都会顺手加上这一行因为它和中文字体问题几乎是绑定出现的不处理就会留下一个“看起来很奇怪但又不确定哪里有问题”的尾部 bug。4.3 不同后端的渲染差异以及 savefig 与 show 的差异另一个容易被忽略的问题是同样一段代码在 Jupyter Notebook 里能正常显示中文到脚本里用plt.show()弹窗却有问题或者反向的也有。这和 Matplotlib 使用的“后端”有关。Matplotlib 有多个渲染后端Agg纯图形输出不弹窗、TkAggTk 窗口、QtAggQt 窗口、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] FalseJupyter 正常弹窗或保存异常后端渲染路径差异统一用 Agg 输出或检查 GUI 工具包中文显示模糊/线条不清使用了 ttc 字体文件且渲染引擎有兼容问题换成单字重的 TTF 文件或换 Noto 系列字体脚本读取 CSV 后中文全乱码文件编码与数据读取编码不一致用encodingutf-8或encodinggbk显式指定源文件编码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 的中文显示问题彻底解决更希望你在以后遇到别的“中文方块”时能快速联想到今晚看过的这套“字形库 渲染引擎”的通用逻辑不再被各种平台的差异绕得团团转。
返回列表