Featured image of post 将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 脚本进行转换。

点击查看转换脚本示例
  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
import os
import re
from bs4 import BeautifulSoup, Tag, NavigableString
from datetime import datetime

def convert_html_to_md(html_path, output_dir):
    try:
        # 读取GB2312编码的HTML文件
        with open(html_path, 'r', encoding='gb2312', errors='ignore') as f:
            html_content = f.read()
        
        soup = BeautifulSoup(html_content, 'html.parser')
        
        # 1. 提取标题
        title = extract_title(soup)
        
        # 2. 提取书签标签
        bookmarks = extract_bookmarks(soup)
        
        # 3. 提取标题图片和info
        title_img, info_content = extract_title_info(soup)
        
        # 4. 提取正文内容
        body_content = extract_body_content(soup)
        
        # 生成YAML frontmatter
        frontmatter = f"""---
title: "{title}"
date: {datetime.now().strftime('%Y-%m-%dT%H:%M:%S+08:00')}
tags: {bookmarks}
title_img: "{title_img}"
info: "{info_content}"
---\n\n"""
        
        # 生成Markdown内容
        markdown_content = frontmatter + body_content
        
        # 保存Markdown文件
        output_path = os.path.join(output_dir, os.path.basename(html_path).replace('.htm', '.md'))
        with open(output_path, 'w', encoding='utf-8') as f:
            f.write(markdown_content)
            
        return f"转换成功: {os.path.basename(html_path)}"
    except Exception as e:
        return f"转换失败 {os.path.basename(html_path)}: {str(e)}"

def extract_title(soup):
    """提取标题"""
    if soup.title:
        return soup.title.string.strip()
    return ""

def extract_bookmarks(soup):
    """提取书签标签,每个标签用双引号包裹"""
    bookmarks = []
    bookmark_element = soup.find(string=re.compile(r'本文的相关书签:'))
    
    if bookmark_element:
        parent = bookmark_element.find_parent(['ul', 'li'])
        if parent:
            # 提取所有<a>标签的文本
            for a_tag in parent.find_all('a'):
                text = a_tag.get_text().strip()
                if text:
                    # 用双引号包裹每个标签
                    bookmarks.append(f'"{text}"')
    
    return f"[{', '.join(bookmarks)}]" if bookmarks else "[]"

def extract_title_info(soup):
    """提取标题图片和info内容"""
    title_img = ""
    info_content = ""
    
    titlebox = soup.find('div', class_='titlebox')
    if titlebox:
        # 提取标题图片
        title_img_div = titlebox.find('div', class_='titleimg')
        if title_img_div and title_img_div.img:
            title_img = title_img_div.img['src']
        
        # 提取info内容
        info_div = titlebox.find('div', class_='info')
        if info_div:
            # 移除所有HTML标签,只保留文本
            info_content = info_div.get_text().strip()
    
    return title_img, info_content

def extract_body_content(soup):
    """提取正文内容并处理图片"""
    body_content = ""
    doc_div = soup.find('div', class_='Doc')  # 注意是大写D
    
    if doc_div:
        # 移除所有小写的doc标签(嵌套的div class="doc")
        for nested_doc in doc_div.find_all('div', class_='doc'):
            nested_doc.decompose()
        
        # 处理图片
        process_images(doc_div)
        
        # 遍历所有子元素并构建Markdown内容
        for element in doc_div.children:
            if isinstance(element, Tag):
                if element.name == 'div' and 'subpagetitle' in element.get('class', []):
                    # 转换为二级标题
                    body_content += f"## {element.get_text().strip()}\n\n"
                else:
                    # 保留其他内容
                    body_content += element.get_text().strip() + "\n\n"
            elif isinstance(element, NavigableString):
                body_content += element.strip() + "\n\n"
    
    return body_content.strip()

def process_images(container):
    """处理图片内容(A/B/C规则)"""
    # A: 处理<li data-src>标签
    for li in container.find_all('li', attrs={'data-src': True}):
        img_url = li['data-src'].replace('..', 'https://soomal.cc', 1)
        caption_div = li.find('div', class_='caption')
        content_div = li.find('div', class_='content')
        
        alt_text = caption_div.get_text().strip() if caption_div else ""
        meta_text = content_div.get_text().strip() if content_div else ""
        
        # 创建Markdown图片语法
        img_md = f"![{alt_text}]({img_url})\n\n{meta_text}\n\n"
        li.replace_with(img_md)
    
    # B: 处理<span class="smallpic">标签
    for span in container.find_all('span', class_='smallpic'):
        img = span.find('img')
        if img and 'src' in img.attrs:
            img_url = img['src'].replace('..', 'https://soomal.cc', 1)
            caption_div = span.find('div', class_='caption')
            content_div = span.find('div', class_='content')
            
            alt_text = caption_div.get_text().strip() if caption_div else ""
            meta_text = content_div.get_text().strip() if content_div else ""
            
            # 创建Markdown图片语法
            img_md = f"![{alt_text}]({img_url})\n\n{meta_text}\n\n"
            span.replace_with(img_md)
            
    # C: 处理<div class="bigpic">标签
    for div in container.find_all('div', class_='bigpic'):
        img = div.find('img')
        if img and 'src' in img.attrs:
            img_url = img['src'].replace('..', 'https://soomal.cc', 1)
            caption_div = div.find('div', class_='caption')
            content_div = div.find('div', class_='content')
            
            alt_text = caption_div.get_text().strip() if caption_div else ""
            meta_text = content_div.get_text().strip() if content_div else ""
            
            # 创建Markdown图片语法
            img_md = f"![{alt_text}]({img_url})\n\n{meta_text}\n\n"
            div.replace_with(img_md)

