参与我们

如果有任何想法或需求,可以在 issue 中告诉我们,同时我们欢迎各种 pull requests

参与讨论

  1. Telegram 群
  2. GitHub Issues

提交新的 RSSHub 规则

开始编写 RSS 源前请确认源站没有提供 RSS,部分网页会在 HTML 头部包含 type 为 application/atom+xmlapplication/rss+xml 的 link 元素来指明 RSS 链接

步骤 1: 编写脚本

/lib/routes/ 中的路由对应路径下创建新的 js 脚本:

获取源数据

  • 获取源数据的主要手段为使用 got 发起 HTTP 请求(请求接口或请求网页)获取数据

  • 个别情况需要使用 puppeteer 模拟浏览器渲染目标页面并获取数据

  • 返回的数据一般为 JSON 或 HTML 格式

  • 对于 HTML 格式的数据,使用 cheerio 进行处理

  • 以下三种获取数据方法按 「推荐优先级」 排列:

    1. 使用 got 从接口获取数据

    样例:/lib/routes/bilibili/coin.js

    使用 got 通过数据源提供的 API 接口获取数据:

    // 发起 HTTP GET 请求
    const response = await got({
        method: 'get',
        url: `https://api.bilibili.com/x/space/coin/video?vmid=${uid}&jsonp=jsonp`,
        headers: {
            Referer: `https://space.bilibili.com/${uid}/`,
        },
    });
    
    const data = response.data.data; // response.data 为 HTTP GET 请求返回的数据对象
    // 这个对象中包含了数组名为 data,所以 response.data.data 则为需要的数据
    

    返回的数据样例之一(response.data.data[0]):

    {
        "aid": 33614333,
        "videos": 2,
        "tid": 20,
        "tname": "宅舞",
        "copyright": 1,
        "pic": "http://i0.hdslb.com/bfs/archive/5649d7fe6ff7f7b431300fc1a0db80d3f174cacd.jpg",
        "title": "【赤九玖】响喜乱舞【和我一起狂舞吧,团长大人(✧◡✧)】",
        "pubdate": 1539259203,
        "ctime": 1539249536,
        "desc": "编舞出处:av31984673\n真心好喜欢这个舞和这首歌,居然恰巧被邀请跳了,感谢《苍之纪元》官方的邀请。这次cos的是游戏的新角色缪斯。然而时间有限很多地方还有很多不足。也没跳够,以后私下还会继续练习,希望能学到更多动作,也能为了有机会把它跳的更好。 \n摄影:绯山圣瞳九命猫 \n后期:炉火"
        // 省略部分数据
    }
    

    对数据进行进一步处理,生成符合 RSS 规范的对象,把获取的标题、链接、描述、发布时间等数据赋值给 ctx.state.data, 生成 RSS 源

    ctx.state.data = {
        // 源标题
        title: `${name} 的 bilibili 投币视频`,
        // 源链接
        link: `https://space.bilibili.com/${uid}`,
        // 源说明
        description: `${name} 的 bilibili 投币视频`,
        //遍历此前获取的数据
        item: data.map((item) => ({
            // 文章标题
            title: item.title,
            // 文章正文
            description: `${item.desc}<br><img referrerpolicy="no-referrer" src="${item.pic}">`,
            // 文章发布时间
            pubDate: new Date(item.time * 1000).toUTCString(),
            // 文章链接
            link: `https://www.bilibili.com/video/av${item.aid}`,
        })),
    };
    
    // 至此本路由结束
    
    1. 使用 got 从 HTML 获取数据

    有时候数据是写在 HTML 里的,没有接口供我们调用,样例: /lib/routes/douban/explore.js

    使用 got 请求 HTML 数据:

    // 发起 HTTP GET 请求
    const response = await got({
        method: 'get',
        url: 'https://www.douban.com/explore',
    });
    
    const data = response.data; // response.data 为 HTTP GET 请求返回的 HTML,也就是简书首页的所有 HTML
    

    使用 cheerio 解析返回的 HTML:

    const $ = cheerio.load(data); // 使用 cheerio 加载返回的 HTML
    const list = $('div[data-item_id]');
    // 使用 cheerio 选择器,选择 class="list-item" 的所有元素,返回 cheerio node 对象数组
    
    // 注:每一个 cheerio node 对应一个 HTML DOM
    // 注:cheerio 选择器与 jquery 选择器几乎相同
    // 参考 cheerio 文档:https://cheerio.js.org/
    

    使用 map 遍历数组,解析出每一个 item 的结果

    ctx.state.data = {
        title: '豆瓣-浏览发现',
        link: 'https://www.douban.com/explore',
        item:
            list &&
            list
                .map((index, item) => {
                    item = $(item);
                    itemPicUrl = `${item.find('a.cover').attr('style')}`.replace('background-image:url(', '').replace(')', '');
                    return {
                        title: item
                            .find('.title a')
                            .first()
                            .text(),
                        description: `作者:${item
                            .find('.usr-pic a')
                            .last()
                            .text()}<br>描述:${item.find('.content p').text()}<br><img referrerpolicy="no-referrer" src="${itemPicUrl}">`,
                        link: item.find('.title a').attr('href'),
                    };
                })
                .get(),
    };
    
    // 至此本路由结束
    
    1. 使用 puppeteer 渲染页面获取数据

    提示

    由于此方法性能较差且消耗较多资源,使用前请确保以上两种方法无法获取数据,不然将导致您的 pull requests 被拒绝!

    部分网站没有接口供调用,且页面有加密 样例:/lib/routes/sspai/series.js

    // 使用 RSSHub 提供的 puppeteer 工具类,初始化 Chrome 进程
    const browser = await require('@/utils/puppeteer')();
    // 创建一个新的浏览器页面
    const page = await browser.newPage();
    // 访问指定的链接
    const link = 'https://sspai.com/series';
    await page.goto(link);
    // 渲染目标网页
    const html = await page.evaluate(
        () =>
            // 选取渲染后的 HTML
            document.querySelector('div.new-series-wrapper').innerHTML
    );
    // 关闭浏览器进程
    browser.close();
    

    使用 cheerio 解析返回的 HTML:

    const $ = cheerio.load(html); // 使用 cheerio 加载返回的 HTML
    const list = $('div.item'); // 使用 cheerio 选择器,选择所有 <div class="item"> 元素,返回 cheerio node 对象数组
    

    赋值给 ctx.state.data

    ctx.state.data = {
        title: '少数派 -- 最新上架付费专栏',
        link,
        description: '少数派 -- 最新上架付费专栏',
        item: list
            .map((i, item) => ({
                // 文章标题
                title: $(item)
                    .find('.item-title a')
                    .text()
                    .trim(),
                // 文章链接
                link: url.resolve(
                    link,
                    $(item)
                        .find('.item-title a')
                        .attr('href')
                ),
                // 文章作者
                author: $(item)
                    .find('.item-author')
                    .text()
                    .trim(),
            }))
            .get(), // cheerio get() 方法将 cheerio node 对象数组转换为 node 对象数组
    };
    
    // 至此本路由结束
    
    // 注:由于此路由只是起到一个新专栏上架提醒的作用,无法访问付费文章,因此没有文章正文
    
    1. 使用通用配置型路由

    很大一部分网站是可以通过一个配置范式来生成 RSS 的。
    通用配置即通过 cherrio(CSS 选择器、jQuery 函数)读取 json 数据来简便的生成 RSS。

    首先我们需要几个数据:

    1. RSS 来源链接
    2. 数据来源链接
    3. RSS 标题(非 item 标题)
    const buildData = require('@/utils/common-config');
    module.exports = async (ctx) => {
        ctx.state.data = await buildData({
            link: RSS来源链接,
            url: 数据来源链接,
            title: '%title%', //这里使用了变量,形如 **%xxx%** 这样的会被解析为变量,值为 **params** 下的同名值
            params: {
                title: RSS标题,
            },
        });
    };
    

    至此,我们的 RSS 还没有任何内容,内容需要由item完成 下面为一个实例

    const buildData = require('@/utils/common-config');
    
    module.exports = async (ctx) => {
        const link = `https://www.uraaka-joshi.com/`;
        ctx.state.data = await buildData({
            link,
            url: link,
            title: `%title%`,
            params: {
                title: '裏垢女子まとめ',
            },
            item: {
                item: '.content-main .stream .stream-item',
                title: `$('.post-account-group').text() + ' - %title%'`, //只支持$().xxx()这样的js语句,也足够使用
                link: `$('.post-account-group').attr('href')`, //.text()代表获取元素的文本,attr()表示获取指定属性
                description: `$('.post .context').html()`, // .html()代表获取元素的html代码
                pubDate: `new Date($('.post-time').attr('datetime')).toUTCString()`, // 日期的格式多种多样,可以尝试使用**/utils/date**
                guid: `new Date($('.post-time').attr('datetime')).getTime()`, // guid必须唯一,这是RSS的不同item的标志
            },
        });
    };
    

    至此我们完成了一个最简单的路由


