生成桌面新闻聚合器

 

Dare Obasanjo
Microsoft Corporation

2003 年 3 月 14 日修订

总结: Dare Obasanjo 生成一个 C# 应用程序,用于检索和显示来自各种网站的新闻源。 应用程序利用.NET Framework中的 XPath、XSLT、XML 架构、DOM 和 XML 序列化。 ) (12 个打印页

下载xml02172003_sample.exe

注意 与本文关联的示例应用程序已于 2003 年 3 月 14 日更新。 已对各种功能进行了重大升级,建议将早期版本升级到此最新版本。

简介

和大多数上网的人一样,我每天都会阅读许多网站。 我最近注意到,当我想查看网站上是否有任何新文章或内容更新时,我平均每隔一小时检查 5 到 10 个网站。 这促使我调查创建桌面应用程序的可能性,该应用程序将为我完成所有工作,并在我最喜欢的网站上出现新内容时提醒我。 我的调查导致我发现了 RSS,并创建我的桌面新闻聚合器 RSS Bandit。

什么是 RSS?

RSS 是一种 XML 格式,用于联合来自在线新闻源的新闻和类似内容。 RSS 由 C|Net连线等新闻网站、在线技术期刊(如 XML.com)和 Web 日志(如 Don Box 的 SpoutletexJoel on Software)使用。

RSS 源是一个定期更新的 XML 文档,其中包含有关新闻源和其中内容的元数据。 RSS 源至少由表示新闻源的 channel ,该源具有 titlelinkdescription 描述新闻源。 此外,RSS 源通常包含一个或多个 item 表示单个新闻项的元素,其中每个元素应具有 titlelinkdescription

注意 上述元素出现在大多数 RSS 源中,但不是唯一可以显示的元素。 许多 RSS 源还包含其他元素,例如 datelanguage。 但是,这些元素和其他许多元素在实践中并不常见。

下面是 MSDN XML 开发人员中心的示例 RSS 0.91 源:

<rss version="0.91">
  <channel>
    <title>MSDN XML Developer Center</title>
    <link>https://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> 
      https://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> https://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 上题为 “什么是 RSS?” 的信息性文章。

新闻聚合器的功能要求

RSS 新闻聚合器是桌面或 Web 应用程序,用于检索和显示来自各种新闻源的 RSS 源。 RSS 新闻聚合器的示例包括 NewzCrawlerNewsGatorAmphetaDesk。 我尝试了一些 RSS 新闻聚合器,但没有找到一个具有适当组合特性和功能的聚合器,适合我的口味。 因此,我决定自己写一个满足我需求的。

我的新闻聚合器具有以下功能要求

  1. 新闻聚合器必须能够处理三个最常用的 RSS 版本 (版本 0.91、1.0 和 2.0) 。
  2. 新闻聚合器必须使用类似于 Microsoft Outlook® Express 的三平移用户界面来显示 RSS 源。
  3. 新闻聚合器必须使用嵌入式 Web 浏览器来查看丰富的内容,并导航到从 RSS 项目链接到的网页。
  4. 新闻聚合器必须允许使用 OPML(其他聚合器使用的标准格式)导入和导出订阅源列表。
  5. 新闻聚合器必须提供用于控制每个源检查频率的选项。
  6. 新闻聚合器应为常见任务(如浏览新项目)提供键盘快捷方式。
  7. 新闻聚合器必须能够跟踪在应用程序调用之间已读取的消息。
  8. 新闻聚合器应能够显示来自特定 RSS 源的原始 XML。
  9. 新闻聚合器应在程序调用之间将 RSS 源缓存在磁盘上。
  10. 新闻聚合器应提供将已读项目标记为未读的功能。
  11. 新闻聚合器应支持 ISA 客户端和/或 Web 代理。
  12. 新闻聚合器必须使用 HTTP 条件 GET 请求 来降低新闻源的带宽成本。
  13. 新闻聚合器必须能够在满足以下最低要求的系统上运行:
    • Microsoft Windows® 2000、Windows XP 或更高版本
    • Microsoft .NET Framework 1.0
    • LAN/拨号 Internet 连接

在实现 RSS 新闻聚合器(称为 RSS Bandit)时,我满足上述所有功能要求,但数字 9 除外。 初始测试表明,从磁盘加载源与从宽带连接从 Web 刷新之间没有明显的性能差异,尽管前者增加了一些代码的复杂性。 其次,鉴于 RSS Bandit 的预期使用模式是 始终打开 的应用程序,此功能似乎并非绝对必要。

查看 RSS Bandit 用户界面

RSS Bandit 的用户界面受到邮件和新闻阅读器(如 Microsoft Outlook 和 Microsoft Outlook Express)的启发。 图 1 是 RSS Bandit 的屏幕截图,其中显示了嵌入的 Web 浏览器。

单击获取较大图像

图 1. 阅读新闻与 RSS 强盗

图 2 是显示弹出消息的屏幕截图,该消息指示已检索到新项目。

单击获取较大图像

图 2. 使用 RSS Bandit 接收新消息

RSS Bandit 体系结构概述

