Zum Inhalt springen
OneKitly

Unsauberen Text bereinigen: die Reihenfolge der Schritte, auf die es ankommt

Veröffentlicht am 8.7.2026 · 14 Min. Lesezeit · Text- & Sprach-Tools

Daniel Okonkwo

Daniel OkonkwoFront-end-Entwickler und Tech-Redakteur bei OneKitly

Web-Performance · Dateiformate

Anhand von 5 Quellen geprüft

Profil ansehen
Kurz gesagt

Textbereinigung ist eine Pipeline, deren Schritte nicht kommutieren: dieselben Operationen in anderer Reihenfolge liefern anderen Text, und der Schaden bleibt unbemerkt. In Node ausgeführt: Wer HTML-Entitäten dekodiert, bevor er Tags entfernt, verwandelt den maskierten Text &lt;b&gt; in ein echtes <b>-Element, das der Entferner anschließend löscht - aus „schreibe <b>, um ein Tag zu zeigen“ wird „schreibe , um ein Tag zu zeigen“. Erst Tags entfernen und dann genau einmal dekodieren reproduziert das, was ein Browser anzeigt. Leerraum mit /\s+/ zusammenzufassen, bevor die Zeilenumbruch-Politik feststeht, plättet drei Absätze zu einer einzigen 80-Zeichen-Kette, und kein späterer Schritt stellt die Grenzen wieder her: nur horizontalen Leerraum zusammenfassen, mit /[^\S\n]+/, und zwar nach dem Zusammenfügen. Zeilen zu deduplizieren, bevor sie getrimmt sind, findet nichts, denn „alpha“ und „alpha“ mit Leerzeichen am Ende sind verschiedene Zeichenketten: fünf Zeilen bleiben fünf; trimmt man zuerst, werden aus denselben fünf drei. Drei Zeichen überleben dann jeden naiven Durchgang: U+00A0 (von \s erfasst, von trim entfernt), U+200B (von keinem erfasst) und U+FEFF, das \s erfasst, die Unicode-Eigenschaft White_Space aber nicht. Erst die Reihenfolge korrigieren, dann die Unsichtbaren, und erst danach messen.

Tags entfernen, bevor Entitäten dekodiert werden, trimmen vor dem Deduplizieren, Leerraum zuletzt zusammenfassen. Drei Reihenfolgen in Node ausgeführt, eine neunstufige Pipeline in der richtigen Abfolge und die unsichtbaren Zeichen — U+00A0, U+200B, U+FEFF — die jede naive Bereinigung überleben.

Bereinigungsschritte sind nicht vertauschbar

Ein Textbereiniger sieht aus wie ein Menü: überflüssige Leerzeichen entfernen, Zeilenumbrüche entfernen, doppelte Zeilen entfernen, HTML-Tags entfernen. Jeder Punkt klingt eigenständig, also hakt man sie in der Reihenfolge ab, in der die Oberfläche sie zufällig auflistet. Sie sind nicht eigenständig. Jeder Schritt schreibt die Eingabe um, die der nächste zu sehen bekommt, und mehrere Paare liefern je nach Reihenfolge ein anderes Ergebnis. Die Fehler sind leise: Sie bekommen weiterhin Text, er sieht weiterhin plausibel aus, und verloren ist genau das, worauf du nicht geschaut hast.

Drei Paare verursachen fast den gesamten realen Schaden: Tags gegen Entitäten, Leerraum gegen Zeilenumbrüche und Deduplizierung gegen Trimmen. Jedes wird unten an einer echten Zeichenkette gezeigt, ausgeführt statt überlegt. Die neun geordneten Schritte sind schlicht das, was sich aus diesen drei Bedingungen ergibt, wenn man sie alle gleichzeitig einhält.

Tags vor Entitäten, und genau einmal dekodieren