if __name__ == "__main__":
    input_dir = 'doc'
    output_dir = 'markdown_output'
    
    # 创建输出目录
    os.makedirs(output_dir, exist_ok=True)
    
    # 处理所有HTML文件
    for filename in os.listdir(input_dir):
        if filename.endswith('.htm'):
            html_path = os.path.join(input_dir, filename)
            result = convert_html_to_md(html_path, output_dir)
            print(result)

第二步:处理分类和摘要信息

受制于原来文章 htm 文件中没有包含分类信息影响,所以只能将文章分类目录单独进行处理,在处理分类时,可以顺便将文章摘要内容一并处理。

  1. 提取分类和摘要信息

    主要是通过 Python 将 2000 多个分类页面中的分类和摘要信息提取出来,并处理成数据格式。

点击查看转换代码
  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
import os
import re
from bs4 import BeautifulSoup
import codecs
from collections import defaultdict

def extract_category_info(folder_path):
    # 使用defaultdict自动初始化嵌套字典
    article_categories = defaultdict(set)  # 存储文章ID到分类的映射
    article_summaries = {}  # 存储文章ID到摘要的映射
    
    # 遍历文件夹中的所有htm文件
    for filename in os.listdir(folder_path):
        if not filename.endswith('.htm'):
            continue
            
        file_path = os.path.join(folder_path, filename)
        
        try:
            # 使用GB2312编码读取文件并转换为UTF-8
            with codecs.open(file_path, 'r', encoding='gb2312', errors='replace') as f:
                content = f.read()
                
            soup = BeautifulSoup(content, 'html.parser')
            
            # 提取分类名称
            title_tag = soup.title
            if title_tag:
                title_text = title_tag.get_text().strip()
                # 提取第一个短横杠前的内容
                category_match = re.search(r'^([^-]+)', title_text)
                if category_match:
                    category_name = category_match.group(1).strip()
                    # 如果分类名称包含空格,则添加双引号
                    if ' ' in category_name:
                        category_name = f'"{category_name}"'
                else:
                    category_name = "Unknown_Category"
            else:
                category_name = "Unknown_Category"
            
            # 提取文章信息
            for item in soup.find_all('div', class_='item'):
                # 提取文章ID
                article_link = item.find('a', href=True)
                if article_link:
                    href = article_link['href']
                    article_id = re.search(r'../doc/(\d+)\.htm', href)
                    if article_id:
                        article_id = article_id.group(1)
                    else:
                        continue
                else:
                    continue
                
                # 提取文章摘要
                synopsis_div = item.find('div', class_='synopsis')
                synopsis = synopsis_div.get_text().strip() if synopsis_div else ""
                
                # 存储文章分类信息
                article_categories[article_id].add(category_name)
                
                # 存储摘要信息(只保存一次,避免重复覆盖)
                if article_id not in article_summaries:
                    article_summaries[article_id] = synopsis
    
        except UnicodeDecodeError:
            # 尝试使用GBK编码作为备选方案
            try:
                with codecs.open(file_path, 'r', encoding='gbk', errors='replace') as f:
                    content = f.read()
                # 重新处理内容...
                # 注意:这里省略了重复的处理代码,实际应用中应提取为函数
                # 但为了保持代码完整,我们将重复处理逻辑
                soup = BeautifulSoup(content, 'html.parser')
                title_tag = soup.title
                if title_tag:
                    title_text = title_tag.get_text().strip()
                    category_match = re.search(r'^([^-]+)', title_text)
                    if category_match:
                        category_name = category_match.group(1).strip()
                        if ' ' in category_name:
                            category_name = f'"{category_name}"'
                    else:
                        category_name = "Unknown_Category"
                else:
                    category_name = "Unknown_Category"
                
                for item in soup.find_all('div', class_='item'):
                    article_link = item.find('a', href=True)
                    if article_link:
                        href = article_link['href']
                        article_id = re.search(r'../doc/(\d+)\.htm', href)
                        if article_id:
                            article_id = article_id.group(1)
                        else:
                            continue
                    else:
                        continue
                    
                    synopsis_div = item.find('div', class_='synopsis')
                    synopsis = synopsis_div.get_text().strip() if synopsis_div else ""
                    
                    article_categories[article_id].add(category_name)
                    
                    if article_id not in article_summaries:
                        article_summaries[article_id] = synopsis
                        
            except Exception as e:
                print(f"处理文件 {filename} 时出错(尝试GBK后): {str(e)}")
                continue
                
        except Exception as e:
            print(f"处理文件 {filename} 时出错: {str(e)}")
            continue
    
    return article_categories, article_summaries

def save_to_markdown(article_categories, article_summaries, output_path):
    with open(output_path, 'w', encoding='utf-8') as md_file:
        # 写入Markdown标题
        md_file.write("# 文章分类与摘要信息\n\n")
        md_file.write("> 本文件包含所有文章的ID、分类和摘要信息\n\n")
        
        # 按文章ID排序
        sorted_article_ids = sorted(article_categories.keys(), key=lambda x: int(x))
        
        for article_id in sorted_article_ids:
            # 获取分类列表并排序
            categories = sorted(article_categories[article_id])
            # 格式化为列表字符串
            categories_str = ", ".join(categories)
            
            # 获取摘要
            summary = article_summaries.get(article_id, "无摘要内容")
            
            # 写入Markdown内容
            md_file.write(f"## 文件名: {article_id}\n")
            md_file.write(f"**分类**: {categories_str}\n")
            md_file.write(f"**摘要**: {summary}\n\n")
            md_file.write("---\n\n")

