Files

276 lines
9.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: 用 Python 给博客字体瘦了一下身
date: '2026-06-04T00:00:00.000Z'
slug: '202606041003'
draft: false
categories:
- 技术
tags:
- Python
- 字体
- 优化
author: 小赵
layout: post
---
## 前言
之前18兄弟推荐我换到astro框架。
https://jb18.cm
这个astro框架我之前了解过,按需加载,0 JavaScript,还支持SSG、SSR和中间件,既可以静态也可以动态。
https://docs.astro.js.cn/en/getting-started
目前这个主题我已经使用了五年了,里面堆砌着大大小小的屎山,要是迁移的话算是一次不小的工程。后面考虑了一下,还是继续待在hugo。
既然不准备换到astro,那么就借鉴一下astro的优点,取长补短。
周末花了一点时间给博客做了一波JS按需加载优化,效果还不错——首页JS从800KB直接砍到了350KB。
打开Chrome DevTools的Network测试了一下,最影响加载速度的还是字体文件。从开始使用这个主题到现在一直使用的这个字体,看习惯了,也没必要因为字体文件大就换。看了一下字体文件大小大概有1.2MB,反正比我的JS还大。
说起来,之前也写过一篇用fontspider压缩字体的文章,不过那个工具年久失修,对静态博客支持不太好。最近正好在学Python,发现了一个更好用的工具——**fonttools**。
这篇就跟各位朋友来聊聊怎么用fonttools,给字体瘦瘦身。
## 为什么字体文件这么大?
先给大家科普一下,为什么中文字体动不动就几MB。
中文字和英文字不一样。英文字母就26个大小写,加上数字和符号,撑死几百个字符。但中文呢?《通用规范汉字表》收录了8105个汉字,如果算上生僻字,得有好几万。
而字体文件里面,是把**所有字符的字形**都打包进去的。也就是说,不管你用没用到“龘”这个字,它都在你的字体文件里躺着,白白占空间。
但实际上,一个博客能用到多少字呢?我统计了一下我的博客,也就**2500个字符**左右。
**1.2MB的字体,实际只需要700多KB,剩下的都是浪费。**
## 解决方案:Python + fonttools
### 什么是fonttools?
fonttools是一个Python库,专门用来处理字体文件。它能做很多事情,比如读取字体信息、转换格式、提取字符等等。
我们要用到的功能是**字体子集化**(Font Subsetting)——简单来说,就是只保留我们用到的字符,把其他的都扔掉。
### 安装环境
首先,你需要安装Python。这个就不多说了,去官网下载安装就行:
https://www.python.org/downloads/
安装的时候记得勾选 **“Add Python to PATH”**,这样就能在命令行里直接用了。
装好之后,打开cmd,输入:
```bash
python --version
```
能看到版本号就说明装好了。
然后安装fonttools和brotli(用于压缩):
```bash
pip install fonttools brotli
```
如果提示权限不够,加上 `--break-system-packages`:
```bash
pip install fonttools brotli --break-system-packages
```
## 编写优化脚本
我写了一个Python脚本,可以自动扫描博客的所有内容,提取用到的字符,然后生成精简版字体。
如果你是Hugo框架,可以直接套用我的脚本;如果是其他框架,可以把这篇文章或者关键词喂给AI,也是同理。
首先在项目根目录创建一个文件:`scripts/subset-font-safe.py`
```python
#!/usr/bin/env python3
"""
字体子集化脚本
从HTML和CSS文件中提取实际使用的字符,生成优化的子集字体
"""
import os
import re
from fontTools.ttLib import TTFont
from fontTools.subset import Subsetter, Options
def extract_chars_from_files(directories):
"""从文件中提取使用的字符"""
chars = set()
for directory in directories:
if not os.path.exists(directory):
print(f"⚠️ 目录不存在,跳过: {directory}")
continue
print(f"🔍 扫描目录: {directory}")
for root, dirs, files in os.walk(directory):
for file in files:
if file.endswith(('.html', '.md', '.css')):
filepath = os.path.join(root, file)
try:
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# 提取中文字符
chinese_chars = re.findall(r'[一-鿿]', content)
chars.update(chinese_chars)
# 提取英文和数字
ascii_chars = re.findall(r'[a-zA-Z0-9]', content)
chars.update(ascii_chars)
# 提取常用标点
en_punct = set('!@#$%^&*()_+-=[]{}|;:,.<>?/\\`~"\'-')
chars.update(en_punct)
except Exception as e:
print(f"⚠️ 读取失败 {filepath}: {e}")
return chars
def main():
print("🔤 字体子集化工具")
print("=" * 50)
# 配置路径
font_dir = "themes/Ying/static/font"
input_font = os.path.join(font_dir, "zql-v2.woff2")
chars_file = os.path.join(font_dir, "used_chars.txt")
# 检查输入文件
if not os.path.exists(input_font):
print(f"❌ 字体文件不存在: {input_font}")
return
# 读取现有的字符列表(如果有)
existing_chars = set()
if os.path.exists(chars_file):
with open(chars_file, 'r', encoding='utf-8') as f:
existing_chars = set(f.read())
print(f"📖 读取现有字符: {len(existing_chars)} 个")
# 扫描目录
scan_dirs = ["content", "layouts"]
if os.path.exists("public"):
scan_dirs.append("public")
# 提取字符
new_chars = extract_chars_from_files(scan_dirs)
print(f"📝 从文件提取了 {len(new_chars)} 个字符")
# 合并字符(保留手动添加的字符)
all_chars = existing_chars | new_chars
print(f"📊 合并后: {len(all_chars)} 个字符")
# 保存字符列表
with open(chars_file, 'w', encoding='utf-8') as f:
f.write(''.join(sorted(all_chars)))
# 生成woff2子集
output_woff2 = os.path.join(font_dir, "zql-v2-subset.woff2")
print(f"\n🎯 生成 woff2 子集字体...")
font = TTFont(input_font)
options = Options()
options.flavor = 'woff2'
subsetter = Subsetter(options=options)
subsetter.populate(text=''.join(all_chars))
subsetter.subset(font)
font.save(output_woff2)
# 计算优化效果
original_size = os.path.getsize(input_font)
subset_size = os.path.getsize(output_woff2)
reduction = original_size - subset_size
percentage = (reduction / original_size) * 100
print(f"\n✅ 优化完成!")
print(f"📊 原始大小: {original_size / 1024:.1f} KB")
print(f"📊 子集大小: {subset_size / 1024:.1f} KB")
print(f"📊 减少: {reduction / 1024:.1f} KB ({percentage:.1f}%)")
if __name__ == "__main__":
main()
```
### 脚本主要干了什么?
其实这个脚本的逻辑很简单,主要分三步:
1. **扫描**:遍历 `content`、`layouts`、`public` 目录,找出所有用到的字符。
2. **提取**:把中文、英文、数字、标点符号都提取出来。
3. **生成**:用fonttools生成只包含这些字符的字体。
这里有个细节:脚本会读取 `used_chars.txt` 里手动添加的字符。这样如果你有一些特殊字符(比如我博客里的古风官职表),可以手动加进去,不会被覆盖。
## 运行效果
首先,确保你已经构建过Hugo:
```bash
hugo --destination=public
```
然后运行脚本:
```bash
python scripts/subset-font-safe.py
```
我这边的运行结果:
```
🔤 字体子集化工具
==================================================
📖 读取现有字符: 2489 个
🔍 扫描目录: content
🔍 扫描目录: layouts
🔍 扫描目录: public
📝 从文件提取了 2492 个字符
📊 合并后: 2500 个字符
🎯 生成 woff2 子集字体...
✅ 优化完成!
📊 原始大小: 1226.6 KB
📊 子集大小: 740.7 KB
📊 减少: 485.9 KB (39.6%)
```
**1.2MB的字体,优化后只剩740KB,减少了40%!**
## 更新CSS
字体文件生成了,还需要把你的字体CSS文件替换一下路径。打开 `themes/Ying/assets/css/main.css`,找到字体声明:
```css
@font-face {
font-family: 'zql';
src: url('../font/zql-v2.woff2') format('woff2'),
url('../font/zql-v2.woff') format('woff');
font-display: swap;
unicode-range: U+0000-007F, U+4E00-9FFF, U+2000-206F, U+3000-303F;
}
```
改成:
```css
@font-face {
font-family: 'zql';
src: url('../font/zql-v2-subset.woff2') format('woff2'),
url('../font/zql-v2-subset.woff') format('woff');
font-display: swap;
unicode-range: U+0000-007F, U+4E00-9FFF, U+2000-206F, U+3000-303F;
}
```
这样就OK了,还是很简单的。如果你觉得手动执行py脚本比较麻烦,其实编写GitHub Action自动处理字体也是一个不错的选择。