Nimm ein HTML-Fragment: <p>Terms &amp;amp; conditions: write &lt;b&gt; to show a bold tag.</p>. Ein Browser stellt es so dar: Terms &amp; conditions: write <b> to show a bold tag. Die maskierten Folgen sind Text - der Autor wollte ein wörtliches Und-Zeichen und ein wörtliches, sichtbares Fett-Tag.

Entferne zuerst die Tags und dekodiere einmal, und du erhältst genau diese Browser-Ausgabe. Dekodiere zuerst und entferne danach, dann ist aus &lt;b&gt; längst ein echtes <b>-Element geworden, wenn der Entferner läuft, und er löscht es: Das Ergebnis lautet „write to show a bold tag“, mit doppeltem Leerzeichen dort, wo das Subjekt des Satzes stand. Entferne zuerst, dekodiere aber zweimal, dann kippt der Fehler in die andere Richtung: „write <b> to show a bold tag“ enthält jetzt ein echtes Tag, das nichts mehr maskiert - und so wird aus einem Klartext-Auszug wieder Markup, sobald jemand ihn in eine Seite einfügt.

Die Regel hinter allen drei Ergebnissen ist kurz: Eine Entität ist maskierter Text, und Dekodieren befördert Text zu Markup. Alles, was Markup besonders behandelt, muss deshalb vor dieser Beförderung laufen. Browser umgehen die Frage, indem sie einmal in einem einzigen Durchgang tokenisieren - deshalb muss ein echter Parser das nie entscheiden. Eine Regex-Pipeline schon, und die Entscheidung lautet: entfernen, dann dekodieren, genau ein Dekodiervorgang.

Leerraum nach den Zeilenumbrüchen, niemals über sie hinweg

Nimm ein Dokument mit drei Absätzen, teils über mehrere Zeilen umbrochen, dazwischen eine oder drei versehentliche Leerzeilen. Lass zuerst /\s+/ durch ein einzelnes Leerzeichen ersetzen, weil „überflüssige Leerzeichen entfernen“ das erste Kästchen war, und du erhältst eine 80 Zeichen lange Kette ohne jede Absatzgrenze. Nichts weiter hinten kann sie wiederherstellen: Die Zeilenumbrüche, die die Struktur trugen, waren Leerraum, und du hast verlangt, Leerraum zusammenzufassen.

Schick dasselbe Dokument durch die richtige Reihenfolge - Zeilenenden normalisieren, jede Zeile trimmen, umbrochene Zeilen zusammenfügen, nur horizontale Folgen mit /[^\S\n]+/ zusammenfassen, dann drei oder mehr Umbrüche auf eine Leerzeile reduzieren -, und es kommt mit 58 Zeichen in zwei Absätzen heraus, Struktur intakt. Der Unterschied liegt nicht darin, dass eine Regex besser wäre als die andere. Er liegt darin, dass /\s+/ \n und \r einschließt und eine Klasse wie [^\S\n] sie bewusst nicht.

Darin steckt eine zweite, feinere Reihenfolge. Nimm „Paragraph one text.“, eine Zeile mit einem einzelnen Leerzeichen und dann „Paragraph two text.“. Teile an /\n\n/, und du findest einen Absatz, nicht zwei, weil die Trennzeile nicht leer ist - sie enthält ein Leerzeichen. Trimm zuerst jede Zeile, und dieselbe Teilung findet zwei. Deshalb steht das Trimmen der Zeilen vor jeder Absatzentscheidung, während das Zusammenfassen von Folgen danach kommt: Die beiden Leerraum-Operationen liegen auf gegenüberliegenden Seiten des Zeilenumbruch-Schritts.

Zuletzt deduplizieren, weil Gleichheit erst am Ende entsteht

