快速开发一个 RSSHub 路由

10 minute

注:由于 RSSHub 已经对整个项目进行了重构,本篇文档所介绍的开发流程不再适用于最新版本的 RSSHub。重构后的 RSSHub 完全使用 TS 进行开发,步骤更加清晰简洁,且官方提供的开发文档很详细,具体可参考官方开发文档。如果你还使用着未重构的老版本 RSSHub,则可以考虑参考本文。—— 2024.4.22 更新


前言

最近写了几个 RSSHub 的路由,由于官方的开发步骤较为杂乱,这里提供一个简易的开发步骤,最后还记录了手动配置部署的过程。

下面以开发 今日热榜 的榜单路由为例进行介绍。

下载安装 RSSHub

1git clone git@github.com:DIYgod/RSSHub.git && cd RSSHub
2pnpm i # npm install -g pnpm
3pnpm run dev # localhost:1200

本文中基本都是用 pnpm 进行操作(pnpm 是新一代包管理器,它解决了 npm 幻影依赖和分身问题并较好地解决了依赖包复用问题,实现了依赖包高效快速的安装),其他操作方式详见官方文档。打开浏览器访问 localhost:1200 即可看到 RSSHub index 页面。

接着在 lib/v2 下开始开发路由,一个文件夹对应一个域,这里创建文件夹 “tophub” 即可。

定义路由规则

首先要分析网站结构,今日热榜一个榜单对应一个唯一的 id,那么可以将这个 id 作为匹配对象:

1// router.js
2module.exports = (router) => {
3    router.get('/list/:id', require('./list'));
4};

这里生成了一个路由规则,用户可通过 /tophub/list/xxxxx 这样的路由来生成 rss,通过同一目录下的 list.js 完成具体抓取和生成逻辑。

定义抓取和生成逻辑

首先引用下依赖包如下:

1// list.js
2const got = require('@/utils/got');
3const cheerio = require('cheerio');
4const config = require('@/config').value;
5const path = require('path');
6const { art } = require('@/utils/render');
  • got:用于发起 http 请求
  • cheerio:一个为服务器 jQuery 实现,可方便地解析页面
  • config:用于读取配置文件,经过分析发现爬取需使用 cookie,所以需要用户手动配置 cookie
  • path:路径生成工具
  • art:一个模板引擎(art-template),由于生成榜单需要定制 html 界面,所以需要使用它(仓库维护者的要求)

下面是具体逻辑:

 1// list.js
 2module.exports = async (ctx) => {
 3    const id = ctx.params.id;
 4    const link = `https://tophub.today/n/${id}`;
 5    const response = await got.get(link, {
 6        headers: {
 7            Referer: 'https://tophub.today',
 8            Cookie: config.tophub.cookie,
 9        },
10    });
11    const $ = cheerio.load(response.data);
12    // 解析榜单标题
13    const title = $('div.Xc-ec-L.b-L').text().trim();
14    // 解析榜单元素
15    const items = $('div.Zd-p-Sc > div:nth-child(1) tr')
16        .toArray()
17        .map((e) => ({
18            title: $(e).find('td.al a').text().trim(),
19            link: $(e).find('td.al a').attr('href'),
20            heatRate: $(e).find('td:nth-child(3)').text().trim(),
21        }));
22    // 将所有条目标题合一用于生成 GUID
23    const combinedTitles = items.map((item) => item.title).join('');
24    // 渲染榜单
25    const renderRank = art(path.join(__dirname, 'templates/rank.art'), { items });
26
27    ctx.state.data = {
28        title,
29        link,
30        item: [
31            {
32                title,
33                link,
34                description: renderRank,
35                guid: combinedTitles,
36            },
37        ],
38    };
39};

这里通过 jquery 语法读取元素: $('div.Xc-ec-L.b-L'),具体可以参考 cheerio 官方文档

由于当榜单中任一个条目顺序或内容发生变化都可认为是更新了,同时 guid 可以唯一标识一个 rss 条目,所以将所有条目标题合一用于生成 guid 即可,下面是关于 guid 的介绍:

guid: A string that uniquely identifies the item.

引自:rss 标准

最后,ctx.state.data 生成了一个 rss,title 用于标识 rss 标题,link 为原网址(tophub.today),item 为一个数组,保存了最新的条目,具体可参考 rss 标准