if __name__ == "__main__":
    # 配置输入和输出路径
    input_folder = '分类'  # 替换为你的HTM文件夹路径
    output_md = 'articles_categories.md'
    
    # 执行提取
    article_categories, article_summaries = extract_category_info(input_folder)
    
    # 保存结果到Markdown文件
    save_to_markdown(article_categories, article_summaries, output_md)
    
    # 打印统计信息
    print(f"成功处理 {len(article_categories)} 篇文章的数据")
    print(f"已保存到 {output_md}")
    print(f"处理过程中发现 {len(article_summaries)} 篇有摘要的文章")
  1. 将分类和摘要信息写入 markdown 文件

    这一步比较简单,将上边提取出的分类和摘要数据逐个写入先前转换的 markdown 文件。

点击查看写入脚本
  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
import os
import re
import ruamel.yaml
from collections import defaultdict

def parse_articles_categories(md_file_path):
    """
    解析articles_categories.md文件,提取文章ID、分类和摘要信息
    """
    article_info = defaultdict(dict)
    current_id = None
    
    try:
        with open(md_file_path, 'r', encoding='utf-8') as f:
            for line in f:
                # 匹配文件名
                filename_match = re.match(r'^## 文件名: (\d+)$', line.strip())
                if filename_match:
                    current_id = filename_match.group(1)
                    continue
                
                # 匹配分类信息
                categories_match = re.match(r'^\*\*分类\*\*: (.+)$', line.strip())
                if categories_match and current_id:
                    categories_str = categories_match.group(1)
                    # 清理分类字符串,移除多余空格和引号
                    categories = [cat.strip().strip('"') for cat in categories_str.split(',')]
                    article_info[current_id]['categories'] = categories
                    continue
                
                # 匹配摘要信息
                summary_match = re.match(r'^\*\*摘要\*\*: (.+)$', line.strip())
                if summary_match and current_id:
                    summary = summary_match.group(1)
                    article_info[current_id]['summary'] = summary
                    continue
                
                # 遇到分隔线时重置当前ID
                if line.startswith('---'):
                    current_id = None
    
    except Exception as e:
        print(f"解析articles_categories.md文件时出错: {str(e)}")
    
    return article_info

def update_markdown_files(article_info, md_folder):
    """
    更新Markdown文件,添加分类和摘要信息到frontmatter
    """
    updated_count = 0
    skipped_count = 0
    
    # 初始化YAML解析器
    yaml = ruamel.yaml.YAML()
    yaml.preserve_quotes = True
    yaml.width = 1000  # 避免长摘要被换行
    
    for filename in os.listdir(md_folder):
        if not filename.endswith('.md'):
            continue
            
        article_id = filename[:-3]  # 去除.md后缀
        file_path = os.path.join(md_folder, filename)
        
        # 检查是否有此文章的信息
        if article_id not in article_info:
            skipped_count += 1
            continue
            
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                content = f.read()
            
            # 解析frontmatter
            frontmatter_match = re.search(r'^---\n(.*?)\n---', content, re.DOTALL)
            if not frontmatter_match:
                print(f"文件 {filename} 中没有找到frontmatter,跳过")
                skipped_count += 1
                continue
                
            frontmatter_content = frontmatter_match.group(1)
            
            # 将frontmatter转为字典
            data = yaml.load(frontmatter_content)
            if data is None:
                data = {}
            
            # 添加分类和摘要信息
            info = article_info[article_id]
            
            # 添加分类
            if 'categories' in info:
                # 如果已存在分类,则合并(去重)
                existing_categories = set(data.get('categories', []))
                new_categories = set(info['categories'])
                combined_categories = sorted(existing_categories.union(new_categories))
                data['categories'] = combined_categories
            
            # 添加摘要(如果摘要存在且不为空)
            if 'summary' in info and info['summary']:
                # 只有当摘要不存在或新摘要不为空时才更新
                if 'summary' not in data or info['summary']:
                    data['summary'] = info['summary']
            
            # 重新生成frontmatter
            new_frontmatter = '---\n'
            with ruamel.yaml.StringIO() as stream:
                yaml.dump(data, stream)
                new_frontmatter += stream.getvalue().strip()
            new_frontmatter += '\n---'
            
            # 替换原frontmatter
            new_content = content.replace(frontmatter_match.group(0), new_frontmatter)
            
            # 写入文件
            with open(file_path, 'w', encoding='utf-8') as f:
                f.write(new_content)
            
            updated_count += 1
            
        except Exception as e:
            print(f"更新文件 {filename} 时出错: {str(e)}")
            skipped_count += 1
    
    return updated_count, skipped_count

