迁移思路

2019年搭建了Hexo博客,考虑到稳定性,使用了无需服务器和数据库的LiveRe,LiveRe评论系统使用了3年多,总体来说速度和稳定性都还不错,借着Butterfly升级,相中了Waline,以及希望后续对评论数据的保留。回顾当时的搭建记录
http://blog.darler.cn/livere/

迁移方法结合LanternD’s Castle教程 把LiveRe评论迁移到Disqus ,先转化为Disqus数据,再使用Waline提供的 数据迁移助手 实现迁移。

迁移LiveRe数据

进入LiveRe后台

首先我们要把评论弄到本地。由于LiveRe没有导出功能,所以我们只能手动导出数据了。理论上这一步爬虫也能做,但是我这评论不多,但也有20多页,于是就直接开始吧。

去到LiveRe的后台(Insight),修改查询的起止时间,囊括完所有的评论。下图顶部区域有个开始使用LiveRe的时间。把起始时间设到它之前。

这一步必须把LiveRe的语言切换到英文,否则转换时会出错

img

之后我们就得到了一个列表,每页有10条评论。

保存评论的数据

接下来就是把这所有的评论扒下来了。如果对爬虫熟悉的话可能可以写个脚本批量弄下来,原作者因为评论不多,所以就手动搞定了。

在Chrome里面(别的浏览器看情况)在Table上面右键,选Inspect,打开DevTools。

img

在Table元素上面右键,Copy Element。

去到代码对应的文件,里面有一个「html_table」文件夹(里面有个示例「table_1.html」)。新建一个HTML文件,把复制的Element粘进去。注意,每个块都是 <table> 开始, </table> 结束。

重复上述过程下载完所有的Table。可以每10条评论一个文件,也可以所有评论放到一个文件里面。

运行脚本

使用LanternD’s Castle的代码弄下来:comment-migration-from-livere-to-disqus - Github ,Clone或者下载Zip。

如果你使用WIndows系统,并且没有安装Python,请到 Python官网 下载Python3,选择Windows installer (64-bit)。

打开 migration_main.py ,,把最下面的func_switch 改成1

如果你使用WIndows系统,请使用修改后的这段代码,否则会出现gbk字符问题。

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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
import json
import os
from html.parser import HTMLParser
from pathlib import Path

json_fp = "./livere_comments.json"


class LiveReTableParser(HTMLParser):
def __init__(self):
super(LiveReTableParser, self).__init__()
self.dict_template = {
'post_title': '',
'post_link': '',
'thread_id': '',
'post_date': '',
'avartar': '',
'comment_id': '',
'comment_group_id': '',
'comment_author': '',
'comment_date': '',
'comment_content': '',
}
self.field_map = {
'reply_seq': 'comment_id',
'member_group_seq': 'comment_group_id',
'member_name': 'comment_author',
'member_icon': 'avartar'
}
self.d_buf = None
self.is_date = False
self.is_title = False
self.is_content = False

def handle_starttag(self, tag, attrs):
# State machine
if tag == 'tr' and len(attrs) >= 6:
# New comment
# print(attrs)
self.d_buf = self.dict_template.copy()
for f in attrs:
# print(f)
if f[0] in self.field_map.keys():
self.d_buf[self.field_map[f[0]]] = f[1]
# print(self.d_buf)

elif tag == 'td' and 'table-content-regdate table-padding' in [
at[1] for at in attrs
]:
self.is_date = True
elif tag == 'td' and 'table-content-title table-padding' in [
at[1] for at in attrs
]:
self.is_title = True

elif tag == 'a' and self.is_title:
link = attrs[0][1]
thread_id = link.split('/')[3]
self.d_buf['thread_id'] = thread_id
self.d_buf['post_link'] = link.split('#')[0]

elif tag == 'p' and ('class', 'content-text') in attrs:
self.is_content = True

elif tag == 'br':
# second part of date
self.is_date = True

