Text bereinigen, der von woanders kommt
Text, der aus einem PDF, einer Tabellenzelle, einem CMS-Feld, einem E-Mail-Programm oder einer Chat-App kam, trägt die Formatierungsentscheidungen dessen mit sich, was ihn erzeugt hat, und fast keine dieser Entscheidungen ist auf dem Bildschirm sichtbar. Das Ergebnis ist eine Zeichenkette, die richtig aussieht, richtig druckt und dann ohne erkennbaren Grund an einem Vergleich, einer Suche, einem JSON-Parse oder einer Datenbankabfrage scheitert.
Zeichen, die du nicht sehen kannst
Für Unicode sind das echte Zeichen. Sie haben nur keine sichtbare Form, oder dieselbe Form wie etwas anderes.
| Zeichen | Codepunkt | Kommt meist von | Was es kaputtmacht |
|---|---|---|---|
| Geschütztes Leerzeichen | U+00A0 | in HTML, Word, PDF-Layout | Aufteilen an Leerzeichen, exakte Treffer |
| Schmales geschütztes Leerzeichen | U+202F | Französische Typografie, Datumsangaben aus Word | Dasselbe, nur weniger offensichtlich |
| Ideografisches Leerzeichen | U+3000 | CJK-Eingabemethoden | Sieht wie eine breite Lücke aus |
| Nullbreiten-Leerzeichen | U+200B | Zeilenumbruchhinweise aus einem CMS, kopierter Webtext | Unsichtbar und kein Leerraum |
| Nullbreiten-Trenner / -Verbinder | U+200C, U+200D | Persisch, indische Schriften, Emoji-Sequenzen | Wortgrenzen, Zeichenzählungen |
| Wortverbinder | U+2060 | Satzprogramme | Nichts Sichtbares, dafür alles Textliche |
| Weicher Trennstrich | U+00AD | Silbentrennung in Word, PDF-Exporte | Ein Wort passt nicht mehr auf sich selbst |
| Byte Order Mark | U+FEFF | Die ersten Bytes einer UTF-8-Datei | Das erste Feld der ersten Zeile |
| Links-nach-rechts- / Rechts-nach-links-Marke | U+200E, U+200F | Bidirektionale Inhalte | Verirrte Zeichen rund um Zahlen |
| Zeilen- und Absatztrenner | U+2028, U+2029 | Manche Mac- und Layout-Programme | Zeilenaufteilung, ältere JavaScript-Parser |
Eine Suche scheitert deshalb, weil sie Codepunkte vergleicht und keine Formen. Wenn ein PDF dir New U+00A0 York geliefert hat und du New York mit einem gewöhnlichen Leerzeichen U+0020 tippst, sind die beiden Zeichenketten verschieden, und nichts sagt dir, warum.
"New York".includes("New York") false, the gap is U+00A0
" text".trim().length 5, the zero-width space survives
Ein geschütztes Leerzeichen gilt für die meisten Trim-Funktionen und für \s in den meisten Regex-Engines als Leerraum, es verschwindet also an den Rändern einer Zeichenkette und überlebt in der Mitte, genau dort, wo eine Aufteilung ein einfaches Leerzeichen erwartet. Ein Nullbreiten-Leerzeichen gilt nirgends als Leerraum, Trimmen und Zusammenziehen lassen es also unangetastet. Deshalb kann der Email Extractor auch nichts zurückliefern, wenn der Text aus einem Mailprogramm kopiert wurde: Ein einziges Nullbreiten-Zeichen in der Adresse verhindert, dass das Muster passt.
Der Hidden Character Detector zeigt, was wirklich da ist, und Unicode Escape liefert die exakten Codepunkte eines einzelnen Werts. Entferne aber nicht alles auf Sicht: Ein Nullbreiten-Verbinder in einer Emoji-Sequenz und ein Nullbreiten-Trenner in persischem oder Devanagari-Text sind Inhalt, kein Rauschen.
Satzzeichen, die eine Textverarbeitung für dich geändert hat
Die Autokorrektur ersetzt beim Tippen Zeichen durch typografische Varianten, und die Ersetzung folgt dem Text aus dem Dokument heraus.
| Du hast getippt | Jetzt hast du | Codepunkt |
|---|---|---|
' | rechtes einfaches Anführungszeichen | U+2019 |
" und " | linkes und rechtes doppeltes Anführungszeichen | U+201C, U+201D |
- zwischen Wörtern | Halbgeviertstrich oder Geviertstrich | U+2013, U+2014 |
... | Auslassungspunkte | U+2026 |
- vor einer Zahl | Minuszeichen oder geschützter Bindestrich | U+2212, U+2011 |
In Fließtext ist das in Ordnung, in allem, was eine Maschine parst, fatal.
{ “name”: “Ada” } unexpected token, only U+0022 is a JSON string quote
git commit -m “fix” the shell sees three words, not one quoted argument
WHERE name = ‘Ada’ SQL syntax error near ‘
10\u201320 not a number range, U+2013 is not a hyphen
In CSV ist der Fehler leiser. Ein Parser erkennt nur das gerade doppelte Anführungszeichen als Feldbegrenzer, ein Wert, den eine Textverarbeitung in typografische Anführungszeichen gesetzt hat, gilt also als nicht eingefasst; jedes Komma darin teilt dann die Zeile, und alle Spalten danach verrutschen. Eine Spalte, deren negative Zahlen U+2212 verwenden, wird als Text importiert, und Summen lassen sie stillschweigend aus.
Find and Replace mit einer kurzen Ersetzungsliste ist die Abhilfe, aber wende sie nur auf Text an, der für Code, CSV oder einen Schlüssel bestimmt ist. Sie über Fließtext laufen zu lassen, den du veröffentlichst, ebnet Satzzeichen ein, die Absicht waren.
Zeilenenden und nachlaufender Leerraum
Windows-Werkzeuge beenden eine Zeile mit CR LF (U+000D U+000A), Unix-Werkzeuge mit LF allein, und ein paar sehr alte Mac-Exporte nutzen bis heute CR allein. Die meisten Parser kommen damit klar. Naives Aufteilen nicht.
"UK\r\n" split on "\n" -> "UK\r"
"UK\r" === "UK" false
"UK\r".length 3
Zwei Zeichenketten, die in einer Tabelle, einem Log oder einer Diff-Ansicht identisch aussehen, können sich trotzdem um einen Wagenrücklauf, ein nachlaufendes Leerzeichen oder einen Tabulator unterscheiden. Wenn Text Diff eine Zeile als geändert markiert und keine Änderung zu sehen ist, ist das die Erklärung. Das ist auch der Grund, warum ein verirrtes Leerzeichen innerhalb der Anführungszeichen landet, wenn eine eingefügte Spalte durch Quote Lines oder Join Lines läuft.
Normalisiere in einem einzigen Durchgang und triff dabei CR LF und ein alleinstehendes CR gemeinsam (\r\n? ersetzt durch \n), sonst verdoppelt eine zweistufige Ersetzung die Leerzeilen.
Ein Buchstabe, zwei Schreibweisen
Unicode erlaubt es, einen akzentuierten Buchstaben als einen einzigen Codepunkt oder als Grundbuchstaben gefolgt von einem kombinierenden Zeichen abzulegen. Beides ist korrekt, und beides ist nicht gleich.
| Form | é wird gespeichert als | Code-Einheiten in JavaScript |
|---|---|---|
| NFC (zusammengesetzt) | U+00E9 | 1 |
| NFD (zerlegt) | U+0065 U+0301 | 2 |
macOS hat historisch zerlegte Dateinamen herausgegeben, und mehrere PDF-Generatoren und Eingabemethoden liefern zerlegten Text, ein Wert aus der einen Quelle stimmt also nicht mit demselben Wert überein, der auf einer Tastatur getippt wurde.
"é" === "é" false
"é".normalize("NFC") === "é" true
Die Folgewirkungen reichen über die Gleichheit hinaus. Eine Sortierung, die Codepunkte vergleicht, stellt zerlegte Formen neben ein einfaches e und zusammengesetzte Formen weit davon weg, eine Namensliste kommt also in zwei Klumpen zurück. Die Zeichenzahl unterscheidet sich zwischen den Formen, was bei einer Grenze von 280 Zeichen oder einer VARCHAR(50)-Spalte zählt, und Text Statistics meldet unterschiedliche Zahlen für scheinbar denselben Text. Das Kürzen ist schlimmer: Eine zerlegte Zeichenkette an einer festen Länge zu schneiden kann zwischen einem Grundbuchstaben und seinem Akzent trennen, sodass dieser Akzent sich an das anhängt, was folgt; Truncate Text kann bei nicht normalisierter Eingabe also ein sichtbar falsches letztes Zeichen erzeugen.
Die Kompatibilitätsformen NFKC und NFKD gehen weiter und falten Zeichen zusammen, die anderen bloß ähneln: Die Ligatur U+FB01 wird zu fi, das Vollbreiten-A U+FF21 wird zu A, das Mikrozeichen U+00B5 wird zum griechischen My U+03BC, und die hochgestellte ² wird zu einer schlichten 2. Für einen Such- oder Deduplizierungsschlüssel ist das hervorragend und für alles, was du anzeigst, zerstörerisch, denn x² wird klammheimlich zu x2.
Die Faustregel: NFC zum Speichern und Anzeigen, NFKC nur für Schlüssel, die niemand je zu sehen bekommt.
Groß- und Kleinschreibung umzuwandeln ist nicht eine Operation
Großschreibung ist keine Abbildung Zeichen für Zeichen, und sie ist nicht unabhängig von der Locale.
- Das deutsche
ßU+00DF wird großgeschrieben zuSS, die Zeichenkette wird also länger, und der Rückweg in die Kleinschreibung liefert nicht das Original. - Das griechische Sigma wird am Wortende zu
ςund sonst zuσkleingeschrieben, was den Rückweg ebenfalls zerstört. - Türkisch und Aserbaidschanisch haben ein punktloses
ıU+0131 und ein großesİU+0130 mit Punkt. In diesen Locales wirdIzuıkleingeschrieben undizuİgroßgeschrieben.
Der letzte Punkt ist der klassische Produktionsfehler. Code, der einen Headernamen, eine Dateiendung oder eine Protokollzeichenkette kleinschreibt, funktioniert überall, bis er auf einer Maschine läuft, deren Standard-Locale Türkisch ist; dort wird "FILE" zu fıle und passt nicht mehr auf file. In Java und .NET nutzen die Methoden ohne Argument die Standard-Locale der Maschine, toUpperCase() und ToUpper() sind also die Falle; benenne für alles Maschinenlesbare eine invariante Locale. Zum Vergleichen ist Case Folding (casefold() in Python) besser als Kleinschreibung, weil es ß als ss behandelt.
Für Title Case gibt es keine einheitliche Definition, deshalb erzeugt „jedes Wort großschreiben“ Ergebnisse wie „The Lord Of The Rings“ und „IPhone“. Die meisten Stilrichtlinien schreiben das erste und das letzte Wort groß sowie alles außer Artikeln, nebenordnenden Konjunktionen und kurzen Präpositionen, halten beide Hälften eines Bindestrichkompositums groß und rühren Akronyme oder Namen mit Großbuchstaben im Inneren wie McDonald, O'Brien oder iPhone nie an.
Schreibweisen aus der Programmierung haben ihr eigenes Grenzproblem. HTTPResponseCode in Snake Case umzuwandeln hängt vollständig davon ab, wie der Trenner eine Folge von Großbuchstaben behandelt; der Case Converter liefert http_response_code, ein naiver Trenner h_t_t_p_response_code.
Eine Reihenfolge, die funktioniert
Jeder Schritt setzt voraus, dass der vorherige gelaufen ist. In falscher Reihenfolge arbeiten sie gegeneinander.
- Kläre die Codierung. Mojibake wie
cafébedeutet, dass die Bytes mit der falschen Codierung gelesen wurden; decodiere also die Originalbytes neu, statt die Symptome zu flicken. Entferne ein BOM nur ganz am Anfang des Textes. - Normalisiere die Zeilenenden in einem einzigen Durchgang auf LF.
- Entferne die Formatzeichen, die du als Rauschen eingestuft hast: weiche Trennstriche, Nullbreiten-Leerzeichen, Wortverbinder, Bidi-Marken. Tu das vor dem Normalisieren, denn NFC kann einen Grundbuchstaben und seinen Akzent nicht über ein unsichtbares Zeichen hinweg zusammensetzen, das zwischen ihnen sitzt.
- Ersetze ähnlich aussehende Satzzeichen, wenn das Ziel Code, CSV, JSON oder ein Schlüssel ist.
- Wende die Unicode-Normalisierung an, standardmäßig NFC.
- Kümmere dich jetzt um den Leerraum. Wandle die verbliebenen exotischen Leerzeichen in U+0020 um, ziehe Folgen zusammen und trimme dann. Das vor Schritt 3 zu tun hinterlässt doppelte Leerzeichen überall dort, wo ein geschütztes Leerzeichen auf ein gewöhnliches traf, und vor Schritt 2 zu trimmen lässt in jeder Zeile einen Wagenrücklauf am letzten Wert kleben.
- Wandle die Groß- und Kleinschreibung zuletzt um, mit ausdrücklich benannter Locale.
Operationen auf Wortebene gehören ebenfalls hinter Schritt 3. Text mit Split Text aufzuteilen, solange weiche Trennstriche darin stehen, ergibt überhöhte Zählungen und halbe Wörter, und dasselbe gilt für alles, was nach Begriffen sucht, einschließlich Censor Text.
Wenn ein Wert nach all dem immer noch nicht passen will, hör auf, ihn anzusehen. Gib seine Länge aus, escape ihn zu Codepunkten und vergleiche die beiden escapten Formen direkt; dort ist der Unterschied offensichtlich, auf dem Bildschirm nie.