if __name__ == "__main__":
    # 配置路径
    articles_md = 'articles_categories.md'  # 包含分类和摘要信息的Markdown文件
    md_folder = 'markdown_output'  # 包含Markdown文章的文件夹
    
    # 解析articles_categories.md文件
    print("正在解析articles_categories.md文件...")
    article_info = parse_articles_categories(articles_md)
    print(f"成功解析 {len(article_info)} 篇文章的信息")
    
    # 更新Markdown文件
    print(f"\n正在更新 {len(article_info)} 篇文章的分类和摘要信息...")
    updated, skipped = update_markdown_files(article_info, md_folder)
    
    # 打印统计信息
    print(f"\n处理完成!")
    print(f"成功更新: {updated} 个文件")
    print(f"跳过处理: {skipped} 个文件")
    print(f"找到信息的文章: {len(article_info)} 篇")

第三步:转换文章 frontmatter 信息

这一步主要是对输出的 markdown 文件中 frontmatter 部分进行修正,以适应 Hugo 主题需要。

  1. 将文章头部信息转按 frontmatter 规范进行修正 主要是处理包括特殊字符,日期格式,作者,文章首图、标签、分类等内容。
查看转换代码
  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
import os
import re
import frontmatter
import yaml
from datetime import datetime

def escape_special_characters(text):
    """转义YAML中的特殊字符"""
    # 转义反斜杠,但保留已经转义的字符
    return re.sub(r'(?<!\\)\\(?!["\\/bfnrt]|u[0-9a-fA-F]{4})', r'\\\\', text)

def process_md_files(folder_path):
    for filename in os.listdir(folder_path):
        if filename.endswith(".md"):
            file_path = os.path.join(folder_path, filename)
            try:
                # 读取文件内容
                with open(file_path, 'r', encoding='utf-8') as f:
                    content = f.read()
                
                # 手动分割frontmatter和内容
                if content.startswith('---\n'):
                    parts = content.split('---\n', 2)
                    if len(parts) >= 3:
                        fm_text = parts[1]
                        body_content = parts[2] if len(parts) > 2 else ""
                        
                        # 转义特殊字符
                        fm_text = escape_special_characters(fm_text)
                        
                        # 重新组合内容
                        new_content = f"---\n{fm_text}---\n{body_content}"
                        
                        # 使用安全加载方式解析frontmatter
                        post = frontmatter.loads(new_content)
                        
                        # 处理info字段
                        if 'info' in post.metadata:
                            info = post.metadata['info']
                            
                            # 提取日期
                            date_match = re.search(r'于 (\d{4}\.\d{1,2}\.\d{1,2} \d{1,2}:\d{2}:\d{2})', info)
                            if date_match:
                                date_str = date_match.group(1)
                                try:
                                    dt = datetime.strptime(date_str, "%Y.%m.%d %H:%M:%S")
                                    post.metadata['date'] = dt.strftime("%Y-%m-%dT%H:%M:%S+08:00")
                                except ValueError:
                                    # 保留原始日期作为备选
                                    pass
                            
                            # 提取作者
                            author_match = re.match(r'^(.+?)作品', info)
                            if author_match:
                                authors = author_match.group(1).strip()
                                # 分割多个作者
                                author_list = [a.strip() for a in re.split(r'\s+', authors) if a.strip()]
                                post.metadata['author'] = author_list
                            
                            # 创建description
                            desc_parts = info.split('|', 1)
                            if len(desc_parts) > 1:
                                post.metadata['description'] = desc_parts[1].strip()
                            
                            # 删除原始info
                            del post.metadata['info']
                        
                        # 处理title_img
                        if 'title_img' in post.metadata:
                            img_url = post.metadata['title_img'].replace("../", "https://soomal.cc/")
                            # 处理可能的双斜杠
                            img_url = re.sub(r'(?<!:)/{2,}', '/', img_url)
                            post.metadata['cover'] = {
                                'image': img_url,
                                'caption': "",
                                'alt': "",
                                'relative': False
                            }
                            del post.metadata['title_img']
                        
                        # 修改title
                        if 'title' in post.metadata:
                            title = post.metadata['title']
                            # 移除"-"之前的内容
                            if '-' in title:
                                new_title = title.split('-', 1)[1].strip()
                                post.metadata['title'] = new_title
                        
                        # 保存修改后的文件
                        with open(file_path, 'w', encoding='utf-8') as f_out:
                            f_out.write(frontmatter.dumps(post))
            except Exception as e:
                print(f"处理文件 {filename} 时出错: {str(e)}")
                # 记录错误文件以便后续检查
                with open("processing_errors.log", "a", encoding="utf-8") as log:
                    log.write(f"Error in {filename}: {str(e)}\n")

if __name__ == "__main__":
    folder_path = "markdown_output"  # 替换为您的实际路径
    process_md_files(folder_path)
    print("所有Markdown文件frontmatter处理完成!")
  1. 精简标签和分类 Soomal.com 原本有 20 多个文章分类,但其中个别分类没有什么意义,比如“全部文章”分类,并且文章分类和文章标签有不少重复现象,为保证分类和标签的唯一性,对这部分进一步精简。另一个目的也是为了在最后生成网站文件时尽量减少文件数量。
查看精简标签和分类代码
 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
import os
import yaml
import frontmatter