使用缓存

所有路由都有一个缓存,全局缓存时间在 lib/config.js 里设定,但某些接口返回的内容更新频率较低,这时应该给这些数据设置一个更长的缓存时间,比如需要额外请求的全文内容

例如 bilibili 专栏 需要获取文章全文:/lib/routes/bilibili/followings_article.js

由于无法从一个接口获取所有文章的全文,所以每篇文章都需要单独请求一次,而这些数据一般是不变的,应该把这些数据保存到缓存里,避免每次访问路由都去请求那么多接口

const description = await ctx.cache.tryGet(link, async () => {
    const result = await got.get(link);

    const $ = cheerio.load(result.data);
    $('img').each(function(i, e) {
        $(e).attr('src', $(e).attr('data-src'));
    });

    return $('.article-holder').html();
});

tryGet 的实现可以看这里,第一个参数为缓存的 key,第二个参数为缓存数据获取方法,第三个参数为缓存时间,正常情况不应该传入,缓存时间默认为 CACHE_CONTENT_EXPIRE,且每次访问缓存会重新计算过期时间


生成 RSS 源

获取到的数据赋给 ctx.state.data, 然后数据会经过 template.js 中间件处理,最后传到 /lib/views/rss.art 来生成最后的 RSS 结果,每个字段的含义如下:

