Das Symptom
Zwei Zeilen stehen untereinander und sehen bis aufs Pixel gleich aus:
Müller
Müller
Trotzdem findet die Suche den Namen nicht, der Abgleich zweier Listen meldet
hunderte Unterschiede, der Datenbank-Eintrag wird ein zweites Mal angelegt, und
a == b gibt hartnäckig False zurück. Besonders zuverlässig passiert das,
sobald Dateien oder Tabellen zwischen einem Mac und dem Rest der Welt hin- und
hergehen.
Die „Layer 8“-Wahrheit
Der Vergleich hat recht. Ein ü ist für Unicode nicht ein Ding, sondern zwei
zulässige Schreibweisen desselben Dings: entweder ein einzelnes fertiges Zeichen
oder ein u, dem ein kombinierendes Trema folgt. Beide Fassungen sind kanonisch
gleichwertig, beide sollen laut Standard identisch aussehen und sich identisch
verhalten – aber sie bestehen nun einmal aus verschiedenen Zeichen. Ein Vergleich,
der Zeichen für Zeichen prüft, meldet folgerichtig einen Unterschied. Er lügt
nicht, er sieht nur etwas, das der Bildschirm nicht zeigt. Deshalb ist die Länge
der Zeichenkette der schnellste Verräter: sechs Zeichen hier, sieben dort, gleiche
Anzeige.
Das ist ausdrücklich nicht das übliche Kodierungsdrama mit ä und
UnicodeDecodeError – dort ist der Text sichtbar kaputt, hier ist er sichtbar in
Ordnung. Und genau das macht ihn so zäh: Man sucht den Fehler in der Suchfunktion,
im Import, in der Datenbank, überall außer in den Zeichen selbst. Die Lösung ist
eine einzige Zeile Code, aber an der richtigen Stelle. Normalisiert wird an der
Systemgrenze, einmal, in eine festgelegte Form – für gespeicherten Text und fürs
Web üblicherweise NFC. Danach vergleicht man wieder ganz normal mit ==, weil
dann tatsächlich dasselbe dasteht. Wer das an jeder Vergleichsstelle einzeln
nachholt, hat es nicht gelöst, sondern nur vertagt.
Die echte, verifizierte Lösung
1000 % faktenbasiert. Jeder Befehl ist real, jeder Pfad existiert. Schritt für Schritt:
1.Nachsehen, ob wirklich derselbe Text dasteht
Erster Schritt ist immer die Länge. Sehen zwei Zeichenketten gleich aus und haben unterschiedlich viele Zeichen, ist der Fall geklärt: Eine davon enthält kombinierende Zeichen. Das Wort Müller ist zusammengesetzt sechs Zeichen lang und zerlegt sieben.
python3 -c "import unicodedata; a='Müller'; b=unicodedata.normalize('NFD', a); print(len(a), len(b), a == b)"
# 6 7 False
2.Die Codepunkte sichtbar machen
Wer es genau wissen will, lässt sich die Codepunkte einzeln ausgeben. Dann steht schwarz auf weiß, dass einmal ein fertiges ü mit dem Codepunkt U+00FC gespeichert ist und einmal ein u, gefolgt von U+0308, dem kombinierenden Trema.
python3 -c "import unicodedata; a='Müller'; b=unicodedata.normalize('NFD', a); print([hex(ord(c)) for c in a]); print([hex(ord(c)) for c in b])"
# ['0x4d', '0xfc', '0x6c', '0x6c', '0x65', '0x72']
# ['0x4d', '0x75', '0x308', '0x6c', '0x6c', '0x65', '0x72']
3.Vor jedem Vergleich normalisieren
Die Lösung ist eine Zeile: Beide Seiten in dieselbe Normalform bringen, dann vergleichen. Danach stimmen die Zeichenketten überein, weil sie jetzt tatsächlich aus denselben Zeichen bestehen – nicht, weil man den Vergleich weichgespült hätte.
python3 -c "import unicodedata; n=lambda s: unicodedata.normalize('NFC', s); a='Müller'; b=unicodedata.normalize('NFD', a); print(n(a) == n(b))"
# True
4.An der Grenze normalisieren, nicht überall im Code
Normalisiert wird dort, wo Text ins System kommt: beim Einlesen der Datei, beim Entgegennehmen des Formulars, beim Import aus der Fremdliste. Wer stattdessen an jeder Vergleichsstelle nachträglich normalisiert, vergisst irgendwann eine – und genau die wird später der Fehlerbericht.
python3 -c "import unicodedata; zeilen=[unicodedata.normalize('NFC', z) for z in open('namen.txt', encoding='utf-8')]; print(len(zeilen))"
5.Auch Dateinamen normalisieren, bevor man Listen abgleicht
Beim Vergleich zweier Verzeichnisse gilt dasselbe. Kommt eine Seite von einem Mac, liegen die Namen dort zerlegt vor – Apples Technote zu HFS Plus schreibt genau das fest. Wer beide Listen vor dem Abgleich durch dieselbe Normalform schickt, bekommt statt hunderter Scheinunterschiede die echten zu sehen.
python3 -c "import os, unicodedata; print(sorted(unicodedata.normalize('NFC', n) for n in os.listdir('.'))[:5])"
6.NFC und NFKC nicht verwechseln
Die K-Formen sind etwas anderes und keine bessere Version. NFKC ersetzt Zeichen durch ihre Kompatibilitätsentsprechung: Aus der römischen Ziffer eins wird ein normales lateinisches I. Für Suchindizes kann das gewollt sein, für gespeicherte Namen ist es Datenverlust – der Unterschied verschwindet unwiederbringlich. Zum Vergleichen von Namen nimmt man NFC, nicht NFKC.
python3 -c "import unicodedata; print(ascii(unicodedata.normalize('NFKC', 'Ⅰ')))"
# 'I'