Ajout d’un nœud Code prêt à l’emploi pour le modèle décrit par @Deepankar, car la partie quasi-dupliquée est là où ça devient généralement délicat.
Configuration : une table de données « Obtenir les lignes » nommée Seen (7 derniers jours, colonnes canonicalUrl et normTitle, Toujours afficher les données), et vos éléments RSS/Reddit fusionnés dans un nœud nommé New Items. Mode nœud Code « Exécuter une fois pour tous les éléments » :
// n8n Code node, "Run Once for All Items".
// Loops over node "New Items" (feed items with `title` and `link`).
// Previous node "Seen" = Data Table rows with canonicalUrl, normTitle (last ~7 days).
const TRACKING = /^(utm_|fbclid$|gclid$|mc_|ref$|ref_src$|igshid$|si$)/i;
const STOP = new Set(['the','a','an','of','to','in','on','for','and','is','are','with','at','by','from','as','its','new']);
function canonicalUrl(u) {
try {
const x = new URL(u);
x.hash = '';
x.hostname = x.hostname.replace(/^(www|m|amp)\./, '');
for (const k of [...x.searchParams.keys()]) if (TRACKING.test(k)) x.searchParams.delete(k);
x.pathname = x.pathname.replace(/\/amp\/?$/, '/').replace(/\/+$/, '') || '/';
return (x.hostname + x.pathname + (x.search || '')).toLowerCase();
} catch { return (u || '').toLowerCase(); }
}
function normTitle(t) {
return (t || '').toLowerCase().replace(/\s[-|:–—]\s[^-|:–—]{2,40}$/, '') // drop " - Site Name"
.replace(/[^\p{L}\p{N}\s]/gu, ' ').split(/\s+/).filter(w => w && !STOP.has(w)).join(' ');
}
function sameStory(a, b) { // Jaccard on word sets + a floor on shared words
const A = new Set(a.split(' ')), B = new Set(b.split(' '));
let inter = 0; for (const w of A) if (B.has(w)) inter++;
return inter >= MIN_SHARED && inter / (A.size + B.size - inter) >= THRESHOLD;
}
const THRESHOLD = 0.6, MIN_SHARED = 4; // tune on your own feeds
const seen = $('Seen').all().map(i => i.json);
const urls = new Set(seen.map(r => r.canonicalUrl));
const titles = seen.map(r => r.normTitle).filter(Boolean);
const out = [];
for (const item of $('New Items').all()) {
const cu = canonicalUrl(item.json.link), nt = normTitle(item.json.title);
if (urls.has(cu)) continue;
if (nt && titles.some(t => sameStory(t, nt))) continue;
urls.add(cu); titles.push(nt); // also dedupes within this batch
out.push({ json: { ...item.json, canonicalUrl: cu, normTitle: nt } });
}
return out;
Je l’ai testé sur un petit échantillon fictif : les versions de The Verge et TechCrunch du même article DevDay se fusionnent en un seul, l’article Shopify de TechCrunch et Reuters se fusionnent en un seul, et deux articles « Show HN » différents survivent tous les deux. Ce dernier cas montre pourquoi MIN_SHARED existe ; les courts titres partagent « show hn » et la similitude Jaccard simple les fusionnerait.
Modification : la boucle lit maintenant $('New Items'), merci @Deepankar. Avec Seen directement en amont, $input aurait été les lignes du tableau.
Pour les publications de lien Reddit, mappez le champ url du post (l’article lié) vers link, et non le permalien, sinon la copie Reddit ne correspond jamais à la copie RSS.
Et écrivez canonicalUrl et normTitle dans la table de données uniquement après que le nœud LLM réussisse, afin qu’une exécution échouée recommence l’élément au lieu de le marquer silencieusement comme vu.