def handle_data(self, data):
# the date must be in 'YYYY-MM-DD HH:MM:SS' 24-hour format.
if self.is_date:
# print(data)
if '/' in data:
date_list = data.split('/')
self.d_buf['comment_date'] = '-'.join(date_list[::-1])
if 'M' in data:
# AM or PM. "PM 12:09 --> 12:09:00"
time_list = data.split(' ')
hms_list = time_list[1].split(':')
hms_list = [int(x) for x in hms_list]
if time_list[0] == 'PM':
if hms_list[0] != 12:
hms_list[0] += 12
self.d_buf['comment_date'] += ' {0:02d}:{1:02d}:33'.format(
hms_list[0], hms_list[1])
self.is_date = False

elif self.is_title:
self.d_buf['post_title'] = data.split('|')[0][:-1]
self.is_title = False
elif self.is_content:
# print(data)
self.d_buf['comment_content'] = data
self.is_content = False

def handle_endtag(self, tag):
if tag == 'tr' and self.d_buf is not None:
# one row is finished
print("1 comment processed.")
print(self.d_buf)

# Write to file.
if Path(json_fp).exists():
print('Append to an existing file.')
with open(json_fp, encoding='utf-8') as jf:
data_obj = json.load(jf)
data_obj.append(self.d_buf)
with open(json_fp, 'w', encoding='utf-8') as jf:
json.dump(data_obj, jf, indent=4, ensure_ascii=False)
else:
# Create file
print('Create a new file.')
with open(json_fp, 'w', encoding='utf-8') as jf:
json.dump([self.d_buf], jf, indent=4, ensure_ascii=False)
return


class CommentMigrator(object):
def __init__(self):
self.html_tab_dir = "./html_tables/"
self.disqus_xml_fp = './disqus_import.xml'

def html_to_json(self):
"""Required fields:
- Post title
- Post link
- thread identifier (post url without the site link)
- post_date
- comment status: "open" by default
- Each comment:
- id: int string
- author
- email (can be empty)
- author url (can be empty)
- author IP (make up one)
- date_gmt: comment timestamp
- content
- approved: 1 by default
- parent: the parent comment_id (can be empty)
"""
f_list = self.get_filename_list_by_ext('./html_tables/', 'html')
print(f_list)
# return # SAFETY

for ftab in f_list:
my_html_parser = LiveReTableParser()
with open(ftab, 'r', encoding='utf-8') as f_tab:
all_txt = ''
for l in f_tab:
all_txt += l
my_html_parser.feed(all_txt)
f_tab.close()

def get_filename_list_by_ext(self, file_path, f_extension):
# This one only gets the current log.
print(file_path)
f_list = []
for root, dirs, files in os.walk(file_path):
for file in files:
if file.endswith(f_extension):
f_list.append(os.path.join(root, file))
return f_list

def json_to_disqus_xml(self):
print("Convert Json to XML pipeline.")
if Path(json_fp).exists():
with open(json_fp, encoding='utf-8') as jf:
cmt_list = json.load(jf)
else:
print(
'json file not exists. Please run the \'html to json\' pipeline first. '
)
header_str = '''<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dsq="http://www.disqus.com/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:wp="http://wordpress.org/export/1.0/"
>
<channel>
'''
ending_str = '''
</channel>
</rss>
'''
formatted_xml_str = header_str
for cmt in cmt_list:
cmt_element = '''
<item>
<title>{0}</title>
<link>{1}</link>
<content:encoded><![CDATA[{2}]]></content:encoded>
<dsq:thread_identifier>{3}</dsq:thread_identifier>
<wp:comment_status>open</wp:comment_status>
<wp:comment>
<wp:comment_id>{4}</wp:comment_id>
<wp:comment_author>{5}</wp:comment_author>
<wp:comment_author_email></wp:comment_author_email>
<wp:comment_author_url></wp:comment_author_url>
<wp:comment_author_IP>127.0.0.2</wp:comment_author_IP>
<wp:comment_date_gmt>{6}</wp:comment_date_gmt>
<wp:comment_content><![CDATA[{2}]]></wp:comment_content>
<wp:comment_approved>1</wp:comment_approved>
<wp:comment_parent></wp:comment_parent>
</wp:comment>
</item>'''.format(cmt['post_title'], cmt['post_link'],
cmt['comment_content'], cmt['thread_id'],
cmt['comment_id'], cmt['comment_author'],
cmt['comment_date'])
formatted_xml_str += cmt_element
formatted_xml_str += ending_str

