At the Forge - 聚合订阅源

作者:Reuven M. Lerner

在过去的几个月中,我们研究了 RSS 和 Atom,这两种基于 XML 的文件格式可以轻松创建和分发网站摘要。虽然这种被称为聚合的技术传统上与博客和新闻站点相关联,但人们对其在其他用途中的潜力越来越感兴趣。任何基于 Web 的信息源都可能是 RSS 或 Atom 的潜在有趣且有用的候选对象。

到目前为止,我们已经研究了人们可能为网站创建 RSS 和 Atom 订阅源的方式。当然,创建聚合订阅源只是等式的一半。同样重要,甚至可能更有用的是了解我们如何检索和使用来自我们自己站点和感兴趣的其他站点的聚合订阅源。

正如我们所见,存在三种不同类型的聚合订阅源:RSS 0.9x 及其更现代的版本 RSS 2.0;不兼容的 RSS 1.0;和 Atom。每个都大致做同样的事情,并且这些标准之间存在相当多的重叠。但是,当我们假设一切都足够好或足够接近时,网络协议无法很好地工作,聚合也不例外。如果我们想阅读所有聚合的站点,那么我们需要了解所有不同的协议以及这些协议的版本。例如,实际上有九个不同的 RSS 版本,当与 Atom 结合使用时,使我们总共有十种不同的站点可能正在使用的聚合格式。大多数差异可能可以忽略不计,但完全忽略它们或假设每个人都在使用最新版本是愚蠢的。理想情况下,我们应该有一个模块或工具,允许我们从各种不同的协议中检索订阅源,尽可能掩盖差异,同时仍然利用每个协议的独特功能。

本月,我们将介绍 Universal Feed Parser,这是 Mark Pilgrim 编写的针对此问题的开源解决方案。Pilgrim 是一位著名的博客作者和 Python 程序员,他也是参与创建 Atom 聚合格式的关键人物之一。考虑到他在编写 Universal Feed Parser 时所经历的痛苦,这应该不足为奇。它还处理 CDF,一种专有的 Microsoft 格式,用于发布活动桌面和软件更新等项目。这部分可能对 Linux 桌面用户不感兴趣,但它为安装了 Microsoft 系统的组织提出了有趣的可能性。Universal Feed Parser (feedparser),截至撰写本文时为 3.3 版本,似乎是同类产品中最好的工具,无论使用何种语言,也无论许可如何。

安装 feedparser

安装 feedparser 非常简单。下载最新版本,移动到其分发目录并键入python setup.py install。这将激活 Python 的标准安装实用程序,将 feedparser 放置在您的 Python site-packages 目录中。完成 feedparser 的安装后,您可以从 shell 窗口使用 Python 交互式地测试它

>>> import feedparser

>>> 符号是 Python 在交互模式下工作时的标准提示符。上面将 feedparser 模块导入到 Python 中。如果您未安装 feedparser,或者安装过程中出现问题,则执行此命令将导致 Python ImportError。

现在我们已将模块导入内存,让我们使用它来查看 Linux Journal 网站的最新新闻。我们输入

>>> ljfeed = feedparser.parse
↪("https://linuxjournal.cn/news.rss")

我们不必指示我们要求 feedparser 使用的订阅源的协议或版本——该软件包足够智能,可以自行确定此类版本控制,即使 RSS 订阅源未能识别其版本也是如此。在撰写本文时,LJ 网站由 PHPNuke 提供支持,并且该订阅源被明确标识为 RSS 0.91。

现在我们已经检索了一个新的订阅源,我们可以准确地找出我们收到了多少条目,这在很大程度上取决于服务器的配置

>>> len(ljfeed.entries)

当然,项目的数量不如项目本身有趣,我们可以通过一个简单的 for 循环来查看项目本身

>>> for entry in ljfeed.entries:
...     print entry['title']
...

记住缩进 print 语句,以告诉 Python 它是循环的一部分。如果您是 Python 新手,您可能会对以 ... 开头的行感到惊讶,这些行表示 Python 已准备就绪并正在等待 for 之后的输入。只需按 <Enter> 键即可结束 for 开始的块,您就可以看到最新的标题。

我们也可以变得更高级,使用 Python 的字符串插值来查看 URL 和标题的组合

>>> for entry in ljfeed.entries:
...     print '<a href="%s">%s</a>' % \
...     (entry['link'], entry['title'])

