12k
All articles

Comment supprimer les lignes en double d'un texte en JavaScript

Supprimez les lignes en double en JavaScript avec Set et gérez la casse, les espaces, les lignes vides, les fins de ligne et les occurrences à conserver.

OpenReplay Team
OpenReplay Team
Comment supprimer les lignes en double d'un texte en JavaScript

Pour supprimer les lignes en double en JavaScript, découpez le texte avec /\r?\n/, faites passer les lignes dans un Set, puis rassemblez-les : [...new Set(text.split(/\r?\n/))].join('\n').

Ce besoin se présente lorsque vous nettoyez une liste d’e-mails ou d’identifiants avant un import en masse, que vous mettez de l’ordre dans un fichier de logs, que vous fusionnez deux exports CSV ou que vous éliminez les répétitions d’une liste de mots. Le one-liner gère les doublons exacts, et l’outil de suppression des lignes en double fait le travail directement dans le navigateur si seul le résultat vous intéresse. Pour les quasi-doublons, qui ne diffèrent que par la casse, les espaces, les fins de ligne ou les lignes vides, cet article propose une fonction prête à copier, avec une option pour conserver la première ou la dernière occurrence.

Points clés à retenir

  • [...new Set(lines)] supprime les lignes strictement identiques et conserve chaque ligne à l’emplacement de sa première apparition, car un Set itère dans l’ordre d’insertion.
  • Un Set compare les chaînes de manière stricte : Apple, apple et apple (avec une espace finale) sont donc trois lignes différentes.
  • Dédoublonnez sur une clé normalisée (sans espaces superflues, éventuellement en minuscules), mais renvoyez la ligne d’origine afin de préserver la casse et l’indentation.
  • Appeler set() sur une clé existante d’une Map remplace la valeur mais conserve la position d’origine de la clé. Appelez d’abord delete() pour déplacer la clé à la fin.
  • sort -u trie la sortie et perd donc l’ordre d’origine. awk '!seen[$0]++' conserve l’ordre de l’entrée.

Le one-liner pour supprimer les lignes en double en JavaScript

La façon la plus concise de supprimer les lignes en double en JavaScript consiste à insérer un Set entre split et join :

const dedupeLines = (text) => [...new Set(text.split(/\r?\n/))].join('\n');

dedupeLines('one\nthree\ntwo\nthree\none');
// 'one\nthree\ntwo'

Cela fonctionne parce qu’un Set itère sur ses valeurs dans l’ordre d’insertion. Chaque ligne reste à la position de sa première occurrence, et les copies suivantes sont ignorées. La spécification ES2015 définit cet ordre : add ajoute chaque nouvelle valeur à la fin de la liste interne du set. Pour des données propres, générées par une machine, c’est tout ce dont vous avez besoin. Le guide sur les méthodes de Set en JavaScript couvre le reste de l’API Set.

Où la version naïve avec Set échoue-t-elle ?

Un Set compare les chaînes de manière stricte (selon l’algorithme SameValueZero) : la moindre différence d’un seul caractère suffit à distinguer deux lignes. Chacune des entrées ci-dessous conserve une ligne que vous vouliez probablement supprimer :

  • Casse : 'Apple\napple' conserve les deux lignes.
  • Espaces finales : 'apple \napple' conserve les deux, car l’une des lignes se termine par une espace.
  • Fins de ligne CRLF : 'apple\r\napple'.split('\n') renvoie ['apple\r', 'apple']. Découper uniquement sur '\n' laisse un \r à la fin de chaque ligne, sauf pour une dernière ligne non suivie d’un saut de ligne : cette dernière ligne ne correspond donc jamais à ses copies précédentes. Le séparateur regex de split /\r?\n/ corrige ce problème. Si vous attendez du texte avec les fins de ligne de l’ancien Mac OS (un \r seul), utilisez /\r\n|\r|\n/.
  • Lignes vides : 'a\n\nb\n\nc' devient 'a\n\nb\nc'. La seconde ligne vide est considérée comme un doublon de la première, si bien que les paragraphes b et c se retrouvent collés.
  • Saut de ligne final : 'a\n\nb\n' devient 'a\n\nb'. La chaîne vide qui suit le dernier saut de ligne correspond à la ligne vide précédente et est supprimée.

