话说一下狠心将自己的博客,从WordPress转到了Hugo,在此之前,只备份了网站的数据库文件和从后台导出的xml文件,也没想着要先在WordPress装插件把文章转为Markdown。重装VPS后才发现后悔已经晚了……主要是不想那么麻烦,重新部署一套环境导入数据,再通过插件导出。

背景

平常为了方便多端记录,一直都是在OneNote里写,等整理好了发布到博客上,没有Markdown文件。那就得想办法把导出的xml文件转换成markdown。

网上搜了许多,这里列两个搜到的文章中提及较多的:

  • 找到一个python写的ExitWP工具,下载到本地一看还是python2的代码,据说是有人改成了python3的,但是没找到,还是不用了。
  • 找到一个是nodejs下的wordpress-to-markdown工具,但是转换出来的不仅仅是文章,还有其他的,感觉好乱,而且转换后的markdown文件,我还得手工去修改Front Matter,还是算了吧。

没办法,那就自己整!

需求

  1. 批量转换,只转换post_typepost,忽略attachmentpagenav_menu_item
  2. 不仅仅要文章标题和内容,还需要文章的发布日期、修改日期、别名(post_name)、分类、标签等
  3. 文章markdown文件中的Front Matter部分需要按照自己的模板自动生成,避免手工修改
  4. 文件名命名为发布日期+文章标题的格式

开整

解析xml文件首先想到的是自己常用语言python下有XML标准库。到python文档中大致了解了一下,然后开整,遇到问题搜索吧。

解析xml文件

WordPress导出的xml文件,主体内容是在<item><\item>标签中,首先将这部分内容提取出来。

from xml.dom.minidom import parse
    with parse('wordpress.xml') as dom:
        elements = dom.documentElement
        items = elements.getElementsByTagName('item')

判断wp:post_type标签的内容是不是post,筛选出文章。对文章提取所需要的信息。这里为了方便后续数据的管理,我将单篇文章的信息构造成字典。

if item.getElementsByTagName('wp:post_type')[0].childNodes[0].data == 'post':
    post = {
        "title": item.getElementsByTagName('title')[0].childNodes[0].data,
        "slug": item.getElementsByTagName('wp:post_name')[0].childNodes[0].data,
        "date": item.getElementsByTagName('wp:post_date')[0].childNodes[0].data,
        "lastmod": item.getElementsByTagName('wp:post_modified')[0].childNodes[0].data,
        "content": item.getElementsByTagName('content:encoded')[0].childNodes[0].data,
        "categories": [],
        "tags": []
    }

这里将默认的categoriestags键赋值为空列表,主要是因为WordPress导出的xml中分类和标签信息用的都是<category>来存储,通过domain属性来区分,而且分类和标签可能会有多个值,所以这里需要单独处理一下。

if item.getElementsByTagName('category'):
    for cat in item.getElementsByTagName('category'):
        if cat.getAttribute("domain") == 'category':
            post['categories'].append(cat.childNodes[0].data)
        elif cat.getAttribute("domain") == 'post_tag':
            post['tags'].append(cat.childNodes[0].data)

以上代码就把单篇文章的信息解析出来存储到了字典中,也可以将所有文章的字典写入到列表中,方便后续的批量操作。

构造内容模板

根据自己的Front Matter构造写入内容,可以根据自己的需要修改。

text = f"""---
title: "{post['title']}"
slug: "{post['slug']}"
date: {post['date'].replace(' ', 'T') + '+08:00'}
lastmod: {post['lastmod'].replace(' ', 'T') + '+08:00'}
keywords: ""
description: ""\n
categories: {str(post['categories']).replace("'", '"')}
tags: {str(post['tags']).replace("'", '"')}
featuredImage: ""
toc: false
---\n
{post['content']}"""

这里对于分类和标签的再次加工是强迫症的需求,python中字符串列表中的字符串是单引号,比如['AAA', 'BBB', 'CCC'],写入文件后也是这种样子,与Front Matter中其他字段都是""有些格格不入,所以这里将单引号替换成双引号,最终输出为["AAA", "BBB", "CCC"]

写入文件

调整好格式后,接下来就是写入markdown文件,先按照需求生成文件名。

filename = f"{post['date'][:10].replace('-', '')}-{post['title']}.md"

再结合前面的文章信息列表,来个循环,搞定!

