前言

最近,我把我的博客从 Halo 彻底迁移到了 Hexo。为什么折腾?一方面是想回归静态博客的纯粹和速度,另一方面也是想对自己的内容有更强的掌控感。

整个过程踩了不少坑,特别是文章元数据(比如发布日期和 URL)的处理,花了我不少时间。为了让后来人能少走弯路,我把整个流程,包括我为了这次迁移专门编写的两个 Python 脚本,都整理成了这篇保姆级教程,希望能帮到有同样需求的朋友。

迁移思路

我们的核心目标是:完美迁移所有文章,并保持原有的 URL 不变。这对于 SEO 来说至关重要,谁也不想迁移后辛辛苦苦积累的搜索引擎收录一夜回到解放前。

整个流程分为三大步:

  1. 导出内容:从 Halo 把所有文章的 Markdown 文件弄下来。
  2. 修正数据:用脚本批量处理这些 Markdown 文件,给它们加上 Hexo 需要的发布日期、URL 等信息。
  3. 验证链接:再次用脚本检查,确保迁移后每一篇文章都能用原来的 URL 正常访问。

听起来不难?我们开干!

第一步:从 Halo 导出文章

工欲善其事,必先利其器。我这里用的是 VSCode 里的一个 Halo 插件(插件地址),可以直接把所有文章以 Markdown 的格式下载到本地。我使用的 Halo 版本是 2.21。这个插件非常方便,推荐给大家。

下载下来后,为了方便管理,我在我的 Hexo 博客根目录(也就是 hexo init 生成的那个文件夹)下,创建了一个叫 halo 的文件夹,把所有导出的 .md 文件都扔了进去。

第二步:安装 Hexo 并处理文章

安装 Hexo

如果你本地还没装 Hexo 环境,可以先跟着 官方文档 走一遍,很简单:

1
2
3
4
5
6
7
8
9
# 全局安装 Hexo 命令行工具
npm install hexo-cli -g

# 在你喜欢的地方初始化一个博客项目
hexo init my-hexo-blog
cd my-hexo-blog

# 安装项目依赖
npm install

关键问题:修复元数据 (Front-matter)

通过插件导出的 Markdown 文件,打开后你会发现,它的 front-matter(就是文件开头 --- 包裹的那部分)里信息严重不足,通常只有一个 title。没有 date,没有 slug,Hexo 根本不知道这篇文章的发布日期和正确的 URL 路径。

一篇一篇手动改?几十上百篇文章,那不得累死。所以,我写了下面这个 Python 脚本来自动化处理。

脚本一:haloToHexo.py

这个脚本是整个迁移工作的核心。它的工作原理有点像“逆向工程”:

  1. 它会先去爬你线上博客的 sitemap.xml(站点地图),这里面有你所有文章的 URL 和最后更新日期,这是最权威的数据源。
  2. 然后,它会顺着 sitemap 里的链接,一个个访问你的线上文章,把文章的真实标题抓下来。
  3. 这样,它就在内存里建立了一个 标题 -> {URL, slug, date} 的对应关系表。
  4. 最后,它会读取我们第一步放在 halo/ 文件夹里的那些 Markdown 文件,用标题去跟内存里的对应表进行匹配。一旦匹配成功,它就会把正确的 date, slug 等信息补全,然后生成一个新的、符合 Hexo 格式的 Markdown 文件,存到 source/_posts 目录里。