正如我上面指出的,feedparser 试图掩盖不同协议之间的差异,使我们能够像处理大致等效的内容一样处理所有聚合内容。因此,我可以对我的博客的聚合订阅源重复上述命令。我最近迁移到了 WordPress,它提供了一个 Atom 订阅源

>>> altneufeed = feedparser.parse(
... "http://altneuland.lerner.co.il/wp-atom.php")
>>> for entry in altneufeed.entries:
...     print '<a href="%s">%s</a>' % \
...     (entry.link, entry.title)

请注意,最后一个示例如何使用属性 entry.link 和 entry.title,而前一个示例使用字典键 entry['link'] 和 entry['title']。feedparser 尝试保持灵活性,为相同的信息提供多个界面,以适应不同的需求和风格。

新闻有多新?

新闻聚合器或使用 RSS 和 Atom 的其他应用程序的目的是收集和呈现新更新的信息。聚合器只能显示服务器提供的项目;如果 RSS 订阅源仅包含两个最新发布的项目,那么聚合器有责任轮询、缓存和显示那些不再被聚合和汇总的项目。

这提出了两个不同但相关的问题:我们如何确保我们的聚合器仅显示我们尚未见过的项目?我们的聚合器是否有一种方法可以减少博客服务器的负载,仅检索自上次访问以来发布的项目?回答第一个问题需要查看每个项目的修改日期(如果存在)。

截至撰写本文时,后一个问题已成为 Web 社区中日益流行的辩论问题。随着博客越来越受欢迎,订阅其聚合订阅源的人数也在增加。如果一个博客有 500 个订阅其聚合订阅源的订阅者,并且如果这些订阅者中的每个聚合器每小时查找更新,则意味着每小时对 Web 服务器发出额外的 500 个请求。如果聚合订阅源提供站点的全部内容,则可能会导致大量带宽浪费——缩短站点对其他访问者的响应时间,并可能迫使站点所有者为超出分配的每月带宽付费。

feedparser 允许我们通过提供一种仅在有新内容显示时才检索聚合订阅源的机制,对聚合服务器和我们自己表示友好。这是可能的,因为现代版本的 HTTP 允许请求客户端包含 If-Modified-Since 标头,后跟一个日期。如果请求的 URL 自请求中提及的日期以来已更改,则服务器会使用 URL 的内容进行响应。但是,如果请求的 URL 未更改,则服务器会返回 304 响应代码,指示先前下载的版本仍然是最新的内容。

我们通过将可选的 modified 参数传递给对 feedparser.parse() 的调用来实现此目的。此参数是 time 模块(Python 元组)定义的标准,其中前六个元素是年、月数、日数、小时、分钟和秒。最后三个项目与我们无关,可以留作零。因此,如果我有兴趣查看自 2004 年 9 月 1 日以来发布的订阅源,我可以这样说

last_retrieval = (2004, 9, 1, 0, 0, 0, 0, 0, 0)
ljfeed = feedparser.parse(
         "https://linuxjournal.cn/news.rss")

如果 Linux Journal 的服务器配置良好,则上面的代码要么导致 ljfeed 包含完整的聚合订阅源——以 HTTP OK 状态消息返回,数字代码为 200——要么指示订阅源自上次检索以来未更改,数字代码为 304。虽然跟踪您上次请求特定聚合订阅源的时间可能需要您进行更多记录,但这很重要,尤其是在您定期请求订阅源更新的情况下。否则,您可能会发现您的应用程序在某些站点不受欢迎。

使用订阅源

现在我们对如何使用 feedparser 有了一个基本的了解,让我们创建一个简单的聚合工具。此工具从名为 feeds.txt 的文件中获取输入,并以名为 feeds.html 的 HTML 文件形式生成其输出。每天通过 cron 运行此程序并查看生成的 HTML 文件,可以从您最感兴趣的站点提供一个粗略但可用的新闻订阅源。

Feeds.txt 包含实际订阅源的 URL,而不是我们想要从中获取订阅源的站点的 URL。换句话说,用户需要查找并输入每个订阅源的 URL。更复杂的聚合工具通常能够从站点主页标题中的 link 标记确定订阅源的 URL。

此外,尽管我上面警告说,每个新闻聚合器都应跟踪其最近的请求,以免使服务器不堪重负,但该程序省略了此类功能,这是我尝试使其保持小巧且可读的一部分。

