RSS - Really Simple Syndication

1. 背景

21世纪的互联网,最不缺的是资讯,我们生活在被大量资讯包裹的时代,在马路上、微信中、微博里时刻都被大量资讯冲刷着,而在面对一个独立个体想要追溯自身感兴趣的资讯时,ta不得不拨开层层云雾(APP首屏广告 - 多次点击),才能见到光☀️,因此基于这一个角度,我开发了浏览器扩展程序 - Tops,它能够实时提供各大互联网平台的热门资讯,免去大量繁杂的手指操作,资讯像是跳脱出来似的直接映入用户眼帘,慢慢地,Tops从最初的1-2个资讯渠道,演变为如今的10个资讯渠道,但随着Tops用户群体越来越多,丰富资讯渠道的需求也变得越来越紧迫,而采集不同的新资讯平台并不是一件简单的事情,在一次机缘巧合中了解到了RSS,得知它能够提供不同资讯渠道的固定内容资讯条目,借助该协议,似乎可以实现用户自定义资讯渠道

2. 概述

RDF: Resource Description Framework

RSS的全称是Really Simple Syndication,中文是简易信息聚合(也称之为聚合内容),它还可以作为RDF Site Summary(资源描述框架站点摘要) 和 Rich Site Summary(网站内容摘要) 的缩写,三种不同的称呼指向都是同一种Syndication技术

什么是Syndication技术?

Syndication(合成技术)一般用于博客、新闻、音频和视频等内容的分发,它实际上是通过特定标准化的格式和协议,将信息从一个初始站点源传递到多个目标,使得信息能够被最简单地进行传播,RSS、Atom、JSON Feed都是Syndication技术的实现

RSS是一种基于XML标准的信息聚合协议,一共存在3个主要版本:0.91, 1.0, 2.0,最早于1999年3月开始发展,于2005-2006年开始得到广泛使用,于2009年发布了RSS 2.0版本(也是如今最为流行的版本)

RSS一般用于包装信息内容,通过特定的格式进行传播,以我为例子打个比方,日常生活中我关注的资讯渠道五花八门,早晨我会固定阅读少数派平台的“派早报”以了解最前沿的资讯,中午我会关注新浪微博、知乎等等来了解时事,晚间我会阅读一些高质量的微信个人公众号推文,上述的所有平台,都需要我每次打开对应的网页/APP来主动找寻它们,而如果有了RSS订阅服务,我可以通过订阅以上平台,并在一个RSS内容聚合站点来访问以上所有渠道的最新资讯,整个流程会转换为如下:

RSS Process

