RSS订阅本质上就是服务端按照既定规范输出一段XML格式的文本,客户端阅读器定期请求这段XML并解析其中的文章列表。借助XML自描述、跨平台的特点,我们不需要复杂的接口协议,就能让博客、新闻站被各类聚合工具识别。

一、RSS背后的XML结构原理
RSS全称 Really Simple Syndication,目前最常用的是 RSS 2.0 规范。它要求根节点为 <rss>,并声明版本;其下包含一个 <channel> 节点,用于描述整个站点信息,例如站点名称、首页链接与描述。channel 内部可以放置多个 <item> 节点,每一个 item 就代表一篇可供订阅的文章。
为什么选 XML 而不是 JSON?因为 RSS 诞生较早,XML 在当时的工具链中解析成熟,且能通过严格标签嵌套表达层级。对阅读器来说,只要按节点名提取文本即可,不必关心服务端用什么语言生成。理解这点后,我们便能任意用 PHP、Python 或 Node.js 拼装出合法订阅源。
1.1 必填与可选节点
一个最小可用的 channel 至少要有 <title>、<link>、<description>。item 则至少需要 title 或 description 之一,并建议带有 link 与 pubDate。可选节点如 <author>、<category> 能丰富元数据,但不填也不影响基础解析。
需要注意,XML 对特殊字符敏感。如果文章标题里出现 &、< 等符号,必须写成实体如 &、<,否则阅读器会遇到格式错误而拒绝加载。这是新手最常踩的坑。
二、手写一个RSS 2.0的XML示例
下面是一段完整且合法的 RSS 2.0 文档示例。我们在代码中用 PHP 数组模拟文章,再拼接成 XML 字符串。实际项目中你可以用框架的响应对象直接输出,但原理一致。
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>我的技术博客</title>
<link>https://ipipp.com/blog</link>
<description>分享编程与系统架构笔记</description>
<item>
<title>如何用XML实现RSS</title>
<link>https://ipipp.com/blog/rss-xml</link>
<description>本文讲解RSS订阅的XML结构</description>
<pubDate>Mon, 01 Jan 2024 08:00:00 GMT</pubDate>
</item>
<item>
<title>性能优化误区&实践</title>
<link>https://ipipp.com/blog/perf</link>
<description>注意标题中的&已转义</description>
<pubDate>Tue, 02 Jan 2024 08:00:00 GMT</pubDate>
</item>
</channel>
</rss>
上述代码里,我们把站点信息和两条文章都写死了。你可以看到第二个 item 的标题包含 &,这就是对 & 的转义。若直接写 性能优化&实践 会导致 XML 解析器报错,订阅源在多数阅读器里直接空白。
生成时建议统一用函数处理文本,例如 PHP 的 htmlspecialchars() 默认就会转义 &、"、'、<、>,能省去手工替换的麻烦。输出前记得设置响应头 Content-Type: application/rss+xml; charset=utf-8,让客户端正确识别。
三、服务端动态输出订阅源
真实博客不可能手写 XML,而是从数据库取最近文章再渲染。下面用 Python Flask 演示一个动态接口,访问 /feed 就返回最新五篇文章的 RSS。
from flask import Response
import xml.sax.saxutils as su
def build_rss(items):
lines = ['<?xml version="1.0" encoding="UTF-8"?>']
lines.append('<rss version="2.0"><channel>')
lines.append('<title>示例站</title>')
lines.append('<link>https://ipipp.com</link>')
lines.append('<description>动态RSS演示</description>')
for it in items:
lines.append('<item>')
lines.append('<title>' + su.escape(it['title']) + '</title>')
lines.append('<link>' + su.escape(it['url']) + '</link>')
lines.append('<description>' + su.escape(it['summary']) + '</description>')
lines.append('</item>')
lines.append('</channel></rss>')
return ''.join(lines)
@app.route('/feed')
def feed():
items = fetch_latest_posts(limit=5)
xml = build_rss(items)
return Response(xml, mimetype='application/rss+xml')
这段代码中我们使用 xml.sax.saxutils.escape 对字段做转义,避免用户提交的内容破坏结构。Flask 的 Response 直接返回字符串并声明 MIME 类型,阅读器就能像访问静态文件一样订阅。
动态源还要考虑缓存与更新频率。若文章更新不频繁,可在服务端缓存生成的 XML 一分钟,减轻数据库压力;同时保证 pubDate 使用标准 RFC 822 时间格式,否则部分老牌阅读器会忽略排序。
四、RSS与Atom的简单对比
除了 RSS 2.0,另一种常见订阅格式是 Atom。Atom 同样基于 XML,但根节点是 <feed>,文章节点叫 <entry>。Atom 对时间格式要求更严,必须使用带时区的 ISO 8601,如 2024-01-01T08:00:00Z。
| 对比项 | RSS 2.0 | Atom |
|---|---|---|
| 根节点 | rss | feed |
| 文章节点 | item | entry |
| 时间格式 | RFC 822 | ISO 8601 |
对新项目而言,RSS 2.0 的客户端覆盖率更高,资料也多;Atom 在严谨性和扩展性上略优。若只做基础文章推送,RSS 2.0 配合本文的 XML 写法已经足够。
五、常见错误与排查建议
当阅读器提示获取失败,第一步应用浏览器直接打开订阅地址,看返回的源码是否以 <?xml 开头且标签闭合。很多框架会前置注入 HTML 或日志,导致 XML 前面多了空行,阅读器便判定格式非法。
另一个易错点是编码声明和实际不一致。若 XML 头写 encoding="UTF-8",文件却以 GBK 保存,中文会乱码。统一用 UTF-8 无 BOM 格式输出最稳妥。只要保证节点完整、字符转义、编码正确,利用 XML 实现 RSS 订阅其实非常直接。