关于我把博客从WordPress迁移到Hexo这件事
一、为什么要迁移博客 我本来以为我已经过了折腾博客的年纪了,没想到最近还是把博客从WordPress
迁移到了Hexo
,主要有以下几个原因:
1、People Die, But Long Live GitHub 最近,我用了10多年的PHP
主机商宣布停运了,突然感到有些失落,我已经经历过太多网站的关停了,包括在我博客留下足迹的众多个人博客,现在还能打开的博客寥寥无几。
我就在思考,如果我有一天去世了,我的空间到期后也会被关闭,域名到期也会被回收,所以我希望我的博客在我大限到来之后能够再运行久一点。
因此我打算将WordPress
博客迁移到静态博客Hexo
,因为静态博客可以托管在GitHub Pages
里,如果GitHub
不倒闭的话,我的博客也不会关闭。
人终有一死,但GitHub
永存不朽。
如果正在看这篇文章的你所在的年份是2121
年之后的话,说明100年后我的博客还存在,至少证明我现在的决定是正确的。
2、使用Markdown
写作 我一直幻想WordPress
的编辑器有一天能够支持Markdown
,可惜每次更新后都没看到希望,而且编辑器也变得越来越非主流。
我使用了第三方的Markdown
插件,但是插件有时会把Markdown
格式的文章又转换成html
编码,导致文章很难编辑。
而Hexo
是为程序员而生的,只需用Markdown
格式编写文章,Hexo
框架会自动渲染,非常丝滑优雅。
3、静态博客的评论系统 我很久以前就想使用静态博客了,但是考虑到静态博客无法进行评论,不能将WordPress
的评论迁移过去,所以还是放弃了。
经过多年的发展,市面上已经涌现出很多优秀的开源评论系统,如beaudar
, valine
, twikoo
, waline
, minivaline
, disqus
, disqusjs
, gitalk
, vssue
, livere
, isso
, hashover
等。
经过对比,我选择了使用waline
评论系统,官网为 https://waline.js.org 。
4、GitHub的访问速度 众所周知,国内访问GitHub Pages
的速度非常慢,有时甚至无法访问。
不过目前已经有别的网站托管服务商可以使用,比如优秀的Vercel
,可以看这篇介绍文章:《Vercel是什么神仙网站?》
主站可以使用Vercel
部署,而GitHub Pages
用来作为博客的备份。
二、博客迁移过程踩的坑 博客迁移过程主要参考了这篇文章:《WordPress迁移到Hexo填坑记录》 。
该文章将博客的评论迁移到valine
评论系统,我使用的是waline
,因为waline
是基于valine
开发的,所以迁移的过程大致相同。
1、博客文章的迁移 登录WordPress
后台,点击工具 - 导出 - 下载导出的文件 ,可以得到WordPress.2021-08-15.xml
文件,存放到hexo/source/
目录下。
使用命令安装文章转换工具:
1 npm install hexo-migrator-wordpress --save
使用以下命令将WordPress
文章转成Hexo
的文章:
1 hexo migrate wordpress WordPress.2021-08-15.xml --paragraph-fix --import-image --skipduplicate
转换后文章保存在hexo/source/_posts
目录下,由于我之前有一半文章是使用富文本编辑器写的,所以转换后Markdown
格式有点问题,因此我手动把所有文章都修整了一遍。
2、文章链接的修改 我的WordPress
文章的固定链接以前一直使用/%category%/%postname%.html
,感觉还是一开始考虑得不够周到,导致现在出现了一点麻烦。
当初使用WordPress
建站的时候,我觉得文章链接加上分类目录的话会更加清晰,能够方便分类。但是我大部分文章都会存放在多个分类里,而分类目录是WordPress
随机添加的。
而Hexo
的博客系统使用目录分类比较麻烦,考虑再三,长痛不如短痛,将Hexo
的链接去掉了分类目录,只保留了文章名。
这样会导致旧的链接跳进来后找不到文章,所以我先在WordPress
做了301
重定向,等搜索引擎更新链接后,再开放Hexo
博客。
3、评论的迁移 我的WordPress
里有1400
多条评论,这些评论必须要保留下来。
新的评论系统使用了waline
+MySQL
部署,所以迁移过程和上面的文章教程类似,用脚本将评论导入到MySQL
数据库即可。
但是我发现了那篇教程的脚本代码有bug
:有些评论的父级id赋值有误,会导致有些评论加载不出来。
比如A发表了一条评论,B评论了A,这时B的pid
和rid
都是A。 如果C又评论了B,那么C的pid
是B,但是rid
是A。rid
是评论的根id,如果设置错误的话,C的评论就无法显示出来。
这个bug
我已经修复了,文章后面会贴出代码。
4、文章浏览数的迁移 文章的浏览数也是必须保留的,所幸的是,waline
也自带了统计文章浏览数的功能。
和评论的迁移一样,也是将浏览数批量保存到MySQL
数据库。
5、数据库迁移代码 我使用了macOS
的MAMP
集成环境运行MySQL
,再使用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 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 from dataclasses import dataclassfrom datetime import datetime, timezone, timedeltaimport pymysql@dataclass class WPComment : post_id: int post_name: str post_type: str comment_ID: int comment_author: str comment_author_email: str comment_author_url: str comment_author_IP: str comment_date: str comment_content: str comment_agent: str comment_parent: int @dataclass class WPCounter : post_id: int post_name: str post_type: str post_views: int replace_post_name = { 'the-chatty-jokers' : 'math-and-magic-the-chatty-jokers' , 'dicing-with-destiny' : 'math-and-magic-dicing-with-destiny' , } class WPDatabase : def __init__ (self ): self.db = pymysql.connect( host='localhost' , user='root' , password='' , database='wordpress' , ) def get_comments (self ): sql_comment = ''' SELECT p.ID, p.post_name, p.post_type, c.comment_ID, c.comment_author, c.comment_author_email, c.comment_author_url, c.comment_author_IP, c.comment_date, c.comment_content, c.comment_agent, c.comment_parent FROM `wp_comments` AS c, `wp_posts` AS p WHERE c.comment_post_ID = p.ID AND c.comment_approved != "spam" ORDER BY c.comment_ID ASC; ''' cursor = self.db.cursor() cursor.execute(sql_comment) results = cursor.fetchall() comments = [] for row in results: wp_comment = WPComment(*row) wp_comment.comment_content = wp_comment.comment_content.replace("'" , "\\'" ) wp_comment.comment_author = wp_comment.comment_author.replace("'" , "\\'" ) if wp_comment.post_name in replace_post_name: wp_comment.post_name = replace_post_name[wp_comment.post_name] comments.append(wp_comment) return comments def get_counters (self ): sql_counter = ''' SELECT p.ID, p.post_name, p.post_type, c.meta_value FROM `wp_postmeta` AS c, `wp_posts` AS p WHERE c.meta_key = "post_views_count" AND p.post_status = "publish" AND c.post_id = p.ID ORDER BY c.post_id ASC; ''' cursor = self.db.cursor() cursor.execute(sql_counter) results = cursor.fetchall() counters = [] for row in results: wp_counter = WPCounter(*row) if wp_counter.post_name in replace_post_name: wp_counter.post_name = replace_post_name[wp_counter.post_name] wp_counter.post_views = int (wp_counter.post_views) counters.append(wp_counter) return counters class WalineDatabase : def __init__ (self ): self.db = pymysql.connect( host='localhost' , user='root' , password='' , database='waline' , ) self.wp_to_waline_id = {} self.wp_reply_id = {} self.wp_reply_nickname = {} def find_root_id (self, child_id ): parent_id = self.wp_reply_id[child_id] if parent_id == child_id: return parent_id else : return self.find_root_id(parent_id) def add_comments (self, wp_comments ): cursor = self.db.cursor() for wp_comment in wp_comments: pid = 'NULL' rid = 'NULL' comment = '' if wp_comment.comment_parent == 0 : comment = '<p>%s</p>\n' %(wp_comment.comment_content) self.wp_reply_id[wp_comment.comment_ID] = wp_comment.comment_ID else : pid = self.wp_to_waline_id[wp_comment.comment_parent] rid = self.wp_to_waline_id[self.find_root_id(wp_comment.comment_parent)] reply_name = self.wp_reply_nickname[wp_comment.comment_parent] comment = '<p><a class="at" href="#%s">@%s</a> , %s</p>\n' %(pid, reply_name, wp_comment.comment_content) self.wp_reply_id[wp_comment.comment_ID] = wp_comment.comment_parent url = '' if wp_comment.post_type == 'page' : url = '/%s/' %(wp_comment.post_name) else : url = '/%s.html' %(wp_comment.post_name) sql_comment = f''' INSERT INTO `wl_Comment` (`comment`, `insertedAt`, `ip`, `link`, `mail`, `nick`, `pid`, `rid`, `status`, `ua`, `url`) VALUES ('{comment} ', '{wp_comment.comment_date} ', '{wp_comment.comment_author_IP} ', '{wp_comment.comment_author_url} ', '{wp_comment.comment_author_email} ', '{wp_comment.comment_author} ', {pid} , {rid} , 'approved', '{wp_comment.comment_agent} ', '{url} '); ''' cursor.execute(sql_comment) self.db.commit() self.wp_to_waline_id[wp_comment.comment_ID] = cursor.lastrowid self.wp_reply_nickname[wp_comment.comment_ID] = wp_comment.comment_author print (wp_comment.comment_date, wp_comment.comment_author) def add_counters (self, wp_counters ): cursor = self.db.cursor() for wp_counter in wp_counters: if wp_counter.post_views == 0 : continue url = '' if wp_counter.post_type == 'page' : url = '/%s/' %(wp_counter.post_name) else : url = '/%s.html' %(wp_counter.post_name) sql_comment = f''' INSERT INTO `wl_Counter` (`time`, `url`) VALUES ({wp_counter.post_views} , '{url} '); ''' cursor.execute(sql_comment) self.db.commit() print (wp_counter.post_id, wp_counter.post_views) if __name__ == '__main__' : wp_database = WPDatabase() waline_database = WalineDatabase() wp_comments = wp_database.get_comments() waline_database.add_comments(wp_comments) wp_counters = wp_database.get_counters() waline_database.add_counters(wp_counters)