Doppelte Textzeilen in JavaScript entfernen
Entfernen Sie doppelte Zeilen in JavaScript mit Set und behandeln Sie Großschreibung, Leerzeichen, Leerzeilen, Zeilenumbrüche und die gewünschte Vorkommnisregel.
Um doppelte Zeilen in JavaScript zu entfernen, teilen Sie den Text mit /\r?\n/ auf, leiten die Zeilen durch ein Set und fügen sie anschließend wieder zusammen: [...new Set(text.split(/\r?\n/))].join('\n').
Das brauchen Sie etwa, um vor einem Massenimport eine Liste mit E-Mail-Adressen oder IDs zu bereinigen, eine Logdatei aufzuräumen, zwei CSV-Exporte zusammenzuführen oder Wiederholungen aus einer Wortliste zu entfernen. Der Einzeiler deckt exakte Duplikate ab, und das Tool zum Entfernen doppelter Zeilen erledigt das direkt im Browser, wenn Sie nur das Ergebnis benötigen. Für Beinahe-Duplikate, die sich nur in Groß-/Kleinschreibung, Whitespace, Zeilenenden oder Leerzeilen unterscheiden, liefert dieser Artikel eine Funktion zum Kopieren, mit der Sie wahlweise das erste oder das letzte Vorkommen behalten.
Das Wichtigste in Kürze
[...new Set(lines)]entfernt exakt doppelte Zeilen und belässt jede Zeile an der Stelle ihres ersten Vorkommens, da einSetin Einfügereihenfolge iteriert.- Ein
Setvergleicht Strings exakt, sodassApple,appleundapple(mit abschließendem Leerzeichen) als drei verschiedene Zeilen gelten. - Deduplizieren Sie anhand eines normalisierten Schlüssels (getrimmt, optional in Kleinbuchstaben), geben Sie aber die Originalzeile aus, damit Schreibweise und Einrückung erhalten bleiben.
- Ein Aufruf von
set()für einen bereits vorhandenenMap-Schlüssel ersetzt den Wert, behält aber die ursprüngliche Position des Schlüssels bei. Rufen Sie zuerstdelete()auf, um den Schlüssel ans Ende zu verschieben. sort -usortiert die Ausgabe und verliert damit die ursprüngliche Reihenfolge.awk '!seen[$0]++'behält die Eingabereihenfolge bei.
Der Einzeiler zum Entfernen doppelter Zeilen in JavaScript
Der kürzeste Weg, doppelte Zeilen in JavaScript zu entfernen, ist ein Set zwischen split und join:
const dedupeLines = (text) => [...new Set(text.split(/\r?\n/))].join('\n');
dedupeLines('one\nthree\ntwo\nthree\none');
// 'one\nthree\ntwo'
Das funktioniert, weil ein Set seine Werte in Einfügereihenfolge durchläuft. Jede Zeile bleibt an der Position ihres ersten Vorkommens, spätere Kopien werden verworfen. Die ES2015-Spezifikation legt diese Reihenfolge fest: add hängt jeden neuen Wert an das Ende der internen Liste des Sets an. Für saubere, maschinell erzeugte Eingaben ist das alles, was Sie brauchen. Der Leitfaden zu JavaScript-Set-Methoden behandelt die übrige Set-API.
Wo versagt die naive Set-Variante?
Ein Set vergleicht Strings exakt (mittels SameValueZero), sodass bereits ein einziges abweichendes Zeichen zwei Zeilen unterscheidbar macht. Jede der folgenden Eingaben behält eine Zeile, die Sie vermutlich entfernen wollten:
- Groß-/Kleinschreibung:
'Apple\napple'behält beide Zeilen. - Abschließender Whitespace:
'apple \napple'behält beide, weil eine Zeile mit einem Leerzeichen endet. - CRLF-Zeilenenden:
'apple\r\napple'.split('\n')ergibt['apple\r', 'apple']. Wird nur an'\n'getrennt, bleibt am Ende jeder Zeile ein\rstehen – außer bei einer letzten Zeile ohne nachfolgenden Zeilenumbruch, die daher nie mit ihren früheren Kopien übereinstimmt. Der Regex-Separator insplit/\r?\n/behebt das. Wenn Sie Text mit klassischen Mac-OS-Zeilenenden (ein einzelnes\r) erwarten, verwenden Sie/\r\n|\r|\n/. - Leerzeilen:
'a\n\nb\n\nc'wird zu'a\n\nb\nc'. Die zweite Leerzeile gilt als Duplikat der ersten, sodass die Absätzebundcineinanderlaufen. - Abschließender Zeilenumbruch:
'a\n\nb\n'wird zu'a\n\nb'. Der leere String nach dem letzten Zeilenumbruch stimmt mit der früheren Leerzeile überein und wird entfernt.
Verzichten Sie auf lines.filter((l, i, a) => a.indexOf(l) === i). indexOf durchsucht das Array linear, sodass der gesamte Durchlauf O(n²) wird, wenn dies für jede Zeile geschieht. Die Spezifikation verlangt, dass Set-Lookups im Durchschnitt schneller als linear sind, sodass ein Set dieses erneute Durchsuchen vermeidet. Ein einfaches Objekt als Liste bereits gesehener Zeilen hat eigene Tücken: Eine Zeile mit dem Inhalt __proto__ oder hasOwnProperty kann es aus dem Tritt bringen.
Anhand eines normalisierten Schlüssels deduplizieren, die Originalzeile ausgeben
Um Beinahe-Duplikate als gleich zu behandeln, prüfen Sie einen normalisierten Schlüssel (die getrimmte Zeile, bei Bedarf für einen Vergleich ohne Beachtung der Groß-/Kleinschreibung in Kleinbuchstaben) gegen das Set, fügen der Ausgabe aber die Originalzeile hinzu.
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'
So verhalten sich die Optionen von removeDuplicateLines:
trimverwendetString.prototype.trim, das auch geschützte Leerzeichen und ein führendes BOM entfernt. Die Ausgabe behält die ursprüngliche Einrückung bei.caseSensitive: falseverwendettoLowerCase, das in jedem Gebietsschema dasselbe Ergebnis liefert.blankLines: 'keep'nimmt Leerzeilen von der Deduplizierung aus, sodass sie an ihrer Stelle bleiben.'collapse'fasst jede Folge von Leerzeilen zu einer zusammen; dabei wird die letzte ausgegebene Zeile geprüft, sodass zwei Leerzeilen, zwischen denen ein verworfenes Duplikat stand, trotzdem zusammengefasst werden.'remove'löscht sämtliche Leerzeilen.- Die Funktion behält den abschließenden Zeilenumbruch der Eingabe bei. Enthält die Eingabe irgendwo ein
\r\n, verwendet die Ausgabe\r\nfür jede Zeile. Bei Dateien mit gemischten Zeilenenden ist das eine Vereinfachung.
Eine Map von Schlüssel zu Zeile funktioniert genauso: if (!map.has(key)) map.set(key, line). Unter Map vs Set vs Object erfahren Sie, wann welche Datenstruktur passt. Unicode-Normalisierung, typografische Anführungszeichen und andere Artefakte aus eingefügtem Text gehören in einen separaten Schritt, der in Cleaning Up Text People Paste Into Your App behandelt wird.
Erstes vs. letztes Vorkommen
Beim Entfernen doppelter Zeilen führt das Behalten des ersten Vorkommens zu einer anderen Ausgabe als das Behalten des letzten. Für das Behalten des letzten Vorkommens gibt es zudem zwei Varianten, die sich in der Reihenfolge unterscheiden. Für die Eingabe ['apple', 'banana', 'Apple', 'cherry'] bei einem Vergleich ohne Beachtung der Groß-/Kleinschreibung gilt:
| Strategie | Ergebnis |
|---|---|
Erstes Vorkommen gewinnt (Set der Schlüssel) | apple, banana, cherry |
Letztes Vorkommen gewinnt, nur überschreiben (map.set) | Apple, banana, cherry |
| Letztes Vorkommen gewinnt, erst löschen, dann setzen | banana, 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()];
}
Eine Map behält ihre Schlüssel in der Reihenfolge bei, in der sie zuerst hinzugefügt wurden, und ein Aufruf von set() für einen bereits vorhandenen Schlüssel ändert den Wert, ohne den Schlüssel zu verschieben. Ein Durchlauf, der nur überschreibt, liefert daher die letzte Version jeder Zeile, allerdings an der Position, an der die Zeile zuerst auftrat. Um jede Zeile an der Stelle ihres letzten Vorkommens zu platzieren, rufen Sie delete(key) vor set(key, line) auf, sodass der Schlüssel erneut am Ende eingefügt wird.
Doppelte Zeilen aus großen Dateien im Terminal entfernen
Für Dateien, die sich nicht mehr sinnvoll per Copy-and-paste verarbeiten lassen, entfernen Sie doppelte Zeilen in der Shell mit sort -u oder awk. sort -u entfernt Duplikate durch Sortieren, wodurch die ursprüngliche Reihenfolge verloren geht. Außerdem vergleicht es Zeilen anhand der Sortierregeln (Collation) des aktuellen Gebietsschemas, was bei anderen Locales als C Zeilen zusammenführen kann, die nicht byteweise identisch sind. Verwenden Sie daher LC_ALL=C sort -u für einen exakten Abgleich.
awk '!seen[$0]++' gibt jede Zeile beim ersten Auftreten aus und behält die Eingabereihenfolge bei, speichert aber jede eindeutige Zeile in einem assoziativen Array im Arbeitsspeicher. awk '!seen[tolower($0)]++' ignoriert die Groß-/Kleinschreibung. Bei CRLF-Dateien landet das \r in beiden Tools mit im Vergleich, entfernen Sie es daher zuerst mit 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
Fazit
Verwenden Sie new Set für exakte Duplikate. Stammt die Eingabe von Menschen oder aus mehreren Quellen, deduplizieren Sie anhand eines normalisierten Schlüssels und geben die Originalzeile aus. Legen Sie eine Strategie für Leerzeilen sowie eine Regel für erstes oder letztes Vorkommen bewusst fest, statt die Standardwerte einfach zu übernehmen. Kopieren Sie removeDuplicateLines, passen Sie die Optionen an Ihre Daten an, und wenn Sie zusammengehörige Zeilen gruppieren statt entfernen möchten, verwenden Sie Object.groupBy.
FAQs
Wie behalte ich in JavaScript nur die Zeilen, die genau einmal vorkommen?
Zählen Sie zuerst die Vorkommen und filtern Sie dann. Erstellen Sie in einem Durchlauf eine Map von Zeile zu Anzahl und behalten Sie anschließend die Zeilen mit der Anzahl 1 mittels lines.filter((line) => counts.get(line) === 1). Das ist keine Deduplizierung, bei der von jeder wiederholten Zeile eine Kopie erhalten bleibt. Hier wird jede Zeile, die zweimal oder öfter vorkommt, vollständig entfernt. Zählen Sie anhand eines normalisierten Schlüssels, wenn Beinahe-Duplikate als dieselbe Zeile gelten sollen.
Wie entferne ich Zeilen aus einer Liste, die bereits in einer anderen Liste vorkommen?
Laden Sie die zweite Liste in ein Set und filtern Sie die erste Liste dagegen: const exclude = new Set(listB); listA.filter((line) => !exclude.has(line)). Jede Prüfung ist ein Set-Lookup, sodass Sie den quadratischen Aufwand vermeiden, der entsteht, wenn Sie für jede Zeile includes oder indexOf auf einem Array aufrufen. Teilen Sie beide Texte zuvor mit /\r?\n/ auf und wenden Sie auf beiden Seiten denselben Schlüssel mit trim bzw. Kleinschreibung an.
Warum überstehen zwei identisch aussehende Zeilen die Deduplizierung?
Meist enthalten sie unterschiedliche Unicode-Codepoints. Ein Buchstabe mit Akzent wie é kann als ein einzelnes vorkombiniertes Zeichen oder als einfaches e mit nachfolgendem kombinierendem Akzent gespeichert sein. Ein Set vergleicht Code Units exakt und behandelt die beiden Zeilen daher als verschieden. Ergänzen Sie die Schlüsselfunktion um line.normalize('NFC'), damit sie übereinstimmen. trim entfernt keine Leerzeichen mit Nullbreite (U+200B), entfernen Sie diese daher separat.
Wie entferne ich in Node.js doppelte Zeilen aus einer großen Datei, ohne die gesamte Datei in einen String einzulesen?
Streamen Sie die Datei mit dem Modul node:readline und prüfen Sie jede Zeile gegen ein Set. Erstellen Sie das Interface mit createInterface({ input: createReadStream(path), crlfDelay: Infinity }), durchlaufen Sie es mit for await und schreiben Sie eine Zeile nur dann, wenn das Set sie noch nicht enthält. Die Option crlfDelay: Infinity sorgt dafür, dass readline jedes CRLF als einen einzigen Zeilenumbruch behandelt. Wie bei awk wächst der Speicherbedarf weiterhin mit der Anzahl eindeutiger Zeilen.
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