ctx.state.data = {
    title: '', // 项目的标题
    link: '', // 指向项目的链接
    description: '', // 描述项目
    language: '', // 频道语言
    item: [
        // 其中一篇文章或一项内容
        {
            title: '', // 文章标题
            author: '', // 文章作者
            category: '', // 文章分类
            // category: [''], // 多个分类
            description: '', // 文章摘要或全文
            pubDate: '', // 文章发布时间
            guid: '', // 文章唯一标示, 必须唯一, 可选, 默认为文章链接
            link: '', // 指向文章的链接
        },
    ],
};
播客源

用于音频类 RSS,额外添加这些字段能使你的 RSS 被泛用型播客软件订阅:

ctx.state.data = {
    itunes_author: '', // 主播名字, 必须填充本字段才会被视为播客
    itunes_category: '', // 播客分类
    image: '', // 专辑图片, 作为播客源时必填
    item: [
        {
            itunes_item_image: '', // 每个track单独的图片
            enclosure_url: '', // 音频链接
            enclosure_length: '', // 时间戳 (播放长度) , 一般是秒数,可选
            enclosure_type: '', // [.mp3就填'audio/mpeg'] [.m4a就填'audio/x-m4a'] [.mp4就填'video/mp4'], 或其他类型.
        },
    ],
};
BT/磁力源

用于下载类 RSS,额外添加这些字段能使你的 RSS 被 BT 客户端识别并自动下载:

ctx.state.data = {
    item: [
        {
            enclosure_url: '', // 磁力链接
            enclosure_length: '', // 时间戳 (播放长度) , 一般是秒数,可选
            enclosure_type: 'application/x-bittorrent', // 固定为 'application/x-bittorrent'
        },
    ],
};

步骤 2: 添加脚本路由

/lib/router.js 里添加路由