Évitez lines.filter((l, i, a) => a.indexOf(l) === i). indexOf parcourt le tableau de façon linéaire : l’appeler pour chaque ligne rend l’ensemble du traitement en O(n²). La spécification impose que les recherches dans un Set soient, en moyenne, plus rapides qu’une recherche linéaire ; un Set évite donc ce reparcours. Un objet littéral utilisé comme liste des éléments déjà vus pose ses propres problèmes : une ligne contenant __proto__ ou hasOwnProperty peut le casser.

Dédoublonner sur une clé normalisée, renvoyer la ligne d’origine

Pour traiter les quasi-doublons comme identiques, comparez une clé normalisée (la ligne débarrassée de ses espaces superflues, mise en minuscules si vous voulez une correspondance insensible à la casse) avec le contenu du Set, mais ajoutez la ligne d’origine à la sortie.

function removeDuplicateLines(text, options = {}) {
  const {
    caseSensitive = true,
    trim = true,
    collapseWhitespace = false,
    blankLines = 'collapse', // 'keep' | 'collapse' | 'remove'
  } = options;

  const eol = text.includes('\r\n') ? '\r\n' : '\n';
  const trailingNewline = /\r?\n$/.test(text);
  const lines = text.replace(/\r?\n$/, '').split(/\r?\n/);

  const toKey = (line) => {
    let key = trim ? line.trim() : line;
    if (collapseWhitespace) key = key.replace(/\s+/g, ' ');
    return caseSensitive ? key : key.toLowerCase();
  };

  const isBlank = (line) => line.trim() === '';
  const seen = new Set();
  const out = [];

  for (const line of lines) {
    if (isBlank(line)) {
      if (blankLines === 'remove') continue;
      if (blankLines === 'collapse' && out.length && isBlank(out[out.length - 1])) continue;
      out.push(line);
      continue;
    }
    const key = toKey(line);
    if (seen.has(key)) continue;
    seen.add(key);
    out.push(line);
  }

  return out.join(eol) + (trailingNewline ? eol : '');
}

removeDuplicateLines('Apple\napple \n\n\nbanana\nBanana\n', { caseSensitive: false });
// 'Apple\n\nbanana\n'

Comportement des options de removeDuplicateLines :

  • trim utilise String.prototype.trim, qui supprime également les espaces insécables et un éventuel BOM en début de texte. La sortie conserve l’indentation d’origine.
  • caseSensitive: false utilise toLowerCase, qui produit le même résultat quelle que soit la locale.
  • blankLines: 'keep' exclut les lignes vides du dédoublonnage : elles restent à leur place. 'collapse' réduit chaque série de lignes vides à une seule ; l’option vérifie la dernière ligne de sortie, de sorte que deux lignes vides séparées par un doublon supprimé sont tout de même fusionnées. 'remove' supprime toutes les lignes vides.
  • La fonction conserve le saut de ligne final de l’entrée. Si l’entrée contient au moins un \r\n, la sortie utilise \r\n pour toutes les lignes. Il s’agit d’une simplification pour les fichiers aux fins de ligne mixtes.

Une Map associant chaque clé à une ligne fonctionne de la même manière : if (!map.has(key)) map.set(key, line). Consultez Map vs Set vs Object pour savoir quand utiliser chaque structure. La normalisation Unicode, les guillemets typographiques et les autres artefacts de copier-coller relèvent d’une étape distincte, traitée dans Cleaning Up Text People Paste Into Your App.

Première ou dernière occurrence

Lorsque vous supprimez les lignes en double, conserver la première occurrence ou la dernière ne produit pas le même résultat. Conserver la dernière admet en outre deux variantes qui diffèrent par l’ordre. Pour l’entrée ['apple', 'banana', 'Apple', 'cherry'], avec une correspondance insensible à la casse :

StratégieRésultat
Première occurrence conservée (Set de clés)apple, banana, cherry
Dernière occurrence conservée, simple écrasement (map.set)Apple, banana, cherry
Dernière occurrence conservée, suppression puis ajoutbanana, Apple, cherry
function keepLast(lines, toKey = (l) => l.trim().toLowerCase(), { moveToEnd = true } = {}) {
  const byKey = new Map();
  for (const line of lines) {
    const key = toKey(line);
    if (moveToEnd) byKey.delete(key);
    byKey.set(key, line);
  }
  return [...byKey.values()];
}