for post in posts:
    with open(os.path.join(path, filename), encoding='utf-8', mode='a') as f:
        f.write(text)

收工

本次折腾基本满足了我的需求,目前还有些不够完美的地方,就是文章中的链接、图片地址没有做处理,考虑到我的博客的文章中配图较少,发布前稍作手工调整就好。

不知道朋友们有没有碰到跟我类似的情况,这里将代码重构了一下,完善了代码的注释和函数的参数说明,给有需要的朋友,不满足需求的,也可以自行修改。

完整代码如下:

from xml.dom.minidom import parse
import os


class XmlToMarkdown:
    """一个xml转markdown的小工具

       解析wordpress导出的xml文件, 并将其中的文章批量转换成markdown文件

    Args
    ----
    file : str
        指定从wordpress导出的xml文件  
    """

    def __init__(self, file: str) -> None:
        self.file = file

    def _read_wordpress_xml(self) -> list:
        """解析wordpress导出的xml文件, 返回解析出的数据
        """
        posts = []
        
        # 解析 wordpress 导出的 xml 文件,并将内容写入字典
        with parse(self.file) as dom:
            elements = dom.documentElement
            items = elements.getElementsByTagName('item')
            for item in items:

                # 只解析 post_type 为 post 节点,即只解析文章,过滤掉附件、菜单等
                if item.getElementsByTagName('wp:post_type')[0].childNodes[0].data == 'post':
                    try:
                        # 单篇文章的内容
                        post = {
                            "title": item.getElementsByTagName('title')[0].childNodes[0].data,
                            "slug": item.getElementsByTagName('wp:post_name')[0].childNodes[0].data,
                            "date": item.getElementsByTagName('wp:post_date')[0].childNodes[0].data,
                            "lastmod": item.getElementsByTagName('wp:post_modified')[0].childNodes[0].data,
                            "content": item.getElementsByTagName('content:encoded')[0].childNodes[0].data,
                            "categories": [],
                            "tags": []
                        }
                        # 分类和标签的 Tag 都是 category, 而且可能有多条,此处做一个循环和判断
                        if item.getElementsByTagName('category'):
                            for cat in item.getElementsByTagName('category'):
                                if cat.getAttribute("domain") == 'category':
                                    post['categories'].append(cat.childNodes[0].data)
                                elif cat.getAttribute("domain") == 'post_tag':
                                    post['tags'].append(cat.childNodes[0].data)
                        # 单篇文章字典追加到文章列表中
                        posts.append(post)
                    except:
                        pass
  
        print(f"一共解析了{len(posts)}篇文章")

        return posts

    def to_md(self, path: str = None) -> None:
        """将解析出的数据写入markdown文件, 命名为`文章日期+文章名称.md`

        输出到指定位置或当前目录下的`output`文件夹中

        Args
        ----
        path : str
            指定输出文件的位置, 不指定默认为当前所在的目录
        """

        data = self._read_wordpress_xml()
        # 不指定 path, 则默认为当前文件所在的目录
        if path:
            path = path + "\\output"
        else:
            path = os.getcwd() + "\\output"
  
        # 创建 output 文件夹
        if not os.path.exists(path):
            os.mkdir(path)

        for post in data:
            # 定义 markdown 文件名
            filename = f"{post['date'][:10].replace('-', '')}-{post['title']}.md"

            with open(os.path.join(path, filename), encoding='utf-8', mode='a') as f:
                # 根据自己的文章模板构造写入内容
                text = f"""---
title: "{post['title']}"
slug: "{post['slug']}"
date: {post['date'].replace(' ', 'T') + '+08:00'}
lastmod: {post['lastmod'].replace(' ', 'T') + '+08:00'}
keywords: ""
description: ""\n
categories: {str(post['categories']).replace("'", '"')}
tags: {str(post['tags']).replace("'", '"')}
featuredImage: ""
toc: false
---\n
{post['content']}"""

                f.write(text)

        print('转换完成!')


if __name__ == '__main__':

    file = 'WordPress.xml'

    tool = XmlToMarkdown(file)
    tool.to_md()

查看:GitHub

标签: Python, WordPress

已有 2 条评论

  1. 我还是喜欢wordpress

    1. wordpress是主流,但搬瓦工的小主机实在是伤不起,主要是平时折腾也比较少,先这样吧,说不定哪一天又搬回去了😂

添加新评论