ubuntu系统下matplotlib中文乱码问题的解决方法

Page content

  ubuntu系统下matplotlib中文乱码问题的解决方法。

1、将字体文件放在matplotlib的字体目录下

查看matplotlib配置文件位置:

import matplotlib
print(matplotlib.matplotlib_fname())

/root/anaconda3/lib/python3.8/site-packages/matplotlib/mpl-data/matplotlibrc

则将字体文件放在/root/anaconda3/lib/python3.8/site-packages/matplotlib/mpl-data/fonts/ttf目录下。

2、修改matplotlib配置文件:

去掉3行注释,并添加字体配置:

font.family         : sans-serif        
font.sans-serif     : SimHei, Bitstream Vera Sans, Lucida Grande, Verdana, Geneva, Lucid, Arial, Helvetica, Avant Garde, sans-serif   

axes.unicode_minus : False #解决负号'-'显示为方块的问题

3、删除当前用户matplotlib 的缓存文件:

cd ~/.cache/matplotlib
rm -rf *.*

4、一键复制字体文件到matplotlib的字体目录下的脚本:

import os
import shutil
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
from matplotlib.textpath import TextPath

# 默认推荐的中文字体关键词,用于匹配系统已安装字体
default_font_keywords = ['SimHei', 'YaHei', 'FangSong', 'Heiti', 'Microsoft YaHei', 'Noto Sans CJK']

# 获取 Matplotlib 的字体目录、缓存目录和配置文件路径
matplotlib_dir = os.path.dirname(matplotlib.matplotlib_fname())
matplotlib_font_dir = os.path.join(matplotlib_dir, 'fonts', 'ttf')  # 字体文件目录
matplotlib_cache_dir = matplotlib.get_cachedir()  # 缓存目录,自动选择合适目录
matplotlibrc_path = matplotlib.matplotlib_fname()  # matplotlibrc 配置文件路径


def supports_chinese(font_path, test_char='汉'):
    """
    判断指定字体文件是否支持中文
    :param font_path: 字体文件路径
    :param test_char: 用于测试的中文字符,默认为 "汉"
    :return: True 支持,False 不支持
    """
    try:
        font_prop = fm.FontProperties(fname=font_path)  # 创建字体属性对象
        text_path = TextPath((0, 0), test_char, prop=font_prop)  # 通过 TextPath 演练
        return text_path.vertices.size > 0  # 如果有顶点,则表明能演练
    except Exception:
        return False  # 如果异常,则不支持


def find_chinese_fonts():
    """
    找出系统支持中文的字体
    :return: 返回 {font_name: font_path} 字典
    """
    font_paths = fm.findSystemFonts(fontpaths=None, fontext='ttf')  # 搜索 TTF 字体文件
    font_dict = {}
    for path in font_paths:
        if supports_chinese(path):  # 如果支持中文
            try:
                font_name = fm.FontProperties(fname=path).get_name()  # 获取字体名
                if font_name not in font_dict:
                    font_dict[font_name] = path  # 保存字体名和路径
            except Exception:
                continue  # 如果获取失败,继续
    return font_dict


def copy_fonts_to_matplotlib(font_dict, keywords):
    """
    将系统中符合关键词的字体备份到 Matplotlib 字体目录
    :return: [(font_name, font_path), ...]
    """
    copied_fonts = []
    for font_name, font_path in font_dict.items():
        if any(k.lower() in font_name.lower() for k in keywords):  # 检查是否包含推荐关键词
            try:
                target_path = os.path.join(matplotlib_font_dir, os.path.basename(font_path))
                if not os.path.exists(target_path):  # 如果未存在,则备份
                    shutil.copy(font_path, target_path)
                    print(f"✅ 已拷贝字体: {font_name}")
                else:
                    print(f"🟡 已存在字体: {font_name},跳过")
                copied_fonts.append((font_name, font_path))
            except Exception as e:
                print(f"❌ 拷贝失败 {font_name}: {e}")
    return copied_fonts


def clear_matplotlib_cache():
    """
    清除 Matplotlib 缓存文件,确保新字体生效
    """
    if os.path.exists(matplotlib_cache_dir):
        for f in os.listdir(matplotlib_cache_dir):
            file_path = os.path.join(matplotlib_cache_dir, f)
            try:
                if os.path.isfile(file_path):
                    os.remove(file_path)
            except Exception:
                continue


