You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

rss-parser实现RSS推送时重复发送相同内容问题求助

RSS推送重复问题排查与修复

问题复现

基于rss-parser开发Discord RSS自动推送脚本时,已推送过的RSS条目会被反复推送到目标频道,不符合新条目仅推送一次的预期,原实现代码如下:

(async function main() {

    // Make a new RSS Parser
    const parser = new Parser();

    // Get all the items in the RSS feed
    const feed = await parser.parseURL("https://imaginescan.com.br/feed"); // https://www.reddit.com/.rss

    let items = [];

    // Clean up the string and replace reserved characters
    const fileName = `${feed.title.replace(/\s+/g, "-").replace(/[/\\?%*:|"<>]/g, '').toLowerCase()}.json`;

    if (fs.existsSync(fileName)) {
        items = require(`./${fileName}`);
    }
    // Add the items to the items array
    await Promise.all(feed.items.map(async (currentItem) => {
        // Add a new item if it doesn't already exist
        if (items.filter((item) => isEquivalent(item, currentItem)).length <= 0) {
            items.push(currentItem);
        }
    }));
// Save the file
fs.writeFileSync(fileName, JSON.stringify(items));
    
feed.items.reverse().forEach(async (item) => {  

const channel = client.channels.cache.get('980990991865626634');
    
    let embed = new Discord.MessageEmbed()
    .setTitle("New Cap")
    .setDescription(item.title)
    .setColor("PURPLE")
    .setImage()

      channel.send(embed)
})
})();

function isEquivalent(a, b) {
    // Create arrays of property names
    let aProps = Object.getOwnPropertyNames(a);
    let bProps = Object.getOwnPropertyNames(b);

    // if number of properties is different, objects are not equivalent
    if (aProps.length != bProps.length) {
        return false;
    }

    for (let i = 0; i < aProps.length; i++) {
        let propName = aProps[i];

        // if values of same property are not equal, objects are not equivalent
        if (a[propName] !== b[propName]) {
            return false;
        }
    }

    // if we made it this far, objects are considered equivalent
    return true;
}

根本原因

  • 无差别全量推送:代码在完成新旧条目判断、写入存储文件后,直接遍历feed.items下的所有条目执行发送逻辑,完全没有过滤已推送过的旧条目,每次脚本运行都会把RSS源返回的所有条目重新发一遍。
  • 对象对比逻辑失效:isEquivalent通过全属性匹配判断条目是否重复,但rss-parser返回的条目对象每次解析可能生成临时动态属性,且require加载JSON文件得到的对象和解析生成的对象属性数量、原型不一致,会把已推送过的旧条目误判为新条目,导致已推送记录完全不可靠。
  • require加载缓存问题:Node.js对require加载的文件有默认缓存,如果脚本是定时循环执行,第二次及之后运行时require会直接返回第一次加载的缓存数据,读不到上一次运行写入的最新已推送记录,新旧判断逻辑完全失效。
  • 存储逻辑无上限:每次检测到"新条目"就直接全量推入数组存储,不清理过期的旧条目,随着运行时间增加存储文件会无限膨胀,对比效率持续降低。

修复后代码

修复点对应如下:

  • 改用fs.readFileSync+JSON.parse读取已推送记录,规避require缓存问题
  • 放弃全对象对比,直接使用RSS条目的固定唯一标识(guid/link字段,RSS规范中每个条目的这两个字段全局唯一且不会变更)判断重复,准确率100%
  • 仅对检测到的未推送新条目执行发送逻辑,发送成功后再把唯一标识加入已推送列表存储
  • 对已推送ID列表做长度截断,仅保留最近50条记录,避免文件无限增大
const fs = require('fs');
const { Client, Intents, MessageEmbed } = require('discord.js');
const Parser = require('rss-parser');
const client = new Client({ intents: [Intents.FLAGS.GUILDS] });

client.on('ready', async () => {
    console.log(`Logged in as ${client.user.tag}`);
    // 定时执行,可自行调整拉取间隔,示例为10分钟拉取一次
    setInterval(runRSSPush, 10 * 60 * 1000);
    // 启动时先执行一次
    runRSSPush();
});

async function runRSSPush() {
    const parser = new Parser();
    const feed = await parser.parseURL("https://imaginescan.com.br/feed");
    const fileName = `${feed.title.replace(/\s+/g, "-").replace(/[/\\?%*:|"<>]/g, '').toLowerCase()}.json`;
    
    // 读取已推送条目ID列表,规避require缓存
    let pushedItemIds = [];
    if (fs.existsSync(fileName)) {
        try {
            pushedItemIds = JSON.parse(fs.readFileSync(fileName, 'utf8'));
        } catch (e) {
            pushedItemIds = [];
        }
    }

    const channel = client.channels.cache.get('980990991865626634');
    if (!channel) return;

    // 按发布时间正序排列,先发较早发布的新条目
    const sortedItems = [...feed.items].reverse();
    for (const item of sortedItems) {
        // 优先用guid作为唯一标识,不存在则用link
        const itemId = item.guid || item.link;
        // 已经推送过直接跳过
        if (pushedItemIds.includes(itemId)) continue;

        // 构造消息发送
        const embed = new MessageEmbed()
            .setTitle("New Cap")
            .setDescription(item.title)
            .setColor("PURPLE");
        // 自动识别RSS附件中的图片,若图片存在其他字段可自行调整取值逻辑
        if (item.enclosure?.url) embed.setImage(item.enclosure.url);

        try {
            await channel.send({ embeds: [embed] });
            // 发送成功再把ID加入已推送列表,避免推送失败漏发
            pushedItemIds.push(itemId);
        } catch (err) {
            console.error('推送消息失败:', err);
        }
    }

    // 仅保留最近50条ID,避免文件过大
    if (pushedItemIds.length > 50) {
        pushedItemIds = pushedItemIds.slice(-50);
    }
    // 写入最新的已推送列表
    fs.writeFileSync(fileName, JSON.stringify(pushedItemIds, null, 2));
}

// 替换为你自己的Discord Bot Token
client.login('YOUR_BOT_TOKEN');

注意:原代码中.setImage()未传参数属于无效调用,修复版已补充自动识别RSS图片地址的逻辑,可根据实际RSS字段结构调整图片取值规则。

内容的提问来源于stack exchange,提问作者felipe

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 05:21:24