使用方法:

  1. 安装依赖库:这个脚本需要用到几个 Python 库,先在终端里安装一下:

    1
    pip install requests python-frontmatter beautifulsoup4
  2. 创建脚本文件:在你的 Hexo 博客根目录下,创建一个名为 haloToHexo.py 的文件,把下面的代码完整地复制进去。记住,一定要修改第 34 行的 SITEMAP_URL 为你自己博客的站点地图地址!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    # -*- coding: utf-8 -*-

    """
    本脚本严格按照您的最终方案执行,将 Halo 导出的 Markdown 文件转换为 Hexo 所需的格式。

    最终版 (v14 - Strict Matching & Encoding Fix):
    1. 从 sitemap.xml 获取所有文章的 URL 和日期,作为权威列表。
    2. 访问每一个 URL,正确处理编码后抓取其线上的真实标题,建立权威数据库: `线上标题 -> {URL, slug, date}`。
    3. 遍历本地 `halo/` 目录,建立本地文章索引: `本地标题 -> 文件路径`。
    4. 以线上数据库为准,与本地索引进行严格、精确的匹配,并进行迁移。
    5. 生成的新文件保留原始文件名,并清理掉 Halo 特有字段。
    6. 提供详细的最终报告,包括处理总数、成功数和失败的URL列表。

    使用前请先安装必要的 Python 库:
    pip install requests python-frontmatter beautifulsoup4
    """

    import os
    import re
    import time
    import logging
    import urllib.parse
    import requests
    import xml.etree.ElementTree as ET
    import frontmatter
    from bs4 import BeautifulSoup

    # --- 配置日志 ---
    logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

    # --- 常量定义 ---
    HALO_DIR = 'halo'
    HEXO_POSTS_DIR = os.path.join('source', '_posts')
    SITEMAP_URL = 'https://sqmn666.com/sitemap.xml'
    HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
    }

    def get_sitemap_links():
    """从 Sitemap 获取所有文章的 URL、slug 和 date。"""
    sitemap_links = []
    try:
    logging.info(f"正在下载 sitemap 从: {SITEMAP_URL}")
    response = requests.get(SITEMAP_URL, headers=HEADERS, timeout=30)
    response.raise_for_status()

    xml_content = re.sub(r' xmlns="[^"]+"', '', response.text, count=1)
    root = ET.fromstring(xml_content)

    for url_node in root.findall('url'):
    loc, lastmod = url_node.find('loc'), url_node.find('lastmod')
    if loc is not None and lastmod is not None and '/archives/' in loc.text:
    match = re.search(r'/archives/([^/]+)', loc.text)
    if match:
    sitemap_links.append({
    'url': loc.text,
    'slug': match.group(1),
    'date': lastmod.text.split('T')[0]
    })
    logging.info(f"Sitemap 分析完成,发现 {len(sitemap_links)} 篇文章链接。")
    return sitemap_links
    except Exception as e:
    logging.error(f"处理 sitemap 时出错: {e}")
    return []

    def build_online_authority(sitemap_links):
    """访问每个文章链接,抓取标题,建立最终的权威数据库。"""
    title_authority = {}
    failed_urls = []

    logging.info("开始从线上文章抓取真实标题以建立权威数据库...")
    for i, link_data in enumerate(sitemap_links):
    url = link_data['url']
    try:
    logging.info(f"正在处理URL ({i+1}/{len(sitemap_links)}): {url}")
    response = requests.get(url, headers=HEADERS, timeout=15)
    response.raise_for_status()
    # 显式使用 utf-8 解码
    response.encoding = 'utf-8'
    soup = BeautifulSoup(response.text, 'html.parser')

    title_tag = soup.find('h1', class_='joe_detail__title')
    if title_tag and title_tag.string:
    title = title_tag.string.strip()
    if title not in title_authority:
    title_authority[title] = link_data
    else:
    logging.warning(f"发现重复的线上标题 '{title}',URL: {url}。将只保留第一个。")
    else:
    logging.warning(f"在 {url} 未找到标题,计入失败列表。")
    failed_urls.append(url)
    time.sleep(0.1) # 短暂延时
    except Exception as e:
    logging.error(f"抓取或解析 {url} 时出错: {e}")
    failed_urls.append(url)

    logging.info(f"权威数据库构建完成,共 {len(title_authority)} 个有效条目。")
    return title_authority, failed_urls

    def build_local_files_index():
    """遍历本地文件,建立 `本地标题 -> 文件路径` 的索引。"""
    local_title_to_path = {}
    all_md_files = [os.path.join(r, f) for r, _, fs in os.walk(HALO_DIR) for f in fs if f.endswith('.md')]
    for path in all_md_files:
    try:
    with open(path, 'r', encoding='utf-8') as f:
    post = frontmatter.load(f)
    title = post.metadata.get('title', '').strip()
    if title:
    local_title_to_path[title] = path
    else:
    backup_key = os.path.splitext(os.path.basename(path))[0]
    local_title_to_path[backup_key] = path
    except Exception as e:
    logging.error(f"读取本地文件 {path} 时出错: {e}")
    return local_title_to_path

    def main():
    sitemap_links = get_sitemap_links()
    if not sitemap_links:
    return

    online_authority, failed_to_scrape_urls = build_online_authority(sitemap_links)
    local_files_index = build_local_files_index()

    os.makedirs(HEXO_POSTS_DIR, exist_ok=True)

    processed_files = 0
    unmatched_urls = list(failed_to_scrape_urls)

    logging.info("权威数据库构建完成,开始反向匹配本地文件...")

    # 以权威的线上数据库为准,进行匹配和迁移
    for online_title, authority_data in online_authority.items():
    if online_title in local_files_index:
    source_path = local_files_index[online_title]
    try:
    with open(source_path, 'r', encoding='utf-8') as f:
    post = frontmatter.load(f)

    post.metadata['title'] = online_title
    post.metadata['date'] = authority_data['date']
    post.metadata['slug'] = urllib.parse.unquote(authority_data['slug'])
    post.metadata['permalink'] = urllib.parse.unquote(urllib.parse.urlparse(authority_data['url']).path.lstrip('/')) + '/'
    post.metadata.pop('halo', None)
    post.metadata.pop('excerpt', None)

    # 使用文章标题作为新文件名,并替换特殊字符
    sanitized_title = re.sub(r'[\\/*?:"<>|]', '_', online_title)
    new_filename = f"{sanitized_title}.md"
    dest_path = os.path.join(HEXO_POSTS_DIR, new_filename)
    with open(dest_path, 'w', encoding='utf-8') as f:
    f.write(frontmatter.dumps(post))

    logging.info(f"成功迁移:《{online_title}》({source_path})")
    processed_files += 1
    except Exception as e:
    logging.error(f"处理并写入文件 {source_path} 时出错: {e}")
    unmatched_urls.append(authority_data['url'])
    else:
    logging.warning(f"线上文章《{online_title}》在本地文件中未找到完全匹配的标题。")
    unmatched_urls.append(authority_data['url'])

    # --- 最终报告 ---
    print("\n" + "="*50)
    print("迁移完成 - 最终报告")
    print("="*50)
    print(f"Sitemap 中的文章总数: {len(sitemap_links)}")
    print(f"成功迁移的文章数量: {processed_files}")
    failed_count = len(unmatched_urls)
    print(f"失败的 URL 数量: {failed_count}")
    if failed_count > 0:
    print("\n以下 URL 未能成功迁移 (原因可能是抓取失败或本地无匹配文件):")
    for url in sorted(list(set(unmatched_urls))):
    print(f"- {url}")
    print("="*50)

    if __name__ == '__main__':
    main()
  3. 运行脚本:确保你的 halo 文件夹和 haloToHexo.py 文件都在 Hexo 根目录,然后运行它:

    1
    python haloToHexo.py

    脚本会跑上一会儿,终端会输出一堆日志。跑完后,去 source/_posts 文件夹看看,是不是所有文章都躺在里面了?

