65.9K
CodeProject 正在变化。 阅读更多。
Home

RealNews - 类似于 FeedReader 的 WinForms RSS 阅读器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (27投票s)

2018年8月2日

CPOL

11分钟阅读

viewsIcon

50421

downloadIcon

2888

我尝试过许多其他的 RSS 阅读器,比如 RSSOwl(它也已不再支持),但没有一个让我觉得满意,所以我决定自己写一个。

引言

xxxxxmain

RealNews 是一款 Winforms RSS 阅读器,其功能类似于 FeedReaderFeedReader 已停止支持 10 年,在 Windows 10 操作系统上已显陈旧。作为 FeedReader 的替代品,我尝试过许多其他的 RSS 阅读器,比如 RSSOwl(它也已不再支持),但没有一个让我觉得满意,所以我决定自己写一个。

这是一个个人需求的开发项目,我已将其公之于众,希望能帮助有类似需求并可能觉得它有用的人。它使用了以下库:

RealNews 的源代码也在 GitHub 上,地址为: https://github.com/mgholam/RealNews

特点

RealNews 具有以下功能:

  • 极快的性能
  • 未使用或不需要数据库
  • 仅 310KB 的微小单个 EXE 文件
  • 完全离线模式,可查看带相关图片的订阅项目
  • 在每日时间窗口内下载图片
  • 仅下载小于指定尺寸限制的图片
  • 全局每 X 分钟更新一次,或根据每个单独订阅的设置进行更新
  • 手动更新单个订阅或全局更新
  • 可编辑的 style.css 用于订阅视图窗口
  • HTML 清理器,用于清理订阅项目内容中的 JavaScript 和潜在的危险标签
  • 从 OPML 文件读取订阅定义
  • 浅色和深色主题

限制

目前(未来可能会有变化),存在以下限制:

  • 不支持写入 OPML 文件
  • 不支持拖放订阅到文件夹

使用 RealNews

您可以导入现有的 OPML 文件或在 文件 菜单中单独添加订阅来开始使用 RealNews。添加单个订阅时,您将看到以下表单:

xxxxxedit

将您的订阅地址放入 订阅地址 文本框,然后按 获取信息 按钮,以便 RealNews 可以为您从 URL 中获取订阅名称。

您可以通过设置类别文本,将此订阅放入 treeview 中的一个类别文件夹。

如果您设置的 更新间隔 数字大于 0,那么该订阅将在全局计划之外,按照自己的时间表进行更新。

如果您最小化 RealNews,它会最小化到系统托盘,您可以从那里恢复或退出。

xxxxxminimized

控制 RealNews

应用程序的所有设置都在下面的表单中,这些设置不言自明。

xxxxxsettings

如果您单击 style.css 链接,您可以编辑在浏览器控件中使用的订阅项目样式。

键盘快捷键

您可以使用以下快捷键控制 RealNews

描述
F2 编辑订阅信息
ctrl+L 显示日志
ctrl+S 标记/取消标记订阅项目
空格 移动到下一个订阅项目
Alt+向上 向上滚动当前订阅
Alt+向下 向下滚动当前订阅
ctrl+D 删除当前或选中的项目

内部实现

大部分代码位于 frmMain.cs 类中,负责应用程序的复杂 UI。

文件和文件夹

RealNews 具有以下文件夹结构:

xxxxxfolders

  • cache:订阅项目的图片存储(参见图片缓存部分)

  • configs

    • settings.config:所有应用程序设置,以 json 格式存储
    • style.css:用于查看订阅项目的 CSS 样式
  • feeds

    • icons:保存 RSS 网站的下载图标(如果存在)
    • lists:保存订阅项目列表

订阅列表是根据您指定的订阅名称生成的订阅项目的 json 文件。在 feed 文件夹中,还有另外 2 个文件:

  • downloadimg.list:是订阅项目中提取的图片的下载队列(json 格式)
  • feeds.list:是您添加的所有订阅的主要订阅列表(json 格式)