Zeilen-Deduplizierung vergleicht ganze Zeichenketten. Gib ihr die fünf Zeilen „alpha“ mit angehängtem Leerzeichen, „beta“, „alpha“, „beta“ mit angehängtem Tabulator und „gamma“, und sie entfernt nichts: fünf Zeilen hinein, fünf hinaus, weil „alpha“ und „alpha“ plus Leerzeichen schlicht verschiedene Zeichenketten sind, ebenso „beta“ und „beta“ plus Tabulator. Trimm zuerst den nachlaufenden Leerraum, und dieselbe Deduplizierung macht aus den fünf drei. Die Duplikate waren immer da; sie waren genau so unsichtbar, wie ein nachlaufendes Leerzeichen unsichtbar ist.

Dasselbe passiert am durchgerechneten Beispiel am Ende dieses Artikels. An zweiter Stelle platziert, direkt nach dem Entfernen der Tags, entfernt die Deduplizierung null Zeilen. Dieselbe Deduplizierung an achter Stelle entfernt eine, denn inzwischen ist das Nullbreiten-Leerzeichen weg und das doppelte Leerzeichen zusammengefasst, sodass die beiden Zeilen, die immer schon derselbe Satz waren, endlich dieselbe Zeichenkette sind. Deduplizierung findet keine Duplikate; die Normalisierung erzeugt sie, und die Deduplizierung sammelt sie danach ein.

Eine Entscheidung bleibt dir: die Groß- und Kleinschreibung. Deduplizierung vergleicht exakt, also sind „Total“ und „total“ zwei Zeilen. Case-Folding ist eine eigene, verlustbehaftete Entscheidung, die einen eigenen Schritt verdient und in der Oberfläche sichtbar bleiben sollte, statt im Deduplizierer vergraben zu sein - dasselbe Argument wie bei Namenskonventionen, wo die Umwandlung nur dann sicher ist, wenn man weiß, von welcher Konvention man ausgeht.

Die Zeichen, die jede naive Bereinigung überleben

Drei Codepunkte verursachen den größten Teil des Rückstands. U+00A0, das geschützte Leerzeichen, kommt aus Textverarbeitungen, aus Webseiten und aus der französischen und spanischen Typografie, wo es vor einem Doppelpunkt oder nach einem öffnenden Anführungszeichen steht. U+200B, das Nullbreiten-Leerzeichen, kommt aus CMS-Editoren und aus eingefügtem Rich Text als unsichtbarer Umbruchhinweis. U+FEFF, das geschützte Nullbreiten-Leerzeichen, ist das Ergebnis der Dekodierung einer UTF-8-Bytereihenfolge-Marke; es taucht ganz am Anfang von Dateien aus Tabellenexporten und Windows-Werkzeugen auf, nach naiver Verkettung manchmal auch mittendrin.

Für eine Bereinigungs-Pipeline zählt, welche davon deine Werkzeuge überhaupt sehen, und die Antwort ist nicht intuitiv. In Node 22 ausgeführt: /\s/ erfasst U+00A0 und U+FEFF, aber nicht U+200B. Der Unicode-Eigenschafts-Escape /\p{White_Space}/u erfasst U+00A0, aber nicht U+FEFF. Beide weichen voneinander ab, und zwar in beide Richtungen: U+0085, das NEL-Steuerzeichen, wird von der Unicode-Eigenschaft erfasst und von /\s/ nicht. Der Grund: Die ECMAScript-Grammatik definiert ihre eigene WhiteSpace-Produktion, die aus historischen Gründen die Bytereihenfolge-Marke hinzunimmt, während die Unicode-Zeichendatenbank White_Space nach eigenen Kriterien vergibt und sie U+FEFF nicht zuspricht.

Normalisierung ist kein Ersatz. NFKC bildet U+00A0 auf ein einfaches U+0020 ab, was wirklich nützlich ist, lässt U+200B aber genau dort, wo es war: Die Zeichenkette ist danach immer noch ein Zeichen lang. Die Pipeline braucht deshalb einen ausdrücklichen Löschschritt für die Formatzeichen - U+200B, U+200C, U+200D, U+FEFF, U+00AD - und einen getrennten Abbildungsschritt für die exotischen Leerzeichen. Keiner von beiden lässt sich an eine Leerraum-Regex delegieren, denn für eine Leerraum-Regex ist die Hälfte davon kein Leerraum.

