4.4 文本渲染与字体


文档摘要

4.4 文本渲染与字体 本节摘要:Matplotlib 的文本渲染,本质是把一串字符交给某个字体文件去"找字形"。标题、刻度、图例、注释最终都要落到具体的字体上。中文在默认配置下常常变成空心方框,根源就是字体族里找不到中文字形。这一节从"中文为什么变方块"讲起,梳理字体家族、全局与局部配置、自定义字体加载几条路径,再讲 mathtext 数学公式渲染,最后落到跨平台出图时的字体嵌入与兼容性坑,让中文标签不再掉链子。 本节导航 阅读完本节,你应当能够: 说清中文显示为方框的直接原因,并区分"字体族选错"和"字形缺失"两种情况。 用 rcParams、FontProperties、函数参数三种方式设置字体,并分清全局与局部的边界。 加载自定义字体文件,并列出系统可用字体做核对。

4.4 文本渲染与字体

本节摘要:Matplotlib 的文本渲染,本质是把一串字符交给某个字体文件去"找字形"。标题、刻度、图例、注释最终都要落到具体的字体上。中文在默认配置下常常变成空心方框,根源就是字体族里找不到中文字形。这一节从"中文为什么变方块"讲起,梳理字体家族、全局与局部配置、自定义字体加载几条路径,再讲 mathtext 数学公式渲染,最后落到跨平台出图时的字体嵌入与兼容性坑,让中文标签不再掉链子。

本节导航

阅读完本节,你应当能够:

  1. 说清中文显示为方框的直接原因,并区分"字体族选错"和"字形缺失"两种情况。
  2. 用 rcParams、FontProperties、函数参数三种方式设置字体,并分清全局与局部的边界。
  3. 加载自定义字体文件,并列出系统可用字体做核对。
  4. 用 mathtext 写出常用数学表达式,理解它和完整 LaTeX 渲染的取舍。
  5. 说出矢量格式(PDF、SVG)下字体嵌入的注意点,给出跨平台出图的基本做法。

一、中文为什么变成方框

先看一个几乎所有新手都撞过的场面:代码里写了 plt.title("月度销量"),图上却印着四个空心方块。图能画出来,线条、坐标都对,唯独中文没显示。这个现象和印刷厂很像——排版师傅手里只有西文字模,你递过去一张中文底稿,他只能摆出几个"缺字"的空铅位。

Matplotlib 的文本引擎做的事类似。每个 Text 对象手里握着两样东西:一串字符,和一组字体属性。真正绘制时,它要把字符逐个映射到"字形",也就是字体文件里那个字的实际轮廓。映射的依据是字体属性里指定的字体族,比如 sans-serifserifmonospace。字体族不是某一个具体字体,而是一个"候选名单":拿 sans-serif 来说,它在不同系统上可能对应 DejaVu Sans、Arial、Liberation Sans 等一串字体。

关键就在这里。Matplotlib 自带的默认字体 DejaVu Sans,覆盖了拉丁字母、希腊字母和一部分符号,但几乎没有 CJK(中日韩)字形。当标题里出现汉字,引擎顺着候选名单一个个问"你这里有没有'月'这个字",名单里的字体都答没有,引擎没辙,就画一个空心方框当作占位。所以你看到方框,不是 Matplotlib 坏了,也不是编码乱了,纯粹是"没找到含这个字的字体"。

import matplotlib as mpl import matplotlib.pyplot as plt # 看看当前 sans-serif 族的候选名单 print(mpl.rcParams['font.sans-serif']) plt.title("月度销量") # 默认名单里没有中文字体,标题显示为方框 plt.plot([1, 2, 3], [3, 1, 2]) plt.show()

解决方向也就清楚了:往候选名单里塞一个真正带中文字形的字体。Windows 上有黑体、宋体、微软雅黑,macOS 上有苹方、冬青黑体,Linux 上常见文泉驿、思源黑体。把它们加进 font.sans-serif 列表,或者干脆把 font.family 指定成某个中文字体名,方框就会变回汉字。

⚠️ 常见坑:把字体名写错一个空格也会失效。字体名必须和系统里登记的"显示名"完全一致,而不是文件名。比如"微软雅黑"对应的英文显示名是 Microsoft YaHei,两者都可能被接受,但"微软雅黑.ttf"这种带扩展名的写法基本不会被识别。拿不准就用 font_manager 把系统字体列出来核对一遍。

二、字体配置的三条路径

Matplotlib 给字体配置留了三条路,作用范围各不相同。选哪条,取决于你是想"全图统一"还是"只改一处"。

第一条路是改 rcParams,这是全局配置。rcParams 是一个运行时参数字典,图表所有默认值都从这里来。改了它,之后画的每一张图都会用新字体,直到你改回来或进程结束。