程序 aggregator.py 可以在清单 1 中阅读,并分为四个部分

  1. 我们首先打开输出文件,这是一个名为 myfeeds.html 的 HTML 格式文本文件。该文件旨在从 Web 浏览器中使用。如果您愿意,可以将此本地文件(具有 file:/// URL)添加到您的个人书签列表中,甚至将其设为您的启动页。在确保我们确实可以写入此文件后,我们启动 HTML 文件。

  2. 然后,我们读取 feeds.txt 的内容,其中每行包含一个订阅源 URL。为了避免空格或空行的问题,我们剥离空格并忽略任何没有至少一个可打印字符的行。

  3. 接下来,我们迭代订阅源列表 feeds_list,对该 URL 调用 feedparser.parse()。当我们收到响应时,我们会将其写入输出文件 myfeeds.html,其中包含 URL 和文章标题。

  4. 最后,我们关闭 HTML 和文件。

清单 1. aggregator.py

#!/usr/bin/python

import feedparser
import sys

# ---------------------------------------------------
# Open the personal feeds output file

aggregation_filename = "myfeeds.html"
max_title_chars = 60

try:
    aggregation_file = open(aggregation_filename,"w")
    aggregation_file.write("""<html>
<head><title>My news</title></head>
<body>""")
except IOError:
    print "Error: cannot write '%s' " % \
    aggregation_filename
    exit

# ---------------------------------------------------
# Each non-blank line in feeds.txt is a feed source.

feeds_filename = "feeds.txt"
feeds_list = []

try:
    feeds_file = open(feeds_filename, 'r')
    for line in feeds_file:
        stripped_line = line.strip().rstrip()

        if len(stripped_line) > 0:
            feeds_list.append(stripped_line)
            sys.stderr.write("Adding feed '" + \
            stripped_line + "'\n")

    feeds_file.close()

except IOError:
    print "Error: cannot read '%s' " % feeds_filename
    exit

# ---------------------------------------------------
# Iterate over feeds_list, grabbing the feed for each

for feed_url in feeds_list:
    sys.stderr.write("Checking '%s'..." % feed_url)
    feed = feedparser.parse(feed_url)
    sys.stderr.write("done.\n")

    aggregation_file.write('<h2>%s</h2>\n' % \
                           feed.entries[0].title)

    # Iterate over each entry from this feed,
    # displaying it and putting it in the summary
    for entry in feed.entries:
        sys.stderr.write("\tWrote: '%s'" % \
                      entry.title[0:max_title_chars])

        if len(entry.title) > max_title_chars:
            sys.stderr.write("...")

        sys.stderr.write("\n")

        aggregation_file.write(
           '<li><a href="%s">%s</a>\n' %
           (entry.link, entry.title))

    aggregation_file.write('</u2>\n')

# ---------------------------------------------------
# Finish up with the HTML

aggregation_file.write("""</body>
</html>
""")
aggregation_file.close()


从代码清单中可以看出,创建这样一个供个人使用的新闻聚合器是相当简单明了的。然而,这仅仅是一个骨架应用程序。为了在现实世界中更有用,我们可能希望将 feeds.txt 和 myfeeds.html 移动到关系数据库中,根据站点 URL 自动或半自动地确定订阅源 URL,并处理订阅源类别,以便可以将多个订阅源读取为好像它们是一个。

如果以上描述听起来很熟悉,那么您可能是 Bloglines.com 的用户,这是一个基于 Web 的博客聚合器,可能以上述方式工作。显然,与我们在这个简单的玩具示例中拥有的相比,Bloglines 处理的订阅源和用户要多得多。但是,如果您有兴趣为您的组织创建内部版本的 Bloglines,那么 Universal Feed Parser 与关系数据库(例如 PostgreSQL)以及一些个性化代码的组合既易于实现又非常有用。

结论

计算机行业经常将重复发明轮子的趋势视为一个普遍存在的问题。Mark Pilgrim 的 Universal Feed Parser 可能只满足软件世界中的一个小需求,但随着个人和组织对聚合的使用增加,这种需求几乎肯定会增长。最重要的是,如果您有兴趣阅读和解析聚合订阅源,则应使用 feedparser。它经过了大量测试和文档记录,经常更新和改进,并且能够快速而出色地完成其工作。

Reuven M. Lerner,一位长期的 Web/数据库顾问和开发人员,现在是西北大学学习科学项目的研究生。他的博客位于 altneuland.lerner.co.il,您可以通过 reuven@lerner.co.il 与他联系。

加载 Disqus 评论