RSS订阅用久了,一个很现实的麻烦会浮现出来:关注的源越加越多,阅读器里订阅列表越来越长,不同类别的信息混在一起反而降低了阅读效率。比如你同时关注十几个技术博客、几个新闻站点和两三个播客,每次打开阅读器都要在不同分类之间来回切换。其实这个问题有一个很经典的解法:在源和阅读器之间加一层聚合服务,把多个RSS源合并成一个Feed,阅读器里只订阅这一个地址即可。本文介绍几种从易到难的合并方案,涵盖在线服务、开源自建和手写脚本三种路线。

一、为什么合并RSS源比逐个订阅更好
先说清楚合并带来的实际收益,你才能判断这件事值不值得做。最直接的好处是简化订阅管理。阅读器里只保留一个订阅地址,源的增加、删除、替换都在聚合层完成,阅读器完全无感知。当你更换阅读器时,只需要迁移一个订阅,而不是几十个。
第二个好处是统一的输出格式和顺序。不同站点的RSS在更新频率、时间戳格式、内容截断方式上差异很大,有的源时间字段甚至有偏差。在聚合层你可以统一按发布时间排序、补全时间戳、过滤掉某些关键词的内容,输出一个干净的Feed。
第三个好处是隐藏真实订阅行为。你订阅的源列表只存在于聚合服务端,阅读器服务商看不到你具体关注了哪些站点,对隐私敏感的用户来说这是个不小的加分项。
二、使用在线合并服务快速生成单一Feed
如果不想自己维护服务器,在线服务是最省事的入口。这类工具的原理都一样:你在后台填入若干个RSS地址,它定时抓取这些源,合成一个新的Feed地址给你。比较有代表性的包括RSS.app、Feedink、Inoreader的付费版捆绑功能等。以RSS.app为例,创建一个Feed集合,把源地址逐个粘贴进去,保存后会得到一个形如https://rss.app/feeds/xxxx.xml的新地址,把它添加到任何阅读器就能收到合并后的内容。
在线服务的优点是零维护、上手快,缺点也明显:免费版通常限制源数量或刷新频率,服务关闭时你的合并Feed会随之失效,而且抓取间隔不受你控制,时效性可能延迟半小时甚至更久。对于只是想把三五个源归拢起来的轻度用户,这条路完全够用;但如果源的数量超过十个,或者你对实时性有要求,建议看下面的自建方案。
三、自建RSSHub或RSS-Bridge实现可控聚合
RSSHub是开源社区维护最活跃的RSS生成与聚合工具之一,用Node.js编写,官方提供了大量路由规则。自建的第一步是部署服务,最简单的方式是Docker:
docker run -d --name rsshub -p 1200:1200 diygod/rsshub # 部署完成后访问 http://127.0.0.1:1200 检查是否正常
部署好之后,除了使用它内置的路由,还可以利用其.merge接口实现多个源的合并。调用方式是把你想要合并的源经过Base64编码后拼到路由里:
# 假设要合并两个源 # 源1: https://blog-a.com/feed.xml # 源2: https://blog-b.com/rss # 先做URL安全的Base64编码,然后用逗号连接 echo -n "https://blog-a.com/feed.xml,https://blog-b.com/rss" | base64 -w0 # 得到编码串后拼接访问地址 http://127.0.0.1:1200/merge/aHR0cHM6Ly9ibG9nLWEuY29tL2ZlZWQueG1sLGh0dHBzOi8vYmxvZy1iLmNvbS9yc3M=
把这个最终地址添加到阅读器,就完成了合并。RSSHub会按条目时间统一排序输出,且支持缓存配置,你可以通过环境变量CACHE_EXPIRE控制抓取频率,避免高频请求被源站封禁。
RSS-Bridge是另一个轻量选择,PHP编写,部署在任意支持PHP的虚拟主机上即可运行。它的优势是对老式主机友好,资源占用极低,缺点是路由生态不如RSSHub丰富,聚合功能需要借助第三方Action实现。两者选型时可以简单记为:有Docker环境选RSSHub,只有PHP主机选RSS-Bridge。
四、用PHP或Python脚本自写聚合逻辑
如果合并需求包含个性化规则,比如去重、关键词过滤、按自定义权重排序,直接写脚本是最灵活的。核心思路分三步:抓取所有源、解析XML提取条目、合并排序后输出标准RSS格式。下面是一个PHP实现的最小可用版本:
<?php
// 待合并的源列表
$sources = [
'https://blog-a.com/feed.xml',
'https://blog-b.com/rss',
'https://news-c.com/feed',
];
$items = [];
foreach ($sources as $url) {
$xml = @simplexml_load_file($url);
if ($xml === false) {
continue; // 某个源挂掉不影响整体输出
}
// 兼容 RSS 和 Atom 两种格式
if (isset($xml->channel)) {
$entries = $xml->channel->item;
foreach ($entries as $item) {
$items[] = [
'title' => (string)$item->title,
'link' => (string)$item->link,
'date' => strtotime((string)$item->pubDate),
'desc' => (string)$item->description,
];
}
} else {
foreach ($xml->entry as $entry) {
$items[] = [
'title' => (string)$entry->title,
'link' => (string)$entry->link['href'],
'date' => strtotime((string)$entry->updated),
'desc' => (string)$entry->summary,
];
}
}
}
// 按发布时间倒序排列
usort($items, fn($a, $b) => $b['date'] - $a['date']);
// 输出标准RSS 2.0
header('Content-Type: application/xml; charset=utf-8');
echo '<?xml version="1.0" encoding="UTF-8"?>';
?>
<rss version="2.0">
<channel>
<title>我的聚合Feed</title>
<link>https://example.ipipp.com</link>
<description>多个源合并后的统一输出</description>
<?php foreach ($items as $it): ?>
<item>
<title><?= htmlspecialchars($it['title']) ?></title>
<link><?= htmlspecialchars($it['link']) ?></link>
<pubDate><?= date('r', $it['date']) ?></pubDate>
<description><![CDATA[<?= $it['desc'] ?>]]></description>
</item>
<?php endforeach; ?>
</channel>
</rss>这段代码做了几件值得注意的事:其一,用@simplexml_load_file加错误抑制,保证单个源失效时整个Feed依然可用,这在生产环境很重要;其二,同时兼容RSS 2.0的item结构和Atom的entry结构,因为现实中两类源都会遇到;其三,输出时对标题和链接做了htmlspecialchars转义,正文用CDATA包裹,避免源内容中的特殊字符破坏XML结构。
偏好Python的话,用feedparser库会更省事,它自动处理RSS与Atom的差异以及格式错误的源:
import feedparser
sources = [
'https://blog-a.com/feed.xml',
'https://blog-b.com/rss',
]
items = []
for url in sources:
d = feedparser.parse(url)
for e in d.entries:
items.append({
'title': e.get('title', ''),
'link': e.get('link', ''),
'date': e.get('published_parsed'),
})
# 按时间倒序,取最新的50条
items.sort(key=lambda x: x['date'] or 0, reverse=True)
items = items[:50]自写脚本的维护成本在于源站格式变更时需要跟进调整,以及要自己处理缓存——建议把抓取结果落地为本地文件,设置一个缓存有效期,避免阅读器每次请求都触发对源站的抓取。
五、合并时的去重、缓存与排序细节
合并多源后有几个容易踩的坑。首先是重复条目:有些站点同一篇文章会出现在RSS和Atom两个源里,如果都订阅了就会收到两次。处理办法是对条目链接做规范化后去重,或者用标题的哈希值作为去重键。其次是时间戳缺失,个别源不输出发布时间,脚本里要给默认值兜底,否则排序会出错。
缓存策略直接决定聚合服务的存活时间。对上游源的抓取频率建议控制在15分钟以上,并在脚本侧缓存解析结果。如果用的是RSSHub,它自带缓存层,只需要调整CACHE_EXPIRE参数;自写脚本则可以用文件的修改时间做简单的过期判断。
最后是输出条数控制。阅读器对单个Feed的条目上限处理不一,合并后条目可能达到几百条,建议只保留最新的50到100条,既能覆盖正常阅读量,又能减小Feed体积、加快阅读器拉取速度。
总结一下选型思路:三五个源、不想折腾,用在线合并服务;十几个源、有服务器,部署RSSHub最稳妥;需要去重过滤等定制规则,就自己写脚本。无论哪条路线,核心都是把分散的订阅收敛到一个地址,让信息获取回到简单有序的状态。