Hexo中字体的引入与压缩

引入tff字体

本文中涉及的文件没有的话再对应的目录下创建即可

首先,找一个你喜欢的字体。你可以在一些网站上免费下载。下载完之后,把那个.ttf格式的文件放到你网站根目录下的/source/fonts文件夹里。

免费字体下载大全,可免费商用中文字体一览表 - 猫啃网

接下来,你需要把下面的代码加到/source/css/custom.css文件里,这样就可以引入字体了。

1
2
3
4
5
6
7
@font-face {
font-family: 'BailuFeiYun';
src: url('/fonts/BailuFeiYu.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap; /* 关键:防止闪烁,先显示后备字体 */
}

记得在_config.anzhiyu.yml文件里加上custom.css,并且设置一下“# Global font settings”那部分。

1
<link rel="stylesheet" href="/css/custom.css">

最后一步就是清理缓存,然后重新生成就行了。

1
2
hexo clean
hexo generate

tff转换woff2

.ttf字体文件有时候会很大,这样会让网站加载变慢。为了解决这个问题,你可以把.ttf文件转换成.woff2格式,这种格式的文件更小,加载更快。你可以在一些免费的在线网站上完成这个转换。

TTF to WOFF2 | CloudConvert

记得改一下这里的代码。

1
2
3
4
5
6
7
8
@font-face {
font-family: 'BailuFeiYun';
src: url('/fonts/BailuFeiYu.woff2') format('woff2'),
url('/fonts/BailuFeiYun.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap; /* 关键:防止闪烁,先显示后备字体 */
}

字体压缩

如果你引用了*.woff2格式的字体文件,但网站加载还是很慢,那可能就得考虑压缩字体了——可以用字蛛(font-spider)这个工具。

1、安装字蛛

1
2
3
4
5
# 全局安装
npm install font-spider -g

# 验证安装
font-spider --version

2、准备工作目录

1
2
3
4
5
6
7
8
9
10
# 创建工作目录
mkdir font-spider-work
cd font-spider-work

# 创建必要文件
touch index.html
mkdir -p source/fonts

# 放入原字体
cp /source/fonts/BailuFeiYun.ttf source/fonts/

3、建一个index.html文件,在里面放上你博客要用的所有字体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!DOCTYPE html>
<html>

<head>
<meta charset="UTF-8">
<title>Font Spider</title>
<style>
@font-face {
font-family: 'BailuFeiYun';
src: url('../source/fonts/BailuFeiYun.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}

.font-test {
font-family: 'BailuFeiYun';
font-size: 16px;
line-height: 1.8;
}
</style>
</head>

<body>
<div class="font-test">
<!-- 把你博客的所有文字放这里 -->
首页文章归档分类标签友链关于
技术分享HexoWordPress建站心路
上一篇下一篇打赏作者复制链接
猫笔千锤岁月长啃文万遍见真功
<!-- 继续添加更多文字... -->

<!-- 常用标点符号 -->
,。!?、;:" " ' ' ()《》【】… — ~ · |

<!-- 常用英文和数字 -->
abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789

<!-- 你的博客标题/昵称(如果有特殊字) -->
安知鱼 白路飞云 手写体 博客 笔记 技术 生活 随笔

<!-- 从你的文章提取的高频文字(复制几篇文章的标题和摘要) -->
<!-- 这里粘贴你几篇文章的标题和内容中的汉字 -->
</div>
</body>

</html>

4、执行压缩

1
font-spider index.html

最后,只需要在custom.css文件里加入压缩后的字体就行了。

压缩脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Hexo 博客字体压缩脚本 - 动态完整版
扫描汉字、英文、数字、标点
"""

import os
import re
import sys
import yaml
import json


def extract_chars_from_file(filepath, skip_comments=False):
"""从文件提取所有字符(汉字、英文、数字、标点)"""
try:
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()

# 如果需要过滤注释(YAML 文件)
if skip_comments:
lines = content.split('\n')
content = '\n'.join([line for line in lines if not line.strip().startswith('#')])

chars = set()

# 1. 汉字
chars.update(re.findall(r'[\u4e00-\u9fa5]', content))

# 2. 英文字母(大小写)
chars.update(re.findall(r'[a-zA-Z]', content))

# 3. 数字
chars.update(re.findall(r'[0-9]', content))

# 4. 常用标点符号
# 中文标点
chars.update(re.findall(r'[,。!?、;:""''()《》【】…—~·|「」『』【】]', content))
# 英文标点
chars.update(re.findall(r'[.,!?;:\'\"()\[\]{}@#$%^&*+=_\-\\/<>~`]', content))

return chars

except Exception as e:
print(f"读取文件失败 {filepath}: {e}")
return set()


def extract_chars_from_json_yaml(filepath):
"""从 JSON/YAML 文件递归提取所有字符串中的字符"""
chars = set()
try:
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()

# 尝试解析为 JSON 或 YAML
data = None
try:
data = json.loads(content)
except:
try:
data = yaml.safe_load(content)
except:
pass

def extract_strings(obj):
if isinstance(obj, str):
result = set()
# 汉字
result.update(re.findall(r'[\u4e00-\u9fa5]', obj))
# 英文
result.update(re.findall(r'[a-zA-Z]', obj))
# 数字
result.update(re.findall(r'[0-9]', obj))
# 标点
result.update(re.findall(r'[,。!?、;:""''()《》【】…—~·|「」『』【】.,!?;:\'\"()\[\]{}@#$%^&*+=_\-\\/<>~`]', obj))
return result
elif isinstance(obj, list):
result = set()
for item in obj:
result.update(extract_strings(item))
return result
elif isinstance(obj, dict):
result = set()
for key, value in obj.items():
result.update(extract_strings(key))
result.update(extract_strings(value))
return result
return set()

if data:
chars.update(extract_strings(data))
else:
# 解析失败,直接正则提取
chars.update(extract_chars_from_file(filepath))

except Exception as e:
print(f"读取失败 {filepath}: {e}")

return chars


def extract_title_from_md(filepath):
"""只提取 Markdown 文件的 Front-matter title"""
try:
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()

# 匹配 Front-matter
fm_match = re.match(r'^---\s*\n(.*?)\n---', content, re.DOTALL)
title = ''
if fm_match:
try:
fm = yaml.safe_load(fm_match.group(1))
title = str(fm.get('title', ''))
except:
pass

# 备用:正则提取 title: xxx
if not title:
title_match = re.search(r'^title:\s*(.+)$', content, re.MULTILINE)
if title_match:
title = title_match.group(1).strip().strip('"\'')

# 提取所有字符
chars = set()
chars.update(re.findall(r'[\u4e00-\u9fa5]', title))
chars.update(re.findall(r'[a-zA-Z]', title))
chars.update(re.findall(r'[0-9]', title))
chars.update(re.findall(r'[,。!?、;:""''()《》【】…—~·|「」『』【】.,!?;:\'\"()\[\]{}@#$%^&*+=_\-\\/<>~`]', title))

return chars

except Exception as e:
print(f"读取失败 {filepath}: {e}")
return set()


def get_dynamic_fixed_chars(hexo_root):
"""从配置文件动态读取固定字符"""
chars = set()

# 1. 从站点配置 _config.yml 读取
site_config = os.path.join(hexo_root, '_config.yml')
if os.path.exists(site_config):
print(f" 读取站点配置: _config.yml")
config_chars = extract_chars_from_file(site_config, skip_comments=True)
chars.update(config_chars)
print(f" -> {len(config_chars)} 字符")

# 2. 从主题配置 _config.anzhiyu.yml 读取(过滤注释)
theme_config = os.path.join(hexo_root, '_config.anzhiyu.yml')
if os.path.exists(theme_config):
print(f" 读取主题配置: _config.anzhiyu.yml")
config_chars = extract_chars_from_file(theme_config, skip_comments=True)
chars.update(config_chars)
print(f" -> {len(config_chars)} 字符")

# 3. 从主题语言文件读取
theme_lang_dir = os.path.join(hexo_root, 'themes', 'anzhiyu', 'languages')
if os.path.exists(theme_lang_dir):
print(f" 读取主题语言文件:")
for lang_file in os.listdir(theme_lang_dir):
if lang_file.endswith(('.yml', '.yaml')):
filepath = os.path.join(theme_lang_dir, lang_file)
lang_chars = extract_chars_from_file(filepath, skip_comments=True)
if lang_chars:
chars.update(lang_chars)
print(f" {lang_file} -> {len(lang_chars)} 字符")

return chars


def scan_directory_full(directory, chars_set, skip_dirs=None, depth=0):
"""递归扫描目录下所有文件"""
if skip_dirs is None:
skip_dirs = ['node_modules', '.git', 'images', 'img', 'fonts', 'css', 'js']

if not os.path.exists(directory):
return 0, 0

file_count = 0
total_chars = 0

prefix = " " * depth

for root, dirs, files in os.walk(directory):
dirs[:] = [d for d in dirs if d not in skip_dirs and not d.startswith('.')]

for file in files:
filepath = os.path.join(root, file)
rel_path = os.path.relpath(filepath, directory)

file_chars = set()

if file.endswith('.md'):
file_chars = extract_chars_from_file(filepath)
elif file.endswith(('.json', '.yml', '.yaml')):
file_chars = extract_chars_from_json_yaml(filepath)
elif file.endswith(('.html', '.htm', '.txt')):
file_chars = extract_chars_from_file(filepath)

if file_chars:
chars_set.update(file_chars)
file_count += 1
total_chars += len(file_chars)
print(f"{prefix} {rel_path} -> {len(file_chars)} 字符")

return file_count, total_chars


def generate_char_file(output_path, hexo_root):
"""生成字符文件"""
print("=" * 60)
print("Hexo 博客字体压缩工具 - 动态完整版")
print("=" * 60)

chars = set()

# 1. 文章目录:只扫描标题
posts_dir = os.path.join(hexo_root, 'source', '_posts')
if os.path.exists(posts_dir):
print(f"\n📁 扫描文章标题(仅title): {posts_dir}")
count = 0
for root, dirs, files in os.walk(posts_dir):
dirs[:] = [d for d in dirs if d not in ['node_modules', '.git']]
for file in files:
if file.endswith('.md'):
filepath = os.path.join(root, file)
title_chars = extract_title_from_md(filepath)
chars.update(title_chars)
count += 1
print(f" 扫描了 {count} 篇文章,提取 {len(chars)} 个独特字符")

# 2. 其他页面目录:全内容扫描
source_dir = os.path.join(hexo_root, 'source')
if os.path.exists(source_dir):
print(f"\n📁 扫描页面目录(全内容递归):")

for item in os.listdir(source_dir):
if item in ['_posts', 'images', 'img', 'fonts', 'css', 'js']:
continue

item_path = os.path.join(source_dir, item)

if os.path.isdir(item_path):
print(f"\n 📂 {item}/")
file_count, total_chars = scan_directory_full(item_path, chars, depth=1)
print(f" 合计: {file_count} 个文件,{total_chars} 字符")

# 3. 扫描 _data 目录
data_dir = os.path.join(hexo_root, 'source', '_data')
if os.path.exists(data_dir):
print(f"\n📁 扫描数据目录(_data):")
file_count, total_chars = scan_directory_full(data_dir, chars, depth=1)
print(f" 合计: {file_count} 个文件,{total_chars} 字符")

# 4. 动态读取配置文件
print(f"\n📁 动态读取配置文件:")
fixed_chars = get_dynamic_fixed_chars(hexo_root)
chars.update(fixed_chars)
print(f" 配置文件共提取: {len(fixed_chars)} 字符")

# 5. 排序并保存
# 按字符类型排序:汉字 -> 英文 -> 数字 -> 标点
hanzi = sorted([c for c in chars if '\u4e00' <= c <= '\u9fa5'])
english = sorted([c for c in chars if c.isalpha()])
digits = sorted([c for c in chars if c.isdigit()])
others = sorted([c for c in chars if not ('\u4e00' <= c <= '\u9fa5' or c.isalnum())])

sorted_chars = ''.join(hanzi + english + digits + others)

with open(output_path, 'w', encoding='utf-8') as f:
f.write(sorted_chars)

print(f"\n" + "=" * 60)
print(f"✅ 共收集 {len(chars)} 个独特字符")
print(f" - 汉字: {len(hanzi)} 个")
print(f" - 英文: {len(english)} 个")
print(f" - 数字: {len(digits)} 个")
print(f" - 标点/其他: {len(others)} 个")
print(f"💾 已保存到: {output_path}")

preview = sorted_chars[:100]
print(f"\n📋 字符预览: {preview}...")

return sorted_chars


def subset_font(input_font, output_font, chars_file):
"""使用 fonttools 子集化字体"""
try:
from fontTools.subset import main as subset_main
except ImportError:
print("\n❌ 缺少 fonttools,正在安装...")
os.system(f"{sys.executable} -m pip install fonttools brotli zopfli")
from fontTools.subset import main as subset_main

print(f"\n🔧 开始压缩字体...")
print(f" 输入: {input_font}")
print(f" 输出: {output_font}")

args = [
input_font,
f"--text-file={chars_file}",
f"--output-file={output_font}",
"--flavor=woff2",
"--desubroutinize",
"--recalc-bounds",
"--canonical-order",
"--layout-features=*",
]

ttf_output = output_font.replace('.woff2', '.ttf')
args_ttf = [
input_font,
f"--text-file={chars_file}",
f"--output-file={ttf_output}",
"--desubroutinize",
"--recalc-bounds",
]

try:
subset_main(args)
subset_main(args_ttf)

original_size = os.path.getsize(input_font)
woff2_size = os.path.getsize(output_font)
ttf_size = os.path.getsize(ttf_output)

print(f"\n✅ 压缩完成!")
print(f" 原始大小: {original_size/1024:.2f} KB")
print(f" WOFF2 大小: {woff2_size/1024:.2f} KB (压缩率: {woff2_size/original_size*100:.1f}%)")
print(f" TTF 大小: {ttf_size/1024:.2f} KB (压缩率: {ttf_size/original_size*100:.1f}%)")

return True

except Exception as e:
print(f"\n❌ 压缩失败: {e}")
return False


def main():
"""主函数"""
HEXO_ROOT = "/www/wwwroot/myblog"
WORK_DIR = "/www/wwwroot/myblog/font-spider-work"
INPUT_FONT = os.path.join(WORK_DIR, "fonts", "BailuFeiYun.ttf")

CHARS_FILE = os.path.join(WORK_DIR, "chars-full.txt")
OUTPUT_WOFF2 = os.path.join(HEXO_ROOT, "source", "fonts", "BailuFeiYun.woff2")

if not os.path.exists(INPUT_FONT):
print(f"❌ 未找到原始字体: {INPUT_FONT}")
sys.exit(1)

os.makedirs(os.path.dirname(OUTPUT_WOFF2), exist_ok=True)

chars = generate_char_file(CHARS_FILE, HEXO_ROOT)

if len(chars) < 50:
print("⚠️ 提取字符过少,请检查路径")
sys.exit(1)

success = subset_font(INPUT_FONT, OUTPUT_WOFF2, CHARS_FILE)

if success:
print(f"\n🎉 全部完成!")
print(f" 压缩后的字体已输出到: {os.path.dirname(OUTPUT_WOFF2)}")
else:
sys.exit(1)


if __name__ == "__main__":
main()

工作流程

1
2
3
4
5
6
7
8
9
10
1. 扫描文章标题 ──┐
2. 扫描页面内容 ──┤
3. 扫描 _data 目录 ─┼──> 收集所有字符 ──> 去重排序 ──> 生成字符文件
4. 扫描配置文件 ──┤
5. 扫描语言文件 ──┘

6. 使用 fonttools 压缩字体
├── 输入:原始 TTF 字体
├── 根据字符文件裁剪
└── 输出:WOFF2 + TTF(子集化后)

输出示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
============================================================
Hexo 博客字体压缩工具 - 动态完整版
============================================================

📁 扫描文章标题(仅title): /www/wwwroot/myblog/source/_posts
扫描了 17 篇文章,提取 143 个独特字符

📁 扫描页面目录(全内容递归):

📂 charts/
index.md -> 67 字符
合计: 1 个文件,67 字符

📂 agreement/
index.md -> 433 字符
合计: 1 个文件,433 字符

📂 link/
index.md -> 126 字符
合计: 1 个文件,126 字符

📂 essay/
index.md -> 29 字符
合计: 1 个文件,29 字符

📂 categories/
index.md -> 28 字符
合计: 1 个文件,28 字符

📂 _data/
link.yml -> 201 字符
bangumis.json -> 231 字符
creativity.yml -> 75 字符
essay.yml -> 73 字符
about.yml -> 232 字符
合计: 5 个文件,812 字符

📂 cdn/
合计: 0 个文件,0 字符

📂 about/
index.md -> 34 字符
合计: 1 个文件,34 字符

📂 sitemap/
合计: 0 个文件,0 字符

📂 tags/
index.md -> 28 字符
合计: 1 个文件,28 字符

📂 privacy/
index.md -> 379 字符
合计: 1 个文件,379 字符

📁 扫描数据目录(_data):
link.yml -> 201 字符
bangumis.json -> 231 字符
creativity.yml -> 75 字符
essay.yml -> 73 字符
about.yml -> 232 字符
合计: 5 个文件,812 字符

📁 动态读取配置文件:
读取站点配置: _config.yml
-> 360 字符
读取主题配置: _config.anzhiyu.yml
-> 526 字符
读取主题语言文件:
zh-TW.yml -> 341 字符
en.yml -> 67 字符
zh-CN.yml -> 346 字符
default.yml -> 251 字符
配置文件共提取: 843 字符

============================================================
✅ 共收集 1180 个独特字符
- 汉字: 1073 个
- 英文: 1125 个
- 数字: 10 个
- 标点/其他: 45 个
💾 已保存到: /www/wwwroot/myblog/font-spider-work/chars-full.txt

📋 字符预览: 一七万三上下不与专且世业东丢个中丰为主举久么义之乌乎乐也习书乾了予二于云互些交亦产享亮人亿什仅仇今介仍从他付仙代以们件价任份优伙会传伦伴伸但位低住体何作你使來例供依侧侵便保信修個們倒值假偏做停健偿備储...

🔧 开始压缩字体...
输入: /www/wwwroot/myblog/font-spider-work/fonts/BailuFeiYun.ttf
输出: /www/wwwroot/myblog/source/fonts/BailuFeiYun.woff2

✅ 压缩完成!
原始大小: 10806.62 KB
WOFF2 大小: 712.62 KB (压缩率: 6.6%)
TTF 大小: 1321.61 KB (压缩率: 12.2%)

🎉 全部完成!
压缩后的字体已输出到: /www/wwwroot/myblog/source/fonts

文件位置

1
2
3
4
5
6
7
8
9
/www/wwwroot/myblog/
├── source/fonts/ # 输出压缩后的字体
│ ├── BailuFeiYun.woff2
│ └── BailuFeiYun.ttf
├── font-spider-work/ # 工作目录
│ ├── fonts/
│ │ └── BailuFeiYun.ttf # 原始字体
│ └── chars-full.txt # 提取的字符列表
└── compress_font.py # 本脚本