Show HN: Feedsmith — Fast parser & generator for RSS, Atom, OPML feed namespaces
Show HN:Feedsmith —— 快速解析器与生成器,支持 RSS、Atom、OPML 等 Feed 命名空间
简介
一个强大且快速的解析器和生成器,用于处理 RSS、Atom、JSON Feed 和 RDF feeds,同时支持 Podcast、iTunes、Dublin Core 和 OPML 文件。
许可证
macieklamberski/feedsmith
main
Feedsmith
一个强大且快速的 JavaScript 解析器和生成器,用于处理 RSS、Atom、JSON Feed 和 RDF feeds,同时支持流行的命名空间和 OPML 文件。它提供了通用和格式特定的解析器,既能保持原始 feed 结构,又能提供有用的标准化功能。
Feedsmith 以清晰的、面向对象的方式维护原始 feed 结构。它能智能地标准化旧版元素,让您可以完全访问所有 feed 数据,而不会牺牲简洁性。
功能特性 · 安装 · 解析 · 生成 · 基准测试 · 常见问题解答
宽容性
- 标准化旧版元素 ✨ — 将 feed 元素升级到其现代等效项,这样您就无需担心读取旧格式的 feeds。
- CaSe INSENsiTive — 处理任何大小写(小写、大写、混合大小写)的字段和属性。
性能和类型安全
- 快速解析 — JavaScript 中最快的 feed 解析器之一(请参阅[基准测试](https://github.com/macieklamberski/<#benchmarks>))。
- 类型安全 API — TypeScript 类型定义可用于每种 feed 格式,从而可以轻松处理数据。
- Tree-shakable — 只包含你需要的库的部分,减小 bundle 大小。
- 经过良好测试 — 包含 1200 多个测试用例和 99% 代码覆盖率的综合测试套件。
兼容性
- 可在 Node.js 和所有现代浏览器中使用。
- 可与纯 JavaScript 一起使用,你无需使用 TypeScript。
支持的格式
✅ 可用 · ⌛️ 开发中 · 📋 计划中
Feeds
| 格式 | 版本 | 解析 | 生成 | | :------------------------------------------------------------- | :--------- | :--- | :--- | | RSS | 0.9x, 2.0 | ✅ | ⏳ | | Atom | 0.3, 1.0 | ✅ | ⏳ | | JSON Feed | 1.0, 1.1 | ✅ | ✅ | | RDF | 0.9, 1.0 | ✅ | ⏳ |
命名空间
| 名称 | 前缀 | 支持于 | 解析 | 生成 |
| :---------------------------------------------------------------- | :------------ | :-------- | :--- | :--- |
| Atom | <atom:*>
, <a10:*>
| RSS, RDF | ✅ | ⏳ |
| Dublin Core | <dc:*>
| RSS, Atom, RDF | ✅ | ⏳ |
| Syndication | <sy:*>
| RSS, Atom, RDF | ✅ | ⏳ |
| Content | <content:*>
| RSS, RDF | ✅ | ⏳ |
| Slash | <slash:*>
| RSS, Atom, RDF | ✅ | ⏳ |
| iTunes | <itunes:*>
| RSS, Atom | ✅ | ⏳ |
| Podcast | <podcast:*>
| RSS | ✅ | ⏳ |
| Media RSS | <media:*>
| RSS, Atom, RDF | ✅ | ⏳ |
| Geo RSS | <georss:*>
| ⏳ | ⏳ | ⏳ |
| Dublin Core Terms | <dcterms:*>
| 📋 | 📋 | 📋 |
| Administrative | <admin:*>
| 📋 | 📋 | 📋 |
| Atom Threading | <thr:*>
| 📋 | 📋 | 📋 |
其他
| 格式 | 版本 | 解析 | 生成 | | :---------------------------------- | :--------- | :--- | :--- | | OPML | 1.0, 2.0 | ✅ | ✅ |
安装
npm install feedsmith
解析
通用 Feeds 解析器
解析任何 feed 的最简单方法是使用通用的 parseFeed
函数:
import { parseFeed } from 'feedsmith'
const { type, feed } = parseFeed('feed content')
console.log('Feed type:', type) // → rss, atom, json, rdf
console.log('Feed title:', feed.title)
if (type === 'rss') {
console.log('RSS feed link:', feed.link)
}
专用 Feeds 解析器
如果您提前知道格式,则可以使用格式特定的解析器:
import { parseAtomFeed, parseJsonFeed, parseRssFeed, parseRdfFeed } from 'feedsmith'
// Parse the feed content
const atomFeed = parseAtomFeed('atom content')
const jsonFeed = parseJsonFeed('json content')
const rssFeed = parseRssFeed('rss content')
const rdfFeed = parseRdfFeed('rdf content')
// Then read the TypeScript suggestions for the specific feed type
rssFeed.title
rssFeed.dc?.creator
rssFeed.dc?.date
rssFeed.sy?.updateBase
rssFeed.items?.[0]?.title
OPML 解析器
解析 OPML 文件非常简单:
import { parseOpml } from 'feedsmith'
// Parse the OPML content
const opml = parseOpml('opml content')
// Then read the TypeScript suggestions
opml.head?.title
opml.body?.outlines?.[0].text
opml.body?.outlines?.[1].xmlUrl
返回值
从解析器函数返回的对象非常全面,旨在重建实际的 feed 结构及其值,包括所有受支持的命名空间。 以下是一些可用示例。
import { parseAtomFeed } from 'feedsmith'
const atomFeed = parseAtomFeed(`
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Example Feed</title>
<id>example-feed</id>
<dc:creator>John Doe</dc:creator>
<dc:contributor>Jane Smith</dc:contributor>
<dc:date>2022-01-01T12:00+00:00</dc:date>
<dc:description>This is an example of description.</dc:description>
<sy:updateBase>2000-01-01T12:00+00:00</sy:updateBase>
<sy:updatePeriod>hourly</sy:updatePeriod>
<sy:updateFrequency>1</sy:updateFrequency>
<entry>
<title>Example Entry</title>
<id>example-entry</id>
<dc:creator>Jack Jackson</dc:creator>
<dc:date>2022-01-01T12:00+00:00</dc:date>
</entry>
</feed>
`)
atomFeed.title // → Example Feed
atomFeed.dc?.contributor // → Jane Smith
atomFeed.dc?.date // → 2022-01-01T12:00+00:00
atomFeed.sy?.updateFrequency // → 1
atomFeed.entries?.[0]?.title // → Example Entry
atomFeed.entries?.[0]?.dc?.creator // → Jack Jackson
返回值:
{
"id": "example-feed",
"title": "Example Feed",
"entries": [
{
"id": "example-entry",
"title": "Example Entry",
"dc": {
"creator": "Jack Jackson",
"date": "2022-01-01T12:00+00:00"
}
}
],
"dc": {
"creator": "John Doe",
"description": "This is an example of description.",
"contributor": "Jane Smith",
"date": "2022-01-01T12:00+00:00"
},
"sy": {
"updatePeriod": "hourly",
"updateFrequency": 1,
"updateBase": "2000-01-01T12:00+00:00"
}
}
更复杂的 RSS feed 示例 📜
import { parseRssFeed } from 'feedsmith'
const rssFeed = parseRssFeed(`
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title><![CDATA[Sample Feed]]></title>
<link>http://example.org/</link>
<description>For documentation <em>only</em></description>
<language>en</language>
<webMaster>webmaster@example.org</webMaster>
<pubDate>Sat, 19 Mar 1988 07:15:00 GMT</pubDate>
<lastBuildDate>Sat, 19 Mar 1988 07:15:00 GMT</lastBuildDate>
<category domain="http://www.example.com/cusips">Examples2</category>
<generator>Sample Toolkit</generator>
<docs>http://feedvalidator.org/docs/rss2.html</docs>
<cloud domain="rpc.example.com" port="80" path="/RPC2" registerProcedure="pingMe" protocol="soap" />
<ttl>60</ttl>
<image>
<title>Example banner</title>
<url>http://example.org/banner.png</url>
<link>http://example.org/</link>
<description>Quos placeat quod ea temporibus ratione</description>
<width>80</width>
<height>15</height>
</image>
<textInput>
<title>Search</title>
<description><![CDATA[Search this site:]]></description>
<name>q</name>
<link>http://example.org/mt/mt-search.cgi</link>
</textInput>
<skipHours>
<hour>0</hour>
<hour>20</hour>
<hour>21</hour>
<hour>22</hour>
<hour>23</hour>
</skipHours>
<skipDays>
<day>Monday</day>
<day>Wednesday</day>
<day>Friday</day>
</skipDays>
<item>
<title>First item title</title>
<link>http://example.org/item/1</link>
<description>Some description of the first item.</description>
<comments>http://example.org/comments/1</comments>
<enclosure url="http://example.org/audio/demo.mp3" length="1069871" type="audio/mpeg" />
<guid isPermaLink="true">http://example.org/guid/1</guid>
<pubDate>Thu, 05 Sep 2002 0:00:01 GMT</pubDate>
<source url="http://www.example.org/links.xml">Example's Realm</source>
</item>
</channel>
</rss>
`)
rssFeed.title // → Sample Feed
rssFeed.textInput?.description // → Search this site:
rssFeed.items?.length // → 1
rssFeed.items?.[0]?.enclosure?.url // → http://example.org/audio/demo.mp3
返回值:
{
"title": "Sample Feed",
"link": "http://example.org/",
"description": "For documentation <em>only</em>",
"language": "en",
"webMaster": "webmaster@example.org",
"pubDate": "Sat, 19 Mar 1988 07:15:00 GMT",
"lastBuildDate": "Sat, 19 Mar 1988 07:15:00 GMT",
"categories": [{ "name": "Examples2", "domain": "http://www.example.com/cusips" }],
"generator": "Sample Toolkit",
"docs": "http://feedvalidator.org/docs/rss2.html",
"cloud": {
"domain": "rpc.example.com",
"port": 80,
"path": "/RPC2",
"registerProcedure": "pingMe",
"protocol": "soap"
},
"ttl": 60,
"image": {
"url": "http://example.org/banner.png",
"title": "Example banner",
"link": "http://example.org/",
"description": "Quos placeat quod ea temporibus ratione",
"height": 15,
"width": 80
},
"textInput": {
"title": "Search",
"description": "Search this site:",
"name": "q",
"link": "http://example.org/mt/mt-search.cgi"
},
"skipHours": [0, 20, 21, 22, 23],
"skipDays": ["Monday", "Wednesday", "Friday"],
"items": [
{
"title": "First item title",
"link": "http://example.org/item/1",
"description": "Some description of the first item.",
"comments": "http://example.org/comments/1",
"enclosure": {
"url": "http://example.org/audio/demo.mp3",
"length": 1069871,
"type": "audio/mpeg"
},
"guid": "http://example.org/guid/1",
"pubDate": "Thu, 05 Sep 2002 0:00:01 GMT",
"source": { "title": "Example's Realm", "url": "http://www.example.org/links.xml" }
}
]
}
OPML 文件示例 📜
import { parseOpml } from 'feedsmith'
const opml = parseOpml(`
<?xml version="1.0" encoding="utf-8"?>
<opml version="2.0">
<head>
<title>Tech Sites</title>
<dateCreated>Mon, 15 Jan 2024 09:45:30 GMT</dateCreated>
<ownerName>Jack Smith</ownerName>
</head>
<body>
<outline text="The Verge" type="rss" xmlUrl="https://www.theverge.com/rss/index.xml" htmlUrl="https://www.theverge.com/" title="The Verge" version="rss" />
<outline text="TechCrunch" type="rss" xmlUrl="https://techcrunch.com/feed/" htmlUrl="https://techcrunch.com/" title="TechCrunch" version="rss" />
</body>
</opml>
`)
opml.head?.title // → Tech Sites
opml.body?.outlines?.[0].text // → The Verge
opml.body?.outlines?.[1].xmlUrl // → https://techcrunch.com/feed/
有关更多示例,请查看源代码中的 */references 文件夹。 在那里,您将找到从各种 feed 格式和版本的解析器函数返回的完整对象。
- Atom 示例:src/feeds/atom/references
- RSS 示例:src/feeds/rss/references
- RDF 示例:src/feeds/rdf/references
- OPML 示例:src/opml/references
错误处理
如果无法识别 feed 或 feed 无效,则会抛出一个带有描述性消息的 Error
。
import { parseFeed, parseJsonFeed } from 'feedsmith'
try {
const universalFeed = parseFeed('<not-a-feed></not-a-feed>')
} catch (error) {
// Error: Unrecognized feed format
}
try {
const jsonFeed = parseJsonFeed('{}')
} catch (error) {
// Error: Invalid feed format
}
格式检测
您可以在不解析 feed 的情况下检测 feed 格式。
import { detectAtomFeed, detectJsonFeed, detectRssFeed, detectRdfFeed } from 'feedsmith'
if (detectAtomFeed(content)) {
console.log('This is an Atom feed')
}
if (detectJsonFeed(content)) {
console.log('This is a JSON feed')
}
if (detectRssFeed(content)) {
console.log('This is an RSS feed')
}
if (detectRdfFeed(content)) {
console.log('This is an RDF feed')
}
警告
Detect 函数旨在通过查找其签名来快速识别 feed 格式,例如 RSS feed 的 <rss>
标记。 但是,即使 feed 无效,该函数也可能会检测到 RSS feed。 仅在使用 parseRssFeed
函数时才会完全验证 feed。
生成
生成 JSON Feed
虽然 JSON feeds 只是可以轻松手动生成的 JSON 对象,但 generateJsonFeed
函数提供了有用的类型提示,可以帮助生成 feed。 此外,您可以将 Date 对象用于日期,这些日期会在后台自动转换为正确的格式。
import { generateJsonFeed } from 'feedsmith'
const jsonFeed = generateJsonFeed({
title: 'My Example Feed',
feed_url: 'https://example.com/feed.json',
authors: [
{
name: 'John Doe',
url: 'https://example.com/johndoe',
},
],
items: [
{
id: '1',
content_html: '<p>Hello world</p>',
url: 'https://example.com/post/1',
title: 'First post',
date_published: new Date('2019-03-07T00:00:00+01:00'),
language: 'en-US',
},
],
})
输出:
{
"version": "https://jsonfeed.org/version/1.1",
"title": "My Example Feed",
"feed_url": "https://example.com/feed.json",
"authors": [
{
"name": "John Doe",
"url": "https://example.com/johndoe",
},
],
"items": [
{
"id": "1",
"content_html": "<p>Hello world</p>",
"url": "https://example.com/post/1",
"title": "First post",
"date_published": "2019-03-06T23:00:00.000Z",
"language": "en-US",
},
],
}
注意
用于生成剩余 feed 格式的功能目前正在开发中,并将逐步推出。 有关更多信息,请参阅支持的格式。
生成 OPML
import { generateOpml } from 'feedsmith'
const opml = generateOpml({
head: {
title: 'My Feed',
dateCreated: new Date(),
},
body: {
outlines: [
{
text: 'My Feed',
type: 'rss',
xmlUrl: 'https://example.com/feed.xml',
htmlUrl: 'https://example.com',
},
],
},
})
输出:
<?xml version="1.0" encoding="utf-8"?>
<opml version="2.0">
<head>
<title>My Feed</title>
<dateCreated>Fri, 11 Apr 2025 13:05:26 GMT</dateCreated>
</head>
<body>
<outline text="My Feed" type="rss" xmlUrl="https://example.com/feed.xml" htmlUrl="https://example.com"/>
</body>
</opml>
基准测试
一组全面的基准测试,按各种文件大小进行分类,可在 /benchmarks 目录中找到。 这些基准测试是使用 Tinybench 和 Benchmark.js 执行的。
为了快速概览,以下是使用各种 JS 包和 Tinybench 解析 RSS、Atom 和 RDF feeds 的结果。 Feedsmith 的结果标有星号 (*
)。
📊 RSS feed parsing (50 files × 100KB–5MB)
┌───┬───────────────────────────────┬─────────┬──────────────┬──────────┬──────────┬──────┐
│ │ Package │ Ops/sec │ Average (ms) │ Min (ms) │ Max (ms) │ Runs │
├───┼───────────────────────────────┼─────────┼──────────────┼──────────┼──────────┼──────┤
│ 0 │ feedsmith * │ 7.34 │ 136.167 │ 128.479 │ 173.223 │ 111 │
│ 1 │ @rowanmanning/feed-parser │ 7.16 │ 139.678 │ 128.722 │ 170.903 │ 108 │
│ 2 │ @ulisesgascon/rss-feed-parser │ 4.14 │ 241.405 │ 230.806 │ 278.534 │ 63 │
│ 3 │ feedparser │ 2.50 │ 399.824 │ 374.049 │ 459.730 │ 38 │
│ 4 │ @extractus/feed-extractor │ 2.26 │ 443.065 │ 430.349 │ 460.195 │ 34 │
│ 5 │ feedme.js │ 2.05 │ 487.222 │ 443.837 │ 535.029 │ 31 │
│ 6 │ rss-parser │ 1.66 │ 603.044 │ 573.516 │ 653.683 │ 25 │
│ 7 │ @gaphub/feed │ 0.94 │ 1068.621 │ 995.044 │ 1138.913 │ 15 │
└───┴───────────────────────────────┴─────────┴──────────────┴──────────┴──────────┴──────┘
📊 Atom feed parsing (50 files × 100KB–5MB)
┌───┬───────────────────────────┬─────────┬──────────────┬──────────┬──────────┬──────┐
│ │ Package │ Ops/sec │ Average (ms) │ Min (ms) │ Max (ms) │ Runs │
├───┼───────────────────────────┼─────────┼──────────────┼──────────┼──────────┼──────┤
│ 0 │ feedsmith * │ 0.98 │ 1020.035 │ 998.660 │ 1084.180 │ 15 │
│ 1 │ @gaphub/feed │ 0.95 │ 1058.126 │ 989.001 │ 1150.486 │ 15 │
│ 2 │ @rowanmanning/feed-parser │ 0.63 │ 1580.462 │ 1563.357 │ 1607.379 │ 10 │
│ 3 │ feedparser │ 0.37 │ 2687.488 │ 2624.427 │ 2751.504 │ 6 │
│ 4 │ @extractus/feed-extractor │ 0.32 │ 3136.880 │ 3107.170 │ 3228.099 │ 5 │
│ 5 │ feedme.js │ 0.26 │ 3812.545 │ 3759.928 │ 3843.974 │ 4 │
│ 6 │ rss-parser │ 0.18 │ 5539.014 │ 5479.560 │ 5609.397 │ 3 │
└───┴───────────────────────────┴─────────┴──────────────┴──────────┴──────────┴──────┘
📊 RDF feed parsing (50 files × 100KB–5MB)
┌───┬───────────────────────────┬─────────┬──────────────┬──────────┬──────────┬──────┐
│ │ Package │ Ops/sec │ Average (ms) │ Min (ms) │ Max (ms) │ Runs │
├───┼───────────────────────────┼─────────┼──────────────┼──────────┼──────────┼──────┤
│ 0 │ @rowanmanning/feed-parser │ 13.52 │ 73.990 │ 69.404 │ 89.504 │ 203 │
│ 1 │ feedsmith * │ 10.16 │ 98.396 │ 92.418 │ 118.053 │ 153 │
│ 2 │ @extractus/feed-extractor │ 3.83 │ 260.946 │ 252.991 │ 274.432 │ 58 │
│ 3 │ feedparser │ 1.96 │ 509.686 │ 494.823 │ 530.224 │ 30 │
│ 4 │ feedme.js │ 1.40 │ 714.442 │ 661.440 │ 789.395 │ 22 │
│ 5 │ rss-parser │ 0.97 │ 1028.245 │ 985.521 │ 1107.122 │ 15 │
│ 6 │ @gaphub/feed │ 0.97 │ 1031.579 │ 1008.220 │ 1060.322 │ 15 │
└───┴───────────────────────────┴─────────┴──────────────┴──────────┴──────────┴──────┘
FAQ
为什么我应该使用 Feedsmith 而不是其他软件包?
Feedsmith 的主要优势在于它可以准确