举例

  1. bilibili/bangumi
名称 说明
路由 /bilibili/bangumi/:seasonid
数据来源 bilibili
路由名称 bangumi
参数 1 :seasonid 必选
参数 2
参数 3
脚本路径 ./routes/bilibili/bangumi
lib/router.js 中的完整代码 router.get('/bilibili/bangumi/:seasonid', require('./routes/bilibili/bangumi'));
  1. github/issue
名称 说明
路由 /github/issue/:user/:repo
数据来源 github
路由名称 issue
参数 1 :user 必选
参数 2 :repo 必选
参数 3
脚本路径 ./routes/github/issue
lib/router.js 中的完整代码 router.get('/github/issue/:user/:repo', require('./routes/github/issue'));
  1. embassy
名称 说明
路由 /embassy/:country/:city?
数据来源 embassy
路由名称
参数 1 :country 必选
参数 2 ?city 可选
参数 3
脚本路径 ./routes/embassy/index
lib/router.js 中的完整代码 router.get('/embassy/:country/:city?', require('./routes/embassy/index'));

步骤 3: 添加脚本文档

  1. 更新 文档 (/docs/README.md) , 可以执行 npm run docs:dev 查看文档效果

    • 文档采用 vue 组件形式,格式如下:

      • author: 路由作者,多位作者使用单个空格分隔
      • example: 路由举例
      • path: 路由路径
      • :paramsDesc: 路由参数说明,数组,支持 markdown
        1. 参数说明必须对应其在路径中出现的顺序
        2. 如缺少说明将会导致npm run docs:dev报错
        3. 说明中的 ' " 必须通过反斜杠转义 \' \"
        4. 不必在说明中标注可选/必选,组件会根据路由?自动判断
    • 文档样例:

      1. 无参数:
      <Route author="HenryQW" example="/sspai/series" path="/sspai/series" />
      

      结果预览:


      作者: @HenryQW

      举例: https://rsshub.app/sspai/series

      路由: /sspai/series

      参数: 无


      1. 多参数:
      <Route author="HenryQW" example="/github/issue/DIYgod/RSSHub" path="/github/issue/:user/:repo" :paramsDesc="['用户名', '仓库名']" />
      

      结果预览:


      作者: @HenryQW

      举例: https://rsshub.app/github/issue/DIYgod/RSSHub

      路由: /github/issue/:user/:repo

      参数:

      • user, 必选 -

        用户名

      • repo, 必选 -

        仓库名


      1. 复杂说明支持 slot:
      <Route author="DIYgod" example="/juejin/category/frontend" path="/juejin/category/:category" :paramsDesc="['分类名']">
      
      | 前端     | Android | iOS | 后端    | 设计   | 产品    | 工具资源 | 阅读    | 人工智能 |
      | -------- | ------- | --- | ------- | ------ | ------- | -------- | ------- | -------- |
      | frontend | android | ios | backend | design | product | freebie  | article | ai       |
      
      </Route>
      

      结果预览:


      作者: @DIYgod

      举例: https://rsshub.app/juejin/category/frontend

      路由: /juejin/category/:category

      参数:

      • category, 必选 -

        分类名

      前端 Android iOS 后端 设计 产品 工具资源 阅读 人工智能
      frontend android ios backend design product freebie article ai

  2. 请一定要注意把<Route>的标签关闭!

  3. 执行 npm run format 自动标准化代码格式,提交代码, 然后提交 pull request

提交新的 RSSHub Radar 规则

/assets/radar-rules.js 里添加规则,然后在 RSSHub 文档里给对应路径加上 radar="1",这样会显示一个 支持浏览器扩展 标记

下面说明中会用到的简化的规则:

