Typecho迁移Halo完整教程

前言

Typecho 作为经典的 PHP 博客系统,以轻量简洁著称;而 Halo 作为基于 Java 的现代博客平台,在功能丰富度与扩展性上更胜一筹。对我来说 WordPress 过于臃肿、Typecho 过于简陋,而Halo 恰好处于两者的平衡点

这段时间自己本地搭建体验后感觉真的不错,就试着研究如何迁移。然而,官方迁移插件在导入 Typecho 备份时无法识别数据,社区方案也各有缺陷。最终,基于 fghwett/typecho_to_halo 项目进行定制改造,实现了远程图床场景下的完美迁移

所以这篇文章将详细介绍如何将 Typecho 的数据(文章、页面、评论、标签、分类等)完整迁移至 Halo,同时保留远程图床链接并自动提取文章封面图。


方案对比:原项目 vs 本方案

核心差异一览

功能维度 原项目 本方案 适用场景
附件迁移 下载至 Halo 附件库 禁用(保留原链接) 远程图床用户
封面图处理 自动提取首图 需要视觉优化的博客
适用场景 本地附件存储 CDN 加速链接保留 又拍云/七牛云/阿里云 OSS 用户

为什么禁用附件迁移?

对于使用又拍云、七牛云等远程图床的博客,附件迁移反而会造成困扰:

  1. 存储冗余:远程图片已具备 CDN 加速,无需重复存储至 Halo
  2. 性能损耗:下载再上传的过程浪费迁移时间
  3. 链接失效风险:迁移过程中可能导致图片链接断裂

解决方案:保留原始图床链接,直接享受 CDN 加速。

为什么添加自动提取封面图?

Halo 支持为文章设置封面图(头图),能显著提升博客的视觉层次感。本方案通过正则表达式自动识别文章中的第一张远程图片,将其设为封面,省去手动配置的繁琐。

  • 自动扫描文章内容中的第一张远程图片
  • 将其设置为文章的封面图
  • 无需手动一一设置,提升迁移效率

本版本更新内容

基于之前版本(2026 年 4 月 14 日发布),本版本进一步优化了数据迁移的稳定性和用户体验,主要更新包括:

  1. 数据去重检查:新增 loadExistingData() 函数,迁移前预加载 Halo 已有数据,通过 slug 对比自动跳过已存在的标签、分类、文章和页面,避免重复导入。
  2. 配置完善:更新配置示例,修复目录名称错误,确保与 config-example.yaml 完全一致。
  3. 常见问题更新:针对用户反馈,更新了关于数据重复的解答,明确说明工具已具备去重能力。

技术实现:代码改造详解

改造一:禁用附件迁移

文件apps/migrate/app.go

// 原代码
actions = append(actions, NewAction("迁移附件", app.migrateAttachments))

// 改造后(注释掉该行)
// actions = append(actions, NewAction("迁移附件", app.migrateAttachments))

改造二:新增封面图提取函数

文件internal/halo/post.go

/**
 * extractFirstImage 从文章内容中提取第一张远程图片URL
 * 支持 Markdown 图片语法:![alt](url)
 * 支持 HTML img 标签:<img src="url">
 * 只提取 http:// 或 https:// 开头的远程图片
 */
func extractFirstImage(content string) string {
    // 匹配 Markdown 图片语法
    mdPattern := `!\[.*?\]\((https?://[^\s\)]+\.(?:jpg|jpeg|png|gif|webp|bmp|svg|ico))`
    mdRe := regexp.MustCompile(mdPattern)
    mdMatches := mdRe.FindStringSubmatch(content)
    if len(mdMatches) > 1 {
        return mdMatches[1]
    }
  
    // 匹配 HTML img 标签
    htmlPattern := `<img[^>]+src=["'](https?://[^"']+\.(?:jpg|jpeg|png|gif|webp|bmp|svg|ico))["']`
    htmlRe := regexp.MustCompile(htmlPattern)
    htmlMatches := htmlRe.FindStringSubmatch(content)
    if len(htmlMatches) > 1 {
        return htmlMatches[1]
    }
  
    return ""
}

改造三:集成封面图至文章迁移

文件apps/migrate/app.go - migratePosts 函数内

// 提取文章封面图
cover := extractFirstImage(post.Content)

