Show HN:Feedsmith —— 快速解析器与生成器,支持 RSS、Atom、OPML 等 Feed 命名空间

简介

macieklamberski / feedsmith

一个强大且快速的解析器和生成器,用于处理 RSS、Atom、JSON Feed 和 RDF feeds,同时支持 Podcast、iTunes、Dublin Core 和 OPML 文件。

feedsmith.dev

许可证

MIT license

37 stars 0 forks

macieklamberski/feedsmith

main

Feedsmith

tests npm version license

一个强大且快速的 JavaScript 解析器和生成器,用于处理 RSS、Atom、JSON Feed 和 RDF feeds,同时支持流行的命名空间和 OPML 文件。它提供了通用和格式特定的解析器,既能保持原始 feed 结构,又能提供有用的标准化功能。

Feedsmith 以清晰的、面向对象的方式维护原始 feed 结构。它能智能地标准化旧版元素,让您可以完全访问所有 feed 数据,而不会牺牲简洁性。

功能特性 · 安装 · 解析 · 生成 · 基准测试 · 常见问题解答

宽容性

性能和类型安全

兼容性

支持的格式

✅ 可用 · ⌛️ 开发中 · 📋 计划中

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 &lt;em&gt;only&lt;/em&gt;</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 格式和版本的解析器函数返回的完整对象。

错误处理

如果无法识别 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 的主要优势在于它可以准确