1. 先别急着下载字体文件
如果你的 Mac 上已经装了 Python 和 matplotlib,第一次画出带中文标签的图时,大概率会看到一排排整整齐齐的方框,或者干脆只显示英文、汉字全部消失。这种时候,打开浏览器搜索"matplotlib 中文乱码",绝大多数教程会甩给你一个百度网盘链接,让你下载SimHei.ttf或者微软雅黑,再丢进 matplotlib 的字体目录里。
我强烈不建议在 macOS 上这么做,原因有几点:
- 网上下载的中文字体大多是 Windows 平台的,虽然 TrueType 字体在 macOS 上也能用,但直接把第三方字体塞进 matplotlib 的字体目录,会让你的环境变得很“脏”。
- 这些字体文件往往没有授权,你在自己的项目里用一用也就罢了,一旦要把环境复制给同事、或者部署到服务器上,会引入不必要的版权和合规风险。
- 你的 Mac 里明明带着一堆高质量中文字体,为什么还要舍近求远?苹果对中文字体渲染的优化是出了名的,直接用系统字体不仅省事,显示效果也更干净、更“苹果”。
这篇文章就解决一个问题:在 macOS 上,不需要额外安装任何字体文件,让 matplotlib 正确显示中文字体。
我会讲清楚背后的原理,给你几套可以“抄作业”的配置方式,并且把最容易踩的坑——比如字体缓存——也一并排除掉。无论你是刚上手 Python 可视化的新手,还是已经在做数据报告的老手,看完应该都不会再被中文方块困扰。
2. 摸清家底:macOS 自带的中文字体库
既然说好“不额外安装字体”,那我们得先知道自己手上有哪些牌可以打。
macOS 自带的中文字体其实相当丰富,配合 matplotlib 使用,最常用、效果也最稳定的是这几款:
| 字体名称 | 系统文件名 / 路径示例 | 风格特点 |
|---|---|---|
| PingFang SC(苹方) | /System/Library/Fonts/PingFang.ttc | 苹果为中文用户打造的现代黑体,macOS 10.11 之后成为系统默认中文字体,字重多、耐看 |
| Hiragino Sans GB(冬青黑体) | /System/Library/Fonts/Hiragino Sans GB.ttc | 经典的中文字体,笔画干净,在中文排版里出镜率很高,常用于代码注释场景 |
| STHeiti(华文黑体) | /Library/Fonts/STHeiti Light.ttc等 | 老牌的华文字体家族,兼容性不错,但字形相对陈旧 |
除了上面这些,还有宋体类的STSong、楷体类的STKaiti,不过做数据可视化时,黑体类因为笔画均匀、无衬线,所以效果最好,日常图表基本用不到宋楷。
你可以先用命令行的方式验证一下系统里到底有哪些中文字体。打开终端,执行:
fc-list :lang=zh如果你的 Mac 上装了fontconfig(通常用 Homebrew 装依赖时会顺带装上),这个命令会列出所有支持中文的字体文件路径。如果你没装fontconfig,也可以用system_profiler SPFontsDataType查看,不过输出要啰嗦不少。
一个更直接的方式是,直接在 Python 里看 matplotlib 能识别到哪些字体:
from matplotlib import font_manager for f in font_manager.fontManager.ttflist: if 'PingFang' in f.name or 'Hiragino' in f.name or 'STHeiti' in f.name: print(f.name, '->', f.fname)运行之后,你大概率会看到:
PingFang SC -> /System/Library/Fonts/PingFang.ttc Hiragino Sans GB -> /System/Library/Fonts/Hiragino Sans GB.ttc STHeiti -> /System/Library/Fonts/STHeiti Light.ttc这行输出就是我们的底气:matplotlib 本来就能看到这些字体,只是默认不优先用它们而已。
3. 核心原理:Matplotlib 如何与中文字体“失联”
在动手配置之前,我得先把原理讲清楚,不然你改了配置也会云里雾里,下次换台电脑又抓瞎。
matplotlib 加载字体时,遵循一套内部优先级:
- 它首先会从
matplotlib.font_manager维护的字体列表中,去查找你指定的字体家族名称。 - 如果没有指定,它默认使用
font.family的默认值,通常是['sans-serif'],而sans-serif里排第一位的默认字体是DejaVu Sans。 DejaVu Sans是一款非常优秀的西文字体,但它的字符集里根本没有 CJK(中日韩统一表意文字)的汉字字形。
当 matplotlib 拿到一个数据标签,比如“销售额”,它会在当前字体里逐字符查找字形。对于“销”这个字,DejaVu Sans里没有对应的字形映射。正常情况下,系统渲染出了一个缺失字形标记——也就是你看到的小方框“豆腐块”。
这里有个非常关键的认知:matplotlib 并不负责渲染汉字,它只负责按照字体文件里的字形信息去绘制图元。你做的所有配置,本质上是让它放弃“找不到字形”的字体,转而去使用系统里有汉字字形的字体。
顺带提一个高频问题:为什么有的图里中文变成小方块之后,负号“-”也变成了方块?这是因为中文字体里往往不包含英文的负号 U+2212,而 matplotlib 默认会把负号渲染成这个特殊字符。解决办法是手动指定axes.unicode_minus为False,让它用普通的 ASCII 连字符代替。这个配置在接下来的步骤里我也会带上。
还有一个概念容易被搞混:字体家族名称(Font Family)和字体文件路径(Font File)的区别。
font.family对应的是逻辑上的字体分类,比如sans-serif、serif。font.sans-serif里的列表,存的是具体的字体家族名称,比如PingFang SC。- 而
/System/Library/Fonts/PingFang.ttc是字体文件在磁盘上的位置。
matplotlib 的font_manager做的事情,就是把这些位置和名称注册到一个内部索引里。你在代码里写的'PingFang SC',只是告诉索引“我要这个家族”,索引再去对应到实际的字体文件。所以,只要系统字体文件存在,并且 matplotlib 认识它,配置就只是改一行字符串的事情。
4. 实战配置:三套方案,从局部到全局
现在进入正题。我不打算给你唯一答案,而是给出三套由轻到重的方案,你可以按自己的实际场景选用。
4.1 方案一:在代码里动态指定 rcParams(最推荐)
这个方案适合绝大多数写脚本、跑 notebook 的场景,优点是侵入性最小,代码即文档,看一眼就知道你做了什么,也方便跟同事保持一致。
关键代码就四行:
import matplotlib import matplotlib.pyplot as plt # 指定使用 sans-serif 字体族(无衬线字体) matplotlib.rcParams['font.family'] = 'sans-serif' # 在这个字体族里,按顺序优先使用以下中文字体 matplotlib.rcParams['font.sans-serif'] = [ 'PingFang SC', # 苹方,macOS 系统默认 'Hiragino Sans GB', # 冬青黑体,经典选择 'STHeiti', # 华文黑体,兜底 ] # 防止负号被渲染成豆腐块 matplotlib.rcParams['axes.unicode_minus'] = False配置完之后,你画图时如果设置了中文label或title,就会自动使用苹方字体。
plt.plot([1, 2, 3], [4, 5, 6], label='销售额') plt.title('月度销售趋势') plt.xlabel('月份') plt.ylabel('金额(元)') plt.legend() plt.show()这里的['PingFang SC', 'Hiragino Sans GB', 'STHeiti']是一个优先级列表。matplotlib 会从左到右查找:第一个字体找不到,就用第二个。这种配置方式特别适合那种“我本地能用,同事的机器上也大概率能用”的场景,因为这几款都是 macOS 标配。
4.2 方案二:用 font_manager 注册字体文件路径(更稳更准)
如果你发现直接指定PingFang SC有时不起作用,或者你在一个精简过的 macOS 环境(比如 Docker 容器)里运行,那直接用font_manager动态注册字体文件路径,会更加稳妥。
from matplotlib import font_manager # 注册具体字体文件 font_manager.fontManager.addfont('/System/Library/Fonts/Hiragino Sans GB.ttc') # 注册之后,字体家族名称以 addfont 后的实际名称为准 import matplotlib.pyplot as plt plt.rcParams['font.family'] = 'Hiragino Sans GB'这里有个比较隐蔽的坑:.ttc是苹果的字体集合文件,里面可能包含多个字重和多种字体。某些版本的 matplotlib 对.ttc文件的支持不完全,addfont注册后可能只识别第一个字重,导致你选中了Hiragino Sans GB,但加粗、斜体这些变体通通失效。
我的建议是:
- 优先使用
.ttf或.otf格式的字体文件。 - 如果必须用
.ttc,注册后务必打印一下font_manager.fontManager.ttflist里的名称,确认注册结果。
4.3 方案三:修改 matplotlibrc 全局配置文件(一劳永逸)
如果你希望整个团队的新人克隆下项目后,不需要任何额外配置就能跑出中文图表,那建议直接修改 matplotlib 的全局配置文件,也就是matplotlibrc。
先找到配置文件位置:
import matplotlib print(matplotlib.get_configdir())通常输出~/.matplotlib。在该目录下如果没有matplotlibrc文件,就在 Python 里生成一份标准模板:
import matplotlib matplotlib.matplotlib_fname() # 这是实际加载的配置文件路径,通常在 site-packages 里不过更推荐的做法是,在matplotlib.get_configdir()目录下新建一个matplotlibrc文件,把你想全局覆盖的配置写进去:
cd ~/.matplotlib cat >> matplotlibrc << EOF font.family: sans-serif font.sans-serif: PingFang SC, Hiragino Sans GB, STHeiti axes.unicode_minus: False EOF注意,~/.matplotlib/matplotlibrc这个用户级配置会覆盖系统默认配置,但不会被site-packages里的升级覆盖掉。之后你开的每一个 Python 进程、每一个 notebook,都会默认加载到这个配置,不用再写那几行 rcParams 了。
我把三个方案的关键区别整理成表格,方便你结合自己的项目选:
| 对比维度 | 代码内 rcParams | font_manager 注册 | matplotlibrc 全局配置 |
|---|---|---|---|
| 侵入性 | 低,只影响当前代码块 | 低,但需要知道路径 | 高,影响该用户所有 matplotlib 绘图 |
| 跨平台可移植性 | 高,换 Linux 也能改写法 | 中,路径需改 | 低,换机器要重新配置 |
| 可维护性 | 随代码走,清晰 | 随脚本走 | 一人配置,全局生效,但新人容易忽略 |
| 遇到问题时的排查难度 | 简单 | 中等 | 较难,容易“为什么换了机器就没字体” |
如果你只是自己写写分析脚本,方案一就够了。如果你是团队项目,比较建议方案一加一个统一的入门文档,或者在项目根目录放一个set_chinese_font.py工具模块,大家都从里面 import。
5. 脚下有坑:清掉字体缓存,让改动生效
这一节值得单独拿出来讲。因为很多人明明按教程改了配置,还是看到方块,最后差点重装 Python。
Matplotlib 为了加速字体查找,会在首次运行后把系统里所有字体信息缓存下来,形成一个fontlist-*.json文件。问题是:当你手动修改了字体配置、或者调用了addfont注册新字体之后,matplotlib 可能还傻乎乎地读旧缓存,根本不知道你有新字体可用。
现象就是:
- 配置改好了,代码重跑了,重启了 Jupyter Kernel,还是方块。
- 输出里虽然没报错,但字体毫无变化。
解决方式很简单,两步走:
第一步,查看缓存文件:
import matplotlib matplotlib.get_cachedir()输出类似:
/Users/你的用户名/.matplotlib目录下会有fontlist-v310.json、fontlist-v330.json这样的文件。直接手动删掉:
rm ~/.matplotlib/fontlist-*.json第二步,删除缓存之后,重新运行 Python,或者直接在脚本里强制重建:
import matplotlib matplotlib.font_manager._load_fontmanager(try_read_cache=False)在实际操作中,如果你用了font_manager.addfont(),我建议你在代码里检测到缓存存在时,直接清除并重建,而不是等着用户手动去删。
再补一个注意事项:Jupyter Notebook 的 Kernel 需要重启,或者至少执行%matplotlib auto重载一下字体管理器,否则旧 cache 可能还驻留在内存里。如果你是在 notebook 里反复调试字体,最容易遇到“改完全局配置不生效”的情况,很多时候不是配置问题,而是 Kernel 没有重启。
6. 收个尾:封装一个 setup_chinese_font 函数
当你摸清了上面的原理和坑,就可以把它们固化成一个工具函数。以后每次画图前调一下,干干净净,不用再记那几行配置。
我来提供一个可以直接复制粘贴的函数:
""" setup_chinese_font.py 在 macOS 上无需安装额外字体,自动为 matplotlib 配置中文字体。 """ import matplotlib from matplotlib import font_manager def setup_chinese_font(prefer: str = "PingFang SC") -> None: """ 自动配置 Matplotlib 使用 macOS 自带的中文字体。 Parameters ---------- prefer : str 优先使用的字体名称,默认 'PingFang SC'。 如果该字体不存在,则自动回退到其他系统内置中文字体。 """ # 1. 尝试按名称直接配置(方案一) available_fonts = {f.name for f in font_manager.fontManager.ttflist} # 2. 如果首选字体在 matplotlib 索引里,直接设置 rcParams chinese_fonts = [ prefer, "Hiragino Sans GB", "STHeiti", "STSong", ] font_to_use = None for font_name in chinese_fonts: if font_name in available_fonts: font_to_use = font_name break if font_to_use is not None: matplotlib.rcParams["font.family"] = "sans-serif" matplotlib.rcParams["font.sans-serif"] = [font_to_use] matplotlib.rcParams["axes.unicode_minus"] = False return # 3. 如果都没有,尝试用 addfont 强制注册 .ttc 文件(方案二) system_zh_fonts = [ "/System/Library/Fonts/PingFang.ttc", "/System/Library/Fonts/Hiragino Sans GB.ttc", "/Library/Fonts/STHeiti Light.ttc", ] for path in system_zh_fonts: import os if os.path.exists(path): try: font_manager.fontManager.addfont(path) # 重新构建可用字体集合 available_fonts = {f.name for f in font_manager.fontManager.ttflist} for font_name in chinese_fonts: if font_name in available_fonts: font_to_use = font_name break if font_to_use is not None: matplotlib.rcParams["font.family"] = "sans-serif" matplotlib.rcParams["font.sans-serif"] = [font_to_use] matplotlib.rcParams["axes.unicode_minus"] = False return except Exception: # 失败就尝试下一个路径 continue raise RuntimeError("未找到可用的系统内置中文字体,请检查 macOS 字体配置。")这个函数里做了一件比较重要的事:先用available_fonts集合判断字体是否已经被 matplotlib 索引。如果不是在,就去扫描几个固定的系统路径,用addfont强制注册。如果还不行,就抛出异常提醒用户。
实际的调用方式就是这样:
from setup_chinese_font import setup_chinese_font setup_chinese_font() import matplotlib.pyplot as plt plt.plot([1, 2, 3], [4, 5, 6], label='销售额') plt.title('中文标题测试') plt.legend() plt.show()在 M 系列芯片的新 MacBook、Mac mini 上,这几款字体都是系统默认存在的,所以这个函数实际上在绝大多数情况下走第一步就直接 return 了,非常快。
最后分享一个我个人的习惯:在项目里,我把这个函数放进一个common/conf.py模块里,所有脚本都从那里引用。这样哪怕某天某个同事换成 Windows 或者 Linux,他只需要改这一个文件里的字体名称列表,剩下的画图代码一行都不用动。这比你把rcParams写得到处都是要好维护得多。
macOS 上确实没必要额外装字体,用好系统自带的那几款黑体,你的图表在观感上也会比默认的 DejaVu Sans 好看不少。下次再遇到中文方块,先清缓存,再喊救命。