{
    'bilibili.com': {
        _name: 'bilibili',
        www: [{
            title: '分区视频',
            description: 'https://docs.rsshub.app/social-media.html#bilibili',
            source: '/v/*tpath',
            target: (params) => {
                let tid;
                switch (params.tpath) {
                    case 'douga/mad':
                        tid = '24';
                        break;
                    default:
                        return false;
                }
                return `/bilibili/partion/${tid}`;
            },
        }],
    },
    'twitter.com': {
        _name: 'Twitter',
        '.': [{  // for twitter.com
            title: '用户时间线',
            description: 'https://docs.rsshub.app/social-media.html#twitter',
            source: '/:id',
            target: '/twitter/user/:id',
            verification: (params) => (params.id !== 'home'),
        }],
    },
    'pixiv.net': {
        _name: 'Pixiv',
        'www': [{
            title: '用户收藏',
            description: 'https://docs.rsshub.app/social-media.html#pixiv',
            source: '/bookmark.php',
            target: (params, url) => `/pixiv/user/bookmarks/${new URL(url).searchParams.get('id')}`,
        }],
    },
    'weibo.com': {
        _name: '微博',
        '.': [{
            title: '博主',
            description: 'https://docs.rsshub.app/social-media.html#%E5%BE%AE%E5%8D%9A',
            source: ['/u/:id', '/:id'],
            target: '/weibo/user/:uid',
            script: '({uid: document.querySelector(\'head\').innerHTML.match(/\\$CONFIG\\[\'uid\']=\'(\\d+)\'/)[1]})',
            verification: (params) => params.uid,
        }],
    },
}

下面详细说明这些字段的含义及用法

title

必填,路由名称

对应 RSSHub 文档中的名称,如 Twitter 用户时间线 规则的 title用户时间线

docs

必填,文档地址

Twitter 用户时间线 规则的 docshttps://docs.rsshub.app/social-media.html#twitter

而不是 https://docs.rsshub.app/social-media.html#用户时间线,因为 #用户时间线 不唯一而 #twitter 唯一

source

可选,源站路径,留空则永远不会匹配成功,只会在 当前网站适用的 RSSHub 中出现

Twitter 用户时间线 规则的 source/:id

比如我们现在在 https://twitter.com/DIYgod 这个页面,twitter.com/:id 匹配成功,结果 params 为 {id: 'DIYgod'},下一步中插件就会根据 params target script verification 字段生成 RSSHub 地址

请注意 source 只可以匹配 URL Path,如果参数在 URL Param 和 URL Hash 里请使用 target

target

可选,RSSHub 路径,留空则不会生成 RSSHub 路径

对应 RSSHub 文档中的 path,如 Twitter 用户时间线 规则的 target/twitter/user/:id

上一步中源站路径匹配出 idDIYgod,则 RSSHub 路径中的 :id 会被替换成 DIYgod,匹配结果为 /twitter/user/DIYgod,就是我们想要的结果

进一步,如果源站路径无法匹配出想要的参数,这时我们可以把 target 设为一个函数,函数有 params 和 url 两个参数

bilibili 分区视频 规则,把 https://www.bilibili.com/v/douga/mad/ 匹配为 /bilibili/partion/24

又如 Pixiv 用户收藏 规则,把 https://www.pixiv.net/bookmark.php?id=15288095 匹配为 /pixiv/user/bookmarks/15288095

script

可选,执行脚本

有时候我们需要的参数不在 URL 中,无法通过上述方法获取,这时可以通过这个参数在页面执行脚本

请注意,由于插件权限限制,无法访问页面的 window 对象

微博博主 规则

verification

可选,验证源站路径

verification 为一个函数,函数有 params 参数

这个参数用于解决 source 匹配成功了,但不是我们想要的页面 的问题

比如 twitter.com/:id 可以匹配 https://twitter.com/DIYgod,也可以匹配 Twitter 主页 https://twitter.com/home,后者显然不是我们想匹配的,就用 verificationidhome 时排除掉

一些开发 tips

VS Code 调试配置

.vscode/launch.js

使用 nodemon 调试

在终端使用 npm run devyarn dev 开始调试。

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "attach",
            "name": "Node: Nodemon",
            "processId": "${command:PickProcess}",
            "restart": true,
            "protocol": "inspector"
        }
    ]
}

不使用 nodemon 调试

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "Launch Program",
            "program": "${workspaceFolder}/lib/index.js",
            "env": { "NODE_ENV": "dev" }
        }
    ]
}