老T博客

将Soomal.cc迁移到Hugo

将Soomal.cc迁移到Hugo

今年初,在拿到 Soomal.com 网站源码后,我将源码上传到自己 VPS 上。但由于原网站架构较为陈旧,不便于管理以及手机访问,近期我对网站进行重构,将其整体转换并迁移到 Hugo。

迁移方案设计

对于 Soomal.cc 的重构,我其实早有想法。此前也简单测试过,但发现存在不少问题,之前就放下此事了。

存在困难和挑战

  1. 原网站文章数量较大

    Soomal 上共有 9630 篇文章,最早能追溯到 2003 年,总字数达到 1900 万

    Soomal 上共有 32.6 万张 JPG 图片,4700 多个图片文件夹,大多数图片都有 3 种尺寸,但也存在少量缺失的情况,整体容量接近 70 GB。

  2. 文章转换难度较大

    由于 Soomal 网站源码中只有文章页面的 htm 文件,虽然这些文件可能都来自同一个程序制作。但我此前对这些 htm 文件进行简单测试时,发现页面内容架构也发生过多次变化,在不同阶段使用过不同的标签来命名,从 htm 中提取信息难度很大。

    • 编码问题:Soomal 原来的 htm 都是使用 GB2312 编码,并且可能使用的是 Windows 服务器,在转换时需要处理特殊字符、转义字符问题。

    • 图片问题:Soomal 网站中有大量图片内容,这些图片正是 Soomal 的精华所在,但图片使用了多种标签和样式,在提取图片链接和描述时,需要尽量避免缺漏。

    • 标签和分类问题: Soomal 网站中标签数量庞大,有近 1.2 万个文章标签,另外有 20 多个文章分类。但在文章的 htm 文件中,缺少分类的内容,分类信息只能在 2000 多个分类切片 htm 中找到;而标签部分有些有空格,有些有特殊字符,还有一些同一篇文章重复标签的。

    • 文章内容: Soomal 文章 htm 中包括正文、相关文章列表、标签等内容,都放在 DOC 标签下,我此前没留意到相关文章列表均使用的是小写的 doc 标签,造成测试时总是提取出错。这次主要是在打开网页时偶尔发现这个问题,才重新启动转换计划。

  3. 存储方案选择困难

    我原本将 Soomal.cc 放在一台 VPS 上,几个月下来,发现虽然访问量不高,但流量掉的飞快,差不多用掉 1.5TB 流量。虽然是无限流量的 VPS,但看着也比较头疼。而在转换至 Hugo 后,主流的免费服务都很难使用,包括 Github 建议仓库小于 1GB,CloudFlare Pages 限制文件 2 万个, CloudFlare R2 存储限制文件 10GB,Vercel 和 Netlify 都限制流量 100GB 等等。


转换方法

考虑到 Soomal 转换为 Hugo 过程中可能存在的诸多问题,我设计了五步走的转换方案。

第一步:将 htm 文件转换为 markdown 文件

  1. 明确转换需求

    • 提取标题:在 <head> 标签中提取出文章标题。例如,<title>刘延作品 - 谈谈手机产业链和手机厂商的相互影响 [Soomal]</title> 中提取 谈谈手机产业链和手机厂商的相互影响 这个标题。
    • 提取标签:使用关键词过滤方式,找到 Htm 中的标签位置,提取标签名称,并加上引号,解决标签命名中的空格问题。
    • 提取正文:在 DOC 标签中提取出文章的正文信息,并截断 doc 标签之后的内容。
    • 提取日期、作者和页首图片信息:在 htm 中查找相应元素,并提取。
    • 提取图片:在页面中通过查找图片元素标签,将 smallpic, bigpic, smallpic2, wrappic 等图片信息全部提取出来。
    • 提取特殊信息:例如:二级标题、下载链接、表格等内容。
  2. 转换文件 由于转换需求较为明确,这里我直接用 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"![{alt_text}]({img_url})\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"![{alt_text}]({img_url})\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"![{alt_text}]({img_url})\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 文件中没有包含分类信息影响,所以只能将文章分类目录单独进行处理,在处理分类时,可以顺便将文章摘要内容一并处理。

  1. 提取分类和摘要信息

    主要是通过 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)} 篇有摘要的文章")
  1. 将分类和摘要信息写入 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 主题需要。

  1. 将文章头部信息转按 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处理完成!")
  1. 精简标签和分类 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 万。

  1. 提取图片链接 从 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}")
  1. 复制对应图片 使用上边提取出的图片链接数据,从原网站目录中查找对应文件并提取。过程中需要注意文件目录的准确性。
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 的限制要求。

  1. 将 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()
  1. 进一步压缩图片 本来我设计了这一步,即在前边转换+缩放后,假如图片未能压缩到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静态网站 #数据转换 #编码处理 #图片处理

评论