RSS Bandit 应用程序主要由两个类驱动。 RssHandler 类管理 RSS 源的下载,而 RssBanditView 类提供用于查看 RSS 源和与 RssHandler 类交互的图形前端。

RssHandler 类按指定的间隔下载 RSS 源并存储它们。 类与用户界面不紧密耦合,可由需要处理 RSS 源的其他应用程序重复使用。 使用 RssHandler 类的客户端在实例化类时注册回调 (委托) 。 然后,当下载新的或更新的源时, RssHandler 对象会调用已注册的回调。 要下载的源和其他配置数据的信息是从用 XML 编写的源订阅列表获取的。 由于每次下载特定 RSS 源之间的时间量是用户可配置的, 因此 RssHandler 类有一个计时器,该计时器每五分钟关闭一次,并检查每个源以查看该特定源的下载尝试之间是否有足够的时间。 这意味着源在五分钟的跨度内不能进行多次下载尝试。

RssBanditView 是一个 Windows 窗体,其中包含用于显示订阅源列表的树视图、用于显示当前所选源中有关项目的信息的列表视图以及用于显示内容的嵌入式 Web 浏览器。 启动时, RssBanditView 会向 RssHandler 注册一个代理,用于处理 RSS 源的下载和处理。 每当下载新的或更新的源时,RssBanditView 都会使用 Chris Sells 在 Windows 窗体 中的安全、简单的多线程处理第 1 部分文章 中所述的技术,以线程安全的方式通过委托进行更新。

用户界面还允许用户管理 RssHandler 类行为的各个方面。 用户可以在订阅列表中添加和删除源、配置应下载源的频率,以及设置代理服务器信息。

XML 技术和 RSS Bandit

RSS Bandit 应用程序在.NET Framework中大量使用 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-rateetagstory 元素可以进行一些说明。 元素 refresh-rate 描述尝试下载源的频率(以秒为单位)。 元素 etag 包含上次下载源时从 Web 服务器发送回 的 ETag 标头的信息。 执行 HTTP 条件 GET 请求时,将使用此信息。 元素 story 包含指向新闻项的链接,该链接是用作用于区分已读报道与未读报道的唯一标识符。

键约束指定中的每个categorycategories元素必须是唯一的,并且可由另一个元素或属性作为键引用。 约束keyref指定 categoryfeed 特性必须与 下的categories元素之category一具有相同的值。

RSS Bandit 中的 XML 序列化

在执行 RSS Bandit 期间,必须经常访问和修改源订阅列表中的信息,这倾向于将信息存储在本机数据结构中,而不是存储在 XML 文档中。 出于此原因,成功验证后,源订阅列表将使用上个月的列(.NET Framework中的 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;
   }

注释 故事查看 属性的两个属性是 类中最有趣的注释。 注释基本上表明,名为 stories-recently-viewed 的元素映射到 ArrayList, 而其子 story 元素映射到 ArrayList 中存储的字符串。

RSS Bandit 中的 XPath 和 DOM

如前所述,RSS 源包含一个或多个 item 元素(可选)将 titlelinkdescription 元素作为子元素。 但是,查找这些元素的方式因应用程序正在处理的 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 无关。 将元素转换为 RssItem 对象时,使用类似的代码来处理每个item元素的子元素。 应用程序代码利用了这样一个事实,即尽管不同版本的 RSS 源的结构不同, item 但它们中的元素是相似的 (即,存在结构) 的共享 孤岛

RSS Bandit 中的 XSLT

热门新闻聚合器(包括 AggieAmphetaDeskNewsGator )支持使用称为 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 强盗的未来计划

我目前每天使用 RSS Bandit,发现它非常有用。 在发布本文之前,我在 GotDotNet 上提供了安装程序,并且已 下载了 1000 次。 鉴于对 RSS Bandit 的积极反馈,我已为该项目创建了 一个 GotDotNet 工作区 ,并将与他人合作以继续开发它。 我想添加许多功能,例如支持在磁盘上缓存 RSS 源、在 RSS 源无效时提供反馈、实现 RSS 自动发现、支持在 Internet Explorer 中嵌入 RSS Bandit,以及使用 后台智能传输服务 API.NET 应用程序更新程序组件自动更新应用程序。 想要进一步开发 RSS Bandit 的开发人员可以加入 GotDotNet 工作区。

查找 RSS 源

RSS Bandit 安装程序在安装时将许多源订阅列表放置在 RSS Bandit 应用程序的子目录中。 这些列表包含技术新闻网站、以 XML 为中心的新闻源和开发人员 Web 日志的源。 搜索更多 RSS 源的好开始位置包括 新闻免费Syndic8 ,以查看你最喜欢的网站是否提供联合。

Dare Obasanjo 是 Microsoft WebData 团队的成员,除其他事项外,该团队在 .NET Framework 的 System.Xml 和 System.Data 命名空间中开发组件、Microsoft XML 核心服务 (MSXML) ,以及 Microsoft 数据访问组件 (MDAC) 。

请随时在 GotDotNet 上的 Extreme XML 消息板上 发布有关本文的任何问题或评论。