def clean_hugo_tags_categories(folder_path):
    """
    清理Hugo文章的标签和分类信息
    1. 删除分类中的"所有文章"
    2. 移除标签中与分类重复的项
    """
    # 需要保留的分类列表(已移除"所有文章")
    valid_categories = [
        "数码设备", "音频", "音乐", "移动数码", "评论", "介绍", "测评报告", "图集", 
        "智能手机", "Android", "耳机", "音乐人", "影像", "数码终端", "音箱", "iOS", 
        "相机", "声卡", "品碟", "平板电脑", "技术", "应用", "随身听", "Windows", 
        "数码附件", "随笔", "解码器", "音响", "镜头", "乐器", "音频编解码"
    ]
    
    # 遍历文件夹中的所有Markdown文件
    for filename in os.listdir(folder_path):
        if not filename.endswith('.md'):
            continue
            
        filepath = os.path.join(folder_path, filename)
        with open(filepath, 'r', encoding='utf-8') as f:
            post = frontmatter.load(f)
            
            # 1. 清理分类(删除无效分类并去重)
            if 'categories' in post.metadata:
                # 转换为集合去重 + 过滤无效分类
                categories = list(set(post.metadata['categories']))
                cleaned_categories = [
                    cat for cat in categories 
                    if cat in valid_categories
                ]
                post.metadata['categories'] = cleaned_categories
            
            # 2. 清理标签(移除与分类重复的项)
            if 'tags' in post.metadata:
                current_cats = post.metadata.get('categories', [])
                # 转换为集合去重 + 过滤与分类重复项
                tags = list(set(post.metadata['tags']))
                cleaned_tags = [
                    tag for tag in tags 
                    if tag not in current_cats
                ]
                post.metadata['tags'] = cleaned_tags
                
            # 保存修改后的文件
            with open(filepath, 'w', encoding='utf-8') as f_out:
                f_out.write(frontmatter.dumps(post))

if __name__ == "__main__":
    # 使用示例(修改为你的实际路径)
    md_folder = "./markdown_output"
    clean_hugo_tags_categories(md_folder)
    print(f"已完成处理: {len(os.listdir(md_folder))} 个文件")

第四步:精简图片数量

在 htm 转 md 文件的过程中,由于只提取文章内的信息,所以原网站中很多裁切图片已不再需要。为此,可以按照转换后的 md 文件内容,对应查找原网站图片,筛选出新网站所需的图片即可。

通过本步骤,网站所需图片数量从原来的 32.6 万下降到 11.8 万。

  1. 提取图片链接 从 md 文件中,提取出所有的图片链接。由于此前在转换图片连接时已经有统一的图片连接格式,所以操作起来比较容易。
查看提取代码
 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
import os
import re
import argparse

def extract_image_links(directory):
    """提取目录中所有md文件的图片链接"""
    image_links = set()
    pattern = re.compile(r'https://soomal\.cc[^\s\)\]\}]*?\.jpg', re.IGNORECASE)
    
    for root, _, files in os.walk(directory):
        for filename in files:
            if filename.endswith('.md'):
                filepath = os.path.join(root, filename)
                try:
                    with open(filepath, 'r', encoding='utf-8') as f:
                        content = f.read()
                        matches = pattern.findall(content)
                        if matches:
                            image_links.update(matches)
                except Exception as e:
                    print(f"处理文件 {filepath} 时出错: {str(e)}")
    
    return sorted(image_links)

def save_links_to_file(links, output_file):
    """将链接保存到文件"""
    with open(output_file, 'w', encoding='utf-8') as f:
        for link in links:
            f.write(link + '\n')

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='提取Markdown中的图片链接')
    parser.add_argument('--input', default='markdown_output', help='Markdown文件目录路径')
    parser.add_argument('--output', default='image_links.txt', help='输出文件路径')
    args = parser.parse_args()

    print(f"正在扫描目录: {args.input}")
    links = extract_image_links(args.input)
    
    print(f"找到 {len(links)} 个唯一图片链接")
    save_links_to_file(links, args.output)
    print(f"链接已保存至: {args.output}")
  1. 复制对应图片 使用上边提取出的图片链接数据,从原网站目录中查找对应文件并提取。过程中需要注意文件目录的准确性。
A.查看Windows中复制代码
  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
import os
import shutil
import time
import sys

