
RSS 技术订阅方案实现指南
一、什么是 RSS?
RSS(Really Simple Syndication / Rich Site Summary)是一种基于 XML 的内容聚合格式,用于发布经常更新的信息,如博客文章、新闻标题、播客等。它允许用户通过“RSS 阅读器”订阅网站内容,无需逐一访问各个网站即可获取最新内容更新。
RSS 的核心价值
- 信息主权:用户自主决定订阅什么,不依赖算法推荐
- 无广告干扰:纯内容获取,没有广告和追踪器
- 高效阅读:多个信息源聚合到一个界面
- 开放协议:任何人都可以构建 RSS 源和阅读器
二、RSS 生态系统概览
一个完整的 RSS 订阅方案包含以下三个核心角色:
┌─────────────┐ RSS Feed (XML) ┌──────────────┐ 展示内容 ┌──────────────┐
│ 内容发布端 │ ──────────────────────> │ RSS 聚合服务 │ ──────────────> │ RSS 阅读器 │
│ (网站/博客) │ │ (解析/存储) │ │ (用户界面) │
└─────────────┘ └──────────────┘ └──────────────┘
- 内容发布端:网站生成符合 RSS/Atom 规范的 XML 文件
- RSS 聚合/解析服务:负责拉取、解析、存储 RSS 源的内容
- RSS 阅读器:将解析后的内容以友好的界面展示给用户
三、RSS 规范格式
3.1 RSS 2.0 格式
RSS 2.0 是目前最广泛使用的版本:
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>示例博客</title>
<link>https://example.com</link>
<description>一个关于技术分享的博客</description>
<language>zh-cn</language>
<lastBuildDate>Mon, 31 Aug 2026 10:00:00 GMT</lastBuildDate>
<item>
<title>如何使用 RSS 订阅技术</title>
<link>https://example.com/rss-guide</link>
<description>本文将介绍 RSS 的基本使用方法...</description>
<pubDate>Mon, 31 Aug 2026 08:00:00 GMT</pubDate>
<guid>https://example.com/rss-guide</guid>
</item>
</channel>
</rss>
3.2 Atom 格式
Atom 是 RSS 的现代化替代方案,由 IETF 标准化:
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>示例博客</title>
<link href="https://example.com"/>
<updated>2026-08-31T10:00:00Z</updated>
<author>
<name>作者名</name>
</author>
<id>tag:example.com,2026:feed</id>
<entry>
<title>如何使用 RSS 订阅技术</title>
<link href="https://example.com/rss-guide"/>
<id>tag:example.com,2026:rss-guide</id>
<updated>2026-08-31T08:00:00Z</updated>
<summary>本文将介绍 RSS 的基本使用方法...</summary>
</entry>
</feed>
四、RSS 订阅方案的技术实现
4.1 方案一:基于服务端的 RSS 聚合服务
这是最经典的实现方式,适用于构建类似 Feedly、Inoreader 这样的在线 RSS 阅读器。
架构设计
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
│ 定时调度器 │────>│ 抓取Worker │────>│ 解析引擎 │────>│ 数据库 │
│ (Scheduler) │ │ (Fetcher) │ │ (Parser) │ │ (Storage) │
└────────────┘ └────────────┘ └────────────┘ └────────────┘
│
▼
┌────────────┐
│ API 层 │
│ (REST/GraphQL)│
└────────────┘
│
▼
┌────────────┐
│ 前端阅读器 │
└────────────┘
核心实现步骤
Step 1:RSS 源管理
// 数据模型设计
const FeedSchema = {
id: 'uuid',
title: 'string', // 源名称
url: 'string', // RSS 源地址
siteUrl: 'string', // 网站地址
lastFetchedAt: 'datetime', // 上次抓取时间
fetchInterval: 'number', // 抓取间隔(秒)
etag: 'string', // HTTP ETag(用于条件请求)
lastModified: 'string', // Last-Modified 头
status: 'enum', // active / error / dead
errorCount: 'number', // 连续错误次数
};
const ArticleSchema = {
id: 'uuid',
feedId: 'uuid', // 所属源
title: 'string', // 标题
link: 'string', // 原文链接
content: 'text', // 正文内容(HTML)
summary: 'text', // 摘要
author: 'string', // 作者
publishedAt: 'datetime', // 发布时间
guid: 'string', // 全局唯一标识(用于去重)
isRead: 'boolean', // 是否已读
isStarred: 'boolean', // 是否收藏
};
Step 2:RSS 抓取与解析
import Parser from 'rss-parser';
class RSSFetcher {
constructor() {
this.parser = new Parser({
timeout: 10000,
headers: {
'User-Agent': 'MyRSSReader/1.0',
},
// 支持自定义字段映射
customFields: {
feed: ['subtitle', 'author'],
item: [
['content:encoded', 'contentEncoded'],
['media:content', 'mediaContent', { keepArray: true }],
],
},
});
}
async fetchFeed(feed) {
try {
const options = {};
// 使用 ETag 和 Last-Modified 实现条件请求,减少带宽
if (feed.etag) {
options.headers = {
...options.headers,
'If-None-Match': feed.etag,
};
}
if (feed.lastModified) {
options.headers = {
...options.headers,
'If-Modified-Since': feed.lastModified,
};
}
const result = await this.parser.parseURL(feed.url);
return {
title: result.title,
items: result.items.map(item => ({
title: item.title,
link: item.link,
content: item['content:encoded'] || item.content,
summary: item.contentSnippet,
author: item.creator || item.author,
publishedAt: item.pubDate ? new Date(item.pubDate) : new Date(),
guid: item.guid || item.link || item.title,
})),
};
} catch (error) {
console.error(`Failed to fetch feed ${feed.url}:`, error.message);
throw error;
}
}
}
Step 3:定时调度与增量更新
import cron from 'node-cron';
class FeedScheduler {
constructor(fetcher, db) {
this.fetcher = fetcher;
this.db = db;
}
// 根据源的更新频率动态调整抓取间隔
calculateNextInterval(feed) {
const avgUpdateFreq = this.db.getAverageUpdateFrequency(feed.id);
// 高频更新的源(如新闻站):每 15 分钟
// 中频更新的源(如博客):每 1 小时
// 低频更新的源(如周刊):每 6 小时
if (avgUpdateFreq < 3600) return 900; // 15 min
if (avgUpdateFreq < 86400) return 3600; // 1 hour
return 21600; // 6 hours
}
async start() {
// 每 5 分钟执行一次批量检查
cron.schedule('*/5 * * * *', async () => {
const dueFeeds = await this.db.getDueFeeds();
// 并发控制:限制同时抓取的数量
const concurrency = 10;
for (let i = 0; i < dueFeeds.length; i += concurrency) {
const batch = dueFeeds.slice(i, i + concurrency);
await Promise.allSettled(
batch.map(feed => this.processFeed(feed))
);
}
});
}
async processFeed(feed) {
try {
const result = await this.fetcher.fetchFeed(feed);
// 去重:通过 guid 判断是否为新文章
const existingGuids = await this.db.getArticleGuids(feed.id);
const newArticles = result.items.filter(
item => !existingGuids.has(item.guid)
);
if (newArticles.length > 0) {
await this.db.insertArticles(feed.id, newArticles);
// 推送通知给订阅用户(WebSocket / Server-Sent Events)
await this.notifySubscribers(feed.id, newArticles);
}
// 更新源的元信息
await this.db.updateFeedMeta(feed.id, {
lastFetchedAt: new Date(),
status: 'active',
errorCount: 0,
fetchInterval: this.calculateNextInterval(feed),
});
} catch (error) {
await this.db.incrementErrorCount(feed.id);
// 连续错误超过 10 次标记为异常
if (feed.errorCount + 1 >= 10) {
await this.db.updateFeedStatus(feed.id, 'error');
}
}
}
}
4.2 方案二:基于 WebSub 的实时推送
WebSub(前身是 PubSubHubbub)是 W3C 推荐的实时订阅协议,它变“拉取”为“推送”,实现了内容的即时分发。
工作流程
┌────────┐ 1.发布内容 ┌────────┐ 2.通知更新 ┌────────────┐ 3.拉取内容 ┌────────┐
│ Publisher│ ─────────────> │ Hub │ ─────────────> │ Subscriber │ ─────────────> │ Reader │
│ (发布者) │ │ (中枢) │ │ (订阅者) │ │ (读者) │
└────────┘ └────────┘ └────────────┘ └────────┘
- Publisher 发布新内容,通知 Hub
- Hub 收到通知后,向所有 Subscriber 发送回调
- Subscriber 收到回调后,拉取最新内容
实现代码
import express from 'express';
import crypto from 'crypto';
// ===== Subscriber 端实现 =====
class WebSubSubscriber {
constructor(hubUrl, callbackBaseUrl) {
this.hubUrl = hubUrl;
this.callbackBaseUrl = callbackBaseUrl;
this.app = express();
this.subscriptions = new Map();
this.setupCallbackEndpoint();
}
setupCallbackEndpoint() {
this.app.post('/callback/:topic', express.text(), (req, res) => {
const topic = req.params.topic;
const content = req.body;
// 验证签名(如果 Hub 支持)
const signature = req.headers['x-hub-signature'];
if (signature && !this.verifySignature(content, signature)) {
return res.status(403).send('Invalid signature');
}
// 处理新内容
this.handleNewContent(topic, content);
res.status(200).send('OK');
});
}
// 订阅某个 RSS 源
async subscribe(topicUrl) {
const callbackUrl = `${this.callbackBaseUrl}/callback/${encodeURIComponent(topicUrl)}`;
const params = new URLSearchParams({
'hub.mode': 'subscribe',
'hub.topic': topicUrl,
'hub.callback': callbackUrl,
'hub.verify': 'async',
});
const response = await fetch(this.hubUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params.toString(),
});
if (response.ok) {
this.subscriptions.set(topicUrl, { status: 'pending' });
}
}
// 验证 Hub 的验证请求
handleVerification(req, res) {
const mode = req.query['hub.mode'];
const topic = req.query['hub.topic'];
const challenge = req.query['hub.challenge'];
const leaseSeconds = req.query['hub.lease_seconds'];
if (mode === 'subscribe') {
this.subscriptions.set(topic, {
status: 'active',
leaseSeconds: parseInt(leaseSeconds),
subscribedAt: Date.now(),
});
res.status(200).send(challenge); // 必须返回 challenge 值
}
}
verifySignature(content, signature) {
const algorithm = signature.split('=')[0];
const expectedSig = signature.split('=')[1];
const hmac = crypto.createHmac(algorithm, this.secret);
hmac.update(content);
return hmac.digest('hex') === expectedSig;
}
}
4.3 方案三:客户端纯前端实现
对于轻量级场景,可以在浏览器端直接实现 RSS 解析和展示。
// 使用浏览器端解析 RSS XML
class ClientRSSReader {
constructor() {
this.feeds = new Map();
this.articles = [];
}
async addFeed(url) {
// 通过 CORS 代理获取(生产环境需要自建代理或使用 CORS 友好的 RSS 源)
const proxyUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(url)}`;
const response = await fetch(proxyUrl);
const xmlText = await response.text();
return this.parseRSS(xmlText, url);
}
parseRSS(xmlText, sourceUrl) {
const parser = new DOMParser();
const doc = parser.parseFromString(xmlText, 'text/xml');
// 检测是 RSS 还是 Atom
const isAtom = doc.querySelector('feed') !== null;
if (isAtom) {
return this.parseAtom(doc, sourceUrl);
} else {
return this.parseRSS2(doc, sourceUrl);
}
}
parseRSS2(doc, sourceUrl) {
const channel = doc.querySelector('channel');
const feed = {
title: channel.querySelector('title')?.textContent || 'Untitled',
link: channel.querySelector('link')?.textContent || sourceUrl,
description: channel.querySelector('description')?.textContent || '',
};
const items = [...doc.querySelectorAll('item')].map(item => ({
title: item.querySelector('title')?.textContent || '',
link: item.querySelector('link')?.textContent || '',
description: item.querySelector('description')?.textContent || '',
content: item.getElementsByTagNameNS('*', 'encoded')[0]?.textContent || '',
pubDate: item.querySelector('pubDate')?.textContent || '',
guid: item.querySelector('guid')?.textContent || '',
}));
return { feed, items };
}
parseAtom(doc, sourceUrl) {
const atomNS = 'http://www.w3.org/2005/Atom';
const feedEl = doc.querySelector('feed');
const feed = {
title: feedEl.querySelector('title')?.textContent || 'Untitled',
link: feedEl.querySelector('link')?.getAttribute('href') || sourceUrl,
};
const entries = [...feedEl.querySelectorAll('entry')].map(entry => ({
title: entry.querySelector('title')?.textContent || '',
link: entry.querySelector('link')?.getAttribute('href') || '',
summary: entry.querySelector('summary')?.textContent || '',
content: entry.querySelector('content')?.textContent || '',
pubDate: entry.querySelector('updated')?.textContent || '',
guid: entry.querySelector('id')?.textContent || '',
}));
return { feed, items: entries };
}
// 按时间排序合并所有源的文章
getAllArticles() {
return this.articles.sort((a, b) =>
new Date(b.pubDate) - new Date(a.pubDate)
);
}
}
五、关键技术要点
5.1 RSS 源自动发现
很多网站在 HTML 中嵌入了 RSS 源的链接,可以通过解析 HTML 自动发现:
async function discoverFeedUrl(pageUrl) {
const response = await fetch(pageUrl);
const html = await response.text();
// 匹配 <link> 标签中的 RSS/Atom 源
const feedTypes = [
'application/rss+xml',
'application/atom+xml',
'application/feed+json',
];
const regex = /<link[^>]+type=["']([^"']+)["'][^>]+href=["']([^"']+)["']/gi;
let match;
while ((match = regex.exec(html)) !== null) {
if (feedTypes.includes(match[1])) {
// 将相对路径转为绝对路径
return new URL(match[2], pageUrl).href;
}
}
return null;
}
5.2 内容清洗与正文提取
RSS 源中的内容通常包含大量 HTML 标签,需要进行清洗:
import { JSDOM } from 'jsdom';
function cleanHTMLContent(html) {
const dom = new JSDOM(html);
const doc = dom.window.document;
// 移除脚本和样式
doc.querySelectorAll('script, style, iframe, noscript').forEach(el => el.remove());
// 移除危险属性(onclick 等)
doc.querySelectorAll('*').forEach(el => {
[...el.attributes].forEach(attr => {
if (attr.name.startsWith('on') || attr.value.startsWith('javascript:')) {
el.removeAttribute(attr.name);
}
});
});
// 保留常用格式标签
const allowedTags = new Set([
'p', 'br', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'code',
'img', 'figure', 'figcaption', 'table', 'thead', 'tbody', 'tr', 'th', 'td',
]);
// 递归清洗节点
function sanitizeNode(node) {
if (node.nodeType === 3) return; // 文本节点保留
if (node.nodeType === 1) { // 元素节点
if (!allowedTags.has(node.tagName.toLowerCase())) {
// 用其内容替换掉该标签
const fragment = dom.window.document.createDocumentFragment();
while (node.firstChild) {
fragment.appendChild(node.firstChild);
}
node.parentNode.replaceChild(fragment, node);
} else {
[...node.childNodes].forEach(child => sanitizeNode(child));
}
}
}
sanitizeNode(doc.body);
return doc.body.innerHTML;
}
5.3 全文获取(Full-Text RSS)
很多 RSS 源只提供摘要,需要额外抓取全文:
import { Readability } from '@mozilla/readability';
import { JSDOM } from 'jsdom';
async function fetchFullText(articleUrl) {
const response = await fetch(articleUrl);
const html = await response.text();
const dom = new JSDOM(html, { url: articleUrl });
const doc = dom.window.document;
// 使用 Mozilla Readability 算法提取正文
const reader = new Readability(doc);
const article = reader.parse();
return {
title: article.title,
content: article.content,
textContent: article.textContent,
excerpt: article.excerpt,
};
}
六、性能优化策略
6.1 抓取层优化
| 策略 | 说明 |
|---|---|
| 条件请求 | 使用 ETag / If-None-Match 和 Last-Modified / If-Modified-Since 减少无效传输 |
| 增量解析 | 只处理新文章,通过 guid 去重 |
| 自适应频率 | 根据源的实际更新频率动态调整抓取间隔 |
| 并发控制 | 使用连接池限制并发数,避免被目标站点封禁 |
| 失败退避 | 连续失败时采用指数退避策略(1min → 2min → 4min → …) |
6.2 存储层优化
// 使用 Redis 缓存热门文章,数据库持久化全量数据
const CACHE_TTL = 3600; // 1 小时
async function getArticles(userId, page, pageSize) {
const cacheKey = `user:${userId}:articles:${page}:${pageSize}`;
// 先查缓存
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
// 查数据库(使用游标分页替代 OFFSET 提升性能)
const articles = await db.query(`
SELECT a.* FROM articles a
JOIN subscriptions s ON a.feed_id = s.feed_id
WHERE s.user_id = $1
ORDER BY a.published_at DESC
LIMIT $2
`, [userId, pageSize]);
// 写入缓存
await redis.setex(cacheKey, CACHE_TTL, JSON.stringify(articles));
return articles;
}
6.3 推送层优化
使用 Server-Sent Events (SSE) 实现服务端向客户端的实时推送:
// 服务端
app.get('/api/stream', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
// 注册此连接到消息总线
const listener = (article) => {
res.write(`data: ${JSON.stringify(article)}\n\n`);
};
eventBus.on(`user:${req.userId}:newArticle`, listener);
req.on('close', () => {
eventBus.off(`user:${req.userId}:newArticle`, listener);
});
});
七、OPML 导入导出
OPML 是 RSS 订阅列表的标准交换格式,支持导入导出是 RSS 阅读器的基本功能:
// 导出为 OPML
function exportOPML(subscriptions) {
const outlines = subscriptions.map(sub =>
`<outline type="rss" text="${escapeXml(sub.title)}"
title="${escapeXml(sub.title)}"
xmlUrl="${escapeXml(sub.feedUrl)}"
htmlUrl="${escapeXml(sub.siteUrl)}"/>`
).join('\n ');
return `<?xml version="1.0" encoding="UTF-8"?>
<opml version="2.0">
<head>
<title>My RSS Subscriptions</title>
<dateCreated>${new Date().toUTCString()}</dateCreated>
</head>
<body>
${outlines}
</body>
</opml>`;
}
// 解析 OPML 导入
function parseOPML(xmlText) {
const dom = new DOMParser().parseFromString(xmlText, 'text/xml');
const outlines = dom.querySelectorAll('outline[xmlUrl]');
return [...outlines].map(outline => ({
title: outline.getAttribute('text') || outline.getAttribute('title'),
feedUrl: outline.getAttribute('xmlUrl'),
siteUrl: outline.getAttribute('htmlUrl'),
}));
}
八、现代 RSS 服务对比
| 服务 | 特点 | 协议支持 | 开源 |
|---|---|---|---|
| Feedly | 商业级体验,AI 辅助筛选 | RSS, Atom | 否 |
| Inoreader | 功能全面,支持规则过滤 | RSS, Atom | 否 |
| Miniflux | 轻量极简,Go 实现 | RSS, Atom | 是 |
| FreshRSS | 功能丰富,PHP 实现 | RSS, Atom | 是 |
| Tiny Tiny RSS | 插件生态丰富,PHP 实现 | RSS, Atom | 是 |
| RSSHub | 万物皆可 RSS,生成源 | 自定义 | 是 |
九、总结
RSS 订阅方案的技术实现可以归纳为三个层次:
- 基础层:XML 解析与内容提取 —— 理解 RSS/Atom 规范,正确解析源内容
- 服务层:定时抓取、增量更新、条件请求 —— 构建高效可靠的聚合后端
- 体验层:实时推送、全文获取、内容清洗 —— 提供优质的阅读体验
对于个人项目,可以选择“纯前端方案”快速验证;对于生产级产品,建议采用“服务端聚合 + WebSub 实时推送”的组合方案,兼顾可靠性与实时性。
RSS 作为开放的信息聚合协议,在算法推荐横行的今天,为用户保留了“主动选择信息”的权利。掌握 RSS 技术实现,不仅是工程能力的体现,更是对信息自由理念的践行。