RSS 技术订阅方案实现指南

RSS 技术订阅方案实现指南

RSS 技术订阅方案实现指南

浏览器插件RSS作者:Stars发布:项目:MS-ToDo Hub
RSSCloudflare Worker

一、什么是 RSS?

RSS(Really Simple Syndication / Rich Site Summary)是一种基于 XML 的内容聚合格式,用于发布经常更新的信息,如博客文章、新闻标题、播客等。它允许用户通过“RSS 阅读器”订阅网站内容,无需逐一访问各个网站即可获取最新内容更新。

RSS 的核心价值

  • 信息主权:用户自主决定订阅什么,不依赖算法推荐
  • 无广告干扰:纯内容获取,没有广告和追踪器
  • 高效阅读:多个信息源聚合到一个界面
  • 开放协议:任何人都可以构建 RSS 源和阅读器

二、RSS 生态系统概览

一个完整的 RSS 订阅方案包含以下三个核心角色:

┌─────────────┐      RSS Feed (XML)      ┌──────────────┐      展示内容      ┌──────────────┐
│  内容发布端   │  ──────────────────────> │  RSS 聚合服务  │  ──────────────> │  RSS 阅读器   │
│ (网站/博客)   │                         │ (解析/存储)    │                  │ (用户界面)    │
└─────────────┘                          └──────────────┘                  └──────────────┘
  1. 内容发布端:网站生成符合 RSS/Atom 规范的 XML 文件
  2. RSS 聚合/解析服务:负责拉取、解析、存储 RSS 源的内容
  3. 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 │
  │ (发布者) │               │ (中枢) │               │  (订阅者)   │               │ (读者) │
  └────────┘                └────────┘               └────────────┘               └────────┘
  1. Publisher 发布新内容,通知 Hub
  2. Hub 收到通知后,向所有 Subscriber 发送回调
  3. 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 订阅方案的技术实现可以归纳为三个层次:

  1. 基础层:XML 解析与内容提取 —— 理解 RSS/Atom 规范,正确解析源内容
  2. 服务层:定时抓取、增量更新、条件请求 —— 构建高效可靠的聚合后端
  3. 体验层:实时推送、全文获取、内容清洗 —— 提供优质的阅读体验

对于个人项目,可以选择“纯前端方案”快速验证;对于生产级产品,建议采用“服务端聚合 + WebSub 实时推送”的组合方案,兼顾可靠性与实时性。

RSS 作为开放的信息聚合协议,在算法推荐横行的今天,为用户保留了“主动选择信息”的权利。掌握 RSS 技术实现,不仅是工程能力的体现,更是对信息自由理念的践行。

关于作者

Stars · 独立开发者

专注于浏览器扩展、AI 编程、Web 开发与 3D / 游戏开发。

正在开发:MS-ToDo Hub ·极速狂飙

关于作者 →