
前言
Typecho 作为经典的 PHP 博客系统,以轻量简洁著称;而 Halo 作为基于 Java 的现代博客平台,在功能丰富度与扩展性上更胜一筹。对我来说 WordPress 过于臃肿、Typecho 过于简陋,而Halo 恰好处于两者的平衡点。
这段时间自己本地搭建体验后感觉真的不错,就试着研究如何迁移。然而,官方迁移插件在导入 Typecho 备份时无法识别数据,社区方案也各有缺陷。最终,基于 fghwett/typecho_to_halo 项目进行定制改造,实现了远程图床场景下的完美迁移。
所以这篇文章将详细介绍如何将 Typecho 的数据(文章、页面、评论、标签、分类等)完整迁移至 Halo,同时保留远程图床链接并自动提取文章封面图。
方案对比:原项目 vs 本方案
核心差异一览
| 功能维度 | 原项目 | 本方案 | 适用场景 |
|---|---|---|---|
| 附件迁移 | 下载至 Halo 附件库 | ❌禁用(保留原链接) | 远程图床用户 |
| 封面图处理 | 无 | ✅自动提取首图 | 需要视觉优化的博客 |
| 适用场景 | 本地附件存储 | CDN 加速链接保留 | 又拍云/七牛云/阿里云 OSS 用户 |
为什么禁用附件迁移?
对于使用又拍云、七牛云等远程图床的博客,附件迁移反而会造成困扰:
- 存储冗余:远程图片已具备 CDN 加速,无需重复存储至 Halo
- 性能损耗:下载再上传的过程浪费迁移时间
- 链接失效风险:迁移过程中可能导致图片链接断裂
解决方案:保留原始图床链接,直接享受 CDN 加速。
为什么添加自动提取封面图?
Halo 支持为文章设置封面图(头图),能显著提升博客的视觉层次感。本方案通过正则表达式自动识别文章中的第一张远程图片,将其设为封面,省去手动配置的繁琐。
- 自动扫描文章内容中的第一张远程图片
- 将其设置为文章的封面图
- 无需手动一一设置,提升迁移效率
本版本更新内容
基于之前版本(2026 年 4 月 14 日发布),本版本进一步优化了数据迁移的稳定性和用户体验,主要更新包括:
- 数据去重检查:新增
loadExistingData()函数,迁移前预加载 Halo 已有数据,通过 slug 对比自动跳过已存在的标签、分类、文章和页面,避免重复导入。 - 配置完善:更新配置示例,修复目录名称错误,确保与
config-example.yaml完全一致。 - 常见问题更新:针对用户反馈,更新了关于数据重复的解答,明确说明工具已具备去重能力。
技术实现:代码改造详解
改造一:禁用附件迁移
文件:apps/migrate/app.go
// 原代码
actions = append(actions, NewAction("迁移附件", app.migrateAttachments))
// 改造后(注释掉该行)
// actions = append(actions, NewAction("迁移附件", app.migrateAttachments))
改造二:新增封面图提取函数
文件:internal/halo/post.go
/**
* extractFirstImage 从文章内容中提取第一张远程图片URL
* 支持 Markdown 图片语法:
* 支持 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
迁移执行顺序:
- ✅ 迁移标签
- ✅ 迁移分类
❌ 迁移附件(已禁用)- ✅ 迁移文章(自动提取封面图)
- ✅ 迁移页面
- ✅ 迁移评论
验证与验收
登录 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  与 HTML  |
| 格式支持 | 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
Typecho 迁移 Halo 完整教程:数据库视图解决表前缀 + 自动提取封面图 + 去重检测
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
赞赏支持
如果觉得文章对你有帮助,可以请作者喝杯咖啡 ☕
评论交流
欢迎留下你的想法