// 创建文章(新增 cover 参数)
postId, err := h.client.AddPost(halo.Post{
    Title:        post.Title,
    Content:      post.Content,
    Slug:         post.Slug,
    Tags:         tags,
    Categories:   categories,
    Status:       status,
    AllowComment: post.AllowComment,
    Cover:        cover,  // 新增封面图字段
}, h.policyName, h.groupName)

改造四:扩展 Post 结构体

文件internal/halo/post.go

type Post struct {
    Title        string   `json:"title"`
    Content      string   `json:"content"`
    Slug         string   `json:"slug"`
    Tags         []string `json:"tags"`
    Categories   []string `json:"categories"`
    Status       string   `json:"status"`
    AllowComment bool     `json:"allowComment"`
    Cover        string   `json:"cover,omitempty"`  // 新增封面图字段
}

// AddPost 函数内添加封面图设置逻辑
func (c *Client) AddPost(post Post, policyName, groupName string) (string, error) {
    // ... 现有代码 ...
  
    if post.Cover != "" {
        postSetting.Spec.Template = "post"
        postSetting.CustomTemplate = fmt.Sprintf(`{"cover": "%s"}`, post.Cover)
    }
  
    // ... 后续代码 ...
}

迁移工具原理剖析

架构设计

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   Typecho DB    │────▶│  typecho_to_halo │────▶│   Halo API      │
│   (MySQL)       │     │   (Go 程序)      │     │   (REST API)    │
└─────────────────┘     └─────────────────┘     └─────────────────┘
        │                        │                       │
        ▼                        ▼                       ▼
   - tc_contents           - 数据读取              - 创建文章
   - tc_metas              - 结构转换              - 创建分类
   - tc_comments           - API 调用              - 创建标签
   - tc_users              - 错误处理              - 创建评论
   - tc_relationships       - 日志输出              - 设置封面图

数据流转过程

阶段 操作 技术细节
读取 GORM 连接 MySQL 通过 DSN 连接 Typecho 数据库
转换 结构体映射 Typecho 表结构 → Halo API 请求体
导入 REST API 调用 使用 Halo Personal Access Token 认证
优化 封面图提取 正则匹配文章内容中的远程图片

迁移内容清单

数据类型 迁移状态 备注
标签 (Tags) ✅ 完整支持 包含名称、缩略名
分类 (Categories) ✅ 完整支持 保留层级关系
文章 (Posts) ✅ 增强支持 自动提取封面图
页面 (Pages) ✅ 完整支持 独立页面如"关于"、“友链”
评论 (Comments) ✅ 完整支持 保留评论层级结构
附件 (Attachments) ❌ 已禁用 远程图床链接保留

环境准备

版本要求

组件 版本 说明
Typecho 1.3.0 测试通过版本
Halo 2.23.3 测试通过版本
Go 1.20+ 如需自行编译