编写渲染模版

编辑 template/rank.art:

 1<table>
 2  <thead>
 3    <tr>
 4      <th>排名</th>
 5      <th>标题</th>
 6      <th>热度</th>
 7    </tr>
 8  </thead>
 9  <tbody>
10    {{each items}}
11      <tr>
12        <td>{{ $index + 1 }}</td>
13        <td>
14          <a href="{{ $value.link }}">
15            {{ $value.title }}
16          </a>
17        </td>
18        <td>{{ $value.heatRate }}</td>
19      </tr>
20    {{/each}}
21  </tbody>
22</table>

这里用 each 操作遍历传入的 items$index 为下标,$value 为对应值,具体语法可以参考官方文档

另外注意传入 items 的方式:

1art(path.join(__dirname, 'templates/rank.art'), { items });

传入的是一个 object,需要用 {} 扩住 items

定义路由维护者

1// maintainer.js
2module.exports = {
3    '/list/:id': ['akynazh'],
4};

/list/:id 这个路由对应 github.com/akynazh 这个开发者。

注意同一路由可有多个维护者,feat 和 fix 提交都可以加入该路由的维护者名单。

编写路由文档

website/docs/routes 下编写路由对应的文档,方便用户使用,首先确定路由类型,这里可以认为是新媒体,所以编辑 new-media.mdx 即可:

1## 今日热榜 {#jin-ri-re-bang}
2
3:::warning
4由于需要登录后的 Cookie 值才能获取原始链接,所以需要自建,需要在环境变量中配置 `TOPHUB_COOKIE`,详情见部署页面的配置模块。
5:::
6
7### 榜单列表 {#jin-ri-re-bang-bang-dan-lie-biao}
8
9<Route author="akynazh" example="/tophub/list/Om4ejxvxEN" path="/tophub/list/:id" paramsDesc={['榜单id,可在 URL 中找到']} />

{#jin-ri-re-bang} 定义了中文名对应的拼写名,用于快速定位。

:::warning::: 定义了一个重要注意事项模块,其他模块可参考官方文档。

Route 标签定义了一条路由。

支持 Radar 插件

 1module.exports = {
 2    'tophub.today': {
 3        _name: '今日热榜',
 4        '.': [
 5            {
 6                title: '榜单列表',
 7                docs: 'https://docs.rsshub.app/routes/new-media#jin-ri-re-bang-bang-dan-lie-biao',
 8                source: ['/n/:id'],
 9                target: '/tophub/list/:id',
10            },
11        ],
12    },
13};

docs 为文档地址。source 为原路由,target 为目标路由,通过 :id 映射了 id 值,另外 target 还支持更复杂的操作(比如正则),具体可参考官方文档。

配置环境变量

前面我们通过 config.tophub.cookie 读取 cookie,对应为全大写+下划线分隔生成的 KEY,在 RSSHub 根目录下编辑 .env 即可:

1TOPHUB_COOKIE=""

测试

接着,即可在本地进行调试了,通过 curl 或者浏览器访问对应路由,注意观察逻辑是否正确和渲染效果即可。

另外最好也把文档进行测试:

1cd website
2pnpm i
3pnpm run start:zh

部署

测试确保没问题后可以向 RSSHub 提交合并请求,需要格外注意格式,比如新增路由为 feat(route): xxx

如果嫌官方处理合并请求太慢,建议自己部署使用(另外需要配置变量的基本都得自己部署):

1git clone git@github.com:DIYgod/RSSHub.git && cd RSSHub
2pnpm i # npm install -g pnpm
3pm2 start lib/index.js --name rsshub # npm install pm2 -g

这里是用 pm2 让 RSSHub 运行在后台,可通过 pnpm start 之类的操作运行在前台。

之后每次更新,拉取完代码后重启即可 pm2 restart rsshub,但如果有新的依赖,需要先运行 pnpm i 以更新依赖。

可能出现的问题

这里记录下遇到的问题,持续更新。

commit 失败

报错提示说缺少某个依赖(pinyin-pro),是文档相关的。

如果修改了文档,那么需要到 website 下运行 pnpm i 安装相关依赖,以便于 commit 检查。

参考