import matplotlib as mpl import matplotlib.pyplot as plt mpl.rcParams['font.family'] = 'Microsoft YaHei' mpl.rcParams['font.size'] = 12 mpl.rcParams['axes.unicode_minus'] = False # 避免负号显示成方块 plt.title("全局字体已经生效") plt.plot([1, 2, 3], [2, 4, 1]) plt.show()

这里顺手带上 axes.unicode_minus 是因为它是个高频坑:Matplotlib 默认用 Unicode 的减号字符画负号,有些中文字体不含这个字形,负号也会变方块,设成 False 让它改用 ASCII 的连字符。和字体问题同源同解,一并处理最省事。

第二条路是 FontProperties 对象,走局部。你建一个字体属性对象,只把它传给某一个标题、某一段注释。它不碰全局默认值,别的文本该怎么显示还怎么显示。

from matplotlib.font_manager import FontProperties title_font = FontProperties(family='SimHei', size=16, weight='bold') note_font = FontProperties(family='KaiTi', size=10) plt.title("黑体标题", fontproperties=title_font) plt.text(2, 2, "楷体注释", fontproperties=note_font)

第三条路是直接在文本函数里写参数,算是最省事的一种局部配置。titlexlabeltextannotate 这些函数大多直接接受 fontsizefontweightfontstylefamily 这些关键字,适合快速微调。

plt.title("直接指定参数", fontsize=18, fontweight='bold', family='monospace') plt.xlabel("横轴", fontsize=14, fontstyle='italic')

这三条路不是互斥的,多数实战是混着用:先用 rcParams 把全局默认定成中文字体,再用函数参数对个别标题调大小粗细。

自定义字体是第四块拼图。系统里没有你想要的字体时,可以手动加载字体文件,把它注册进字体管理器。

from matplotlib import font_manager font_manager.fontManager.addfont(font_path) # font_path 指向字体文件 name = font_manager.FontProperties(fname=font_path).get_name() print(name) # 拿到登记后的显示名,再去 rcParams 里用

加载之后,可以用 font_manager.fontManager.get_font_names() 把系统里所有可用字体名都列出来,核对你要用的名字到底叫什么。也可以反过来用 findfont 查某个字体名到底命中了哪个字体文件,出图前确认一遍,能少踩很多"改了半天没生效"的坑。

配置方式 作用范围 适用场景 代价
rcParams 全局 全图统一默认字体 影响后续所有图
FontProperties 单个文本 只改某处特殊字体 代码略长
函数参数 单个文本 快速微调大小粗细 可设的属性有限
自定义字体加载 全局或局部 系统没有的字体 部署时要带字体文件

💡 关键直觉:全局管"默认",局部管"例外"。先定全局中文字体,再用局部参数覆盖个别地方,是最省心也最不容易乱的组合。

三、mathtext:把公式写进图里

科研图里常要标公式:坐标轴写个 ρ、标题带个 e 的负指数、图例里出现积分号。Matplotlib 对此给了两套引擎。

内置的那套叫 mathtext,用法是拿一对美元符号把内容包起来,Matplotlib 就会按 LaTeX 的一个子集去解析并渲染。它不需要你装任何额外软件,开箱即用,速度也快,代价是语法覆盖有限,复杂排版(比如对齐的方程组、自定义宏)它做不了。

import matplotlib.pyplot as plt plt.title(r'衰减曲线 $y = A e^{-\lambda t}$') plt.xlabel(r'时间 $t$') plt.ylabel(r'幅度 $A$') plt.text(1, 0.5, r'积分 $\int_0^\infty e^{-x^2} dx$')

注意字符串前面的 r,它告诉 Python 别去解析反斜杠,把 \lambda\int 原样交给 mathtext。漏了这个 r,反斜杠转义会先捣一次乱,公式十有八九报错或显示异常。

另一套是完整 LaTeX 渲染,靠 text.usetex 打开。它把文本交给系统里安装的 TeX 引擎去排版,能支持 mathtext 做不到的完整语法,出来的效果也更接近论文排版。

import matplotlib as mpl mpl.rcParams['text.usetex'] = True

两套怎么选?我个人的建议是:能 mathtext 解决就别上 usetex。usetex 的收益是完整度和美观,代价是必须装一整套 TeX 发行版、每次绘图都要等它编译、还要处理它和字体配置的联动。多数公式图,mathtext 那点语法已经够用。

⚠️ 常见坑:mathtext 和 usetex 是互斥的两条路,text.usetex 打开后,mathtext 的字符串会被丢给 TeX 处理,行为会变。另外中文字体配置和 usetex 的组合比较麻烦——TeX 默认不认中文字体,想中文和公式同框,往往得单独配置 TeX 的 CJK 支持,否则中文照样变方块。

