4、Python 字体处理入门:使用 matplotlib 和 PIL 加载字体并渲染文字
好,咱们进入实战环节。前面几章聊了那么多字体原理和乱码成因,说白了都是在打地基。从这章开始,我们要亲手用 Python 操作字体了。
我个人习惯把字体处理分成两步:加载和渲染。加载是告诉程序「我要用哪个字体文件」,渲染是「把文字画出来」。Python 里最常用的两个库——matplotlib 和 PIL(Pillow),正好覆盖了这两个场景。
本章核心目标:学会用 matplotlib 和 PIL 加载系统字体或自定义字体文件,并正确渲染中文字符,避免出现方框乱码。
4.1 为什么选这两个库?
你可能会问:Python 能处理字体的库不少啊,为什么单拎 matplotlib 和 PIL 出来?
我遇到过不少项目,数据可视化用 matplotlib,图像处理用 PIL。这两个库几乎覆盖了 90% 的日常字体渲染需求。而且它们对字体加载的机制完全不同——matplotlib 走的是「字体回退链」,PIL 走的是「直接指定」。理解这两套逻辑,你就能应对绝大多数场景了。
| 特性 | matplotlib | PIL (Pillow) |
|---|---|---|
| 主要用途 | 图表文字、标签、标题 | 图像水印、文字合成、验证码 |
| 字体加载方式 | 通过 rcParams 或 FontProperties | 通过 ImageFont.truetype() |
| 回退机制 | 自动回退(但中文常出问题) | 无回退,找不到字体直接报错 |
| 适用场景 | 数据分析、科学绘图 | 图像处理、封面制作 |
4.2 matplotlib 字体加载:从「方框」到「汉字」
先说说 matplotlib。很多初学者第一次画图加中文标题,结果出来一排小方框。嗯,这几乎是每个用 matplotlib 的人都会踩的坑。
4.2.1 默认字体的问题
matplotlib 默认字体是 DejaVu Sans,这货不支持中文。你直接写 plt.title('中文标题'),它找不到对应字形,就给你画个方框。
我曾经在一个项目里,给客户演示数据看板,满屏的方框……那场面,别提多尴尬了。从那以后,我每次新建 matplotlib 项目,第一件事就是配字体。
4.2.2 方法一:全局设置(推荐)
import matplotlib.pyplot as plt
import matplotlib as mpl
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei'] # 黑体
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
# 测试
plt.plot([1, 2, 3], [4, 5, 6])
plt.title('中文标题测试')
plt.show()
小提示:axes.unicode_minus 设为 False 是因为 SimHei 等中文字体里的负号「-」和标准减号不一样,不设置的话坐标轴负号会显示成乱码。
4.2.3 方法二:局部指定(灵活)
如果你不想全局改,只想某个图表用中文字体,可以用 FontProperties:
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
# 指定字体文件路径
font_path = '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc'
font_prop = FontProperties(fname=font_path, size=12)
plt.plot([1, 2, 3], [4, 5, 6])
plt.title('局部字体测试', fontproperties=font_prop)
plt.show()
我个人更推荐方法一。为什么?因为项目大了以后,每个图都传一遍 fontproperties 太容易遗漏。全局设一次,省心。
4.2.4 查看可用字体
有时候你设了字体名,但 matplotlib 找不到。这时候可以列一下它认识哪些字体:
from matplotlib.font_manager import fontManager
# 列出所有已识别的字体
for font in fontManager.ttflist:
print(font.name, font.fname)
这个列表很长。你可以搜一下中文字体名,比如 SimHei、Microsoft YaHei、WenQuanYi Zen Hei。如果列表里没有,说明 matplotlib 没找到你的字体文件。
注意:matplotlib 不会自动扫描系统所有字体目录。它默认只搜 matplotlib/mpl-data/fonts/ 和系统标准字体路径。如果你把字体放在自定义目录,需要手动添加路径:
fontManager.addfont('/path/to/your/font.ttf')
4.3 PIL 字体加载:精确控制每一像素
PIL(Pillow)是另一个常用库。它不像 matplotlib 那样有回退机制,加载字体必须给明确路径。好处是——你指哪打哪,不会出现「明明设了字体却还是方框」的玄学问题。
4.3.1 基本用法
from PIL import Image, ImageDraw, ImageFont
# 创建空白图片
img = Image.new('RGB', (400, 100), color='white')
draw = ImageDraw.Draw(img)
# 加载字体
font = ImageFont.truetype('SimHei.ttf', size=24)
# 渲染文字
draw.text((20, 30), 'PIL 中文渲染测试', font=font, fill='black')
img.show()
这里有个坑:ImageFont.truetype() 的第一个参数可以是字体文件名(如果在当前目录),也可以是绝对路径。我建议用绝对路径,避免「找不到字体」的报错。
4.3.2 处理字体文件路径
在实际项目中,字体文件通常放在项目目录的 fonts/ 文件夹下。我习惯这样写:
import os
from PIL import Image, ImageDraw, ImageFont
# 获取当前脚本所在目录
base_dir = os.path.dirname(os.path.abspath(__file__))
font_path = os.path.join(base_dir, 'fonts', 'NotoSansSC-Regular.ttf')
# 检查文件是否存在
if not os.path.exists(font_path):
raise FileNotFoundError(f'字体文件未找到: {font_path}')
font = ImageFont.truetype(font_path, size=28)
经验之谈:我曾经把字体文件放在项目根目录,结果部署到服务器后路径变了,程序直接崩溃。后来我统一用 os.path.join 拼接路径,再也没出过问题。
4.3.3 多行文字与对齐
PIL 的 text() 方法不支持自动换行。你需要自己计算位置:
lines = ['第一行文字', '第二行文字', '第三行文字']
y_offset = 20
for line in lines:
draw.text((20, y_offset), line, font=font, fill='black')
y_offset += font.getbbox(line)[3] + 10 # 行高 + 间距
这里 font.getbbox() 返回的是字形的边界框,取第三个元素(高度)作为行高。嗯,这个方法比老版的 getsize() 更准确。
4.4 核心知识体系
我把这章的知识点整理成了一张图,方便你对照理解:
4.5 避坑指南
最后,分享几个我踩过的坑,你遇到了可以直接跳过:
- 字体名 vs 字体文件名:matplotlib 的
font.sans-serif里填的是字体名(如 'SimHei'),不是文件名。PIL 的truetype()里填的是文件路径。别搞混了。 - ttf 和 ttc 的区别:ttc 是集合包,里面可能包含多个字体。PIL 加载 ttc 时需要指定索引,比如
ImageFont.truetype('msyh.ttc', index=0)。 - 缓存问题:matplotlib 会缓存字体信息。如果你新装了字体但 matplotlib 不认,可以删除
~/.cache/matplotlib/目录下的缓存文件,重启程序。 - 服务器环境:很多 Linux 服务器默认没有中文字体。我建议在项目里自带一个开源中文字体文件(比如 Noto Sans SC),用绝对路径加载,避免依赖系统字体。
重要提醒:如果你在 Jupyter Notebook 里运行 matplotlib,改了 rcParams 后需要重启 kernel 才能生效。或者用 %matplotlib inline 配合 plt.rcParams.update() 强制刷新。