Une Map conserve ses clés dans l’ordre de leur premier ajout, et appeler set() sur une clé déjà présente modifie la valeur sans déplacer la clé. Un traitement qui se contente d’écraser renvoie donc la dernière version de chaque ligne, mais à la position de sa première apparition. Pour placer chaque ligne à l’endroit de sa dernière apparition, appelez delete(key) avant set(key, line) afin que la clé soit réinsérée à la fin.

Supprimer les lignes en double de fichiers volumineux dans le terminal

Pour les fichiers trop volumineux pour être copiés-collés, supprimez les lignes en double dans le shell avec sort -u ou awk. sort -u élimine les doublons en triant, ce qui fait perdre l’ordre d’origine. Il compare en outre les lignes selon les règles de collation de la locale courante, ce qui, avec une locale autre que C, peut fusionner des lignes qui ne sont pas identiques octet par octet : utilisez LC_ALL=C sort -u pour une correspondance exacte.

awk '!seen[$0]++' affiche chaque ligne lors de sa première apparition et conserve l’ordre de l’entrée, mais stocke chaque ligne unique dans un tableau associatif conservé en mémoire. awk '!seen[tolower($0)]++' ignore la casse. Avec les fichiers CRLF, le \r est transmis aux deux outils : supprimez-le d’abord avec tr -d '\r'.

LC_ALL=C sort -u input.txt > output.txt
awk '!seen[$0]++' input.txt > output.txt
tr -d '\r' < input.txt | awk '!seen[$0]++' > output.txt

Conclusion

Utilisez new Set pour les doublons exacts. Lorsque les données proviennent d’utilisateurs ou de plusieurs sources, dédoublonnez sur une clé normalisée et renvoyez la ligne d’origine. Choisissez délibérément une politique de gestion des lignes vides et une règle de conservation (première ou dernière occurrence) plutôt que de vous contenter des valeurs par défaut. Copiez removeDuplicateLines, adaptez les options à vos données et, si vous souhaitez regrouper des lignes liées plutôt que les supprimer, utilisez Object.groupBy.

FAQ

Comment ne conserver que les lignes qui apparaissent exactement une fois en JavaScript ?

Comptez d'abord les occurrences, puis filtrez. Construisez en un seul passage une Map associant chaque ligne à son nombre d'occurrences, puis conservez les lignes dont le compte vaut 1 avec lines.filter((line) => counts.get(line) === 1). Il ne s'agit pas de dédoublonnage, qui conserve un exemplaire de chaque ligne répétée : ici, toute ligne apparaissant deux fois ou plus est entièrement supprimée. Comptez sur une clé normalisée si les quasi-doublons doivent être considérés comme une même ligne.

Comment supprimer d'une liste les lignes qui figurent déjà dans une autre liste ?

Chargez la seconde liste dans un Set, puis filtrez la première liste en vous appuyant sur celui-ci : const exclude = new Set(listB); listA.filter((line) => !exclude.has(line)). Chaque vérification est une recherche dans un Set, ce qui évite le coût quadratique d'un appel à includes ou indexOf sur un tableau pour chaque ligne. Découpez d'abord les deux textes avec /\r?\n/, et appliquez la même clé (trim ou mise en minuscules) des deux côtés.

Pourquoi deux lignes visuellement identiques survivent-elles au dédoublonnage ?

Elles contiennent généralement des points de code Unicode différents. Une lettre accentuée comme é peut être stockée sous la forme d'un caractère précomposé unique ou d'un e simple suivi d'un accent combinant. Un Set compare les unités de code de manière stricte et considère donc ces deux lignes comme différentes. Ajoutez line.normalize('NFC') à la fonction de calcul de clé pour les faire correspondre. trim ne supprime pas les espaces sans chasse (U+200B) : retirez-les séparément.

Comment supprimer les lignes en double d'un fichier volumineux en Node.js sans lire tout le fichier dans une chaîne ?

Lisez le fichier en streaming avec le module node:readline et vérifiez chaque ligne à l'aide d'un Set. Créez l'interface avec createInterface({ input: createReadStream(path), crlfDelay: Infinity }), parcourez-la avec for await, et n'écrivez une ligne que si le Set ne la contient pas déjà. L'option crlfDelay: Infinity fait en sorte que readline traite chaque CRLF comme un seul saut de ligne. Comme avec awk, la consommation mémoire augmente tout de même avec le nombre de lignes uniques.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.