发布: 用 Python 给博客字体瘦了一下身
This commit is contained in:
1 parent
54900c6dc8
commit
a03cea6805
1 file changed
+275
@@ -0,0 +1,275 @@
|
||||
---
|
||||
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自动处理字体也是一个不错的选择。
|
||||
Reference in new issue
Block a user