构建桌面新闻聚合器
摘要:Dare Obasanjo 生成了一个 C# 应用程序,以便检索和显示来自各种 Web 站点的新闻快递。该应用程序利用了 .NET 框架中的 XPath、XSLT、XML 架构、DOM 和 XML 序列化。
注 与本文相关联的示例应用程序已于 2003 年 3 月 14 日更新。该新版本对多种功能进行了重大升级,因此,建议您将以前的版本更新到这个最新版本。
本页内容
简介
什么是 RSS?
新闻聚合器的功能要求
了解 RSS Bandit 的用户界面
RSS Bandit 体系结构概述
XML 技术和 RSS Bandit
RSS Bandit 的未来规划
定位 RSS 新闻快递
简介
像 大多数将时间花在网上的人一样,我每天都要从大量 Web 站点阅读信息。最近我注意到,当我希望查看站点上是否有新文章或内容更新时,我总是每隔一小时平均查看五到十个 Web 站点。这提示我研究如下可能性:创建一个桌面应用程序,让它为我做所有的新闻搜集工作,并在有新内容出现在我感兴趣的 Web 站点上时通知我。经过一番研究,我发现了 RSS 并创建了我的桌面新闻聚合器 — RSS Bandit。
什么是 RSS?
RSS 是一种 XML 格式,它用于将联机新闻源中的新闻和类似内容组合在一起。RSS 由新闻站点(例如,C|Net 和 Wired)、联机技术杂志(例如,XML.com)和 Web 日志(例如,Don Box's Spoutletex 和 Joel on Software)使用。
RSS 新闻快递是一个定期更新的 XML 文档,其中包含有关新闻源和其中内容的元数据。一个 RSS 新闻快递至少包含一个代表新闻源的 channel,channel 包含一个 title、一个 link 和一个用来描述新闻源的 description。另外,一个 RSS 新闻快递通常包含一个或多个表示单个新闻项的 item 元素,其中的每个元素都应当有一个 title、一个 link 或一个 description。
注 上面提到的元素出现在大多数 RSS 新闻快递中,但它们不是可能出现的唯一元素。许多 RSS 新闻快递还包含其他元素,例如,date 或 language。但是,这些元素和许多其他元素实际很少出现。
以下代码片段是 MSDN XML 开发人员中心的示例 RSS 0.91 新闻快递:
<rss version="0.91">
<channel>
<title>MSDN XML Developer Center</title>
<link>http://msdn.microsoft.com/xml/</link>
<description> Extensible Markup Language (XML) is the universal format
for data on the Web. XML allows developers to easily describe and
deliver rich, structured data from any application in a standard,
consistent way. XML does not replace HTML; rather, it is a
complementary format. </description>
<item>
<title> XML Files: XPath Selections and Custom Functions, and More
</title>
<link>
http://msdn.microsoft.com/msdnmag/issues/03/02/xmlfiles/TOC.asp
</link>
<description> Get your questions about XPath selections, custom
functions, and more answered in this month's column. (February 5,
Article)</description>
</item>
<item>
<title> Extreme XML: XML Serialization in the .NET Framework </title>
<link> http://msdn.microsoft.com/library/en-us/dnexxml/html/xml01202003.asp
</link>
<description> Dare Obasanjo discusses XML serialization and how you
can use it within the .NET Framework to improve interoperability and
meet W3C standards. (February 3, Article) </description>
</item>
</channel>
</rss>
有关 RSS 的详细信息,请阅读 Mark Pilgrim 的一篇有关 XML.com 的内容丰富的文章,题目为 What is RSS?。
新闻聚合器的功能要求
RSS 新闻聚合器是桌面或 Web 应用程序,它们用来从多个新闻源检索 RSS 新闻快递并显示这些新闻快递。RSS 新闻聚合器的示例有 NewzCrawler、NewsGator 和 AmphetaDesk。我试过几个 RSS 新闻聚合器,但它们中没有一个恰当地结合了我所需要的特性和功能。因此,我决定自己编写一个能够满足我的需要的聚合器。
对于我的新闻聚合器,我提出了下列功能要求
-
该新闻聚合器必须能够处理 RSS 的三个最常见版本(0.91、1.0 和 2.0 版)。
-
该新闻聚合器必须使用类似于 Microsoft Outlook? Express 的、包含三个窗格的用户界面,来显示 RSS 新闻快递。
-
该新闻聚合器必须使用嵌入的 Web 浏览器,以便允许用户查看丰富的内容并导航到从 RSS 项链接到的网页。
-
该新闻聚合器必须允许使用 OPML(其他聚合器使用的标准格式)来导入和导出所订阅新闻快递的列表。
-
该新闻聚合器必须提供用于控制每个新闻快递检索频率的选项。
-
该新闻聚合器应当为常见的任务(例如,在新项中导航)提供键盘快捷方式。
-
该新闻聚合器必须能够跟踪在应用程序被调用期间已经阅读了哪些消息。
-
该新闻聚合器应当能够向您显示特定 RSS 新闻快递中的原始 XML。
-
该新闻聚合器应当在程序被调用期间将 RSS 新闻快递缓存到硬盘上。
-
该新闻聚合器应当提供将已读项标记为未读的功能。
-
该新闻聚合器应当支持 ISA 客户端和/或 Web 代理。
-
该新闻聚合器必须使用 HTTP conditional GET requests(HTTP 条件 GET 请求)来降低新闻源的带宽成本。
-
该新闻聚合器必须能够运行在满足下列最低要求的系统上:
-
Microsoft Windows? 2000、Windows XP 或更高版本
-
Microsoft .NET 框架 1.0
-
LAN/拨号 Internet 连接
-
在 实现我的名为 RSS Bandit 的 RSS 新闻聚合器时,除了第 9 条以外,我满足了上述所有的功能要求。最初的测试表明,从硬盘加载新闻快递与通过宽带连接从 Web 刷新相比没有显著的性能差别(尽管前者增加了代码的复杂性)。其次,假设 RSS Bandit 的预期使用模式是作为一直打开 的应用程序使用,那么,此功能似乎并不是绝对必要的。
了解 RSS Bandit 的用户界面
RSS Bandit 的用户界面的设计灵感来源于邮件和新闻阅读器(例如,Microsoft Outlook 和 Microsoft Outlook Express)。图 1 是 RSS Bandit 的屏幕快照,它显示操作中的嵌入 Web 浏览器。
图 2 是一个屏幕快照,它显示一则指示新项已被检索的弹出消息。
RSS Bandit 体系结构概述
RSS Bandit 应用程序主要由两个类驱动。RssHandler 类管理 RSS 新闻快递的下载,而 RssBanditView 类提供一个用来查看 RSS 新闻快递并与 RssHandler 类交互的图形前端。
RssHandler 类在指定的时间间隔内下载 RSS 新闻快递并存储它们。该类与用户界面之间不是紧密耦合的,它可以由需要处理 RSS 新闻快递的其他应用程序重新使用。利用 RssHandler 类的客户端会在实例化该类时注册一个回调(委托)。然后,在下载新的新闻快递或更新后的新闻快递时,RssHandler 对象会调用已注册的回调。有关要下载哪些新闻快递以及其他配置数据的信息,可从用 XML 编写的新闻快递订阅列表中获取。由于用户可以配置每次下载特定 RSS 新闻快递之间的时间间隔,因此 RssHandler 类有一个计时器,该计时器会每隔五分钟开启一次并检查每个新闻快递,看看对该特定新闻快递进行的多次下载尝试之间是否经过了足够的时间。这意味着在五分钟的时间间隔内,不能对一个新闻快递尝试下载多次。
RssBanditView 是一个 Windows 窗体,其中包含一个树视图(用来显示所订阅新闻快递的列表)、一个列表视图(用来显示有关当前选定新闻快递中的项的信息)以及一个嵌入的 Web 浏览器(用来显示内容)。在启动时,RssBanditView 会用负责下载和处理 RSS 新闻快递的 RssHandler 注册一个委托。在下载新的或更新后的新闻快递时,RssBanditView 会使用 Safe, Simple Multithreading in Windows Forms, Part 1 一文(作者是 Chris Sells)中描述的方法,以线程安全的方式通过该委托进行更新。
该用户界面还允许用户管理 RssHandler 类各个方面的行为。用户可以在订阅列表中添加和删除新闻快递、配置新闻快递的下载频率以及设置代理服务器信息。
XML 技术和 RSS Bandit
RSS Bandit 应用程序充分利用了 .NET 框架中的 XML 技术。RSS Bandit 使用 XML 序列化将新闻快递订阅列表转换为对象(反之亦然),使用 XSLT 将 OPML 文件转换为新闻快递订阅列表格式,使用 XSD 验证来确保新闻快递订阅列表有效,使用 XPath 来处理 RSS 新闻快递。
RSS Bandit 中的 W3C XML 架构
在 使用 RSS Bandit 时,第一步是确定使应用程序在启动时工作所必需的信息。在经过一番冥思苦想之后,我总结出了两大类信息 — 新闻快递订阅和配置数据。该应用程序将必须能够确定我需要阅读哪些新闻快递、其自身获取新项的频率以及哪些新项已进行了阅读。其次,该应用程序将必须知道 有关用来定向 Web 请求的代理服务器的信息。
在确定了应用程序在启动时所需的信息之后,我需要确定是将这些信息存储在配置文件 中,还是将它们存储在 Windows 注册表中。基于几个原因,我决定将数据存储在 XML 配置文件中,而不是存储在 Windows 注册表中。XML 配置文件的可移植性比注册表设置强,此外,它还允许我使用广泛的 XML 信息处理技术来处理我的设置。
让我们首先看一下 RSS Bandit 的配置文件的架构:
<xs:schema targetNamespace='http://www.25hoursaday.com/2003/RSSBandit/feeds/'
xmlns:xs='http://www.w3.org/2001/XMLSchema' elementFormDefault='qualified'
xmlns:f='http://www.25hoursaday.com/2003/RSSBandit/feeds/'>
<xs:element name='feeds'>
<xs:complexType>
<xs:sequence>
<xs:element name='feed' minOccurs='0' maxOccurs='unbounded'>
<xs:complexType>
<xs:sequence>
<xs:element name='title' type='xs:string' />
<xs:element name='link' type='xs:anyURI' />
<xs:element name='refresh-rate' type='xs:int' minOccurs='0'>
<xs:annotation>
<xs:documentation>
This describes how often the feed must be refreshed in
milliseconds.
</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name='last-retrieved' type='xs:dateTime'
minOccurs='0' />
<xs:element name='etag' type='xs:string' minOccurs='0' />
<xs:element name='stories-recently-viewed' minOccurs='0'>
<xs:complexType>
<xs:sequence>
<xs:element name='story' type='xs:string'
minOccurs='0' maxOccurs='unbounded' />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name='category' type='xs:string' use='optional' />
</xs:complexType>
</xs:element>
<xs:element name='categories' minOccurs='0'>
<xs:complexType>
<xs:sequence>
<xs:element name='category' type='xs:string' maxOccurs='unbounded' />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name='refresh-rate' type='xs:int' use='optional' />
<xs:attribute name='proxy-server' type='xs:string' use='optional' />
<xs:attribute name='proxy-port' type='xs:positiveInteger'
use='optional' />
</xs:complexType>
<xs:key name='categories-key'>
<xs:selector xpath='f:categories/f:category'/>
<xs:field xpath='.'/>
</xs:key>
<xs:keyref name='categories-keyref' refer='f:categories-key' >
<xs:selector xpath='f:feed'/>
<xs:field xpath='@category'/>
</xs:keyref>
</xs:element>
</xs:schema>
在启动时,应用程序首先尝试在当前的目录中查找一个名为 feeds.xml 的文件。如果找到了该文件,则随后对其进行加载并针对上面的架构进行验证,以便确保它是有效的新闻快递订阅列表。如果在 RSS Bandit 应用程序的执行过程中尝试导入新闻快递订阅列表,则会出现类似的验证。
尽管 refresh-rate、etag 和 story 元素可以提供一些说明,但是该架构中的大多数信息都非常简单易懂。refresh-rate 元素描述多长时间尝试下载一次新闻快递(用秒表示)。etag 元素包含 ETag 标题中在上次下载新闻快递时从 Web 服务器发回的信息。这些信息会在执行 HTTP conditional GET requests(HTTP 条件 GET 请求)时使用。story 元素包含指向新闻项的链接,该链接还兼做用来区分已读项和未读项的唯一标识符。
键约束指定 categories 中的每个category 元素都必须唯一且可作为键由另一个元素或属性引用。keyref 约束指定 feed 的 category 属性必须与 categories 下的某个 category 元素具有相同的值。
RSS Bandit 中的 XML 序列化
在 RSS Bandit 的执行过程中,订阅列表中的信息必须非常频繁地访问和修改,这使得如果将信息存储在本机数据结构中,会比存储在 XML 文档中更为有利。因此,在成功验证时,新闻快递订阅列表会使用上个月专栏 XML Serialization in the .NET Framework(.NET 框架中的 XML 序列化)中描述的 XML 序列化技术转换为对象。
下面的类映射到充当 feed 元素的类型定义的匿名复杂类型:
/// <remarks/>
[System.Xml.Serialization.XmlTypeAttribute
(Namespace="http://www.25hoursaday.com/2003/RSSBandit/feeds/")]
public class feedsFeed {
/// <remarks/>
public string title;
/// <remarks/>
[System.Xml.Serialization.XmlElementAttribute(DataType="anyURI")]
public string link;
/// <remarks/>
[System.Xml.Serialization.XmlElementAttribute("refresh-rate")]
public int refreshrate;
/// <remarks/>
[System.Xml.Serialization.XmlIgnoreAttribute()]
public bool refreshrateSpecified;
/// <remarks/>
[System.Xml.Serialization.XmlElementAttribute("last-retrieved")]
public System.DateTime lastretrieved;
/// <remarks/>
[System.Xml.Serialization.XmlIgnoreAttribute()]
public bool lastretrievedSpecified;
/// <remarks/>
public string etag;
/// <remarks/>
[System.Xml.Serialization.XmlIgnoreAttribute()]
public bool containsNewMessages;
/// <remarks/>
[System.Xml.Serialization.XmlArrayAttribute(ElementName =
"stories-recently-viewed", IsNullable = false)]
[System.Xml.Serialization.XmlArrayItemAttribute("story", Type =
typeof(System.String), IsNullable = false)]
public ArrayList storiesrecentlyviewed;
/// <remarks/>
[System.Xml.Serialization.XmlAttributeAttribute("category")]
public string category;
}
在该类中,最有趣的批注是对 storiesrecentlyviewed 查看属性进行批注的两个属性。这些批注主要声明,名为 stories-recently-viewed 的元素映射到 ArrayList,而它的 story 子元素映射到存储在 ArrayList 中的字符串。
RSS Bandit 中的 XPath 和 DOM
如 上所述,一个 RSS 新闻快递包含一个或多个 item 元素,这些元素又可以包含 title、link 或 description 子元素。但是,如何查找这些元素会因应用程序所处理的 RSS 版本而异。在 RSS 0.91 中,item 元素是 channel 元素的子级,这两个元素都没有命名空间名称。在 RSS 1.0 中,item 元素是“http://purl.org/rss/1.0/”命名空间的一部分,它是 RDF 元素的子级。RDF 元素本身属于“http://www.w3.org/1999/02/22-rdf-syntax-ns#”命名空间。在 RSS 2.0 中,item 元素是 channel 元素的子级,这两个元素要么都没有命名空间名称,要么具有相同的命名空间名称。
我决定不考虑各种 RSS 中的上述区别,而是创建一个表示 RSS 项中典型信息的类。在处理 RSS 新闻快递的过程中,RssHandler 类从 RSS 新闻快递检索 item 元素,并将它们转换为 RssItem 对象。下面的代码片段显示了如何从存储在 XmlDocument 中的 RSS 新闻快递中查找所有 item 元素,而不管正在处理哪个版本的 RSS。
string rssNamespaceUri = String.Empty;
if(feed.DocumentElement.LocalName.Equals("RDF") &&
feed.DocumentElement.NamespaceURI.Equals
("http://www.w3.org/1999/02/22-rdf-syntax-ns#")){
rssNamespaceUri = "http://purl.org/rss/1.0/";
}else if(feed.DocumentElement.LocalName.Equals("rss")){
rssNamespaceUri = feed.DocumentElement.NamespaceURI;
}else{
throw new ApplicationException("This XML document does not
look like an RSS feed");
}
//convert RSS items in feed to RssItem objects and add to list
XmlNamespaceManager nsMgr = new
XmlNamespaceManager(feed.NameTable);
nsMgr.AddNamespace("rss", rssNamespaceUri);
foreach(XmlNode node in feed.SelectNodes("//rss:item", nsMgr)){
RssItem item = MakeRssItem((XmlElement)node);
items.Add(item);
}//foreach
上面的代码片段依次处理 XML 文档中的每个 item 元素,而与正在处理的 RSS 版本是 0.91、1.0 还是 2.0 无关。使用类似的代码,可以在将每个 item 转换为 RssItem 对象时,处理该元素的子元素。此应用程序代码利用了如下事实:尽管不同版本的 RSS 新闻快递的结构有所不同,但是其中的 item 元素却是相似的(即,存在共享的结构孤岛)。
RSS Bandit 中的 XSLT
常见的新闻聚合器(包括 Aggie、AmphetaDesk 和 NewsGator)支持使用被称为 OPML 的 XML 格式来导入和导出 RSS 新闻快递订阅。因为互操作性总是好东西,所以我决定支持在我的新闻快递订阅列表格式和 OPML 之间互换。
结果表明,将我的新闻快递订阅列表转换为 OPML 相当简单,因为我只须将从新闻快递订阅列表生成的对象中的某些信息写为 XML 即可。代码如下所示:
StringBuilder sb = new StringBuilder("<opml>"n<body>"n");
if(_feedsTable != null){
foreach(feedsFeed f in _feedsTable.Values){
sb.AppendFormat("<outline title='{0}' xmlUrl='{1}'
/>"n", f.title, f.link);
}
}
sb.Append("</body>"n</opml>");
但是,将 OPML 文件转换为我的新闻快递订阅列表格式需要更多的工作。我确定的最佳方法是:使用专门为在 XML 格式之间转换而设计的技术 — XSLT。下面的样式表将 OPML 文件转换为我的新闻快递订阅列表格式:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" />
<xsl:template match="/">
<feeds xmlns="http://www.25hoursaday.com/2003/RSSBandit/feeds/">
<xsl:for-each select="/opml/body/outline">
<feed>
<title>
<xsl:value-of select="@title" />
</title>
<link>
<xsl:choose>
<xsl:when test="@xmlUrl">
<xsl:value-of select="@xmlUrl" />
</xsl:when>
<xsl:when test="@xmlurl">
<xsl:value-of select="@xmlurl" />
</xsl:when>
<xsl:otherwise>ERROR: No RSS Feed URL in OPML
File</xsl:otherwise>
</xsl:choose>
</link>
</feed>
</xsl:for-each>
</feeds>
</xsl:template>
</xsl:stylesheet>
在将 OPML 文件转换为 RSS Bandit 新闻快递订阅列表格式之后,它会与在启动时处理的新闻快递订阅列表的内部表示形式合并。
RSS Bandit 的未来规划
目前,我每天都使用 RSS Bandit,而且我发现它相当有用。在发表本文之前,我将 RSS Bandit 的安装程序放在了 GotDotNet 上,它已经被 downloaded 1000 times(下载过 1000 次)。根据对 RSS Bandit 的积极反馈,我已经为该项目创建了一个 GotDotNet Workspace(GotDotNet 工作区),并将与他人一起继续开发它。我希望添加许多功能,如对于将 RSS 新闻快递缓存到硬盘上的支持、在 RSS 新闻快递无效时提供反馈、实现 RSS autodiscovery(RSS 自动发现)、对于在 Internet Explorer 中嵌入 RSS Bandit 的支持以及使用 Background Intelligent Transfer Service API 或 .NET Application Updater Component 自动更新应用程序。希望致力于进一步开发 RSS Bandit 的开发人员能够加入 GotDotNet 工作区。
定位 RSS 新闻快递
在安装 RSS Bandit 应用程序时,RSS Bandit 安装程序会将许多新闻快递订阅列表放在该应用程序的子目录中。这些列表包含技术新闻站点、以 XML 为中心的新闻源和开发人员 Web 日志的新闻快递。在搜索更多 RSS 新闻快递时,最好从 News Is Free 或 Syndic8 开始,看您所钟爱的 Web 站点是否提供联合功能。
Dare Obasanjo 是 Microsoft 的 WebData 工作组的成员,该工作组在 .NET 框架的 System.Xml 和 System.Data 命名空间、Microsoft XML 核心服务 (MSXML) 和 Microsoft 数据访问组件 (MDAC) 中开发组件。
有关本文的任何问题或意见,欢迎张贴到 GotDotNet 上的 Extreme XML 留言板。