Ein unsauberes Beispiel, neun Schritte, vorher und nachher

Das Beispiel ist 126 Zeichen lang und enthält absichtlich alle oben besprochenen Probleme: zwei führende Leerzeichen, CRLF-Zeilenenden, ein h2 und drei p-Elemente, ein doppelt kodiertes Und-Zeichen, ein Nullbreiten-Leerzeichen direkt am Satzende, drei aufeinanderfolgende Zeilenumbrüche, einen wortgleich wiederholten Satz, ein verdoppeltes Leerzeichen im Inneren, einen über zwei Zeilen umbrochenen Absatz mit eingerückter Fortsetzung sowie eine Bytereihenfolge-Marke plus zwei Leerzeichen ganz am Ende.

Durch die neun Schritte in der richtigen Reihenfolge geschickt, wird es 58 Zeichen lang: eine Überschriftszeile „Q3 &amp; Q4 report“, eine Zeile „Revenue rose 12%.“, eine Leerzeile, dann „Costs fell“ und „slightly.“ als zwei Zeilen eines Absatzes. Die Zwischenlängen betragen 122 nach dem Normalisieren der Zeilenenden, 92 nach dem Entfernen der Tags, 88 nach der einmaligen Entitäten-Dekodierung, 86 sobald Nullbreiten-Leerzeichen und Bytereihenfolge-Marke gelöscht sind, 80 nach dem Trimmen jeder Zeile, 77 nach dem horizontalen Zusammenfassen, 76 nach der Reduktion des dreifachen Umbruchs und 58 nach der Deduplizierung.

Zwei dieser Zahlen sind das Argument des ganzen Artikels. Der Sprung von 76 auf 58 ist die doppelte Zeile, und es gibt ihn nur, weil die Schritte 4 und 8 vorher liefen; verschieb die Deduplizierung auf Position zwei, und dieser Sprung ist null. Der Schritt von 86 auf 80 ist das zeilenweise Trimmen, und er ist es, der die Absatztrennung später eine wirklich leere Trennzeile sehen lässt. Alles andere ist Buchführung.

Bevor du den bereinigten Text weitergibst

Bewahre das Original auf. Jeder Schritt dieser Pipeline ist absichtlich verlustbehaftet, und keiner ist umkehrbar: Du kannst nicht rekonstruieren, welche Leerzeichen geschützt waren, welcher Zeilenumbruch ein Umbruch und welcher ein Absatz war, oder welche von zwei identischen Zeilen die gemeinte war. Bereinigter Text ist ein abgeleitetes Artefakt, und ein abgeleitetes Artefakt darf nie die einzige Kopie sein.

Lass die Pipeline anschließend zweimal über ihre eigene Ausgabe laufen. Eine richtig geordnete Bereinigung ist idempotent: Der zweite Durchgang darf nichts verändern. Tut er es doch, hast du einen Schritt, der unter Wiederholung nicht stabil ist, und in der Praxis ist es fast immer die Entitäten-Dekodierung - die einzige Operation der Liste, die sich selbst neue Arbeit schaffen kann. Eine Idempotenzprüfung kostet eine Zeile Code und fängt die Fehlerklasse ab, die erst auftaucht, wenn ein Dokument sechs Monate später ein zweites Mal durch das Werkzeug geht, weil es jemand erneut importiert hat.

Zähl schließlich erst am Ende. Jede Länge, jede Wortzahl und jeder Lesbarkeitswert, der mitten in der Pipeline genommen wird, misst eine Zeichenkette, die es nicht mehr gibt - und eine Länge hängt insbesondere davon ab, was du als Zeichen gelten lässt: eine Frage, die man gesondert klären sollte, bevor man einer Zahl aus irgendeinem Zähler traut.

