SCHeng

It all returns to nothing.

python

关于opml,使用lxml解析opml文件小记

scheng

首先来介绍一下什么是opml文件。

了解到这种文件主要是因为eaf-rss-reader的一个issue。一般来说像feedly,Inoreader这种的rss-reader是提供了引入/导出opml文件的功能的。opml文件将你的feed链接都收集到一个文件里,方便将所有的feed订阅转移到其他的阅读器上。

opml文件其实就是一个xml文件,使用能够解析xml文件的一些库就能将它解析了。但是比较怪的是,feedly网站上居然无法解析由Inoreader生成的xml文件…

对python来说,解析xml通常会用xml库或者lxml库,我觉得lxml操作起来更加方便,于是就用lxml了。值得注意的一个地方是,xml文件里面有默认的几个特殊字符 < > & ‘ “ ,如果需要被提取的地方出现了这几个字符,那么就会报一个出现特殊字符的错误:lxml.etree.XMLSyntaxError: xmlParseEntityRef: no name,这个问题是由Emacs China论坛的用户提出的。

导入解析opml

接下來介绍一下解析过程。

  1. 首先需要一个文件输入,就是将opml文件的位置传入到程序里。

  2. 获取到文件,开始解析。将opml文件转换成一个feeds_list,里面记录feed链接。

  3. 逐个将链接解析,添加到本地rss库中。

第1步没有什么好展开讲的,现在在mini-buffer里面直接输入地址还不能补全,也没有检查路径是否存在,所以说实现方案还不够友好,需要再改进一下。

解析过程最重要的步骤也就是第2步了,这里贴出我的代码:

def handle_import_opml(self, opml_file):
        message_to_emacs("Importing...")
        parser = etree.XMLParser(encoding = "utf-8", recover = True)
        tree = etree.parse(opml_file, parser = parser)
        feeds = tree.xpath('/opml/body/outline')
        feeds_list = []
        feeds_list = tree.xpath('/opml/body/outline/@xmlUrl')

        # The above method may not be able to obtain xmlUrl.
        for feed in feeds:
            for rss in feed.iterchildren():
                feeds_list.append(rss.get('xmlUrl'))
        feeds_list = sorted(set(feeds_list), key = feeds_list.index)

        # unique feed
        for index, feed in enumerate(feeds_list):
            if feed in self.main_item.feedlink_list:
                feeds_list.pop(index)
                message_to_emacs("Feedlink '{}' exists.".format(feed))

        self.import_opml_thread(feeds_list)

第三行是定义了一个解析器,因为要解决上面提到的特殊字符的问题,所以引入了recover=True这个参数,作用是当碰到特殊字符时直接把他们覆盖掉(用空格)。因为这里并不取到特殊字符所在的那一部分内容,所以是可以直接覆盖掉的。

第四行使用上面定义的解析器解析xml文件。之后用xpath先读到outline所在位置,这里其实是一个temp。直接用xpath读到xmlUrl标签,记录在feeds_list中,但是有一些网站提供的opml又会读不到。就需要对上面生成的那个temp再操作一次,这个temp主要是为了解决多层outline包裹的问题(比如feedly如果定义了分类那么就会多层包裹)。

10~12是对多层包裹的处理,考虑到这里实际上是进行了两次解析,可能会生成重复的feeds,于是在13进行一个去重。16~18是与本地feeds进行对比,然后去重,先去重再抓取,提高效率。

第3步,我最初想的是单个调用原先写的多线程处理抓取的方法,但是这样实现起来会难以控制抓取结果,feed的index也会出现问题(标号index的操作比抓取线程快很多,就会导致index出错)。于是就重新为引入opml重新写了一个独立的线程,不过我觉得这样其实并不优雅。

导出opml

导出opml比引入opml实现起来要简单些。

因为直接使用python的xml库生成的xml文件是没有排版的,整个文件就一行,十分地丑,于是我就写了一个美化排版的递归函数:

 def beautify_opml(self, element, indent, newline, level = 0):
        if element:
            if element.text == None or element.text.isspace():
                element.text = newline + indent * (level + 1)
            else:
                element.text = newline + indent * (level + 1) + element.text.strip() + newline + indent * (level + 1)     
        temp = list(element)

        for subelement in temp:
            if temp.index(subelement) < (len(temp) - 1):     
                subelement.tail = newline + indent * (level + 1)    
            else:
                subelement.tail = newline + indent * level   
            self.beautify_opml(subelement, indent, newline, level = level + 1)

主要思路就是根据层级关系来判断是否需要加缩进以及换行,然后对子树节点再进行递归操作。

接下来是生成函数:

    def generate_opml(self):
        message_to_emacs("Exporting...")
        root = Element('opml')
        root.set('version', '1.0')
        head = SubElement(root, 'head')
        title = SubElement(head, 'title')
        user_name = get_emacs_var("user-login-name")
        title.text = "Feeds of {} from eaf-rss-reader".format(user_name)
        body = SubElement(root, 'body')

        for item in self.rss_list:
            outline = SubElement(body, 'outline')
            outline.set('type', 'rss')
            outline.set('text', item['feed_title'])
            outline.set('title', item['feed_title'])
            outline.set('xmlUrl', item['feed_link'])
            if 'link' in item:
                link = item['link']
            else:
                try:
                    link = feedparser.parse(item['feed_link']).feed.link
                except AttributeError:
                    link = item['feed_link']
                message_to_emacs("Failed to export {} {}, please try again later.".format(
                    item['feed_title']
                    ,item['feed_link']
                ))
            outline.set('htmlUrl', link)

        tree = ElementTree(root)
        root = tree.getroot()
        self.beautify_opml(root, '\t', '\n')
        file_location = os.path.join(self.location,
        'eaf-rss-reader-' + time.strftime("%Y-%m-%d-%H%M%S", time.localtime()) + '.opml')
        tree.write(file_location, encoding = 'utf-8', xml_declaration=True)
        message_to_emacs("All feeds have been exported. Location is {}".format(file_location))

生成下一层子节点的方法是SubElement(),eaf-rss-reader的opml文件的层级结构大致是这样的:opml -> head / body -> outline1/outline2/…,某个feed的信息储存在outline的标签参数里面。11~28生成outline,主要包括了feed的title,feed的链接,feed的原链接(网页链接)这三个参数,因为最开始抓取feed的解析疏忽了,没有保存原链接地址,现在已经补上了,对于仍然没有原链接的那部分feed,会进行一个重新抓取,获取到原链接。抓取不到那就只能报错了。

30~36就是美化排版函数的调用和生成文件了,为了文件不重名,所以采用了时间戳命名的方法。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注