def main():
    # 配置参数
    source_drive = "F:\\"
    target_drive = "D:\\"
    image_list_file = r"D:\trans-soomal\image_links.txt"
    log_file = r"D:\trans-soomal\image_copy_log.txt"
    error_log_file = r"D:\trans-soomal\image_copy_errors.txt"
    
    print("图片复制脚本启动...")
    
    # 记录开始时间
    start_time = time.time()
    
    # 创建日志文件
    with open(log_file, "w", encoding="utf-8") as log, open(error_log_file, "w", encoding="utf-8") as err_log:
        log.write(f"图片复制日志 - 开始时间: {time.ctime(start_time)}\n")
        err_log.write("以下文件复制失败:\n")
        
        try:
            # 读取图片列表
            with open(image_list_file, "r", encoding="utf-8") as f:
                image_paths = [line.strip() for line in f if line.strip()]
            
            total_files = len(image_paths)
            success_count = 0
            fail_count = 0
            skipped_count = 0
            
            print(f"找到 {total_files} 个待复制的图片文件")
            
            # 处理每个文件
            for i, relative_path in enumerate(image_paths):
                # 显示进度
                progress = (i + 1) / total_files * 100
                sys.stdout.write(f"\r进度: {progress:.2f}% ({i+1}/{total_files})")
                sys.stdout.flush()
                
                # 构建完整路径
                source_path = os.path.join(source_drive, relative_path)
                target_path = os.path.join(target_drive, relative_path)
                
                try:
                    # 检查源文件是否存在
                    if not os.path.exists(source_path):
                        err_log.write(f"源文件不存在: {source_path}\n")
                        fail_count += 1
                        continue
                    
                    # 检查目标文件是否已存在
                    if os.path.exists(target_path):
                        log.write(f"文件已存在,跳过: {target_path}\n")
                        skipped_count += 1
                        continue
                    
                    # 创建目标目录
                    target_dir = os.path.dirname(target_path)
                    os.makedirs(target_dir, exist_ok=True)
                    
                    # 复制文件
                    shutil.copy2(source_path, target_path)
                    
                    # 记录成功
                    log.write(f"[成功] 复制 {source_path}{target_path}\n")
                    success_count += 1
                    
                except Exception as e:
                    # 记录失败
                    err_log.write(f"[失败] {source_path} -> {target_path} : {str(e)}\n")
                    fail_count += 1
            
            # 计算耗时
            end_time = time.time()
            elapsed_time = end_time - start_time
            minutes, seconds = divmod(elapsed_time, 60)
            hours, minutes = divmod(minutes, 60)
            
            # 写入统计信息
            summary = f"""
================================
复制操作完成
开始时间: {time.ctime(start_time)}
结束时间: {time.ctime(end_time)}
总耗时: {int(hours)}小时 {int(minutes)}分钟 {seconds:.2f}
文件总数: {total_files}
成功复制: {success_count}
跳过(已存在): {skipped_count}
失败: {fail_count}
================================
"""
            log.write(summary)
            print(summary)
            
        except Exception as e:
            print(f"\n发生错误: {str(e)}")
            err_log.write(f"脚本错误: {str(e)}\n")

if __name__ == "__main__":
    main()
B.查看Linux中复制代码
  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
#!/bin/bash

# 配置参数
LINK_FILE="/user/image_links.txt"  # 替换为实际链接文件路径
SOURCE_BASE="/user/soomal.cc/index"
DEST_BASE="/user/images.soomal.cc/index"
LOG_FILE="/var/log/image_copy_$(date +%Y%m%d_%H%M%S).log"
THREADS=3  # 自动获取CPU核心数作为线程数

# 开始记录日志
{
echo "===== 复制任务开始: $(date) ====="
echo "源基础目录: $SOURCE_BASE"
echo "目标基础目录: $DEST_BASE"
echo "链接文件: $LINK_FILE"
echo "使用线程数: $THREADS"

# 验证路径示例
echo -e "\n=== 路径验证 ==="
sample_url="https://soomal.cc/images/doc/20090406/00000007.jpg"
expected_src="${SOURCE_BASE}/images/doc/20090406/00000007.jpg"
expected_dest="${DEST_BASE}/images/doc/20090406/00000007.jpg"

echo "示例URL: $sample_url"
echo "预期源路径: $expected_src"
echo "预期目标路径: $expected_dest"

if [[ -f "$expected_src" ]]; then
    echo "验证成功:示例源文件存在"
else
    echo "验证失败:示例源文件不存在!请检查路径"
    exit 1
fi

# 创建目标基础目录
mkdir -p "${DEST_BASE}/images"

# 准备并行处理
echo -e "\n=== 开始处理 ==="
total=$(wc -l < "$LINK_FILE")
echo "总链接数: $total"
counter=0

# 处理函数
process_link() {
    local url="$1"
    local rel_path="${url#https://soomal.cc}"
    
    # 构建完整路径
    local src_path="${SOURCE_BASE}${rel_path}"
    local dest_path="${DEST_BASE}${rel_path}"
    
    # 创建目标目录
    mkdir -p "$(dirname "$dest_path")"
    
    # 复制文件
    if [[ -f "$src_path" ]]; then
        if cp -f "$src_path" "$dest_path"; then
            echo "SUCCESS: $rel_path"
            return 0
        else
            echo "COPY FAILED: $rel_path"
            return 2
        fi
    else
        echo "MISSING: $rel_path"
        return 1
    fi
}

# 导出函数以便并行使用
export -f process_link
export SOURCE_BASE DEST_BASE

# 使用parallel进行并行处理
echo "启动并行复制..."
parallel --bar --jobs $THREADS --progress \
         --halt soon,fail=1 \
         --joblog "${LOG_FILE}.jobs" \
         --tagstring "{}" \
         "process_link {}" < "$LINK_FILE" | tee -a "$LOG_FILE"

# 统计结果
success=$(grep -c 'SUCCESS:' "$LOG_FILE")
missing=$(grep -c 'MISSING:' "$LOG_FILE")
failed=$(grep -c 'COPY FAILED:' "$LOG_FILE")

# 最终统计
echo -e "\n===== 复制任务完成: $(date) ====="
echo "总链接数: $total"
echo "成功复制: $success"
echo "缺失文件: $missing"
echo "复制失败: $failed"
echo "成功率: $((success * 100 / total))%"

} | tee "$LOG_FILE"

# 保存缺失文件列表
grep '^MISSING:' "$LOG_FILE" | cut -d' ' -f2- > "${LOG_FILE%.log}_missing.txt"
echo "缺失文件列表: ${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 体积要求。

查看图片转换代码
  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
import os
import subprocess
import time
import sys
import shutil
from pathlib import Path