Sechs unsichtbare Zeichen und ob die Leerraum-Werkzeuge von JavaScript sie sehen. In Node 22 ausgeführt: U+FEFF und U+0085 weichen in entgegengesetzte Richtungen ab, weil die ECMAScript-Produktion WhiteSpace und die Unicode-Eigenschaft White_Space nicht dieselbe Liste sind.
ZeichenCodepunkt/\s/ erfasst es/\p{White_Space}/u erfasst es.trim() entfernt es
Geschütztes LeerzeichenU+00A0JaJaJa
Schmales geschütztes LeerzeichenU+202FJaJaJa
Nullbreiten-LeerzeichenU+200BNeinNeinNein
Geschütztes Nullbreiten-Leerzeichen, die Bytereihenfolge-MarkeU+FEFFJaNeinJa
Nächste ZeileU+0085NeinJaNein
Weiches TrennzeichenU+00ADNeinNeinNein
Zusätzliche Leerzeichen entfernenReduziere wiederholte Leerzeichen und trimme jede Zeile für sauberen Text.Tool ausprobieren

Häufige Fragen

Wenn ich den Bereiniger zweimal laufen lasse, spielt die Reihenfolge dann noch eine Rolle?
Ja, und zweimaliges Ausführen kann es sogar verschlimmern. Wiederholung stellt keine Information wieder her, die ein früherer Schritt zerstört hat: zusammengefasste Absatzgrenzen bleiben zusammengefasst, egal wie viele Durchgänge du machst. Zugleich ist die Entitäten-Dekodierung nicht idempotent: Ein zweiter Durchgang dekodiert &amp;amp; erneut und macht aus einem bewusst wörtlichen Und-Zeichen im Text ein strukturelles. Der richtige Test ist nicht, zweimal zu laufen, um ein besseres Ergebnis zu bekommen, sondern zweimal zu laufen, um zu bestätigen, dass der zweite Durchgang nichts ändert.
Warum entfernt .trim() die Bytereihenfolge-Marke, aber nicht das Nullbreiten-Leerzeichen?
Weil trim gegen die ECMAScript-Produktion WhiteSpace definiert ist und nicht gegen die Unicode-Eigenschaft White_Space, und weil diese Produktion U+FEFF aus historischen Gründen ausdrücklich aufführt - aus einer Zeit, in der die Bytereihenfolge-Marke regelmäßig am Anfang eines Datenstroms stand. U+200B stand nie auf dieser Liste: Unicode ordnet es als Formatzeichen der allgemeinen Kategorie Cf zu, weil es eine Umbruchgelegenheit markiert und keinen Zwischenwortraum. Also entfernt trim das eine und nicht das andere, und weder /\s/ noch trim werden dir bei U+200B je helfen. Lösch es ausdrücklich.
Gibt es je einen guten Grund, /\s+/ auf ein ganzes Dokument anzuwenden?
Ja, in genau einer Situation: wenn du entschieden hast, dass die Ausgabe eine einzige Zeile ist und Struktur keine Rolle spielt - ein Suchschlüssel, ein Vergleichs-Fingerabdruck, ein Wert für eine einzeilige CSV-Zelle. Dort ist das Plätten auf einfache Leerzeichen genau der Zweck. Überall dort, wo die Ausgabe von einem Menschen gelesen wird, ist /\s+/ die falsche Klasse, weil sie den Zeilenumbruch zwischen zwei Absätzen und die zwei Leerzeichen nach einem Punkt für dasselbe hält. Setz sie bewusst für Schlüssel ein und nie als Voreinstellung für Fließtext.
Wie sehe ich überhaupt, dass ein unsichtbares Zeichen da ist?
Vergleiche die erwartete Länge mit der tatsächlichen und gib dann die Codepunkte aus. Zwei Zeichenketten, die auf dem Bildschirm identisch aussehen, können unterschiedlich lang sein - das ist der ganze Hinweis. Sobald die Länge überrascht, listest du jedes Zeichen mit seinem hexadezimalen Codepunkt auf, und der Übeltäter ist offensichtlich: ein U+00A0 dort, wo du U+0020 vermutet hast, oder ein U+200B am Satzende. Wenn du das einmal für eine Stichprobe aus jeder importierten Quelle tun, weißt dich, welche Erzeuger in deiner Kette welche Zeichen ausgeben, und können genau diese löschen.
Sollte die Deduplizierung die ursprüngliche Reihenfolge der Zeilen bewahren?
Fast immer ja, und sie sollte das erste Vorkommen behalten, nicht das letzte. Sortieren, um Duplikate zu finden, ist eine aus Kommandozeilen-Pipelines geerbte Gewohnheit, und sie ordnet stillschweigend ein Dokument um, dessen Reihenfolge Bedeutung trug - eine Schrittliste, ein Änderungsprotokoll, ein Transkript. Das erste Vorkommen zu behalten entspricht auch dem Lesen: Die frühere Zeile hat meist den Kontext um sich herum. Wenn ein Werkzeug anbietet, beim Deduplizieren zu sortieren, behandelst du das als zwei getrennte Operationen und verlange nur die, die du willst.