四、跨平台出图的字体坑

字体问题最容易在"换一台机器"时爆出来。你本机配好了微软雅黑,图存成 PNG 发给同事,一切正常;可要是把绘图脚本放到 Linux 服务器上跑,或者直接交给别人用他的机器复现,对方系统里没有微软雅黑,图又变回方框了。

这背后是两种输出格式的差异。PNG 这类位图格式,出图时把文字光栅化成像素点,字形已经"烧"进图里了,别人打开不依赖字体。PDF、SVG 这类矢量格式不同,它们要么把字体嵌入文件,要么只记一个字体引用、等打开时再在读者机器上找。字体没嵌入、读者机器又没装,就会看到字体被替换,甚至中文消失。

针对跨平台,有三条实用做法。

一是给 font.family 传一个列表而不是单个名字,让它按顺序回退:

mpl.rcParams['font.family'] = ['Microsoft YaHei', 'SimHei', 'WenQuanYi Micro Hei', 'sans-serif']

这样 Windows 上命中第一个,Linux 上跳过后两个,最后回退到通用 sans-serif。列表式的回退链是应对环境差异最稳的一招。

二是尽量选跨平台可得的字体。思源黑体、文泉驿、Noto 系列在中英文环境下都比较容易找到,或者干脆把字体文件随项目一起分发、用 addfont 手动加载,让所有环境用同一份字体,效果最一致。

三是矢量出图时留意嵌入设置。保存 PDF 时把字体子集嵌入文件,能保证拿到文件的人看到同样的字;SVG 则可以要求把文本转成路径,彻底摆脱对读者端字体的依赖,代价是文件变大、文字不再可选可搜索。

plt.savefig("figure.pdf") # 矢量格式,注意字体嵌入 plt.savefig("figure.svg")

出图前最好先确认一件事:当前这张图实际用了哪个字体。可以用 findfont 反查,看看它到底命中了哪个字体文件,再决定要不要调整。

💡 关键直觉:位图把结果"烧死"在像素里,矢量把"字体引用"留给打开方。要分享就给位图,要可编辑的矢量就确保字体嵌进去,否则宁可把文字转路径。

五、文本的摆放:对齐、旋转与注释

字体选对了,还得把字摆对地方。文本对象有两个对齐参数:horizontalalignment 管水平,verticalalignment 管垂直。水平对齐 left、center、right 决定文字锚点在哪,垂直对齐 top、center、bottom 决定文字相对给定坐标是压在上面、居中还是垫在下面。默认的组合在很多场景下会让字"漂"出你想放的位置,尤其是给数据点加标签时,字常常盖住点本身。

plt.text(1.5, 4.5, '居中', ha='center', va='center') plt.text(1.5, 5, '靠左', ha='left', va='bottom')

rotation 是另一件常用工具。横轴刻度标签一多,互相挤在一起,转个 45 度立刻清爽。它不是装饰,是解决标签重叠的实用手段。

plt.xticks(rotation=45)

要给某个数据点加带箭头的说明,用 annotate。它比 text 多一个 xy 参数,指定箭头指向哪,再配一个 arrowprops 控制箭头样式,就能把"这个点很关键"这样的信息钉在图上。

plt.annotate('最小值', xy=(3, 1), xytext=(2, 2.5), arrowprops=dict(arrowstyle='->', color='black'))

这三样——对齐、旋转、箭头注释——是文本渲染里最常用的"摆位"操作。它们和字体选择是两个维度:字体决定"字长什么样",摆位决定"字放在哪、怎么指向"。一张信息密集的图,往往是字体不出错、摆位也讲究,两者缺一不可。

一节小结

  • 方框的本质是缺字形:中文变方块,是候选字体名单里没有含中文字形的字体,不是编码错乱。
  • 字体族是候选名单sans-serif 等家族会映射到一串具体字体,按顺序回退。
  • 三条配置路径各司其职:rcParams 管全局默认,FontProperties 和函数参数管局部覆盖。
  • axes.unicode_minus 要记得关:它决定负号用 Unicode 还是 ASCII,是中文图里的高频坑。
  • mathtext 够用再上 usetex:内置 mathtext 零依赖、快,覆盖了多数公式;完整 LaTeX 好看但要装 TeX 且更慢。
  • 跨平台靠回退链:把 font.family 写成列表,配合跨平台字体或随项目分发字体,最稳。
  • 矢量格式注意嵌入:PDF 要嵌字体子集,SVG 可转路径,否则换台机器就可能显示异常。

字体是图表的"脸面",解决了它,下一步我们跳到 Matplotlib 的周边生态,看看 Seaborn、Cartopy、mplfinance 这些扩展库是如何在它之上各管一摊的。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U