Feed Reader

Feed Reader 代码来自文章顶部的 URL,它已被修改以支持提取“非标准”标准 RSS 格式中的所有信息。

内部 Web 服务器

RealNews 拥有一个集成的 Web 服务器,用于为浏览器控件显示订阅项目。Web 服务器提供订阅项目的 HTML,并通过 ImageChache 类处理内容中图片的提供。

您还可以通过访问设置文件中配置的 localhost URL 和关联端口,在普通浏览器中查看内容。

生成的内容页面会将图片标签的 URL 替换为指向该 Web 服务器的链接,因此图片是从图片缓存提供的,而不是直接从互联网加载。

图片缓存

大多数 RSS 阅读器使用数据库来存储内容(图片等)。我在这上面来回斟酌,最初使用了 RaptorDB,但在提出这个解决方案后,我放弃了它。首先,让我陈述一下问题:

  • RSS 订阅中的图片 URL 可能非常长,并且可能不以图片扩展名结尾。
  • 将此 URL 存储在磁盘上可能不可行,因为字符长度和文件系统限制可能包含文件系统不喜欢的特殊字符。

下面是一个 URL 的示例(摘自 downloadimg.list,并且是 JSON 字符串化后的):

"https://o.aolcdn.com/images/dims?crop=5916%2C3944%2C0%2C0&quality=85&
amp;format=jpg&resize=1600%2C1067&image_uri=
 http%3A%2F%2Fo.aolcdn.com%2Fhss%2Fstorage%2Fmidas%2Ff9c858ae2e3bcaa713b57e2e88624c74%
 2F206499306%2Fside-view-of-a-bright-red-tesla-model-3-automobile-from-tesla-motors-
 picture-id974894362&client=a1acac3e1b3290917d92&
 signature=f1743691ec12a1171bdb6e03cdf1e407a93dcc58"

这只是一个图片 URL,正如您所见,例如,没有可靠的方法可以获取图片名称来保存到磁盘。

所以我提出的解决方案非常简单:与其进行复杂的解析来获取图片名称以便存储到磁盘,并处理上述所有限制,我所做的就是生成 URL 的哈希值,并使用该数字作为磁盘上的文件名(最简单的哈希就是 .NET 中已经为 string 定义的哈希)。

为了进一步隔离图片,我还从 URL 中提取域名,并将哈希值存储在该域名对应的文件夹中,这对于人类在需要时浏览非常方便。

xxxxxcache

所有这些都消除了对数据库的需求,数据库本质上只是将查找 URL 映射到文件名。

HTML 清理器

HTML 清理器的作用是移除订阅项目中有潜在问题的 HTML 标签,并用安全的标签替换它们,从而降低查看该订阅项目的安全风险。

图片下载器

在设置文件中指定的时间段内,RealNews 会开始下载其下载队列中的图片。图片下载任务首先会从服务器获取图片信息,检查图片大小。如果图片大小低于您指定的设置,它就会下载并将其保存到缓存中。

如果下载因超时而失败,它将重新排队以便稍后重试,否则它将跳过该图片。

图片下载器一次启动 200 个任务,批次之间间隔 4 秒,直到队列为空。

关注点

WebClient 和 https

奇怪的是,为了让 WebClient 在 .NET 4 中正确处理 https,您必须添加以下代码(这是未公开的,没有智能提示):

ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;

另外,为了让 WebClient 强制对连接使用 http 压缩,您需要更改请求。以下是完整的代码:

public class mWebClient : WebClient
{
	public mWebClient()
	{
		Timeout = 10 * 1000;
		// KLUDGE : https security for .NET 4
		ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;
		base.Encoding = System.Text.Encoding.UTF8;
		if (Settings.UseSytemProxy) // FEATURE : else define a proxy
			Proxy = WebRequest.DefaultWebProxy;
	}
	public int Timeout { get; set; }

