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 核心知识体系

我把这章的知识点整理成了一张图,方便你对照理解:

Python 字体加载与渲染知识体系 字体加载与渲染 matplotlib 字体处理 PIL (Pillow) 字体处理 全局设置:rcParams['font.sans-serif'] 局部指定:FontProperties(fname=...) 查看字体:fontManager.ttflist 添加字体:fontManager.addfont() 加载字体:ImageFont.truetype() 渲染文字:draw.text() 路径处理:os.path 绝对路径 多行渲染:getbbox() 计算行高 核心原则 matplotlib 适合图表场景,PIL 适合图像场景 无论用哪个库,指定正确的字体文件路径是避免乱码的关键

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() 强制刷新。