Unicode oder der Fall der wandernden Umlaute

Es scheint geradezu selbstverständlich, dass jeder, der mit Python oder auch nur irgendwie computergestützt mit größeren Textmengen arbeitet, ein gewisses Grundverständnis davon haben sollte, wie die scheinbar einfach hingetippten Buchstaben unter der Oberfläche codiert werden. Joel Spolskys Artikel The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!) ist zwar mittlerweile mehr als 15 Jahre alt, aber immer noch so wichtig wie Anfang des 21. Jahrhunderts.

Bis vor kurzem dachte ich, diesbezüglich kaum Defizite zu haben. Ich wusste um den Unterschied zwischen ASCII (langweilige standardenglische Zeichen) und Unicode (Umlaute! Emojis!) und verstand nach irritierenden Fehlermeldungen bei der Verwendung von Python 2.7, dass Version 2.x eben nicht selbstverständlich in Unicode codiert, sondern dass man explizit seine Vorliebe betonen muss.

Dass Unicode nicht gleich Unicode ist, bemerkte ich erst, als etwas kaputt ging. Für mein Projekt Reclaimbook implementierte ich einen einfachen Vorschlagsmechanismus, der aus einem Set von Buchtiteln randomisiert Vorschläge auswählt. Die vorgeschlagenen Titel wurden mit Hilfe eines textgenerierenden künstlichen neuronalen Netzwerks generiert (Max Woolfs textgenrnn). Janelle Shane hatte zu diesem Zeitpunkt bereits gezeigt, dass textgenerierende künstliche neuronale Netzwerke oft recht lustige und manchmal faszinierend reale Ergebnisse zu erzeugen vermögen.

Nach einigem Herumprobieren mit textgenrnn kompilierte ich die generierte Titelliste und implementierte sie in den Websitenprototyp. Ich lud ein paar Leute ein, mit den Funktionen der Seite herumzuspielen und, siehe da, plötzlich war da etwas, das ich nie zuvor bemerkt hatte:

Wandernder Umlaut
Abb. 1: Wandernde Umlaute

Ich hatte mich zuvor durch bestimmt hunderte von Textvorschlägen geklickt, kein einziges Mal hatte ich ein solches Verhalten bei Umlauten bemerkt. Mein erster Verdacht lag auf dem Webfont “Playfair Display”, der auf der Seite verwendet wird. Dieser Verdacht stellte sich jedoch schnell als falsch heraus. Ein Blick in den Code der Website und auf die Liste mit Titelvorschlägen half ebensowenig weiter, hier sah alles normal aus. Was auffällig war: Die verschobenen Umlaute betrafen lediglich Firefox, Chrome und Safari zeigten die Titel korrekt an.

Mit einigem Googlen kam ich dem Problem nach und nach näher und lernte dabei das Konzept der “Unicode equivalence” kennen: »the specification by the Unicode character encoding standard that some sequences of code points represent essentially the same character.« Oder, kurz gesagt: Unterschiedliche Codierung für gleiche Zeichen.

»You’re never actually directly dealing with “characters” or “text”, you’re always dealing with bits as seen through several layers of abstractions. Incorrect results are a sign of one of the abstraction layers failing.«1

Es gibt insgesamt vier Unicode-Normalformen:2

    kanonische Äquivalenz kompatible Äquivalenz    
kombiniert   Normalization Form Canonical Composition (NFC) Normalization Form Compatibility Composition (NFKC)    
zerlegt   Normalization Form Canonical Decomposition (NFD) Normalization Form Compatibility Decomposition (NFKD)    
           

Gewünscht waren die Umlaute in der Form NFC (“ü”), stattdessen waren die Umlaute als NFD (“u” und “_̈”3) codiert. Während Safari und Chrome NFD automatisch zu NFC normalisieren, stellt Firefox NFD dar.4

Es scheint so, als hätte entweder textgenrnn NFD-codierten Unicode generiert oder als sei irgendwo auf dem Weg meiner Python-Pipeline irgendetwas schief gegangen, wobei mir erstere Option als die plausiblere erscheint.

Wie nun das Codierungsproblem lösen? Ich habe mich in diesem Fall dafür entschieden, schlichtweg alle NFD-Umlaute per Suche/Ersetzen in der Titelliste mit NFC-Umlauten zu ersetzen. Wenig elegant, aber effektiv. Um zukünftige Fälle von NFC-Codierung zu verhindern, gibt es die praktische Funktion “normalize” aus dem Standardmodul “unicodedata”:

unicodedata.normalize(form, unistr)
Return the normal form form for the Unicode string unistr. Valid values for form are ‘NFC’,‘NFKC’, ‘NFD’, and ‘NFKD’. (Python 3.6 Documentation)

Zudem versuche ich, beim Programmieren die “Good Practices“-Leitlinien aus Victor Stinners Buch Programming with Unicode im Kopf zu behalten:

  • decode all bytes data as early as possible: keyboard strokes, files, data received from the network, …
  • encode back Unicode to bytes as late as possible: write text to a file, log a message, send data to the network, …
  • always store and manipulate text as character strings
  • if you have to encode text and you can choose the encoding: prefer the UTF-8 encoding. It is able to encode all Unicode 6.0 characters (including non-BMP characters), does not *depend on endianness, is well supported by most programs, and its size is a good compromise.

  1. David C. Zentgraf: _What Every Programmer Absolutely, Positively Needs To Know About Encodings And Character Sets To Work With Text, abgerufen am 12. März 2019. 

  2. Vgl. den deutsprachigen Wikipedia-Artikel zu Unicode-Normalisierung, abgerufen am 12. März 2019. 

  3. Besser bekannt als “Combining Diaeresis”, abgerufen am 12. März 2019. 

  4. Siehe dazu auch die ausführliche Erklärung von Jukka K. Korpela auf Stackoverflow zum Verhalten von Firefox vs Chrome/Safari: How to avoid Unicode normalization, abgerufen am 12. März 2019. 

Manuel Häußermann

Manuel Häußermann

I'm interested in the intersections between Literature, Culture and the Digital.

rss facebook twitter github gitlab youtube mail spotify lastfm instagram linkedin pinterest medium vimeo stackoverflow reddit quora microphone tinyletter piqd geisterwissenschaft