


























Typecho 作为经典的 PHP 博客系统,以轻量简洁著称;而 Halo 作为基于 Java 的现代博客平台,在功能丰富度与扩展性上更胜一筹。对我来说 WordPress 过于臃肿、Typecho 过于简陋,而Halo 恰好处于两者的平衡点。
这段时间自己本地搭建体验后感觉真的不错,就试着研究如何迁移。然而,官方迁移插件在导入 Typecho 备份时无法识别数据,社区方案也各有缺陷。最终,基于 fghwett/typecho_to_halo 项目进行定制改造,实现了远程图床场景下的完美迁移。
所以这篇文章将详细介绍如何将 Typecho 的数据(文章、页面、评论、标签、分类等)完整迁移至 Halo,同时保留远程图床链接并自动提取文章封面图。
| 功能维度 | 原项目 | 本方案 | 适用场景 |
|---|---|---|---|
| 附件迁移 | 下载至 Halo 附件库 | ❌禁用(保留原链接) | 远程图床用户 |
| 封面图处理 | 无 | ✅自动提取首图 | 需要视觉优化的博客 |
| 适用场景 | 本地附件存储 | CDN 加速链接保留 | 又拍云/七牛云/阿里云 OSS 用户 |
对于使用又拍云、七牛云等远程图床的博客,附件迁移反而会造成困扰:
解决方案:保留原始图床链接,直接享受 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)
文件: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+ | 如需自行编译 |
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 条(示例) | ✅ |
现象:多次运行后标签 / 分类重复。
原因:工具未做重复检测。 更新:工具已添加数据去重检查。
解决:
lookup xxx.comfiles: no such host原因:baseUrl 缺少末尾斜杠,导致 URL 拼接错误。
解决:确保 baseUrl 以 / 结尾,如 https://example.com/。
解决:使用上述数据库视图方案,或修改源码中的表名常量。
如需将附件下载至 Halo 本地(不推荐远程图床场景):
// apps/migrate/app.go
// 取消注释以下行
actions = append(actions, NewAction("迁移附件", app.migrateAttachments))
| 项目 | 规则 |
|---|---|
| 匹配范围 | 仅远程图片(http:// 或 https:// 开头) |
| 语法支持 | Markdown  与 HTML  |
| 格式支持 | jpg, jpeg, png, gif, webp, bmp, svg, ico |
| 优先级 | 取文章中第一张符合条件的图片 |
| 手动调整 | 可在 Halo 后台修改封面图 |
通过定制化的 ty2halo 工具,我们实现了:
| 优化点 | 效果 |
|---|---|
| 禁用附件迁移 | 保留远程图床 CDN 加速,节省存储与时间 |
| 自动提取封面图 | 提升博客视觉体验,减少手动配置 |
| 数据去重检查 | 新增:自动跳过已存在的数据,避免重复导入,提升迁移稳定性 |
| 最小化代码改动 | 基于原项目结构,降低维护成本 |
这篇文章会持续更新,因为迁移后还有各种其他问题需要调试,比如固定链接等,有点小麻烦。
最后更新:2026 年 4 月 20 日(添加数据去重检查等优化)
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。