第三步:验证迁移结果,确保万无一失

迁移是完成了,但我们怎么知道对不对呢?特别是 URL,有没有可能因为一些编码问题或者其他幺蛾子,导致和原来的不一样?

这时候,就需要第二个脚本登场了。

这个脚本是个“审计员”。它会再次去读你线上的 sitemap,然后把每一个 URL 都转换成本地 http://localhost:4000 的地址,再用程序去访问一下,看能不能访问通。

使用方法:

  1. 启动本地服务:首先,我们得让 Hexo 在本地跑起来。在 Hexo 根目录的终端里,运行:

    1
    hexo server

    看到 Hexo is running at http://localhost:4000 就说明成功了。先不要关闭这个终端。

  2. 创建校验脚本新开一个终端窗口,还是在 Hexo 根目录,创建一个叫 verifyHexoLinks.py 的文件,把下面的代码复制进去。同样,记得修改第 30 行的 SITEMAP_URL 为你自己的地址!

    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
    # -*- coding: utf-8 -*-

    """
    本脚本用于验证 Hexo 迁移结果。

    功能 (v2.1 - Final URL Encoding Fix):
    1. 从线上网站的 sitemap.xml 中提取所有 URL。
    2. 将每个线上 URL 转换为对应的本地 Hexo 服务地址。
    3. 对 URL 路径中的中文字符进行正确的 URL 编码,以匹配 Hexo 的行为。
    4. 逐一访问本地 URL,检查其是否返回成功状态码 (200 OK)。
    5. 统计成功和失败的数量,并打印出所有验证失败的 URL 列表。

    使用前请确保:
    1. 您已经启动了本地 Hexo 服务,并且服务运行在 http://localhost:4000
    2. 安装必要的 Python 库:
    pip install requests
    """

    import re
    import logging
    import requests
    import xml.etree.ElementTree as ET
    from urllib.parse import urlparse, quote
    import time

    # --- 配置日志 ---
    logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

    # --- 常量定义 ---
    SITEMAP_URL = 'https://sqmn666.com/sitemap.xml'
    LOCAL_HEXO_BASE = 'http://localhost:4000'
    HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
    }

    def get_all_links_from_sitemap():
    """从 Sitemap 获取所有类型的链接。"""
    all_links = []
    try:
    logging.info(f"正在下载 sitemap 从: {SITEMAP_URL}")
    response = requests.get(SITEMAP_URL, headers=HEADERS, timeout=30)
    response.raise_for_status()

    xml_content = re.sub(r' xmlns="[^"]+"', '', response.text, count=1)
    root = ET.fromstring(xml_content)

    sitemap_urls = [loc.text for sitemap in root.findall('sitemap') for loc in sitemap.findall('loc')]
    if not sitemap_urls:
    sitemap_urls = [SITEMAP_URL]

    for sitemap_url in sitemap_urls:
    logging.info(f"正在解析 sitemap 子链接: {sitemap_url}")
    s_response = requests.get(sitemap_url, headers=HEADERS, timeout=30)
    s_response.raise_for_status()
    s_xml_content = re.sub(r' xmlns="[^"]+"', '', s_response.text, count=1)
    s_root = ET.fromstring(s_xml_content)

    for url_node in s_root.findall('url'):
    loc_tag = url_node.find('loc')
    if loc_tag is not None:
    all_links.append(loc_tag.text)

    logging.info(f"Sitemap 分析完成,共发现 {len(all_links)} 个链接。")
    return all_links
    except Exception as e:
    logging.error(f"处理 sitemap 时出错: {e}")
    return []

    def main():
    online_links = get_all_links_from_sitemap()
    if not online_links:
    logging.error("未能从 sitemap 获取任何链接,脚本终止。")
    return

    total_links = len(online_links)
    success_count = 0
    failed_urls = []

    logging.info(f"开始验证 {total_links} 个链接在本地 Hexo 服务上的可用性...")

    session = requests.Session()
    session.headers.update(HEADERS)

    for i, online_url in enumerate(online_links):
    try:
    parsed_url = urlparse(online_url)
    # 检查路径是否已经编码,如果已编码则不再进行二次编码
    path = parsed_url.path
    if '%' in path:
    encoded_path = path # 已经编码,直接使用
    else:
    encoded_path = quote(path) # 未编码,进行编码
    local_url = f"{LOCAL_HEXO_BASE}{encoded_path}"

    logging.info(f"正在验证 ({i+1}/{total_links}): {local_url}")

    response = session.get(local_url, timeout=10, allow_redirects=True)
    response.raise_for_status()

    success_count += 1

    except requests.exceptions.RequestException as e:
    logging.error(f"验证失败: {local_url} - {e}")
    failed_urls.append(local_url)

    # --- 最终报告 ---
    print("\n" + "="*50)
    print("迁移验证完成 - 最终报告")
    print("="*50)
    print(f"验证的链接总数: {total_links}")
    print(f"成功访问的链接数: {success_count}")
    failed_count = len(failed_urls)
    print(f"验证失败的链接数: {failed_count}")

    if failed_count > 0:
    print("\n以下本地 URL 验证失败 (请确保您的 Hexo 服务正在运行):")
    for url in sorted(failed_urls):
    print(f"- {url}")
    else:
    print("\n恭喜!所有链接均已成功验证!")
    print("="*50)

    if __name__ == '__main__':
    print("=" * 50)
    print("Hexo 迁移验证脚本 (v2.1 - 最终修正版)")
    print("=" * 50)
    print("在运行此脚本前,请确保您的本地 Hexo 服务已通过 `hexo server` 启动。")
    print("脚本将在一秒后自动开始验证...")
    time.sleep(1)
    main()
  3. 运行校验:在第二个终端窗口中,运行校验脚本:

    1
    python verifyHexoLinks.py

    它会开始疯狂刷日志,一个个地检查链接。最后,你会看到一份报告。如果失败的链接数是 0,那就完美了!