def main():
    # 配置文件路径
    source_dir = Path("D:\\images")  # 原始图片目录
    output_dir = Path("D:\\images_webp")  # WebP输出目录
    temp_dir = Path("D:\\temp_webp")  # 临时处理目录
    magick_path = "C:\\webp\\magick.exe"  # ImageMagick路径
    
    # 创建必要的目录
    output_dir.mkdir(parents=True, exist_ok=True)
    temp_dir.mkdir(parents=True, exist_ok=True)
    
    # 日志文件
    log_file = output_dir / "conversion_log.txt"
    stats_file = output_dir / "conversion_stats.csv"
    
    print("图片转换脚本启动...")
    print(f"源目录: {source_dir}")
    print(f"输出目录: {output_dir}")
    print(f"临时目录: {temp_dir}")
    
    # 初始化日志
    with open(log_file, "w", encoding="utf-8") as log:
        log.write(f"图片转换日志 - 开始时间: {time.ctime()}\n")
    
    # 初始化统计文件
    with open(stats_file, "w", encoding="utf-8") as stats:
        stats.write("原始文件,转换后文件,原始大小(KB),转换后大小(KB),节省空间(KB),节省百分比\n")
    
    # 收集所有图片文件
    image_exts = ('.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.gif')
    all_images = []
    for root, _, files in os.walk(source_dir):
        for file in files:
            if file.lower().endswith(image_exts):
                all_images.append(Path(root) / file)
    
    total_files = len(all_images)
    converted_files = 0
    skipped_files = 0
    error_files = 0
    
    print(f"找到 {total_files} 个图片文件需要处理")
    
    # 处理每个图片
    for idx, img_path in enumerate(all_images):
        try:
            # 显示进度
            progress = (idx + 1) / total_files * 100
            sys.stdout.write(f"\r进度: {progress:.2f}% ({idx+1}/{total_files})")
            sys.stdout.flush()
            
            # 创建相对路径结构
            rel_path = img_path.relative_to(source_dir)
            webp_path = output_dir / rel_path.with_suffix('.webp')
            webp_path.parent.mkdir(parents=True, exist_ok=True)
            
            # 检查是否已存在
            if webp_path.exists():
                skipped_files += 1
                continue
            
            # 创建临时文件路径
            temp_path = temp_dir / f"{img_path.stem}_temp.webp"
            
            # 获取原始文件大小
            orig_size = img_path.stat().st_size / 1024  # KB
            
            # 使用ImageMagick进行转换和大小调整
            cmd = [
                magick_path,
                str(img_path),
                "-resize", "960>",   # 仅当宽度大于960时调整
                "-quality", "85",    # 初始质量85
                "-define", "webp:lossless=false",
                str(temp_path)
            ]
            
            # 执行命令
            result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
            
            if result.returncode != 0:
                # 转换失败,记录错误
                with open(log_file, "a", encoding="utf-8") as log:
                    log.write(f"[错误] 转换 {img_path} 失败: {result.stderr}\n")
                error_files += 1
                continue
            
            # 移动临时文件到目标位置
            shutil.move(str(temp_path), str(webp_path))
            
            # 获取转换后文件大小
            new_size = webp_path.stat().st_size / 1024  # KB
            
            # 计算节省空间
            saved = orig_size - new_size
            saved_percent = (saved / orig_size) * 100 if orig_size > 0 else 0
            
            # 记录统计信息
            with open(stats_file, "a", encoding="utf-8") as stats:
                stats.write(f"{img_path},{webp_path},{orig_size:.2f},{new_size:.2f},{saved:.2f},{saved_percent:.2f}\n")
            
            converted_files += 1
        
        except Exception as e:
            with open(log_file, "a", encoding="utf-8") as log:
                log.write(f"[异常] 处理 {img_path} 时出错: {str(e)}\n")
            error_files += 1
    
    # 完成报告
    total_size = sum(f.stat().st_size for f in output_dir.glob('**/*') if f.is_file())
    total_size_gb = total_size / (1024 ** 3)  # 转换为GB
    
    end_time = time.time()
    elapsed = end_time - time.time()
    mins, secs = divmod(elapsed, 60)
    hours, mins = divmod(mins, 60)
    
    with open(log_file, "a", encoding="utf-8") as log:
        log.write("\n转换完成报告:\n")
        log.write(f"总文件数: {total_files}\n")
        log.write(f"成功转换: {converted_files}\n")
        log.write(f"跳过文件: {skipped_files}\n")
        log.write(f"错误文件: {error_files}\n")
        log.write(f"输出目录总大小: {total_size_gb:.2f} GB\n")
    
    print("\n\n转换完成!")
    print(f"总文件数: {total_files}")
    print(f"成功转换: {converted_files}")
    print(f"跳过文件: {skipped_files}")
    print(f"错误文件: {error_files}")
    print(f"输出目录总大小: {total_size_gb:.2f} GB")
    
    # 清理临时目录
    try:
        shutil.rmtree(temp_dir)
        print(f"已清理临时目录: {temp_dir}")
    except Exception as e:
        print(f"清理临时目录时出错: {str(e)}")
    
    print(f"日志文件: {log_file}")
    print(f"统计文件: {stats_file}")
    print(f"总耗时: {int(hours)}小时 {int(mins)}分钟 {secs:.2f}秒")

if __name__ == "__main__":
    main()
  1. 进一步压缩图片 本来我设计了这一步,即在前边转换+缩放后,假如图片未能压缩到10GB以下,就继续启用压缩,但没想到前一步就把图片问题解决,也就没必要继续压缩。但我还是测试了一下,按照短边最大 1280px 60% 质量压缩为 webp 后,总容量只有 9GB。