def auto_update_matplotlibrc(font_names):
    """
    自动更新 matplotlibrc 配置文件,设置默认字体和设置负号显示

    :param font_names: 列表,包含要设置为默认无衬线字体的字体名称
    """
    # 检查 matplotlibrc 配置文件是否存在
    if not os.path.exists(matplotlibrc_path):
        print(f"❌ 找不到配置文件: {matplotlibrc_path}")
        return

    # 以只读模式打开配置文件,将文件内容按行读取到列表中
    with open(matplotlibrc_path, 'r', encoding='utf-8') as f:
        lines = f.readlines()

    # 用于存储更新后的配置文件行
    updated_lines = []
    # 标记是否已经更新了 font.family、font.sans-serif 和 axes.unicode_minus 配置
    has_family, has_sans, has_minus = False, False, False
    # 生成新的 font.sans-serif 配置行,使用传入的字体名称列表
    new_sans_line = f"font.sans-serif: {', '.join(font_names)}\n"
    new_family_line = "font.family: sans-serif\n"
    new_minus_line = "axes.unicode_minus: False\n"

    # 遍历配置文件的每一行
    for line in lines:
        # 去除当前行首尾的空白字符
        stripped = line.strip()
        if stripped == new_family_line.strip():
            updated_lines.append(new_family_line)
            has_family = True
        elif stripped == new_sans_line.strip():
            updated_lines.append(new_sans_line)
            has_sans = True
        elif stripped == new_minus_line.strip():
            updated_lines.append(new_minus_line)
            has_minus = True
        elif stripped.startswith('#') and 'font.family' in stripped:
            if not has_family:
                updated_lines.append(new_family_line)
                has_family = True
            else:
                updated_lines.append(line)
        elif stripped.startswith('font.family'):
            if not has_family:
                updated_lines.append(new_family_line)
                has_family = True
            else:
                updated_lines.append(line)
        elif stripped.startswith('#') and 'font.sans-serif' in stripped:
            if not has_sans:
                updated_lines.append(new_sans_line)
                has_sans = True
            else:
                updated_lines.append(line)
        elif stripped.startswith('font.sans-serif'):
            if not has_sans:
                updated_lines.append(new_sans_line)
                has_sans = True
            else:
                updated_lines.append(line)
        elif stripped.startswith('axes.unicode_minus'):
            if not has_minus:
                updated_lines.append(new_minus_line)
                has_minus = True
            else:
                updated_lines.append(line)
        elif stripped.startswith('#') and 'axes.unicode_minus' in stripped:
            if not has_minus:
                updated_lines.append(new_minus_line)
                has_minus = True
            else:
                updated_lines.append(line)
        else:
            updated_lines.append(line)

    # 如果未更新 font.family 配置,在更新后的行列表末尾添加新的 font.family 配置
    if not has_family:
        updated_lines.append(new_family_line)
    # 如果未更新 font.sans-serif 配置,在更新后的行列表末尾添加新的 font.sans-serif 配置
    if not has_sans:
        updated_lines.append(new_sans_line)
    # 如果未更新 axes.unicode_minus 配置,在更新后的行列表末尾添加新的 axes.unicode_minus 配置
    if not has_minus:
        updated_lines.append(new_minus_line)

    # 以写入模式打开配置文件,将更新后的内容写入
    with open(matplotlibrc_path, 'w', encoding='utf-8') as f:
        f.writelines(updated_lines)

    # 打印配置文件更新成功信息
    print(f"\n✅ 配置文件已更新: {matplotlibrc_path}")
    print(" - font.family         : sans-serif")
    print(f" - font.sans-serif     : {', '.join(font_names)}")
    print(" - axes.unicode_minus  : False")


def draw_test_plot():
    """
    绘制一个测试图表,确保中文字体生效
    """
    plt.figure()
    plt.plot([1, 2, 3], [1, 4, 9], label="示例曲线")
    plt.title("中文标题测试")
    plt.xlabel("横坐标")
    plt.ylabel("纵坐标")
    plt.legend()

    plt.show()
    print("\n📈 已保存测试图像 test_plot.png")


def main():
    print("🔍 正在查找系统中支持中文的字体...\n")
    font_dict = find_chinese_fonts()
    font_list = sorted(font_dict.keys(), key=lambda x: x.lower())

    print("✅ 推荐字体中系统已安装的有:\n")
    available_fonts = []
    for font_name in font_list:
        if any(k.lower() in font_name.lower() for k in default_font_keywords):
            available_fonts.append(font_name)
            print(" -", font_name)

    print("\n📦 正在拷贝字体到 Matplotlib 字体目录...\n")
    copied_fonts = copy_fonts_to_matplotlib(font_dict, default_font_keywords)

    if copied_fonts:
        copied_font_names = [name for name, _ in copied_fonts]
        clear_matplotlib_cache()  # 清除缓存
        auto_update_matplotlibrc(copied_font_names)  # 配置 matplotlibrc
        draw_test_plot()  # 生成测试图
    else:
        print("⚠️ 未找到推荐字体,未备份或修改配置。")

    print("\n✅ 完成!你现在可以在 Matplotlib 中使用正确的中文字体了。")


if __name__ == '__main__':
    main()