可选步骤:图片本地化

如果你文章中的图片图床上,此步骤可以将所有图片下载到本地,并更新文章中的链接。

这里我提供另一个脚本 localize_images.py 来自动完成这件事。

脚本三:localize_images.py

这个脚本会:

  1. 遍历 source/_posts 文件夹下所有的 Markdown 文件。
  2. 查找文中的所有外部图片链接。
  3. 将图片下载到与文章同名的文件夹下。
  4. 将文中的链接替换为本地相对路径。

使用方法:

  1. 在 Hexo 博客根目录创建一个名为 localize_images.py 的文件,并将以下代码完整复制进去。
    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
    import os
    import re
    import requests
    from urllib.parse import urlparse
    import logging

    # --- 配置 ---
    # 配置日志记录
    logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

    # Hexo 博客文章所在的目录
    POSTS_DIR = 'source/_posts'
    # 图片链接的正则表达式,用于匹配 Markdown 格式的外部图片链接
    IMAGE_REGEX = re.compile(r'!\[(.*?)\]\((https?://.*?)\)')
    # 设置请求头,模拟浏览器访问,防止某些网站的反爬虫机制
    HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
    }

    def process_markdown_file(md_file_path):
    """处理单个 Markdown 文件,下载图片并更新链接。"""
    logging.info(f"--- 开始处理文件: {md_file_path} ---")

    # 根据 Markdown 文件名确定资源文件夹的路径
    asset_folder_path = os.path.splitext(md_file_path)[0]

    try:
    with open(md_file_path, 'r', encoding='utf-8') as f:
    content = f.read()
    except Exception as e:
    logging.error(f"读取文件 {md_file_path} 失败: {e}")
    return

    # 查找所有匹配的外部图片链接
    image_urls = IMAGE_REGEX.findall(content)
    if not image_urls:
    logging.info("文件中未找到外部图片链接。")
    return

    logging.info(f"在 {md_file_path} 中找到 {len(image_urls)} 个外部图片链接。")

    # 如果资源文件夹不存在,则创建它
    if not os.path.exists(asset_folder_path):
    logging.info(f"创建资源文件夹: {asset_folder_path}")
    os.makedirs(asset_folder_path)

    original_content = content

    # 遍历所有找到的图片链接
    for alt_text, url in image_urls:
    try:
    # 从 URL 中解析出文件名
    parsed_url = urlparse(url)
    # 移除查询参数,确保文件名干净
    image_filename = os.path.basename(parsed_url.path)

    if not image_filename:
    logging.warning(f"无法从 URL 中提取有效的文件名: {url}")
    continue

    # 构造图片的本地保存路径
    local_image_path = os.path.join(asset_folder_path, image_filename)

    # 如果文件已存在,则跳过下载
    if os.path.exists(local_image_path):
    logging.info(f"图片已存在,跳过下载: {image_filename}")
    else:
    # 下载图片
    logging.info(f"正在下载: {url} -> {local_image_path}")
    response = requests.get(url, headers=HEADERS, stream=True, timeout=20)

    # 检查请求是否成功
    if response.status_code == 200:
    with open(local_image_path, 'wb') as img_f:
    for chunk in response.iter_content(1024):
    img_f.write(chunk)
    logging.info("下载成功。")
    else:
    logging.error(f"下载失败,状态码: {response.status_code}, URL: {url}")
    continue # 如果下载失败,则不替换链接

    # 准备替换文章中的链接
    original_link = f"![{alt_text}]({url})"
    # 将 alt text 中的单引号转义,以避免破坏 asset_img 标签的结构
    alt_text_escaped = alt_text.replace("'", "\\'")
    # 生成符合 Hexo 规范的 asset_img 标签
    new_link = f"{{% asset_img '{image_filename}' '{alt_text_escaped}' %}}"
    content = content.replace(original_link, new_link)

    except requests.exceptions.RequestException as e:
    logging.error(f"下载图片时发生网络错误: {url}, 错误: {e}")
    except Exception as e:
    logging.error(f"处理图片链接 {url} 时发生未知错误: {e}")

    # 如果内容有变化,则写回文件
    if content != original_content:
    try:
    with open(md_file_path, 'w', encoding='utf-8') as f:
    f.write(content)
    logging.info(f"文件 {md_file_path} 已更新。")
    except Exception as e:
    logging.error(f"写入文件 {md_file_path} 失败: {e}")
    else:
    logging.info("文件内容无变化。")


    def main():
    """主函数,遍历所有文章并处理。"""
    logging.info("=== 开始执行图片本地化脚本 ===")

    # 建议用户备份
    print("重要提示:此脚本将直接修改 'source/_posts' 文件夹中的文件。建议在运行前进行备份。")

    # 遍历 `source/_posts` 目录
    for root, _, files in os.walk(POSTS_DIR):
    for file in files:
    if file.endswith('.md'):
    md_file_path = os.path.join(root, file)
    process_markdown_file(md_file_path)

    logging.info("=== 脚本执行完毕 ===")


    if __name__ == '__main__':
    main()
  2. 运行脚本:
    1
    python localize_images.py
    脚本会自动处理所有文章。

尾声

到这里,整个迁移工作基本就大功告成了。剩下的就是部署你的新博客了。

1
hexo clean && hexo generate && hexo deploy

虽然过程有点折腾,但看到所有文章都完美地在新家里安顿下来,并且保留了原始的 URL,这种感觉还是相当不错的。希望这篇超详细的教程和这两个小脚本,能帮到正在或准备从 Halo 迁移到 Hexo 的你。