
写 Python 图表的朋友几乎都遇到过这个场景自己辛苦用 matplotlib 画了一版图发给同事或者投到报告里结果上面的中文全部变成一排排小方框。换个字体后中文倒是能显示了可英文和数字也全变成了中文字体整个图看起来就像公文排版一点学术感都没有。这篇内容专门解决这个问题在 matplotlib 里让中文和英文各用各的字体中文走中文字体英文和数字走西文字体并且可以在同一个图、同一个文本对象里按需混排。不管你是刚开始学 Python 画图的新手还是被字体问题纠缠许久的项目组员看完这篇基本就能一次性搞定。1. 先搞清楚matplotlib 到底是怎么给文字选字体的1.1 字体族font family和文字渲染的基本规则matplotlib 渲染任何一段文字都会先去查找一个字体族。字体族可以简单理解成一组风格相近的字体比如sans-serif无衬线、serif衬线、monospace等宽等。常用的中文字体像黑体、微软雅黑属于无衬线类宋体属于衬线类Times New Roman 也是衬线类Arial 则是典型的无衬线西文字体。当你在 rcParams 里设置font.familysans-serif时matplotlib 会去font.sans-serif这个列表里按顺序找能用的字体。这里说的“能用”指的是系统里已经存在这个字体并且 matplotlib 能把它加载进fontManager。如果列表里的字体一个都找不到最终会退回默认的 DejaVu Sans。很多人对这里有一个误解以为font.sans-serif列表设置得足够长把中文字体放在后面、西文字体放在前面就能让中英文自动各用各的字体。实际上这个逻辑在旧版 matplotlib 里是不成立的关键在于一个叫“字体回退font fallback”的机制。旧版 matplotlib3.5 及更早处理文字时会从字体列表里挑出第一个本机存在且能渲染当前文本的字体然后把整段文字交给它。如果这个字体恰好缺了某个字符比如中文字符就直接显示为方框根本不会去看列表里的下一个字体。这就是很多老教程里配置了中文字体却依然乱码的根本原因。理解了这一层再看网上五花八门的字体配置代码你就能一眼判断哪些写法是有效的哪些只是在碰运气。1.2 版本差异字体回退font fallback不是一直存在的从 matplotlib 3.6 开始官方才真正实现了字体回退机制。也就是说当第一个字体缺少某个字形时matplotlib 会自动尝试列表里的下一个字体、再下一个直到找到能覆盖这个字符的字体或者遍历完整个列表。注意这个机制的前提是 matplotlib 3.6 及以上版本。如果你的环境还是 3.5 或更早列表形式的font.family只会取第一个满足条件的字体根本不会继续回退。实际项目中我见过不少这样的案例同一份绘图脚本在 A 同事的电脑上跑得好好的中英文各用各的字体换到 B 同事的电脑上中文突然全部变方块。排查到最后发现A 用的是新版 matplotlibB 的项目环境还锁在 3.4。所以在动手配置字体前先确认自己的 matplotlib 版本import matplotlib print(matplotlib.__version__)如果你需要用到“中英文自动分离”这种特性我建议直接把 matplotlib 升级到 3.6。如果是公司项目锁版本不方便升级那就老老实实采用后面第 3 节的局部指定方案不要硬套全局配置。这个版本差异是整篇内容的一个关键分水岭很多令人头痛的字体问题本质上不是配置写法错了而是版本能力不同。2. 全局设置一行配置让中英文自动各用各的字体2.1 最简单但有效的 rcParams 组合先聊最基础的场景你只是想快速让图上的中文不变成方块对英文是否单独用某种字体没有要求。这种情况下直接走经典三行配置全图中文、英文、数字都能正常显示import matplotlib.pyplot as plt plt.rcParams[font.family] sans-serif plt.rcParams[font.sans-serif] [Microsoft YaHei, SimHei, Arial] plt.rcParams[axes.unicode_minus] False这段配置的意义很好理解把默认字体族切到sans-serif然后在候选列表里放上中文字体和西文字体。只要本机装有微软雅黑或黑体中英文就都能用它们渲染。因为微软雅黑、黑体这类字体本身包含英文字形所以英文也用中文字体。如果只是追求“不报错、不难看”这个方案已经够用尤其适合日常探索性分析、临时画折线图柱状图的场景。我也见过一些团队的内部模板在font.sans-serif列表里堆了十几个字体[SimHei, Microsoft YaHei, Arial, Times New Roman, ...]。坦白说这种写法在旧版 matplotlib 里毫无意义因为列表里的非首选项几乎不会被用到在新版 matplotlib 里虽然能触发回退但顺序不对的话也会导致中英文被同一个字体吃掉。配置字体列表的正确姿势不是“把能想到的都塞进去”而是想清楚优先级。2.2 用字体回退实现英文优先、中文兜底如果你的诉求是英文和数字走 Times New Roman、中文走 SimHei在新版 matplotlib3.6里最省事的全局设置其实就一行plt.rcParams[font.family] [Times New Roman, SimHei]这行配置利用了前面讲的字体回退机制。对英文字符和数字来说Times New Roman 有字形直接用 Times对中文字符来说Times 没有字形自动回退到 SimHei于是中文用黑体。这个方案放在脚本开头整个 pyplot 的所有文本对象都会继承包括标题、坐标轴标签、刻度、图例、文本标注。省去了第 3 节那种逐一指定的麻烦。用这个方案时有两个坑要避开。第一Times New Roman 属于衬线字体如果你同时还保留font.familysans-serifTimes 是不会生效的。正确做法就是像上面这样直接把font.family设为字体名列表绕开sans-serif这一层映射。第二要确保这两类字体本机都存在。Times New Roman 在 Windows 和 macOS 上一般都有但 Linux 服务器上用默认的 matplotlib 经常会找不到这时需要安装fonts-liberation或者改用其他西文字体。如果你还想让图表里的数学公式字体风格更统一可以再加一行plt.rcParams[mathtext.fontset] stixSTIX 字体在字形比例上与 Times New Roman 比较接近画学术图时这条配置能减少数学公式和正文英文字体不一致带来的突兀感。当然这属于追求细节的加分项不影响基本功能。2.3 顺带解决负号变方块的问题很多人在设完中文字体后又发现 Y 轴负号显示成一个小方块原因很简单matplotlib 渲染坐标轴负号时默认使用 Unicode 的减号U2212某些中文字体没有对应字形或者字形渲染异常。一条配置就能解决plt.rcParams[axes.unicode_minus] False设置之后负号会使用普通连字符替代不影响阅读也不会再出现方块。我见过有人为了这个负号问题反复修改字体列表改了半天都没用其实跟字体关系不大就是这个 unicode_minus 开关的问题。建议在使用任何中文字体配置时无条件带上这一行防患于未然。3. 局部指定让不同文本对象用上自己的专属字体3.1 FontProperties 是什么、怎么构造全局配置适合整图字体风格统一。但实际项目里往往有更细的需求图表的标题用宋体坐标轴标签用黑体图例里的英文部分又想用 Arial。这时候就要用到FontProperties它是 matplotlib 专门用来描述字体属性的对象可以在任意文本接口里单独传递。基本构造方式有两种。第一种是直接指定字体族名from matplotlib.font_manager import FontProperties font_cn FontProperties(familySimHei, size12) font_en FontProperties(familyTimes New Roman, size10)第二种是指定字体文件路径font_cn FontProperties(fnameC:/Windows/Fonts/simhei.ttf, size12)这两种方式的取舍我的经验是family方式依赖系统里已经安装的字体且字体名必须和 matplotlib 内部识别名一致跨平台复制代码时容易踩坑fname方式直接指向文件只要路径确实存在就能用可控性更强适合在多个环境里交付同一套绘图脚本。缺点就是换机器时要改路径建议把所有字体路径抽到一个配置区统一管理。3.2 标题、坐标轴、刻度、图例的字体设置把FontProperties对象和对应接口对应起来实际操作就是这个效果import matplotlib.pyplot as plt from matplotlib.font_manager import FontProperties font_cn FontProperties(familySimHei, size13) font_en FontProperties(familyTimes New Roman, size11) fig, ax plt.subplots(figsize(8, 5)) ax.plot([1, 2, 3], [4, 5, 6], labelSample data) ax.set_title(中文标题 Title, fontpropertiesfont_cn) ax.set_xlabel(横轴标签 X Axis, fontpropertiesfont_cn) ax.set_ylabel(纵轴标签 Y Axis, fontpropertiesfont_cn) ax.legend(propfont_cn, locbest) plt.show()这段代码里标题、坐标轴标签和图例都用了font_cn整图文字风格一致。刻度标签要格外注意tick_params只能控制刻度标签的size、color、rotation等属性不能直接传字体。要单独修改刻度字体需要遍历取出标签再设置for label in ax.get_xticklabels(): label.set_fontproperties(font_cn) for label in ax.get_yticklabels(): label.set_fontproperties(font_cn)这一步经常被忽略结果是标题和坐标轴标签都换成目标字体了刻度上的数字却还是默认字体一眼看去非常不协调。在交付图表给客户时这种细节往往是最容易被挑刺的务必检查到位。3.3 单个文本内部的中英文混排有些图表里一个标题或注释本身就混着中英文比如“2024年销售额 Sales Trend”。用FontProperties指定一个字体时同样是单字体渲染整段文本没法做到中英文分开。但利用新版 matplotlib 的字体回退机制可以给一个文本对象同时准备两个字体font_mixed FontProperties(family[Times New Roman, SimHei]) ax.set_title(2024年销售额 Sales Trend, fontpropertiesfont_mixed)渲染时英文和数字优先使用 Times New Roman中文字符因为 Times 没有字形回退到 SimHei。这样就不需要为了追求英文效果把整个标题都改成中文字体标题里的英文字母和数字会明显更有西文排版的味道。这个方式同样可以应用在text、legend、annotate等所有接受fontproperties参数的地方适用范围非常广。如果你用的是 matplotlib 3.5 及更早版本FontProperties(family[...])这种列表形式不会触发回退只能通过fname指定一个同时支持中英文的混合字体文件来解决视觉效果上就没有那么灵活了。这也是我前面反复强调要关注版本的原因。4. 字体管理的细节查看、安装与绕过字体名4.1 查看本机 matplotlib 到底认哪些字体有时候配置写了警告也不报但渲染出来还是方块。最可能的原因就是你写的字体名不是 matplotlib 内部的识别名。用下面这段代码可以列出 matplotlib 当前真实加载的所有字体from matplotlib import font_manager for f in font_manager.fontManager.ttflist: print(f.name, |, f.fname)输出结果会是一大堆字体族名和对应的文件路径。比如你会看到Microsoft YaHei对应的fname是C:/Windows/Fonts/msyh.ttcSimHei对应C:/Windows/Fonts/simhei.ttf。你在配置里要使用f.name这个值而不是桌面字体管理器显示的中文名称。想快速筛选可用中文字体可以直接过滤fonts [f.name for f in font_manager.fontManager.ttflist if Hei in f.name or YaHei in f.name or CJK in f.name or Song in f.name] print(fonts)这个技巧在 Linux 服务器上尤其有用。你不会想在服务器上一遍遍试字体名直接一条过滤语句就能知道系统里有没有中文字体、matplotlib 认不认。检查完这一步很多报错都能从源头排除。4.2 用字体文件路径绕过字体注册问题字体名字匹配是个很磨人的问题。你有三台机器Windows 上叫Microsoft YaHeimacOS 上叫PingFang SCLinux 上叫Noto Sans CJK SC同一份代码要换三次名字。更耐造的方式是直接给fname参数写绝对路径。比如 Windows 下font_cn FontProperties(fnameC:/Windows/Fonts/simhei.ttf)Linux 下安装完 Noto 字体后路径通常是font_cn FontProperties(fname/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc)只要文件存在就能稳定渲染完全不管系统里那个字体被识别成什么名字。缺点就是换机器时需要改路径。我的做法是在代码里集中定义路径常量比如FONT_CN os.environ.get(FONT_CN, C:/Windows/Fonts/simhei.ttf)这样换环境只需要改环境变量不动绘图代码。4.3 各平台中文字体安装与验证如果你在 Linux 服务器上画图发现 matplotlib 里一个中文字体都没有不要急着怀疑 matplotlib先确认系统到底有没有中文字体。命令行下检查fc-list :langzh如果输出为空安装中文字体sudo apt update sudo apt install fonts-noto-cjk fonts-wqy-zenhei装完以后清除 matplotlib 缓存重启 Python 进程再执行一次前面第 4.1 节的字体列表基本就能看到Noto Sans CJK SC或WenQuanYi了。Windows 一般自带微软雅黑、宋体、黑体缺的不是字体本身而是 matplotlib 缓存没识别到。macOS 自带苹方和宋体也不需要额外操作关键是字体名要写对。平台中文黑体类中文宋体类西文字体WindowsMicrosoft YaHei / SimHeiSimSunTimes New Roman / ArialmacOSPingFang SC / STHeitiSongti SCTimes New Roman / ArialLinuxNoto Sans CJK SC / WenQuanYiNoto Serif CJK SCDejaVu Sans / Liberation Serif这个表适合要在不同机器上交付图表项目的同学参考。建议把选定字体的f.name和f.fname截图或注释到代码里免得换台电脑全部重来。5. 高频问题排查与避坑实录5.1 常见问题速查表现象原因解决方案中文显示为方框当前字体没有中文字形或系统中未安装中文字体安装中文字体或改用包含中文字形的字体族报警告 Font family xxx not foundmatplotlib 无法在系统中找到该字体名检查字体名是否为 matplotlib 识别名或改用 fname负号显示成方块Unicode 减号 U2212 缺乏合适字形设置axes.unicode_minusFalse设置了字体列表但完全无效果版本低于 3.6字体回退机制不存在升级 matplotlib或改用局部 FontPropertiessavefig 保存后中文乱码但 show 正常保存后端与显示后端的字体处理差异设置pdf.fonttype42等或更换后端Linux 服务器画图中文全部丢失系统没有安装任何 CJK 字体apt 安装 fonts-noto-cjk再清理缓存5.2 字体缓存损坏时的标准处理流程设置好字体后第一次重启 Python 进程通常才会生效。如果反复修改字体名都报同样的警告多半是缓存吃掉了旧状态。清理流程很简单import matplotlib print(matplotlib.get_cachedir())拿到缓存路径后关闭所有 Python 进程删除该目录下的缓存文件重新运行脚本。Windows 上缓存目录通常在%USERPROFILE%\.matplotlib或临时目录里macOS 和 Linux 则在~/.matplotlib或~/.cache/matplotlib下。清理缓存是一个看似简单但解决问题的绝招很多字体配置改了但图表没变化都是缓存没清掉导致的。5.3 保存图片前的重要补充配置很多人只在 Jupyter 里用plt.show()看图表觉得字体一切正常一用plt.savefig()导出 PDF 就出问题。这多数是后端字体嵌入策略不一样。推荐在画图脚本开头统一设置plt.rcParams[pdf.fonttype] 42 plt.rcParams[ps.fonttype] 42 plt.rcParams[svg.fonttype] none这样导出的矢量图里文字以字形轮廓信息保存不依赖查看方的字体库发论文、投期刊、交付报告都稳。如果是 PNG 之类的位图导出时记得给足dpi比如plt.savefig(out.png, dpi300)能减少部分渲染怪象。5.4 绘图案例常用代码组合把上面的方法串成一个可以直接抄的完整案例效果如下import matplotlib.pyplot as plt from matplotlib.font_manager import FontProperties # 全局配置英文优先 Times中文回退 SimHei plt.rcParams[font.family] [Times New Roman, SimHei] plt.rcParams[axes.unicode_minus] False # 局部覆盖某些文本强制中文字体 font_strong_cn FontProperties(familySimHei, size14) fig, ax plt.subplots(figsize(8, 5)) ax.plot([1, 2, 3], [4, -5, 6], markero, labelSales Data) ax.set_title(季度销售额趋势 Quarter Sales Trend, fontpropertiesfont_strong_cn) ax.set_xlabel(月份 Month, fontpropertiesfont_strong_cn) ax.set_ylabel(销售额万元Sales (10k CNY), fontpropertiesfont_strong_cn) ax.legend(propfont_strong_cn, locbest) # 刻度字体单独处理 for label in ax.get_xticklabels() ax.get_yticklabels(): label.set_fontproperties(font_strong_cn) plt.savefig(report_chart.png, dpi300, bbox_inchestight) plt.close()这段代码里全局配置已经让大多数文字自动中英分离局部用font_strong_cn强调中文标题和标签的显示避免大段英文标题与中文标题字体风格差距过大。实际项目里你可以根据需要调整具体字体名和字号。学过这一整套操作之后再遇到 matplotlib 字体问题就不要慌乱地乱改配置了。先看版本再查系统字体列表然后决定是用全局配置还是局部指定最后保存图片时注意后端字体设置。按这个流程走一遍基本半小时内就能定位问题。我个人在实际操作中最深的体会是交付用的图表尽量在代码里用字体文件路径直接指定别依赖系统字体名另外每次处理完字体问题后记得清理 matplotlib 缓存这一条能省下很多无谓的反复调试。