将Soomal.cc迁移到Hugo

今年初,在拿到 Soomal.com 网站源码后,我将源码上传到自己 VPS 上。但由于原网站架构较为陈旧,不便于管理以及手机访问,近期我对网站进行重构,将其整体转换并迁移到 Hugo。
迁移方案设计
对于 Soomal.cc 的重构,我其实早有想法。此前也简单测试过,但发现存在不少问题,之前就放下此事了。
存在困难和挑战
原网站文章数量较大
Soomal 上共有 9630 篇文章,最早能追溯到 2003 年,总字数达到 1900 万。
Soomal 上共有 32.6 万张 JPG 图片,4700 多个图片文件夹,大多数图片都有 3 种尺寸,但也存在少量缺失的情况,整体容量接近 70 GB。
文章转换难度较大
由于 Soomal 网站源码中只有文章页面的 htm 文件,虽然这些文件可能都来自同一个程序制作。但我此前对这些 htm 文件进行简单测试时,发现页面内容架构也发生过多次变化,在不同阶段使用过不同的标签来命名,从 htm 中提取信息难度很大。
编码问题:Soomal 原来的 htm 都是使用 GB2312 编码,并且可能使用的是 Windows 服务器,在转换时需要处理特殊字符、转义字符问题。
图片问题:Soomal 网站中有大量图片内容,这些图片正是 Soomal 的精华所在,但图片使用了多种标签和样式,在提取图片链接和描述时,需要尽量避免缺漏。
标签和分类问题: Soomal 网站中标签数量庞大,有近 1.2 万个文章标签,另外有 20 多个文章分类。但在文章的 htm 文件中,缺少分类的内容,分类信息只能在 2000 多个分类切片 htm 中找到;而标签部分有些有空格,有些有特殊字符,还有一些同一篇文章重复标签的。
文章内容: Soomal 文章 htm 中包括正文、相关文章列表、标签等内容,都放在 DOC 标签下,我此前没留意到相关文章列表均使用的是小写的 doc 标签,造成测试时总是提取出错。这次主要是在打开网页时偶尔发现这个问题,才重新启动转换计划。
存储方案选择困难
我原本将 Soomal.cc 放在一台 VPS 上,几个月下来,发现虽然访问量不高,但流量掉的飞快,差不多用掉 1.5TB 流量。虽然是无限流量的 VPS,但看着也比较头疼。而在转换至 Hugo 后,主流的免费服务都很难使用,包括 Github 建议仓库小于 1GB,CloudFlare Pages 限制文件 2 万个, CloudFlare R2 存储限制文件 10GB,Vercel 和 Netlify 都限制流量 100GB 等等。
转换方法
考虑到 Soomal 转换为 Hugo 过程中可能存在的诸多问题,我设计了五步走的转换方案。
第一步:将 htm 文件转换为 markdown 文件
明确转换需求
- 提取标题:在
<head>标签中提取出文章标题。例如,<title>刘延作品 - 谈谈手机产业链和手机厂商的相互影响 [Soomal]</title>中提取谈谈手机产业链和手机厂商的相互影响这个标题。 - 提取标签:使用关键词过滤方式,找到 Htm 中的标签位置,提取标签名称,并加上引号,解决标签命名中的空格问题。
- 提取正文:在
DOC标签中提取出文章的正文信息,并截断doc标签之后的内容。 - 提取日期、作者和页首图片信息:在 htm 中查找相应元素,并提取。
- 提取图片:在页面中通过查找图片元素标签,将
smallpic, bigpic, smallpic2, wrappic等图片信息全部提取出来。 - 提取特殊信息:例如:二级标题、下载链接、表格等内容。
- 提取标题:在
转换文件 由于转换需求较为明确,这里我直接用 Python 脚本进行转换。
点击查看转换脚本示例
1import os
2import re
3from bs4 import BeautifulSoup, Tag, NavigableString
4from datetime import datetime
5
6def convert_html_to_md(html_path, output_dir):
7 try:
8 # 读取GB2312编码的HTML文件
9 with open(html_path, 'r', encoding='gb2312', errors='ignore') as f:
10 html_content = f.read()
11
12 soup = BeautifulSoup(html_content, 'html.parser')
13
14 # 1. 提取标题
15 title = extract_title(soup)
16
17 # 2. 提取书签标签
18 bookmarks = extract_bookmarks(soup)
19
20 # 3. 提取标题图片和info
21 title_img, info_content = extract_title_info(soup)
22
23 # 4. 提取正文内容
24 body_content = extract_body_content(soup)
25
26 # 生成YAML frontmatter
27 frontmatter = f"""---
28title: "{title}"
29date: {datetime.now().strftime('%Y-%m-%dT%H:%M:%S+08:00')}
30tags: {bookmarks}
31title_img: "{title_img}"
32info: "{info_content}"
33---\n\n"""
34
35 # 生成Markdown内容
36 markdown_content = frontmatter + body_content
37
38 # 保存Markdown文件
39 output_path = os.path.join(output_dir, os.path.basename(html_path).replace('.htm', '.md'))
40 with open(output_path, 'w', encoding='utf-8') as f:
41 f.write(markdown_content)
42
43 return f"转换成功: {os.path.basename(html_path)}"
44 except Exception as e:
45 return f"转换失败 {os.path.basename(html_path)}: {str(e)}"
46
47def extract_title(soup):
48 """提取标题"""
49 if soup.title:
50 return soup.title.string.strip()
51 return ""
52
53def extract_bookmarks(soup):
54 """提取书签标签,每个标签用双引号包裹"""
55 bookmarks = []
56 bookmark_element = soup.find(string=re.compile(r'本文的相关书签:'))
57
58 if bookmark_element:
59 parent = bookmark_element.find_parent(['ul', 'li'])
60 if parent:
61 # 提取所有<a>标签的文本
62 for a_tag in parent.find_all('a'):
63 text = a_tag.get_text().strip()
64 if text:
65 # 用双引号包裹每个标签
66 bookmarks.append(f'"{text}"')
67
68 return f"[{', '.join(bookmarks)}]" if bookmarks else "[]"
69
70def extract_title_info(soup):
71 """提取标题图片和info内容"""
72 title_img = ""
73 info_content = ""
74
75 titlebox = soup.find('div', class_='titlebox')
76 if titlebox:
77 # 提取标题图片
78 title_img_div = titlebox.find('div', class_='titleimg')
79 if title_img_div and title_img_div.img:
80 title_img = title_img_div.img['src']
81
82 # 提取info内容
83 info_div = titlebox.find('div', class_='info')
84 if info_div:
85 # 移除所有HTML标签,只保留文本
86 info_content = info_div.get_text().strip()
87
88 return title_img, info_content
89
90def extract_body_content(soup):
91 """提取正文内容并处理图片"""
92 body_content = ""
93 doc_div = soup.find('div', class_='Doc') # 注意是大写D
94
95 if doc_div:
96 # 移除所有小写的doc标签(嵌套的div class="doc")
97 for nested_doc in doc_div.find_all('div', class_='doc'):
98 nested_doc.decompose()
99
100 # 处理图片
101 process_images(doc_div)
102
103 # 遍历所有子元素并构建Markdown内容
104 for element in doc_div.children:
105 if isinstance(element, Tag):
106 if element.name == 'div' and 'subpagetitle' in element.get('class', []):
107 # 转换为二级标题
108 body_content += f"## {element.get_text().strip()}\n\n"
109 else:
110 # 保留其他内容
111 body_content += element.get_text().strip() + "\n\n"
112 elif isinstance(element, NavigableString):
113 body_content += element.strip() + "\n\n"
114
115 return body_content.strip()
116
117def process_images(container):
118 """处理图片内容(A/B/C规则)"""
119 # A: 处理<li data-src>标签
120 for li in container.find_all('li', attrs={'data-src': True}):
121 img_url = li['data-src'].replace('..', 'https://soomal.cc', 1)
122 caption_div = li.find('div', class_='caption')
123 content_div = li.find('div', class_='content')
124
125 alt_text = caption_div.get_text().strip() if caption_div else ""
126 meta_text = content_div.get_text().strip() if content_div else ""
127
128 # 创建Markdown图片语法
129 img_md = f"\n\n{meta_text}\n\n"
130 li.replace_with(img_md)
131
132 # B: 处理<span class="smallpic">标签
133 for span in container.find_all('span', class_='smallpic'):
134 img = span.find('img')
135 if img and 'src' in img.attrs:
136 img_url = img['src'].replace('..', 'https://soomal.cc', 1)
137 caption_div = span.find('div', class_='caption')
138 content_div = span.find('div', class_='content')
139
140 alt_text = caption_div.get_text().strip() if caption_div else ""
141 meta_text = content_div.get_text().strip() if content_div else ""
142
143 # 创建Markdown图片语法
144 img_md = f"\n\n{meta_text}\n\n"
145 span.replace_with(img_md)
146
147 # C: 处理<div class="bigpic">标签
148 for div in container.find_all('div', class_='bigpic'):
149 img = div.find('img')
150 if img and 'src' in img.attrs:
151 img_url = img['src'].replace('..', 'https://soomal.cc', 1)
152 caption_div = div.find('div', class_='caption')
153 content_div = div.find('div', class_='content')
154
155 alt_text = caption_div.get_text().strip() if caption_div else ""
156 meta_text = content_div.get_text().strip() if content_div else ""
157
158 # 创建Markdown图片语法
159 img_md = f"\n\n{meta_text}\n\n"
160 div.replace_with(img_md)
161
162if __name__ == "__main__":
163 input_dir = 'doc'
164 output_dir = 'markdown_output'
165
166 # 创建输出目录
167 os.makedirs(output_dir, exist_ok=True)
168
169 # 处理所有HTML文件
170 for filename in os.listdir(input_dir):
171 if filename.endswith('.htm'):
172 html_path = os.path.join(input_dir, filename)
173 result = convert_html_to_md(html_path, output_dir)
174 print(result)第二步:处理分类和摘要信息
受制于原来文章 htm 文件中没有包含分类信息影响,所以只能将文章分类目录单独进行处理,在处理分类时,可以顺便将文章摘要内容一并处理。
提取分类和摘要信息
主要是通过 Python 将 2000 多个分类页面中的分类和摘要信息提取出来,并处理成数据格式。
点击查看转换代码
1import os
2import re
3from bs4 import BeautifulSoup
4import codecs
5from collections import defaultdict
6
7def extract_category_info(folder_path):
8 # 使用defaultdict自动初始化嵌套字典
9 article_categories = defaultdict(set) # 存储文章ID到分类的映射
10 article_summaries = {} # 存储文章ID到摘要的映射
11
12 # 遍历文件夹中的所有htm文件
13 for filename in os.listdir(folder_path):
14 if not filename.endswith('.htm'):
15 continue
16
17 file_path = os.path.join(folder_path, filename)
18
19 try:
20 # 使用GB2312编码读取文件并转换为UTF-8
21 with codecs.open(file_path, 'r', encoding='gb2312', errors='replace') as f:
22 content = f.read()
23
24 soup = BeautifulSoup(content, 'html.parser')
25
26 # 提取分类名称
27 title_tag = soup.title
28 if title_tag:
29 title_text = title_tag.get_text().strip()
30 # 提取第一个短横杠前的内容
31 category_match = re.search(r'^([^-]+)', title_text)
32 if category_match:
33 category_name = category_match.group(1).strip()
34 # 如果分类名称包含空格,则添加双引号
35 if ' ' in category_name:
36 category_name = f'"{category_name}"'
37 else:
38 category_name = "Unknown_Category"
39 else:
40 category_name = "Unknown_Category"
41
42 # 提取文章信息
43 for item in soup.find_all('div', class_='item'):
44 # 提取文章ID
45 article_link = item.find('a', href=True)
46 if article_link:
47 href = article_link['href']
48 article_id = re.search(r'../doc/(\d+)\.htm', href)
49 if article_id:
50 article_id = article_id.group(1)
51 else:
52 continue
53 else:
54 continue
55
56 # 提取文章摘要
57 synopsis_div = item.find('div', class_='synopsis')
58 synopsis = synopsis_div.get_text().strip() if synopsis_div else ""
59
60 # 存储文章分类信息
61 article_categories[article_id].add(category_name)
62
63 # 存储摘要信息(只保存一次,避免重复覆盖)
64 if article_id not in article_summaries:
65 article_summaries[article_id] = synopsis
66
67 except UnicodeDecodeError:
68 # 尝试使用GBK编码作为备选方案
69 try:
70 with codecs.open(file_path, 'r', encoding='gbk', errors='replace') as f:
71 content = f.read()
72 # 重新处理内容...
73 # 注意:这里省略了重复的处理代码,实际应用中应提取为函数
74 # 但为了保持代码完整,我们将重复处理逻辑
75 soup = BeautifulSoup(content, 'html.parser')
76 title_tag = soup.title
77 if title_tag:
78 title_text = title_tag.get_text().strip()
79 category_match = re.search(r'^([^-]+)', title_text)
80 if category_match:
81 category_name = category_match.group(1).strip()
82 if ' ' in category_name:
83 category_name = f'"{category_name}"'
84 else:
85 category_name = "Unknown_Category"
86 else:
87 category_name = "Unknown_Category"
88
89 for item in soup.find_all('div', class_='item'):
90 article_link = item.find('a', href=True)
91 if article_link:
92 href = article_link['href']
93 article_id = re.search(r'../doc/(\d+)\.htm', href)
94 if article_id:
95 article_id = article_id.group(1)
96 else:
97 continue
98 else:
99 continue
100
101 synopsis_div = item.find('div', class_='synopsis')
102 synopsis = synopsis_div.get_text().strip() if synopsis_div else ""
103
104 article_categories[article_id].add(category_name)
105
106 if article_id not in article_summaries:
107 article_summaries[article_id] = synopsis
108
109 except Exception as e:
110 print(f"处理文件 {filename} 时出错(尝试GBK后): {str(e)}")
111 continue
112
113 except Exception as e:
114 print(f"处理文件 {filename} 时出错: {str(e)}")
115 continue
116
117 return article_categories, article_summaries
118
119def save_to_markdown(article_categories, article_summaries, output_path):
120 with open(output_path, 'w', encoding='utf-8') as md_file:
121 # 写入Markdown标题
122 md_file.write("# 文章分类与摘要信息\n\n")
123 md_file.write("> 本文件包含所有文章的ID、分类和摘要信息\n\n")
124
125 # 按文章ID排序
126 sorted_article_ids = sorted(article_categories.keys(), key=lambda x: int(x))
127
128 for article_id in sorted_article_ids:
129 # 获取分类列表并排序
130 categories = sorted(article_categories[article_id])
131 # 格式化为列表字符串
132 categories_str = ", ".join(categories)
133
134 # 获取摘要
135 summary = article_summaries.get(article_id, "无摘要内容")
136
137 # 写入Markdown内容
138 md_file.write(f"## 文件名: {article_id}\n")
139 md_file.write(f"**分类**: {categories_str}\n")
140 md_file.write(f"**摘要**: {summary}\n\n")
141 md_file.write("---\n\n")
142
143if __name__ == "__main__":
144 # 配置输入和输出路径
145 input_folder = '分类' # 替换为你的HTM文件夹路径
146 output_md = 'articles_categories.md'
147
148 # 执行提取
149 article_categories, article_summaries = extract_category_info(input_folder)
150
151 # 保存结果到Markdown文件
152 save_to_markdown(article_categories, article_summaries, output_md)
153
154 # 打印统计信息
155 print(f"成功处理 {len(article_categories)} 篇文章的数据")
156 print(f"已保存到 {output_md}")
157 print(f"处理过程中发现 {len(article_summaries)} 篇有摘要的文章")将分类和摘要信息写入 markdown 文件
这一步比较简单,将上边提取出的分类和摘要数据逐个写入先前转换的 markdown 文件。
点击查看写入脚本
1import os
2import re
3import ruamel.yaml
4from collections import defaultdict
5
6def parse_articles_categories(md_file_path):
7 """
8 解析articles_categories.md文件,提取文章ID、分类和摘要信息
9 """
10 article_info = defaultdict(dict)
11 current_id = None
12
13 try:
14 with open(md_file_path, 'r', encoding='utf-8') as f:
15 for line in f:
16 # 匹配文件名
17 filename_match = re.match(r'^## 文件名: (\d+)$', line.strip())
18 if filename_match:
19 current_id = filename_match.group(1)
20 continue
21
22 # 匹配分类信息
23 categories_match = re.match(r'^\*\*分类\*\*: (.+)$', line.strip())
24 if categories_match and current_id:
25 categories_str = categories_match.group(1)
26 # 清理分类字符串,移除多余空格和引号
27 categories = [cat.strip().strip('"') for cat in categories_str.split(',')]
28 article_info[current_id]['categories'] = categories
29 continue
30
31 # 匹配摘要信息
32 summary_match = re.match(r'^\*\*摘要\*\*: (.+)$', line.strip())
33 if summary_match and current_id:
34 summary = summary_match.group(1)
35 article_info[current_id]['summary'] = summary
36 continue
37
38 # 遇到分隔线时重置当前ID
39 if line.startswith('---'):
40 current_id = None
41
42 except Exception as e:
43 print(f"解析articles_categories.md文件时出错: {str(e)}")
44
45 return article_info
46
47def update_markdown_files(article_info, md_folder):
48 """
49 更新Markdown文件,添加分类和摘要信息到frontmatter
50 """
51 updated_count = 0
52 skipped_count = 0
53
54 # 初始化YAML解析器
55 yaml = ruamel.yaml.YAML()
56 yaml.preserve_quotes = True
57 yaml.width = 1000 # 避免长摘要被换行
58
59 for filename in os.listdir(md_folder):
60 if not filename.endswith('.md'):
61 continue
62
63 article_id = filename[:-3] # 去除.md后缀
64 file_path = os.path.join(md_folder, filename)
65
66 # 检查是否有此文章的信息
67 if article_id not in article_info:
68 skipped_count += 1
69 continue
70
71 try:
72 with open(file_path, 'r', encoding='utf-8') as f:
73 content = f.read()
74
75 # 解析frontmatter
76 frontmatter_match = re.search(r'^---\n(.*?)\n---', content, re.DOTALL)
77 if not frontmatter_match:
78 print(f"文件 {filename} 中没有找到frontmatter,跳过")
79 skipped_count += 1
80 continue
81
82 frontmatter_content = frontmatter_match.group(1)
83
84 # 将frontmatter转为字典
85 data = yaml.load(frontmatter_content)
86 if data is None:
87 data = {}
88
89 # 添加分类和摘要信息
90 info = article_info[article_id]
91
92 # 添加分类
93 if 'categories' in info:
94 # 如果已存在分类,则合并(去重)
95 existing_categories = set(data.get('categories', []))
96 new_categories = set(info['categories'])
97 combined_categories = sorted(existing_categories.union(new_categories))
98 data['categories'] = combined_categories
99
100 # 添加摘要(如果摘要存在且不为空)
101 if 'summary' in info and info['summary']:
102 # 只有当摘要不存在或新摘要不为空时才更新
103 if 'summary' not in data or info['summary']:
104 data['summary'] = info['summary']
105
106 # 重新生成frontmatter
107 new_frontmatter = '---\n'
108 with ruamel.yaml.StringIO() as stream:
109 yaml.dump(data, stream)
110 new_frontmatter += stream.getvalue().strip()
111 new_frontmatter += '\n---'
112
113 # 替换原frontmatter
114 new_content = content.replace(frontmatter_match.group(0), new_frontmatter)
115
116 # 写入文件
117 with open(file_path, 'w', encoding='utf-8') as f:
118 f.write(new_content)
119
120 updated_count += 1
121
122 except Exception as e:
123 print(f"更新文件 {filename} 时出错: {str(e)}")
124 skipped_count += 1
125
126 return updated_count, skipped_count
127
128if __name__ == "__main__":
129 # 配置路径
130 articles_md = 'articles_categories.md' # 包含分类和摘要信息的Markdown文件
131 md_folder = 'markdown_output' # 包含Markdown文章的文件夹
132
133 # 解析articles_categories.md文件
134 print("正在解析articles_categories.md文件...")
135 article_info = parse_articles_categories(articles_md)
136 print(f"成功解析 {len(article_info)} 篇文章的信息")
137
138 # 更新Markdown文件
139 print(f"\n正在更新 {len(article_info)} 篇文章的分类和摘要信息...")
140 updated, skipped = update_markdown_files(article_info, md_folder)
141
142 # 打印统计信息
143 print(f"\n处理完成!")
144 print(f"成功更新: {updated} 个文件")
145 print(f"跳过处理: {skipped} 个文件")
146 print(f"找到信息的文章: {len(article_info)} 篇")第三步:转换文章 frontmatter 信息
这一步主要是对输出的 markdown 文件中 frontmatter 部分进行修正,以适应 Hugo 主题需要。
- 将文章头部信息转按 frontmatter 规范进行修正 主要是处理包括特殊字符,日期格式,作者,文章首图、标签、分类等内容。
查看转换代码
1import os
2import re
3import frontmatter
4import yaml
5from datetime import datetime
6
7def escape_special_characters(text):
8 """转义YAML中的特殊字符"""
9 # 转义反斜杠,但保留已经转义的字符
10 return re.sub(r'(?<!\\)\\(?!["\\/bfnrt]|u[0-9a-fA-F]{4})', r'\\\\', text)
11
12def process_md_files(folder_path):
13 for filename in os.listdir(folder_path):
14 if filename.endswith(".md"):
15 file_path = os.path.join(folder_path, filename)
16 try:
17 # 读取文件内容
18 with open(file_path, 'r', encoding='utf-8') as f:
19 content = f.read()
20
21 # 手动分割frontmatter和内容
22 if content.startswith('---\n'):
23 parts = content.split('---\n', 2)
24 if len(parts) >= 3:
25 fm_text = parts[1]
26 body_content = parts[2] if len(parts) > 2 else ""
27
28 # 转义特殊字符
29 fm_text = escape_special_characters(fm_text)
30
31 # 重新组合内容
32 new_content = f"---\n{fm_text}---\n{body_content}"
33
34 # 使用安全加载方式解析frontmatter
35 post = frontmatter.loads(new_content)
36
37 # 处理info字段
38 if 'info' in post.metadata:
39 info = post.metadata['info']
40
41 # 提取日期
42 date_match = re.search(r'于 (\d{4}\.\d{1,2}\.\d{1,2} \d{1,2}:\d{2}:\d{2})', info)
43 if date_match:
44 date_str = date_match.group(1)
45 try:
46 dt = datetime.strptime(date_str, "%Y.%m.%d %H:%M:%S")
47 post.metadata['date'] = dt.strftime("%Y-%m-%dT%H:%M:%S+08:00")
48 except ValueError:
49 # 保留原始日期作为备选
50 pass
51
52 # 提取作者
53 author_match = re.match(r'^(.+?)作品', info)
54 if author_match:
55 authors = author_match.group(1).strip()
56 # 分割多个作者
57 author_list = [a.strip() for a in re.split(r'\s+', authors) if a.strip()]
58 post.metadata['author'] = author_list
59
60 # 创建description
61 desc_parts = info.split('|', 1)
62 if len(desc_parts) > 1:
63 post.metadata['description'] = desc_parts[1].strip()
64
65 # 删除原始info
66 del post.metadata['info']
67
68 # 处理title_img
69 if 'title_img' in post.metadata:
70 img_url = post.metadata['title_img'].replace("../", "https://soomal.cc/")
71 # 处理可能的双斜杠
72 img_url = re.sub(r'(?<!:)/{2,}', '/', img_url)
73 post.metadata['cover'] = {
74 'image': img_url,
75 'caption': "",
76 'alt': "",
77 'relative': False
78 }
79 del post.metadata['title_img']
80
81 # 修改title
82 if 'title' in post.metadata:
83 title = post.metadata['title']
84 # 移除"-"之前的内容
85 if '-' in title:
86 new_title = title.split('-', 1)[1].strip()
87 post.metadata['title'] = new_title
88
89 # 保存修改后的文件
90 with open(file_path, 'w', encoding='utf-8') as f_out:
91 f_out.write(frontmatter.dumps(post))
92 except Exception as e:
93 print(f"处理文件 {filename} 时出错: {str(e)}")
94 # 记录错误文件以便后续检查
95 with open("processing_errors.log", "a", encoding="utf-8") as log:
96 log.write(f"Error in {filename}: {str(e)}\n")
97
98if __name__ == "__main__":
99 folder_path = "markdown_output" # 替换为您的实际路径
100 process_md_files(folder_path)
101 print("所有Markdown文件frontmatter处理完成!")- 精简标签和分类 Soomal.com 原本有 20 多个文章分类,但其中个别分类没有什么意义,比如“全部文章”分类,并且文章分类和文章标签有不少重复现象,为保证分类和标签的唯一性,对这部分进一步精简。另一个目的也是为了在最后生成网站文件时尽量减少文件数量。
查看精简标签和分类代码
1import os
2import yaml
3import frontmatter
4
5def clean_hugo_tags_categories(folder_path):
6 """
7 清理Hugo文章的标签和分类信息
8 1. 删除分类中的"所有文章"
9 2. 移除标签中与分类重复的项
10 """
11 # 需要保留的分类列表(已移除"所有文章")
12 valid_categories = [
13 "数码设备", "音频", "音乐", "移动数码", "评论", "介绍", "测评报告", "图集",
14 "智能手机", "Android", "耳机", "音乐人", "影像", "数码终端", "音箱", "iOS",
15 "相机", "声卡", "品碟", "平板电脑", "技术", "应用", "随身听", "Windows",
16 "数码附件", "随笔", "解码器", "音响", "镜头", "乐器", "音频编解码"
17 ]
18
19 # 遍历文件夹中的所有Markdown文件
20 for filename in os.listdir(folder_path):
21 if not filename.endswith('.md'):
22 continue
23
24 filepath = os.path.join(folder_path, filename)
25 with open(filepath, 'r', encoding='utf-8') as f:
26 post = frontmatter.load(f)
27
28 # 1. 清理分类(删除无效分类并去重)
29 if 'categories' in post.metadata:
30 # 转换为集合去重 + 过滤无效分类
31 categories = list(set(post.metadata['categories']))
32 cleaned_categories = [
33 cat for cat in categories
34 if cat in valid_categories
35 ]
36 post.metadata['categories'] = cleaned_categories
37
38 # 2. 清理标签(移除与分类重复的项)
39 if 'tags' in post.metadata:
40 current_cats = post.metadata.get('categories', [])
41 # 转换为集合去重 + 过滤与分类重复项
42 tags = list(set(post.metadata['tags']))
43 cleaned_tags = [
44 tag for tag in tags
45 if tag not in current_cats
46 ]
47 post.metadata['tags'] = cleaned_tags
48
49 # 保存修改后的文件
50 with open(filepath, 'w', encoding='utf-8') as f_out:
51 f_out.write(frontmatter.dumps(post))
52
53if __name__ == "__main__":
54 # 使用示例(修改为你的实际路径)
55 md_folder = "./markdown_output"
56 clean_hugo_tags_categories(md_folder)
57 print(f"已完成处理: {len(os.listdir(md_folder))} 个文件")第四步:精简图片数量
在 htm 转 md 文件的过程中,由于只提取文章内的信息,所以原网站中很多裁切图片已不再需要。为此,可以按照转换后的 md 文件内容,对应查找原网站图片,筛选出新网站所需的图片即可。
通过本步骤,网站所需图片数量从原来的 32.6 万下降到 11.8 万。
- 提取图片链接 从 md 文件中,提取出所有的图片链接。由于此前在转换图片连接时已经有统一的图片连接格式,所以操作起来比较容易。
查看提取代码
1import os
2import re
3import argparse
4
5def extract_image_links(directory):
6 """提取目录中所有md文件的图片链接"""
7 image_links = set()
8 pattern = re.compile(r'https://soomal\.cc[^\s\)\]\}]*?\.jpg', re.IGNORECASE)
9
10 for root, _, files in os.walk(directory):
11 for filename in files:
12 if filename.endswith('.md'):
13 filepath = os.path.join(root, filename)
14 try:
15 with open(filepath, 'r', encoding='utf-8') as f:
16 content = f.read()
17 matches = pattern.findall(content)
18 if matches:
19 image_links.update(matches)
20 except Exception as e:
21 print(f"处理文件 {filepath} 时出错: {str(e)}")
22
23 return sorted(image_links)
24
25def save_links_to_file(links, output_file):
26 """将链接保存到文件"""
27 with open(output_file, 'w', encoding='utf-8') as f:
28 for link in links:
29 f.write(link + '\n')
30
31if __name__ == "__main__":
32 parser = argparse.ArgumentParser(description='提取Markdown中的图片链接')
33 parser.add_argument('--input', default='markdown_output', help='Markdown文件目录路径')
34 parser.add_argument('--output', default='image_links.txt', help='输出文件路径')
35 args = parser.parse_args()
36
37 print(f"正在扫描目录: {args.input}")
38 links = extract_image_links(args.input)
39
40 print(f"找到 {len(links)} 个唯一图片链接")
41 save_links_to_file(links, args.output)
42 print(f"链接已保存至: {args.output}")- 复制对应图片 使用上边提取出的图片链接数据,从原网站目录中查找对应文件并提取。过程中需要注意文件目录的准确性。
A.查看Windows中复制代码
1import os
2import shutil
3import time
4import sys
5
6def main():
7 # 配置参数
8 source_drive = "F:\\"
9 target_drive = "D:\\"
10 image_list_file = r"D:\trans-soomal\image_links.txt"
11 log_file = r"D:\trans-soomal\image_copy_log.txt"
12 error_log_file = r"D:\trans-soomal\image_copy_errors.txt"
13
14 print("图片复制脚本启动...")
15
16 # 记录开始时间
17 start_time = time.time()
18
19 # 创建日志文件
20 with open(log_file, "w", encoding="utf-8") as log, open(error_log_file, "w", encoding="utf-8") as err_log:
21 log.write(f"图片复制日志 - 开始时间: {time.ctime(start_time)}\n")
22 err_log.write("以下文件复制失败:\n")
23
24 try:
25 # 读取图片列表
26 with open(image_list_file, "r", encoding="utf-8") as f:
27 image_paths = [line.strip() for line in f if line.strip()]
28
29 total_files = len(image_paths)
30 success_count = 0
31 fail_count = 0
32 skipped_count = 0
33
34 print(f"找到 {total_files} 个待复制的图片文件")
35
36 # 处理每个文件
37 for i, relative_path in enumerate(image_paths):
38 # 显示进度
39 progress = (i + 1) / total_files * 100
40 sys.stdout.write(f"\r进度: {progress:.2f}% ({i+1}/{total_files})")
41 sys.stdout.flush()
42
43 # 构建完整路径
44 source_path = os.path.join(source_drive, relative_path)
45 target_path = os.path.join(target_drive, relative_path)
46
47 try:
48 # 检查源文件是否存在
49 if not os.path.exists(source_path):
50 err_log.write(f"源文件不存在: {source_path}\n")
51 fail_count += 1
52 continue
53
54 # 检查目标文件是否已存在
55 if os.path.exists(target_path):
56 log.write(f"文件已存在,跳过: {target_path}\n")
57 skipped_count += 1
58 continue
59
60 # 创建目标目录
61 target_dir = os.path.dirname(target_path)
62 os.makedirs(target_dir, exist_ok=True)
63
64 # 复制文件
65 shutil.copy2(source_path, target_path)
66
67 # 记录成功
68 log.write(f"[成功] 复制 {source_path} 到 {target_path}\n")
69 success_count += 1
70
71 except Exception as e:
72 # 记录失败
73 err_log.write(f"[失败] {source_path} -> {target_path} : {str(e)}\n")
74 fail_count += 1
75
76 # 计算耗时
77 end_time = time.time()
78 elapsed_time = end_time - start_time
79 minutes, seconds = divmod(elapsed_time, 60)
80 hours, minutes = divmod(minutes, 60)
81
82 # 写入统计信息
83 summary = f"""
84================================
85复制操作完成
86开始时间: {time.ctime(start_time)}
87结束时间: {time.ctime(end_time)}
88总耗时: {int(hours)}小时 {int(minutes)}分钟 {seconds:.2f}秒
89
90文件总数: {total_files}
91成功复制: {success_count}
92跳过(已存在): {skipped_count}
93失败: {fail_count}
94================================
95"""
96 log.write(summary)
97 print(summary)
98
99 except Exception as e:
100 print(f"\n发生错误: {str(e)}")
101 err_log.write(f"脚本错误: {str(e)}\n")
102
103if __name__ == "__main__":
104 main()B.查看Linux中复制代码
1#!/bin/bash
2
3# 配置参数
4LINK_FILE="/user/image_links.txt" # 替换为实际链接文件路径
5SOURCE_BASE="/user/soomal.cc/index"
6DEST_BASE="/user/images.soomal.cc/index"
7LOG_FILE="/var/log/image_copy_$(date +%Y%m%d_%H%M%S).log"
8THREADS=3 # 自动获取CPU核心数作为线程数
9
10# 开始记录日志
11{
12echo "===== 复制任务开始: $(date) ====="
13echo "源基础目录: $SOURCE_BASE"
14echo "目标基础目录: $DEST_BASE"
15echo "链接文件: $LINK_FILE"
16echo "使用线程数: $THREADS"
17
18# 验证路径示例
19echo -e "\n=== 路径验证 ==="
20sample_url="https://soomal.cc/images/doc/20090406/00000007.jpg"
21expected_src="${SOURCE_BASE}/images/doc/20090406/00000007.jpg"
22expected_dest="${DEST_BASE}/images/doc/20090406/00000007.jpg"
23
24echo "示例URL: $sample_url"
25echo "预期源路径: $expected_src"
26echo "预期目标路径: $expected_dest"
27
28if [[ -f "$expected_src" ]]; then
29 echo "验证成功:示例源文件存在"
30else
31 echo "验证失败:示例源文件不存在!请检查路径"
32 exit 1
33fi
34
35# 创建目标基础目录
36mkdir -p "${DEST_BASE}/images"
37
38# 准备并行处理
39echo -e "\n=== 开始处理 ==="
40total=$(wc -l < "$LINK_FILE")
41echo "总链接数: $total"
42counter=0
43
44# 处理函数
45process_link() {
46 local url="$1"
47 local rel_path="${url#https://soomal.cc}"
48
49 # 构建完整路径
50 local src_path="${SOURCE_BASE}${rel_path}"
51 local dest_path="${DEST_BASE}${rel_path}"
52
53 # 创建目标目录
54 mkdir -p "$(dirname "$dest_path")"
55
56 # 复制文件
57 if [[ -f "$src_path" ]]; then
58 if cp -f "$src_path" "$dest_path"; then
59 echo "SUCCESS: $rel_path"
60 return 0
61 else
62 echo "COPY FAILED: $rel_path"
63 return 2
64 fi
65 else
66 echo "MISSING: $rel_path"
67 return 1
68 fi
69}
70
71# 导出函数以便并行使用
72export -f process_link
73export SOURCE_BASE DEST_BASE
74
75# 使用parallel进行并行处理
76echo "启动并行复制..."
77parallel --bar --jobs $THREADS --progress \
78 --halt soon,fail=1 \
79 --joblog "${LOG_FILE}.jobs" \
80 --tagstring "{}" \
81 "process_link {}" < "$LINK_FILE" | tee -a "$LOG_FILE"
82
83# 统计结果
84success=$(grep -c 'SUCCESS:' "$LOG_FILE")
85missing=$(grep -c 'MISSING:' "$LOG_FILE")
86failed=$(grep -c 'COPY FAILED:' "$LOG_FILE")
87
88# 最终统计
89echo -e "\n===== 复制任务完成: $(date) ====="
90echo "总链接数: $total"
91echo "成功复制: $success"
92echo "缺失文件: $missing"
93echo "复制失败: $failed"
94echo "成功率: $((success * 100 / total))%"
95
96} | tee "$LOG_FILE"
97
98# 保存缺失文件列表
99grep '^MISSING:' "$LOG_FILE" | cut -d' ' -f2- > "${LOG_FILE%.log}_missing.txt"
100echo "缺失文件列表: ${LOG_FILE%.log}_missing.txt"第五步:压缩图片体积
我此前已经对网站源图进行过一次压缩,但还不够,我期望是将图片容量压缩到 10 GB 以内,用以适应日后可能需要迁移到 CloudFlare R2 的限制要求。
- 将 JPG 转换为 Webp 我此前使用 webp 对图片压缩后,考虑到 htm 众多,为避免图片无法访问,仍以 JPG 格式将图片保存。由于这次需要搬迁到 Hugo,JPG 格式也就没必要继续保留,直接转换为 Webp 即可。另外,由于我网页已经设置 960px 宽度,考虑到网站体积,也没有引入 fancy 灯箱等插件,直接使用 960px 缩放图片可以进一步压缩体积。
实测经过这次压缩,图片体积下降到 7.7GB ,但是我发现图片处理逻辑还是有点小问题。主要是 Soomal 上不仅有很多竖版图片,也有不少横版图片,另外,960px 的宽度,在 4K 显示器下还是显得有点不够看。我最终按照图片中短边最大 1280px 质量 85% 的设定转换了图片,体积约 14GB,刚好可以放入我 20GB 硬盘的 VPS 中。另外我也按短边最大 1150px 质量 80% 测试了一下,刚好可以达到 10GB 体积要求。
查看图片转换代码
1import os
2import subprocess
3import time
4import sys
5import shutil
6from pathlib import Path
7
8def main():
9 # 配置文件路径
10 source_dir = Path("D:\\images") # 原始图片目录
11 output_dir = Path("D:\\images_webp") # WebP输出目录
12 temp_dir = Path("D:\\temp_webp") # 临时处理目录
13 magick_path = "C:\\webp\\magick.exe" # ImageMagick路径
14
15 # 创建必要的目录
16 output_dir.mkdir(parents=True, exist_ok=True)
17 temp_dir.mkdir(parents=True, exist_ok=True)
18
19 # 日志文件
20 log_file = output_dir / "conversion_log.txt"
21 stats_file = output_dir / "conversion_stats.csv"
22
23 print("图片转换脚本启动...")
24 print(f"源目录: {source_dir}")
25 print(f"输出目录: {output_dir}")
26 print(f"临时目录: {temp_dir}")
27
28 # 初始化日志
29 with open(log_file, "w", encoding="utf-8") as log:
30 log.write(f"图片转换日志 - 开始时间: {time.ctime()}\n")
31
32 # 初始化统计文件
33 with open(stats_file, "w", encoding="utf-8") as stats:
34 stats.write("原始文件,转换后文件,原始大小(KB),转换后大小(KB),节省空间(KB),节省百分比\n")
35
36 # 收集所有图片文件
37 image_exts = ('.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.gif')
38 all_images = []
39 for root, _, files in os.walk(source_dir):
40 for file in files:
41 if file.lower().endswith(image_exts):
42 all_images.append(Path(root) / file)
43
44 total_files = len(all_images)
45 converted_files = 0
46 skipped_files = 0
47 error_files = 0
48
49 print(f"找到 {total_files} 个图片文件需要处理")
50
51 # 处理每个图片
52 for idx, img_path in enumerate(all_images):
53 try:
54 # 显示进度
55 progress = (idx + 1) / total_files * 100
56 sys.stdout.write(f"\r进度: {progress:.2f}% ({idx+1}/{total_files})")
57 sys.stdout.flush()
58
59 # 创建相对路径结构
60 rel_path = img_path.relative_to(source_dir)
61 webp_path = output_dir / rel_path.with_suffix('.webp')
62 webp_path.parent.mkdir(parents=True, exist_ok=True)
63
64 # 检查是否已存在
65 if webp_path.exists():
66 skipped_files += 1
67 continue
68
69 # 创建临时文件路径
70 temp_path = temp_dir / f"{img_path.stem}_temp.webp"
71
72 # 获取原始文件大小
73 orig_size = img_path.stat().st_size / 1024 # KB
74
75 # 使用ImageMagick进行转换和大小调整
76 cmd = [
77 magick_path,
78 str(img_path),
79 "-resize", "960>", # 仅当宽度大于960时调整
80 "-quality", "85", # 初始质量85
81 "-define", "webp:lossless=false",
82 str(temp_path)
83 ]
84
85 # 执行命令
86 result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
87
88 if result.returncode != 0:
89 # 转换失败,记录错误
90 with open(log_file, "a", encoding="utf-8") as log:
91 log.write(f"[错误] 转换 {img_path} 失败: {result.stderr}\n")
92 error_files += 1
93 continue
94
95 # 移动临时文件到目标位置
96 shutil.move(str(temp_path), str(webp_path))
97
98 # 获取转换后文件大小
99 new_size = webp_path.stat().st_size / 1024 # KB
100
101 # 计算节省空间
102 saved = orig_size - new_size
103 saved_percent = (saved / orig_size) * 100 if orig_size > 0 else 0
104
105 # 记录统计信息
106 with open(stats_file, "a", encoding="utf-8") as stats:
107 stats.write(f"{img_path},{webp_path},{orig_size:.2f},{new_size:.2f},{saved:.2f},{saved_percent:.2f}\n")
108
109 converted_files += 1
110
111 except Exception as e:
112 with open(log_file, "a", encoding="utf-8") as log:
113 log.write(f"[异常] 处理 {img_path} 时出错: {str(e)}\n")
114 error_files += 1
115
116 # 完成报告
117 total_size = sum(f.stat().st_size for f in output_dir.glob('**/*') if f.is_file())
118 total_size_gb = total_size / (1024 ** 3) # 转换为GB
119
120 end_time = time.time()
121 elapsed = end_time - time.time()
122 mins, secs = divmod(elapsed, 60)
123 hours, mins = divmod(mins, 60)
124
125 with open(log_file, "a", encoding="utf-8") as log:
126 log.write("\n转换完成报告:\n")
127 log.write(f"总文件数: {total_files}\n")
128 log.write(f"成功转换: {converted_files}\n")
129 log.write(f"跳过文件: {skipped_files}\n")
130 log.write(f"错误文件: {error_files}\n")
131 log.write(f"输出目录总大小: {total_size_gb:.2f} GB\n")
132
133 print("\n\n转换完成!")
134 print(f"总文件数: {total_files}")
135 print(f"成功转换: {converted_files}")
136 print(f"跳过文件: {skipped_files}")
137 print(f"错误文件: {error_files}")
138 print(f"输出目录总大小: {total_size_gb:.2f} GB")
139
140 # 清理临时目录
141 try:
142 shutil.rmtree(temp_dir)
143 print(f"已清理临时目录: {temp_dir}")
144 except Exception as e:
145 print(f"清理临时目录时出错: {str(e)}")
146
147 print(f"日志文件: {log_file}")
148 print(f"统计文件: {stats_file}")
149 print(f"总耗时: {int(hours)}小时 {int(mins)}分钟 {secs:.2f}秒")
150
151if __name__ == "__main__":
152 main()- 进一步压缩图片 本来我设计了这一步,即在前边转换+缩放后,假如图片未能压缩到10GB以下,就继续启用压缩,但没想到前一步就把图片问题解决,也就没必要继续压缩。但我还是测试了一下,按照短边最大 1280px 60% 质量压缩为 webp 后,总容量只有 9GB。
查看图片二次压缩代码
1import os
2import subprocess
3import time
4import sys
5import shutil
6from pathlib import Path
7
8def main():
9 # 配置文件路径
10 webp_dir = Path("D:\\images_webp") # WebP图片目录
11 temp_dir = Path("D:\\temp_compress") # 临时处理目录
12 cwebp_path = "C:\\Windows\\System32\\cwebp.exe" # cwebp路径
13
14 # 创建临时目录
15 temp_dir.mkdir(parents=True, exist_ok=True)
16
17 # 日志文件
18 log_file = webp_dir / "compression_log.txt"
19 stats_file = webp_dir / "compression_stats.csv"
20
21 print("WebP压缩脚本启动...")
22 print(f"处理目录: {webp_dir}")
23 print(f"临时目录: {temp_dir}")
24
25 # 初始化日志
26 with open(log_file, "w", encoding="utf-8") as log:
27 log.write(f"WebP压缩日志 - 开始时间: {time.ctime()}\n")
28
29 # 初始化统计文件
30 with open(stats_file, "w", encoding="utf-8") as stats:
31 stats.write("原始文件,压缩后文件,原始大小(KB),新大小(KB),节省空间(KB),节省百分比\n")
32
33 # 收集所有WebP文件
34 all_webp = list(webp_dir.glob('**/*.webp'))
35 total_files = len(all_webp)
36
37 if total_files == 0:
38 print("未找到WebP文件,请先运行转换脚本")
39 return
40
41 print(f"找到 {total_files} 个WebP文件需要压缩")
42
43 compressed_count = 0
44 skipped_count = 0
45 error_count = 0
46
47 # 处理每个WebP文件
48 for idx, webp_path in enumerate(all_webp):
49 try:
50 # 显示进度
51 progress = (idx + 1) / total_files * 100
52 sys.stdout.write(f"\r进度: {progress:.2f}% ({idx+1}/{total_files})")
53 sys.stdout.flush()
54
55 # 原始大小
56 orig_size = webp_path.stat().st_size / 1024 # KB
57
58 # 创建临时文件路径
59 temp_path = temp_dir / f"{webp_path.stem}_compressed.webp"
60
61 # 使用cwebp进行二次压缩
62 cmd = [
63 cwebp_path,
64 "-q", "75", # 质量参数
65 "-m", "6", # 最高压缩模式
66 str(webp_path),
67 "-o", str(temp_path)
68 ]
69
70 # 执行命令
71 result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
72
73 if result.returncode != 0:
74 # 压缩失败,记录错误
75 with open(log_file, "a", encoding="utf-8") as log:
76 log.write(f"[错误] 压缩 {webp_path} 失败: {result.stderr}\n")
77 error_count += 1
78 continue
79
80 # 获取新文件大小
81 new_size = temp_path.stat().st_size / 1024 # KB
82
83 # 如果新文件比原文件大,则跳过
84 if new_size >= orig_size:
85 skipped_count += 1
86 temp_path.unlink() # 删除临时文件
87 continue
88
89 # 计算节省空间
90 saved = orig_size - new_size
91 saved_percent = (saved / orig_size) * 100 if orig_size > 0 else 0
92
93 # 记录统计信息
94 with open(stats_file, "a", encoding="utf-8") as stats:
95 stats.write(f"{webp_path},{webp_path},{orig_size:.2f},{new_size:.2f},{saved:.2f},{saved_percent:.2f}\n")
96
97 # 替换原文件
98 webp_path.unlink() # 删除原文件
99 shutil.move(str(temp_path), str(webp_path))
100 compressed_count += 1
101
102 except Exception as e:
103 with open(log_file, "a", encoding="utf-8") as log:
104 log.write(f"[异常] 处理 {webp_path} 时出错: {str(e)}\n")
105 error_count += 1
106
107 # 完成报告
108 total_size = sum(f.stat().st_size for f in webp_dir.glob('**/*') if f.is_file())
109 total_size_gb = total_size / (1024 ** 3) # 转换为GB
110
111 end_time = time.time()
112 elapsed = end_time - time.time()
113 mins, secs = divmod(elapsed, 60)
114 hours, mins = divmod(mins, 60)
115
116 with open(log_file, "a", encoding="utf-8") as log:
117 log.write("\n压缩完成报告:\n")
118 log.write(f"处理文件数: {total_files}\n")
119 log.write(f"成功压缩: {compressed_count}\n")
120 log.write(f"跳过文件: {skipped_count}\n")
121 log.write(f"错误文件: {error_count}\n")
122 log.write(f"输出目录总大小: {total_size_gb:.2f} GB\n")
123
124 print("\n\n压缩完成!")
125 print(f"处理文件数: {total_files}")
126 print(f"成功压缩: {compressed_count}")
127 print(f"跳过文件: {skipped_count}")
128 print(f"错误文件: {error_count}")
129 print(f"输出目录总大小: {total_size_gb:.2f} GB")
130
131 # 清理临时目录
132 try:
133 shutil.rmtree(temp_dir)
134 print(f"已清理临时目录: {temp_dir}")
135 except Exception as e:
136 print(f"清理临时目录时出错: {str(e)}")
137
138 print(f"日志文件: {log_file}")
139 print(f"统计文件: {stats_file}")
140 print(f"总耗时: {int(hours)}小时 {int(mins)}分钟 {secs:.2f}秒")
141
142if __name__ == "__main__":
143 main()构建方案
选择合适 Hugo 模板
对于一个上万 md 文件的 Hugo 项目来说,选择模板真是很折磨人。
我试过用一款比较精美的模板测试,发现连续构建三个小时都没能生成结束;试过有的模板生成过程中不断的报错;试过有的模板生成文件数量超过 20 万个。
最后我选择了最稳妥的 PaperMod 模板,这个模板默认只有 100 多个文件,生成网站后文件总数不到 5 万,总体来说还算好。
虽然达不到 Cloudflare Page 2万个文件的限制标准,但也相当精简了,在 Github Pages 上构建花了 6 分半钟,在 Vercel 上构建花了 8 分钟。
不过,在构建过程中还是发生一些小问题,比如搜索功能,因为文章数据实在有点大,默认索引文件达到 80MB,几乎没有实用性,最后只能忍痛将索引限制在文章标题和摘要内容上。
还有站点地图生成问题,默认生成站点地图 4MB,在提交到 Google Console 后一直读取失败。Bing Webmaster 那边倒是没问题。
另外,分页问题也是个比较头疼的是。默认 12000 个标签,采用 20 篇文章一个页面,都能生成 6 万文件,在我将文章数提高到 200 一个页面后,仍然有 3.7 万个文件。与此同时,其他文件加起来也只有 1.2 万个。
不过,这个标签问题倒也也给了进一步改造的可能性,即只提取前使用数量在前 1000 位的标签,将其他标签作为标题的一部分。这样,应该可以将文件数控制在 2 万以内,满足 Cloudflare Pages 的限制需求。
选择合适的静态页面托管服务
由于 Hugo 项目本身只有 100MB 不到(其中文章 md 文件 80M),所以托管到 Github 没有任何问题。考虑到 Github Pages 访问速度较慢,我选择将网站部署到 Vercel,虽然 Vercel 只有 100GB 流量,但对于静态页面来说,应该是够用了。
选择合适的图片托管服务
目前仍在找。本想将图片托管到 Cloudflare R2,但看那个免费计划也有点不敢用,虽然有一定免费额度,但怕爆账单。先继续用我 7 美刀包年的假阿里云 VPS 吧。
#网站迁移 #hugo静态网站 #数据转换 #编码处理 #图片处理