查看图片二次压缩代码
  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
import os
import subprocess
import time
import sys
import shutil
from pathlib import Path

def main():
    # 配置文件路径
    webp_dir = Path("D:\\images_webp")  # WebP图片目录
    temp_dir = Path("D:\\temp_compress")  # 临时处理目录
    cwebp_path = "C:\\Windows\\System32\\cwebp.exe"  # cwebp路径
    
    # 创建临时目录
    temp_dir.mkdir(parents=True, exist_ok=True)
    
    # 日志文件
    log_file = webp_dir / "compression_log.txt"
    stats_file = webp_dir / "compression_stats.csv"
    
    print("WebP压缩脚本启动...")
    print(f"处理目录: {webp_dir}")
    print(f"临时目录: {temp_dir}")
    
    # 初始化日志
    with open(log_file, "w", encoding="utf-8") as log:
        log.write(f"WebP压缩日志 - 开始时间: {time.ctime()}\n")
    
    # 初始化统计文件
    with open(stats_file, "w", encoding="utf-8") as stats:
        stats.write("原始文件,压缩后文件,原始大小(KB),新大小(KB),节省空间(KB),节省百分比\n")
    
    # 收集所有WebP文件
    all_webp = list(webp_dir.glob('**/*.webp'))
    total_files = len(all_webp)
    
    if total_files == 0:
        print("未找到WebP文件,请先运行转换脚本")
        return
    
    print(f"找到 {total_files} 个WebP文件需要压缩")
    
    compressed_count = 0
    skipped_count = 0
    error_count = 0
    
    # 处理每个WebP文件
    for idx, webp_path in enumerate(all_webp):
        try:
            # 显示进度
            progress = (idx + 1) / total_files * 100
            sys.stdout.write(f"\r进度: {progress:.2f}% ({idx+1}/{total_files})")
            sys.stdout.flush()
            
            # 原始大小
            orig_size = webp_path.stat().st_size / 1024  # KB
            
            # 创建临时文件路径
            temp_path = temp_dir / f"{webp_path.stem}_compressed.webp"
            
            # 使用cwebp进行二次压缩
            cmd = [
                cwebp_path,
                "-q", "75",  # 质量参数
                "-m", "6",   # 最高压缩模式
                str(webp_path),
                "-o", str(temp_path)
            ]
            
            # 执行命令
            result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
            
            if result.returncode != 0:
                # 压缩失败,记录错误
                with open(log_file, "a", encoding="utf-8") as log:
                    log.write(f"[错误] 压缩 {webp_path} 失败: {result.stderr}\n")
                error_count += 1
                continue
            
            # 获取新文件大小
            new_size = temp_path.stat().st_size / 1024  # KB
            
            # 如果新文件比原文件大,则跳过
            if new_size >= orig_size:
                skipped_count += 1
                temp_path.unlink()  # 删除临时文件
                continue
            
            # 计算节省空间
            saved = orig_size - new_size
            saved_percent = (saved / orig_size) * 100 if orig_size > 0 else 0
            
            # 记录统计信息
            with open(stats_file, "a", encoding="utf-8") as stats:
                stats.write(f"{webp_path},{webp_path},{orig_size:.2f},{new_size:.2f},{saved:.2f},{saved_percent:.2f}\n")
            
            # 替换原文件
            webp_path.unlink()  # 删除原文件
            shutil.move(str(temp_path), str(webp_path))
            compressed_count += 1
        
        except Exception as e:
            with open(log_file, "a", encoding="utf-8") as log:
                log.write(f"[异常] 处理 {webp_path} 时出错: {str(e)}\n")
            error_count += 1
    
    # 完成报告
    total_size = sum(f.stat().st_size for f in webp_dir.glob('**/*') if f.is_file())
    total_size_gb = total_size / (1024 ** 3)  # 转换为GB
    
    end_time = time.time()
    elapsed = end_time - time.time()
    mins, secs = divmod(elapsed, 60)
    hours, mins = divmod(mins, 60)
    
    with open(log_file, "a", encoding="utf-8") as log:
        log.write("\n压缩完成报告:\n")
        log.write(f"处理文件数: {total_files}\n")
        log.write(f"成功压缩: {compressed_count}\n")
        log.write(f"跳过文件: {skipped_count}\n")
        log.write(f"错误文件: {error_count}\n")
        log.write(f"输出目录总大小: {total_size_gb:.2f} GB\n")
    
    print("\n\n压缩完成!")
    print(f"处理文件数: {total_files}")
    print(f"成功压缩: {compressed_count}")
    print(f"跳过文件: {skipped_count}")
    print(f"错误文件: {error_count}")
    print(f"输出目录总大小: {total_size_gb:.2f} GB")
    
    # 清理临时目录
    try:
        shutil.rmtree(temp_dir)
        print(f"已清理临时目录: {temp_dir}")
    except Exception as e:
        print(f"清理临时目录时出错: {str(e)}")
    
    print(f"日志文件: {log_file}")
    print(f"统计文件: {stats_file}")
    print(f"总耗时: {int(hours)}小时 {int(mins)}分钟 {secs:.2f}秒")

if __name__ == "__main__":
    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 吧。

Built with Hugo, Powered by Github.
全站约 338 篇文章 合计约 966539 字
本站已加入BLOGS·CN