# Write to file
with open(self.disqus_xml_fp, 'w', encoding='utf-8') as f_xml:
f_xml.writelines(formatted_xml_str)

print("Convertion done --> disqus_import.xml")


if __name__ == "__main__":
my_cm = CommentMigrator()
func_switch = 1
if func_switch == 1:
my_cm.html_to_json()
elif func_switch == 2:
my_cm.json_to_disqus_xml()

MacOS

1
python3 migration_main.py

Windows

1
python migration_main.py

里面有个简单的状态机处理HTML文件,把关键的field提取出来,然后存到一个 .json 文件里面。每条评论对应json的一个Element。

弄完以后打开 livere_comments.json 检查里面有没有问题,粗略看看就好了。

转化为Disqus格式

migration_main.pyfunc_switch 改成2。

再跑一次就可以得到用于导入的XML文件了 disqus_import.xml 。打开文件检查一下。目录里有个 disqus_official_example.xml ,可以对比一下整体有没有异常。

原作者在每条评论前面都加上了标记「【LiveRe】」,不喜欢的话可以在这一行删掉,我给出的代码已将这行删除

到Disqus导入数据

Disqus后台 -=> Moderate Comments -=> (左侧)Import。新建一个Dev站点用来测试。

img

选WordPress那个,上传生成的 disqus_import.xml 就好了。

上传之后到站点查看数据到Moderate Comments -=> (左侧)Discussions -=> ,Created显示Just now属于正常现象。

不足之处

目前程序没法记录评论树,也就是A回复了B,但是A/B各自是独立的评论。这个也没办法,因为LiveRe的输出里就不包含这个。想手动添加的话可以在 <wp:comment_parent></wp:comment_parent> 这行添加父评论的 comment_id

目前没法添加头像,应该是下面这个field需要ID和头像链接同时完整,对应Disqus内部的用户账号才行。

邮箱是留空的(Disqus在导入的时候会分配一个)。

IP地址都改成「127.0.0.2」了,因为LiveRe也不提供这个。


导出到Waline

从Disqus导出评论

Disqus后台 -=> Moderate Comments -=> (左侧)Export -=> 选择刚刚新建的站点 -=> Export Comments

Disqus将会发送一封邮件把备份发送到你邮箱,下载.xml.gz压缩包,解压后得到xml格式的数据。

Waline迁移助手

用代码编辑软件,例如VS Code打开Disqus评论的xml文件,复制里面的内容到Waline提供的 数据迁移助手

从 Disqus 迁移至 Waline LeanCloud 存储服务(取决于你把数据库部署在什么位置),点击转换。下载后将文件名改为 Comment.json

到这里数据已经转化为Waline格式了,后面的操作取决于你部署在哪种数据库上,如果你不是使用Leancloud,请参考Waline的多数据库服务支持教程。

Leancloud导入数据

到你的Leancloud应用 -=> 数据存储 -=> 导入导出 -=> 选择Comment.json -=> 导入。导入后到 数据存储 -=> 结构化数据 中可查看。

以上就是我结合LanternD’s Castle教程,做的从LiveRe导出评论到Waline的流程了,尽管是个小众需求,但对于目前使用LiveRe的博主,也许日后会有需求。希望这篇文章有帮助,如果不好使或者有问题欢迎留言。