多说难民的畅言搬家过程[附wordpress评论转换畅言json格式脚本]

本博客在很早以前就启用了多说社交评论功能,这么多年虽然不能说很完美吧,但在同类产品中多说已经算是做的相当不错的了。但从大概这周1开始,多说后台开始出现大量垃圾评论,其量之大真是这些年从来没有过的!(之前虽然也有垃圾评论,但也就极偶尔的1-2条而已,而且看了下网站访问统计,也没有大量的PV,明显是直接刷的多说评论接口),最后实在无法忍受,开启了所有评论的审核机制,即使这样,后台还是会定时出现大量的新垃圾评论。去多说官方论坛看了下,发现也没人提,官方也没反应,而且看论坛上的消息貌似是已经放弃治疗了,果不其然在前天,多说官方放出了即将关闭多说项目的通知:http://dev.duoshuo.com/threads/58d1169ae293b89a20c57241。其实之前也曾一度想从多说转移到其他的社交评论系统,但几番对比后,发现多说还是有较绝对的优势的,这回却是不得不搬家了!

大致扫了一眼wordpress的社会化评论系统,国内的友言、灯鹭什么的看起来也半死不活的(查灯鹭时看到个貌似是插件作者自己搞的新版本叫”Wordpress连接微博”:http://blogqun.com/doc/wp-connect.html,看着是积极开发的状态,功能也比较丰富,但就是要至少199RMB的使用费,而且看了下前端UI不是我喜欢的风格,后端又有诸多要求,于是就不作考虑了),剩下的就是搜狐的畅言和美帝的disqus,本来想直接disqus得了,功能和前端样式都是我喜欢的类型(本地评论也能顺利导入,虽然丢了头像等信息),但遗憾的是一方面近来disqus被墙的死死的,更重要的是另一方面disqus连接的社交系统都是fb,twitter等,也许以后哪天我决定写全英文博客时才更适合用吧…于是,也排除掉了!
所以,基本上,转移阵地的目标就是搜狐畅言了,看了下畅言的更新频率也还可以,而且功能也算丰富,尤其喜欢那个小印章功能!注册畅言账号后,问题就来了:怎么把现有的评论内容全部迁移过去呢?
wp后台装上畅言的插件后,倒是有同步本地评论的功能,满心欢喜的点了一下,然后插件也痛快的给我报了个错:

同步失败:import to changyan error!

再看畅言后台上的数据导入部分,选项倒是不少,畅言、友言、多说的json数据都可以(官方说明有个很奇葩的“1、从畅言导出的评论格式不支持直接导入畅言”,不明觉厉…),看到有多说,于是直接从多说后台导出一份“包含文章数据+包含评论数据”的zip,上传畅言,又爽快的给我报了个忘记什么内容的错!不过这是刚开始被垃圾评论刷屏的时候试的,后来多说放出要关项目消息之后,再上友言后台,发现选多说json时直接弹出了个小提示框(呵呵,反应够快的),解释了下导入要点(看了以后还是没明白那个二级域名是干啥的!),然后又试了下导入多说导出的json,好,这次不再爽快的报错了,看了下貌似也导入成功了,到后台按提示找到导入的评论一看,我勒个去的!丢了不少评论不说,评论内的回复引用关系也没导入成功,于是果断删除刚导入的“脏数据”,自己动手写个转换工具吧。
仔细看了下畅言文档中的json数据格式(这点还是要给个赞的,文档比较全面规范),决定不从多说数据入手,而改用wordpress本身的导出数据xml(当然,在这之前需要把多说的评论同步回本地,默认情况下多说是会自动回写wp本地评论的),经过几次试验,写了个基本能用的python脚本,不能说多完美转换吧,反正该有的用户名、评论内容、回复引用等都有了。
脚本如下(python 3.x版):

import sys
import time
import re
import xml.etree.cElementTree as ET

# 处理中文为转义\u格式并替换可能存在的双引号
def unicode_escape(raw_string):
    return str(raw_string.encode('unicode_escape')).replace("\\\\", "\\")[2:-1].replace("\"", "\\\"")

# 清除所有html标签
def clean_html(raw_html):
    cleanr = re.compile('<.*?>')
    cleantext = re.sub(cleanr, '', raw_html)
    #return unicode_escape(cleantext.replace('\n', '').replace('\r', ''))
    return unicode_escape(cleantext)

# 获取wordpress导出xml中的ns
def parse_and_get_ns(file):
    events = "start", "start-ns"
    root = None
    ns = {}
    for event, elem in ET.iterparse(file, events):
        if event == "start-ns":
            if elem[0] in ns and ns[elem[0]] != elem[1]:
                # NOTE: It is perfectly valid to have the same prefix refer
                #     to different URI namespaces in different parts of the
                #     document. This exception serves as a reminder that this
                #     solution is not robust.    Use at your own peril.
                raise KeyError("Duplicate prefix with different URI found.")
            ns[elem[0]] = "{%s}" % elem[1]
        elif event == "start":
            if root is None:
                root = elem
    return ET.ElementTree(root), ns

# 默认源xml
wp_xml = "D:\\blogc\\k-resblog.wordpress.2017-03-22.xml"

# 可能存在的第一参数作为源
if len(sys.argv)>2 :
    wp_xml = sys.argv[1]

# print重定向为输出json文件
sys.stdout = open(wp_xml+".json", "w")

#tree = ET.ElementTree(file=wp_xml)
tree, ns = parse_and_get_ns(wp_xml)

root = tree.getroot()

wp_users = []
# 评论id起始偏移,可供需要修复错误时重复导入用
cmt_offset = 1000
# 用户唯一id起始偏移
userid_offset = 1000

# 遍历所有有评论的post挨个处理
for elem in root.iter(tag="item"):
    post_type = elem.find(ns["wp"]+"post_type")
    post_comments = elem.findall(ns["wp"]+"comment")
    if len(post_comments)>0 and post_type.text=="post" or post_type.text=="page":
        print("{\"title\":\""+unicode_escape(elem.find("title").text)+"\",", end="")
        print("\"url\":\""+elem.find("link").text+"\",", end="")
        # 这个时间畅言貌似并没有处理时区问题,也许应该用GMT+8的本地时间
        pub_date_gmt_str = elem.find(ns["wp"]+"post_date_gmt").text
        pub_timestamp = time.mktime(time.strptime(pub_date_gmt_str, '%Y-%m-%d %H:%M:%S'))
        print("\"ttime\":\""+str(int(pub_timestamp*1000))+"\",", end="")
        print("\"sourceid\":\""+elem.find(ns["wp"]+"post_id").text+"\",", end="")
        print("\"parentid\":\"0\",\"categoryid\":\"\",\"ownerid\":\"\",\"metadata\":\"\",", end="")
        print("\"comments\":[", end="")
        for post_comm in post_comments:
            cmt_id = int(post_comm.find(ns["wp"]+"comment_id").text)+cmt_offset
            print("{\"cmtid\":\""+str(cmt_id)+"\",", end="")
            cmt_time_str = post_comm.find(ns["wp"]+"comment_date_gmt").text
            cmt_time = time.mktime(time.strptime(cmt_time_str, '%Y-%m-%d %H:%M:%S'))
            print("\"ctime\":\""+str(int(cmt_time*1000))+"\",", end="")
            print("\"content\":\""+clean_html(post_comm.find(ns["wp"]+"comment_content").text)+"\",", end="")
            cmt_id = int(post_comm.find(ns["wp"]+"comment_parent").text)+cmt_offset
            print("\"replyid\":\""+str(cmt_id)+"\",", end="")

            cmt_author = post_comm.find(ns["wp"]+"comment_author").text
            if cmt_author==None:
                cmt_author = "本博客网友"
            try:
                cmt_userid = wp_users.index(cmt_author)+userid_offset
            except ValueError:
                cmt_userid = len(wp_users)+userid_offset
                wp_users.append(cmt_author)
            
            print("\"user\":{\"userid\":\""+str(cmt_userid)+"\",", end="")
            print("\"nickname\":\""+unicode_escape(cmt_author)+"\",", end="")
            cmt_author_url = post_comm.find(ns["wp"]+"comment_author_url").text
            if None==cmt_author_url:
                cmt_author_url = ""
            print("\"userurl\":\""+cmt_author_url+"\"", end="")
            cmt_author_email = post_comm.find(ns["wp"]+"comment_author_email").text
            if None!=cmt_author_email:
                print(",\"usermetadata\":{\"email\":\""+cmt_author_email+"\"}", end="")
            print("},", end="")
            print("\"ip\":\""+post_comm.find(ns["wp"]+"comment_author_IP").text+"\",", end="")
            print("\"channeltype\":\"1\"},", end="")
        print("]}")
        sys.stdout.flush()

用法是先在wp后台的工具->导出->所有内容,如下:

拿到xml文件,然后改写脚本里的xml路径也好,直接拖到.py上运行也好,就能得到同名的畅言格式的.json文件,然后直接在畅言后台上导入畅言json格式评论即可(如果文件直接传文件过大的话就zip一下,反正我的乡下小博也就生成了几百k的json文件…)。
最后,由于畅言不像多说会默认回写wp本地评论,虽然有手动同步和实验室定时同步,但想想之前同步本地到畅言时报那个错…还是算了吧,我可不想我宝贵的wp本地评论里出现什么脏数据!但这样文章下面的评论数显示就又不正确了,看了下畅言文档,发现有js的获取畅言评论数说明,于是小改了一下,变成现在这样先显示本地评论数,再显示畅言评论数了。等什么时候畅言官方把这正反同步都完善了再改回去好了。

博主友情提示:

如您在评论中需要提及如QQ号、电子邮件地址或其他隐私敏感信息,欢迎使用>>博主专用加密工具v3<<处理后发布,原文只有博主可以看到。