	protected override WebRequest GetWebRequest(Uri uri)
	{
		WebRequest request = base.GetWebRequest(uri);
		var http = request as HttpWebRequest;
		if (http != null)
		{
			http.AutomaticDecompression = DecompressionMethods.GZip |
            							DecompressionMethods.Deflate;
			http.ReadWriteTimeout = Timeout;
		}

		request.Timeout = Timeout;
		return request;
	}
}

非标准、订阅标准

尽管 RSS 订阅有其标准(有多种),但每个提供商之间存在解释差异,这给处理带来了真正的麻烦,即使使用了出色的 feed reader 代码。

例如,The GaurdianCNN 的订阅在其主要订阅描述中没有图片,而是在“附加”内容中。

另外,像 Android Central 这样的订阅将它们的全部内容放在附加部分而不是描述部分。

对于所有这些非标准的“标准”,我不得不修改 feed reader 代码来提取这些附加部分,以便在视图区域显示。

单例实例

为了防止数据冲突,应用程序需要作为单例实例运行,并有一个附加要求:从运行的文件夹中运行(如果您从自己的文件夹运行,则可以有多个实例)。

为此,我最初在启动时在 data 文件夹中创建了一个“temp”文件,并在关闭时删除。在启动时,我检查了该文件的存在来处理单例实例。这样做的问题是,如果计算机因断电而死亡,它就会失败,我必须手动删除 temp 文件才能再次工作。

最终的解决方案是检查计算机上的运行进程,并检查正在运行的 EXE 的路径是否与该进程的路径匹配:

_path = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
var name = Process.GetCurrentProcess().MainModule.ModuleName.Split('.')[0];
if (_path.EndsWith("\\") == false) _path += "\\";
var pp = Process.GetProcessesByName(name);
var found = 0;
foreach (var pi in pp)
{
	if (pi.MainModule.FileName.StartsWith(_path))
        found++;
}
if(found == 1)
{
    // run app here
}
else
    MessageBox.Show("Only one instance at a time");

如果找到的 count = 1(这包括自身),那么我们就可以运行了,否则已经有一个在运行了。

HTML 渲染 vs 浏览器控件

起初,出于安全原因,我决定使用各种 HTML 渲染控件来渲染订阅内容,但这些控件都不适合我需要的任务,而且都有各自的局限性,所以都被放弃了。

经过大量测试,我退回到使用 WinForms 浏览器控件(这是一个非常老的 IE 包装器),但对于 RealNews 来说,它工作得很好,因为应用程序的 HTML 清理器会移除所有 JavaScript 和潜在的威胁标签,而浏览器控件可以处理这些(对于较新的网页,IE 控件会因不理解的 JavaScript 而产生很多错误)。

我还查看了 Chrome 浏览器控件 for Winforms,但这会使应用程序的大小增加 50MB,对于当前的需求来说是过多的。

图标

黑白图标来自 FontAwesome Web 图标字体,这些图标是由 我在 CodeProject 上找到的一个小程序提取的,该程序可以为您需要的任何图像尺寸生成 .png 文件。

带 target=_blank 的 a 标签

如果描述中的 HTML 包含带有 target 设置的 a 标签,那么浏览器控件会打开 IE 到该 URL,这是一个灾难。为了克服这一点,并在您的默认浏览器中打开这些标签,您需要添加以下代码(还需要添加对“Microsoft Internet Controls”的 COM 引用):

(this.webBrowser1.ActiveXInstance as SHDocVw.WebBrowser).NewWindow3 += FrmMain_NewWindow3;  

private void FrmMain_NewWindow3(ref object ppDisp, ref bool Cancel, 
                                uint dwFlags, string bstrUrlContext, string bstrUrl)
{
	// fix for a tags with target = new window
	Cancel = true;
	Process.Start(bstrUrl);
}

MoveNextUnread() 的多个版本

这个方法经历了大量的修订和调整,最初是大量的 foreach 循环,不支持文件夹/类别,最终精简为一些非常酷的 linq 语句。