打个比方,在平时作为知乎用户期望阅读知乎每日精选,一般是用户主动进入知乎站点,进行资讯阅览,而知乎官方提供了一份RSS文件,通过HTTP的方式可以直接访问这份文件的内容,从而根据文件的内容可以解析进而得到相同的文章,如下是通过知乎官方RSS链接(https://www.zhihu.com/rss)进行获取的每日精选列表(RSS数据格式):

Tips: 由于以下的原生RSS数据内容过多,下述XML中仅保留一个文章条目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>知乎每日精选</title>
<link>http://www.zhihu.com</link>
<description>中文互联网最大的知识平台,帮助人们便捷地分享彼此的知识、经验和见解</description>
<atom:link href="http://www.zhihu.com/rss" rel="self"></atom:link>
<language>zh-cn</language>
<copyright>© 2024 知乎(http://www.zhihu.com)</copyright>
<lastBuildDate>Wed, 31 Jan 2024 11:59:49 +0800</lastBuildDate>
<ttl>180</ttl>
<item>
<title>有哪些令人叹为观止的细节?</title>
<link>http://www.zhihu.com/question/63537524/answer/3364481763?utm_campaign=rss&amp;utm_medium=rss&amp;utm_source=rss&amp;utm_content=title</link>
<description>马克西米利安的凯旋门(The Triumphal Arch),是神圣罗马帝国皇帝马克西米.....</description>
<dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Theuerdank</dc:creator>
<pubDate>Wed, 31 Jan 2024 11:59:49 +0800</pubDate>
<guid>http://www.zhihu.com/question/63537524/answer/3364481763</guid>
</item>
</channel>
</rss>

通过以上RSS内容,可以识别到该RSS源的标题(rss.channel.title)为”知乎每日精选”,同时能够获得该站点的描述(rss.channel.description)以及最重要的文章列表(rss.channel.item),所有的资讯条目都将以item节点的方式提供,item节点主要会提供标题、链接、描述和资讯发布时间等统一的字段以供订阅方根据这些字段来识别并定位一个资讯条目

而一般来说,配套使用的还会有对应的RSS阅读器(也就是内容聚合平台,Feedly是当今最受欢迎的RSS阅读器)来负责RSS数据的定期拉取与解析,将最新的资讯统一呈现给用户

总的来说,RSS的存在可以使得用户不再受推荐算法的侵扰,所阅读的资讯均是由用户精准确认过的,说白了就是对抗推荐算法,但在当今流量时代,可以见得资本主义无法容忍这种行为,因此各大互联网平台的反爬机制都相当严格,比如微信公众号和小红书

Tips: RSSHub提供了大量常见的RSS订阅源链接

3. 格式

现阶段,主要流行的RSS格式为2.0版本,通过RSS数据中的 <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">可以确定对应的RSS版本,其中包含3个必填字段和16个可选字段

Tips:下述字段中仅筛选出了常见的字段

a. 根节点必填字段

字段名称 作用 示例
title 一般用于标识源站的名称 <title>知乎每日精选</title>
link 通常用于标识源站的资源地址,资源地址可以不一定是HTTP链接,比如ftp://或者news://等等都是可以被接受的 <link>http://www.zhihu.com</link>
description 用于描述源站,字段虽然无做内容格式限制,但建议只存放存文本更加友好 <description>This is a nice RSS 2.0 feed of an even nicer weblog</description>

b. 根节点选填字段

字段名称 作用 示例
language 用于描述RSS资讯的语言,该代码需要遵循RFC 1766标准 <language>en-US</language>
copyright 用于描述版权相关的信息 <copyright>Copyright zchengb</copyright>
managingEditor 一般存储的是RSS源的负责人邮箱 <managingEditor>ben@benhammersley.com (Ben Hammersley)</managingEditor>
webMaster 存储的是RSS源负责人的邮件地址 <webMaster>techsupport@benhammersley.com (Geek McNerdy)</webMaster>
pubDate RSS源资讯条目的发布时间,时间格式运用的是RFC 822标准,这个时间可以是未来的时间,如果是未来的时间,则时代的是即将要发布的资讯条目 <pubDate>Sun, 12 Sep 2004 19:00:40 GMT</pubDate>
lastBuildDate 资讯条目的变更时间,时间格式同样是RFC 822标准,同时这个时间只能是过去时间 <lastBuildDate>Sun, 12 Sep 2004 19:00:40 GMT</lastBuildDate>
generator 表示创建了该RSS源的程序 <generator>Movable Type v3.1b3</generator><br/>
docs RSS文档链接 <docs>http://blogs.law.harvard.edu/tech/rss</docs>
ttl 表示“Time-to-Live”的缩写,应包含一个数字,表示RSS阅读器应从源处刷新 Feed 的最短分钟数 <ttl>60</ttl>
image 描述 资讯源站 的图像,具有三个必需和两个可选的子元素:url、title、link、width和height <image><url>http://www.exampleurl.com/example/ images/logo.gif</url></image>

c. 资讯条目字段

Tips: 以下字段均包含于channel -> item

字段名称 作用 示例
title 资讯条目标题 <title>Some Title</title>
link 资讯条目对应的链接 <link>http://www.example.com/some-story</link>
description 资讯条目简介 <description>A synopsis of the story</description>
author 资讯资源的作者邮箱地址 <author>example@example.com (Example Author)</author>
enclosure 与资讯相关联的文件 <enclosure url="http://www.example.com/some-file" length="12345" type="audio/mpeg"/>
guid 全局唯一标识符,应包含唯一标识条目的字符串 <guid isPermalink="true">http://www.example.com/some-story</guid>
pubDate 资讯条目的发布时间 <pubDate>Mon, 13 Sep 2004 00:23:05 GMT</pubDate>

4. 解析

以下提供了使用Java将RSS源解析为JSON的例子,其中主要使用的工具包为com.rometools:rome:2.1.0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Slf4j
@Service
@RequiredArgsConstructor
public class RssApplicationService {


public RssResourceDto parse(String rssUrl) {
try {
var url = new URL(rssUrl);
var feed = new SyndFeedInput().build(new XmlReader(url.openStream()));
var syndEntries = feed.getEntries();
var dateFormatter = new SimpleDateFormat(DateFormatConfig.PATTERN, Locale.getDefault());
var rssItems = syndEntries.stream()
.map(entry -> {
var link = entry.getLink();
var thumbnail = parseRssItemThumbnail(entry);
var rssItem = RssResourceDto.RssItem.builder()
.title(entry.getTitle())
.link(link)
.thumbnail(thumbnail)
.description(entry.getDescription().getValue());
var publishedDate = entry.getPublishedDate();
if (Objects.nonNull(publishedDate)) {
rssItem.publishDate(dateFormatter.format(publishedDate));
}
return rssItem.build();
}
).collect(Collectors.toList());

return RssResourceDto.builder()
.url(rssUrl)
.title(feed.getTitle())
.link(feed.getLink())
.description(feed.getDescription())
.author(feed.getAuthor())
.items(rssItems)
.build();
} catch (IOException | FeedException e) {
log.error("Invalid RSS feed URL: " + rssUrl, e);
throw new IllegalArgumentException("无法解析RSS地址内容");
}
}

private String parseRssItemThumbnail(SyndEntry entry) {
return entry.getEnclosures().stream()
.filter(enclosure -> enclosure.getType().startsWith("image"))
.findFirst()
.map(SyndEnclosure::getUrl)
.orElse(null);
}
}