前置检查清单

  • Halo 已安装并正常运行
  • 已获取 Halo Personal Access Token(路径:Halo 后台 → 用户 → 个人令牌 → 生成新令牌)
  • 确认 Typecho 数据库可远程访问
  • 记录 Typecho 表前缀(如 tc_ 或默认的 typecho_

实操步骤

第一步:获取迁移工具

# 克隆仓库(注意是下划线,非连字符)
git clone https://github.com/NoEggEgg/ty2halo.git
cd typecho_to_halo

第二步:配置文件

# 复制示例配置
cp config-example.yaml config.yaml

config.yaml 配置详解

更新说明:配置格式已更新,与 config-example.yaml 保持完全一致。新增 prefix 字段,优化了配置结构。

typecho:
  type: mysql
  # ⚠️ 必须以斜杠结尾!否则附件下载会报错:lookup xxx.comfiles: no such host
  baseUrl: https://your-domain.com/
  dsn: "username:password@tcp(host:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
  prefix: typecho_ # 数据库表前缀,默认为 typecho_
  
halo:
  host: your-halo-host:8090
  schema: http        # 或 https
  token: "your-pat-token"
  debug: false
  policyName: default-policy
  groupName: ""

fileManager:
  dir: ./_tmp/        # 临时文件目录

第三步:处理非默认表前缀

问题现象:若 Typecho 使用 tc_ 而非默认的 typecho_ 前缀,运行时会报错 Table 'dbname.typecho_metas' doesn't exist

解决方案:在 MySQL 中创建视图映射

USE your_database_name;

-- 创建视图映射(将 tc_* 映射为 typecho_*)
CREATE OR REPLACE VIEW typecho_metas AS SELECT * FROM tc_metas;
CREATE OR REPLACE VIEW typecho_contents AS SELECT * FROM tc_contents;
CREATE OR REPLACE VIEW typecho_comments AS SELECT * FROM tc_comments;
CREATE OR REPLACE VIEW typecho_users AS SELECT * FROM tc_users;
CREATE OR REPLACE VIEW typecho_options AS SELECT * FROM tc_options;
CREATE OR REPLACE VIEW typecho_fields AS SELECT * FROM tc_fields;
CREATE OR REPLACE VIEW typecho_relationships AS SELECT * FROM tc_relationships;

清理视图(迁移完成后执行):

USE your_database_name;

DROP VIEW IF EXISTS typecho_metas;
DROP VIEW IF EXISTS typecho_contents;
DROP VIEW IF EXISTS typecho_comments;
DROP VIEW IF EXISTS typecho_users;
DROP VIEW IF EXISTS typecho_options;
DROP VIEW IF EXISTS typecho_fields;
DROP VIEW IF EXISTS typecho_relationships;

第四步:执行迁移

方式一:使用预编译版本(推荐)

# Windows
ty2halo.exe

# Linux / macOS
./ty2halo

方式二:自行编译

go mod tidy
go build -o ty2halo.exe

迁移执行顺序

  1. ✅ 迁移标签
  2. ✅ 迁移分类
  3. ❌ 迁移附件(已禁用)
  4. ✅ 迁移文章(自动提取封面图)
  5. ✅ 迁移页面
  6. ✅ 迁移评论

验证与验收

登录 Halo 后台核对数据完整性:

数据类型 预期结果 验证状态
标签 与 Typecho 一致
分类 层级结构保留
附件 0 个(远程图床保留)
文章 111 篇(示例)
页面 独立页面完整
评论 739 条(示例)

常见问题速查

Q1:重复数据如何处理?

现象:多次运行后标签 / 分类重复。

原因工具未做重复检测。 更新:工具已添加数据去重检查。

解决

  • 在 Halo 后台删除重复数据后重新运行
  • 或修改源码添加存在性检查(需重新编译)
  • 新增:工具已内置数据去重功能,会先查询 Halo 中已存在的数据(通过 slug 对比),自动跳过已存在的标签、分类、文章和页面。但如果手动删除 Halo 数据后重新迁移,仍然会正常导入。

Q2:附件下载报错 lookup xxx.comfiles: no such host

原因baseUrl 缺少末尾斜杠,导致 URL 拼接错误。

解决:确保 baseUrl/ 结尾,如 https://example.com/

Q3:表前缀不匹配

解决:使用上述数据库视图方案,或修改源码中的表名常量。

Q4:如何恢复附件迁移功能?

如需将附件下载至 Halo 本地(不推荐远程图床场景):

// apps/migrate/app.go
// 取消注释以下行
actions = append(actions, NewAction("迁移附件", app.migrateAttachments))

Q5:封面图提取规则是什么?

项目 规则
匹配范围 仅远程图片(http://https:// 开头)
语法支持 Markdown ![alt](url) 与 HTML ![](url)
格式支持 jpg, jpeg, png, gif, webp, bmp, svg, ico
优先级 取文章中第一张符合条件的图片
手动调整 可在 Halo 后台修改封面图

总结

通过定制化的 ty2halo 工具,我们实现了:

优化点 效果
禁用附件迁移 保留远程图床 CDN 加速,节省存储与时间
自动提取封面图 提升博客视觉体验,减少手动配置
数据去重检查 新增:自动跳过已存在的数据,避免重复导入,提升迁移稳定性
最小化代码改动 基于原项目结构,降低维护成本

后续

这篇文章会持续更新,因为迁移后还有各种其他问题需要调试,比如固定链接等,有点小麻烦。

最后更新:2026 年 4 月 20 日(添加数据去重检查等优化)


参考资源

  • 仓库地址:https://github.com/NoEggEgg/ty2halo
  • 原项目仓库地址:https://github.com/fghwett/typecho_to_halo
  • Halo 官方文档:https://docs.halo.run
  • Typecho 官方网站:https://typecho.org