var found = false;
if (_currentFeed.Folder != "") // in a folder 
{
	var f = _feeds.FindAll(x => x.UnreadCount > 0 &&
					x.Folder != "" &&
					x.FullTitle.CompareTo(_currentFeed.FullTitle) > 0)
						  .OrderBy(x => x.FullTitle)
						  .ToList();
	if (f.Count() > 0)
	{
		_currentFeed = f[0];
		found = true;
	}
}
if (found == false)
{
    // not in folder
	var f = _feeds.FindAll(x => x.UnreadCount > 0 &&
							  x.Folder == "" )
						  .OrderBy(x => x.Title)
						  .ToList();
	if (f.Count() > 0)
		_currentFeed = f[0];
	else
	{
        // loop back to start 
		f = _feeds.FindAll(x => x.UnreadCount > 0 &&
							  x.Folder != "")
						  .OrderBy(x => x.FullTitle)
						  .ToList();
		if (f.Count() > 0)
			_currentFeed = f[0];
	}
}

这个版本似乎可以直观地向下遍历树,循环回到顶部,找到下一个未读订阅(希望如此!)。

旧版本

历史

  • 初始版本 v1.0: 2018 年 8 月 2 日
  • 更新 v1.1: 2018 年 8 月 23 日
    • 升级到 fastJSON v2.2.0
    • 搜索清除按钮仅在输入文本时可见
    • 修复搜索和当前未读计数更新
    • 添加了清理缓存中孤立图片的图像清理功能
  • 更新 v1.3: 2019 年 2 月 25 日
    • 可用性调整
    • 下载前检查图片是否已下载
    • 代码清理
    • 添加了订阅更新超时
    • settings.config 中的本地日期时间
    • 选择文件夹/类别将显示其下的所有订阅
  • 更新 v1.4: 2019 年 4 月 11 日
    • 添加了 alt+向上和向下键来导航订阅列表
    • 可用性调整
    • 修复了部分网站返回 403 的问题
  • 更新 v1.4.1: 2019 年 6 月 8 日
    • 在工具栏中添加了根据订阅项标题进行 Google 搜索的功能
    • 清理了 UI 样式
    • 更新到新的 zipstorer.cs
  • 更新 v1.4.3: 2019 年 8 月 23 日
    • 添加了自定义代理的使用
    • 对无法获取数据的订阅进行颜色编码
    • 搜索标题预处理插件
  • 更新 v1.4.4: 2019 年 8 月 30 日
    • 添加/编辑订阅表单验证检查
    • 添加了 OnCloseMinimize 配置,点击关闭按钮时是关闭还是最小化
    • 添加带类别的订阅可正确更新树状结构
    • 添加了 SkipFeedItemsDaysOlderThan 配置,用于在发送订阅项时不过滤较旧的项目
    • 单例应用程序现在可以正常工作,即使在托盘中也能打开现有窗口
  • 更新 v1.5.0: 2019 年 12 月 11 日
    • bug 修复:过滤没有发布日期的旧项目
    • 全部清理,重置失败订阅的颜色
    • 添加了 dark theme(深色主题)
  • 更新 v1.5.1: 2019 年 12 月 30 日
    • bug 修复:确保订阅列表末尾的项目可见
    • 加载和保存订阅时的内存清理
    • bug 修复:在 WinXP 上运行
  • 更新 v1.5.2: 2020 年 1 月 14 日
    • bug 修复:在 Win7 上运行
    • 检查缓存中长度为 0 的图片
  • 更新 v1.5.4: 2020 年 5 月 28 日
    • 用自定义 ListBox 替换 ListView,以解决主题问题
    • 图像缓存检查零长度文件
    • 缓存编译后的插件 MethodInfo
    • 内部功能标志
  • 更新 v1.5.5: 2020 年 5 月 29 日
    • bug 修复:排序订阅项目
  • 更新 v1.5.6: 2020 年 6 月 4 日
    • bug 修复
© . All rights reserved.