Artikel, die dich interessieren könnten

Alle Ratgeber
RatgeberMarkdown entfernen: was der Klartext verliert und was eine Regex falsch machtEin Link wird zu Text, dessen Ziel gelöscht ist, eine verschachtelte Liste verliert ihre Hierarchie, eine Tabelle wird zu einer Reihe von Wörtern. Dann die technische Hälfte: Markdown hat keine einzige Spezifikation, und ein Regex-Entferner verstümmelt einen Dateinamen, ein Malzeichen und das Innere eines Codeblocks - alles gegen einen echten Parser gehalten.RatgeberHTML sauber entfernen: was ein Tag-Entferner kann und was nichtTags entfernen und HTML bereinigen sind zwei verschiedene Aufgaben. Ein echtes Fragment durch eine naive Regex und durch einen formatbewussten Entferner geschickt, mit Script- und Style-Inhalten, Blockumbrüchen, Kommentaren, CDATA und der Reihenfolge der Entitäten als sichtbare Ausgabe.ErklärungSatz- und Titelschreibweise: Die Regeln unterscheiden sich je nach SpracheDie englische Titelschreibweise hat je nach Stilhandbuch drei verschiedene Grenzen. Französisch, Spanisch, Portugiesisch und Italienisch haben gar keine. Deutsch schreibt jedes Substantiv groß. Das Werkzeug weiß von all dem nichts — hier steht genau, was es tut.ErklärungDuplikate in einer Liste finden — ohne TabellenkalkulationZwei Zeilen, die identisch aussehen, sind es oft nicht. Groß- und Kleinschreibung, ein Leerzeichen am Ende, ein geschütztes Leerzeichen und zwei verschiedene Kodierungen desselben Akzentbuchstabens liefen je durch den Duplikatfinder — in drei von vier Fällen meldete er keine Duplikate.RatgeberEine aus Tabelle oder PDF eingefügte Liste bereinigenEin Einfügen bringt unsichtbare Zeichen mit: geschützte Leerzeichen, weiche Trennstriche, Nullbreiten-Leerzeichen, Tabs und CRLF. Vier Bereinigungswerkzeuge liefen über jedes davon — und sie verwenden drei verschiedene Definitionen von Leerraum.ErklärungDie Sprache eines Textes erkennen — und warum kurze Texte scheiternGemessen, nicht behauptet: 90 kurze echte Wendungen in sechs Sprachen, keine abgelehnt und 68 richtig — 76 %, unter sechzehn Buchstaben nur noch 64 %. Vier der falschen Antworten kamen mit 100 % Sicherheit zurück.

Ähnliche Tools

Quellen

Hast du einen Fehler in diesem Artikel entdeckt?