UTF-8 und die Frage, wie Unicode in den Rechner kommt
In Teil 1 haben wir die Frage beantwortet, warum es Unicode gibt und warum es in unserer global vernetzten Welt wichtig ist, solche Standards zu schaffen. Wir haben uns außerdem angeschaut, wie Unicode sprachtypische Besonderheiten abbildet. Doch bislang haben wir nur Unicode-Codepoints betrachtet, letztlich also Zahlen. Heute erzähle ich, wie diese Zahlen auf Dateibytes abgebildet werden.
Von Zahlen und Bytes
Alle bisherigen Überlegungen zur Abbildung von Zeichen bildeten lediglich eine Nummerierung der Zeichen in Unicode-Codepoints ab. Wenn wir uns aber eine Textdatei vorstellen, so ist diese in einzelnen Bytes organisiert. Ein Byte besteht aus 8 Bit und kann daher 256 verschiedene Werte abbilden. Ohne Weiteres kann man aber nicht erkennen, welche Bedeutung die einzelnen Bytes haben. Insbesondere kann ich aus den Bytes nicht unmittelbar erkennen, ob diese ASCII-kodierte Textdaten enthalten oder ISO-8859-15. Genaugenommen kann ich nicht einmal wissen, ob es sich überhaupt um Text handelt und nicht um ein JPEG der letzten Bundesligatabelle. Selbst bei reinen Textdaten muss die Information über die verwendete Kodierung – also die Abbildung von Bytes auf Zeichen eines Zeichensatzes – immer zusätzlich transportiert werden. Erst damit gibt man den nackten Bits überhaupt eine Bedeutung.
Nun ist es in der Praxis eher selten, dass man beim Austausch von Daten explizit angibt, wie sie kodiert sind. Das liegt daran, dass man so etwas auch per Konvention regeln kann. Eine Textdatei, die auf einem deutschen Windows 98 angelegt wurde, verwendet beispielsweise mit hoher Wahrscheinlichkeit die Kodierung Windows-1252 (ein Ableger von ISO-8859-1). Manche Programme sind inzwischen auch ziemlich gut darin, per Heuristik quasi zu „raten“, welche Kodierung verwendet wird.
Im Netzwerkbereich – dazu zählt natürlich auch das Internet – ist es hingegen üblich, die Netzwerkprotokolle mit Methoden zur Übertragung der verwendeten Kodierung auszustatten. Im HTTP-Protokoll existiert beispielsweise der Content-Type
-Header. Die Angabe: Content-Type: text/plain; charset=UTF-8
kündigt dann etwa eine UTF-8-kodierte Textdatei an. Dazu aber in einem späteren Beitrag mehr.
Doch was ist überhaupt eine Kodierung?
Zu ASCII-Zeiten reichte ein Byte pro Zeichen, sodass immer klar war, wo in der Datei ein Zeichen beginnt und wo eins endet. Mit Unicode ist das nicht mehr so einfach. Da es deutlich mehr als 256 Codepoints gibt, gibt es auf jeden Fall Zeichen, für deren Darstellung mehr als ein Byte gebraucht werden.
Die erste Idee ist nicht unbedingt die beste
Schauen wir uns als Beispiel das Wort „Unicode“ an und wandeln es in Unicode-Codepoints um:
U+0055 U+006E U+0069 U+0063 U+006F U+0064 U+0065
Einer der ersten Ansätze, und im Prinzip derjenige, der ab Windows NT von Microsoft eingesetzt wurde, ist jedes Zeichen in zwei Bytes zu speichern. Damit gelangt man zur sog. UCS-2-Kodierung (Universal Coded Character Set), die 2 Bytes pro Zeichen benötigt. Hexadezimal sieht das dann z.B. so aus:
00 55 00 6E 00 69 00 63 00 6F 00 64 00 65
Allerdings ist ein Aspekt zunächst völlige Willkür. Möglich wäre nämlich auch Folgendes:
55 00 6E 00 69 00 63 00 6F 00 64 00 65 00
Der Unterschied ist, dass ich jedes Bytepaar vertauscht habe. Der Informatiker spricht von der Byte Order oder Endianness, in der sich Intel-CPUs z.B. von PowerPC- oder ARM-Prozessoren unterscheiden (ja ich weiß, dass manche PPCs und ARM-CPUs umschaltbare Endianness haben). Das erste Beispiel verwendet dabei das Big-Endian-Format, d.h. jede Zahl wird mit dem höchsten Bit zuerst gespeichert. Das zweite Beispiel hingegen verwendet – man ahnt es – das Little-Endian-Format und damit genau die Konvention, für die sich Intel entschieden hat.
Je nachdem, ob man Windows NT also jetzt auf einem Intel-Prozessor oder einem DEC Alpha benutzt hat, wurde standardmäßig entweder Big-Endian- oder Little-Endian-kodiert.
Nicht so gut, wenn man daran nicht denkt und Daten austauschen will. Um es möglich zu machen, zu erkennen, welche Endianness vorliegt, kann, wenn gewünscht, eine Markierung an den Dateianfang gestellt werden, die sog. Unicode-BOM (Byte Order Mark). Die besteht immer entweder aus FE FF
(Big Endian) oder aus FF FE
(Little Endian). So lässt sich leicht an den ersten beiden Bytes erkennen, wie der Rest kodiert ist. Fairerweise muss man jedoch sagen, dass in der Praxis auch sehr viele Programme Probleme mit den zwei Extrabytes am Anfang hatten – dazu zählte früher z.B. auch PHP.
Die UCS-2-Kodierung hat zwei erhebliche Nachteile. Zum einen kann sie nur 65536 Zeichen abbilden, mehr ist mit zwei Bytes einfach nicht möglich. Das deckt die Möglichkeiten von Unicode nur zu etwa sechs Prozent ab.
Viel schlimmer ist aber der andere Nachteil: Jedes Zeichen belegt 2 Bytes. Zu Zeiten von Windows NT, wo noch nicht jedes Mobiltelefon 128GB Speicher und mehr hatte – „640 K“, wir erinnern uns 😉 – hieß das, dass jede Datei doppelt so groß wurde, wie eine ASCII-Datei gleichen Inhalts. Besonders in den USA, die ja zuvor mit ASCII prima klarkamen, empörte man sich in der Fachwelt über den derart barocken Umgang mit kostbarem Speicherplatz.
Geht es auch effizienter?
Das erste Problem, nämlich den ganzen Unicode-Umfang abbilden zu können, ging man zunächst relativ grobschlächtig an: indem man einfach vier statt zwei Bytes benutzte. Das nennt sich UTF-32-Kodierung (UCS Transformation Format), die jedes Zeichen in 32 Bits (= 4 Bytes) kodiert. Damit lässt sich zwar der gesamte Vorrat an Codepoints abdecken. Aber jede ASCII-Datei auf das Vierfache der Dateigröße aufblähen, um ein, zwei Kanjis benutzen zu können – dazu war in der Praxis praktisch niemand bereit – und so verschwand diese Idee schnell wieder von der Bildfläche.
Den Durchbruch erlebte erst UTF-8. Die erste Festlegung, die UTF-8 trifft, ist, dass es eine Kodierung mit variabler Länge definiert, ein sog. Multibyte Character Set. (An dieser Stelle ein Gruß an die PHP-Entwickler: Ja, genau, daher kommt der Name der mbstring-Erweiterung.) Innerhalb von UTF-8 existieren Zeichen mit Längen zwischen einem und (derzeit) vier Bytes, wobei der Algorithmus auch Längen von bis zu acht Bytes zulässt. Das erste Byte definiert dabei die Länge des Zeichens über die Zahl der binären Einsen, mit denen es beginnt. Alle folgenden Bytes haben dann die Form 10xxxxxx.
Zeichen, die genau ein Byte lang sind, fangen immer mit einer binären 0 am Anfang an und haben folglich immer einen Wert von 0-127. Wir erinnern uns, die ersten 127 Unicode-Codepoints waren definiert als identisch zu ASCII. Dadurch stimmen hier auch die binären Repräsentationen 1:1 überein. Jede (7-Bit)-ASCII-Datei kann ich daher immer als UTF-8-kodiert interpretieren, es ist also aufwärtskompatibel zu ASCII. Besonders dieser Aspekt verhalf UTF-8 sicherlich zu seinem Siegeszug.
UTF-8 in der Praxis, …
Schauen wir uns noch einmal das Zeichen 🤷 (U+1F937) an und wie es UTF-8-kodiert aussieht:
11110000 10011111 10100100 10110111
Wir nehmen einmal an, dass immer eine Datei angelegt wurde, die nur die dargestellten Bytes enthält. Vor und nach der dargestellten Bytefolge gibt es also nichts mehr.
Unschwer ist zu erkennen, dass das erste Byte mit vier Einsen anfängt und das Zeichen daher mit vier Bytes kodiert wird. Genau wie oben behauptet, fangen alle weiteren Bytes mit 10xx an. Ein UTF-8-kompatibles Programm weiß also immer, wo ein Zeichen anfängt – eine entscheidende Eigenschaft. Wenn ich nämlich – etwa aus purer Böswilligkeit – einfach einmal die ersten Bytes vertausche, …
10011111 11110000 10100100 10110111
… dann ist sofort klar, dass das Ergebnis kein gültiges UTF-8 mehr ist. Nicht nur, dass die Datei mit einem Byte anfängt, dass kein erstes Byte eines UTF-8-Zeichens sein kann, danach kommt auch noch das erste Byte eines Vier-Byte-Zeichens und es gibt aber nur noch drei Bytes.
… aber was, wenn etwas nicht so läuft, wie erwartet?
Es ist also sofort klar, dass hier etwas nicht stimmt und man könnte abbrechen und einfach eine Fehlermeldung anzeigen. Stattdessen versuchen die meisten Programme, das Beste aus den Bytes zu machen und lesen solange weiter Bytes ein, bis es wieder mit gültigen Bytefolgen weitergeht. Das ist meist auch die sinnvollere Vorgehensweise, denn es kann ja Gründe geben, warum der Fehler aufgetreten ist. Kratzer auf der Bluray, gekipptes Bit im Netzwerkkabel (kommt öfter vor, als man denkt) oder was auch immer.
Damit man trotzdem erkennt, dass es einen Dekodierungsfehler gab, definiert Unicode sogenannte Unicode Replacement Characters. Das bekannteste dürfe das � sein, dass sich nicht zuletzt als Gag auf den bekannten Schei�-Encoding-T-Shirts findet. Es wird immer dann als Ersatz dargestellt, wenn ein Programm auf eine Bytefolge gestoßen ist, die in der aktuellen Unicode-Kodierung – egal, ob UTF-8 oder eine der weniger gebräuchlichen – nicht vorkommen darf.
Im nächsten Blogpost zum Thema Unicode beschäftigen wir uns mit Webseiten und der Nutzung von Unicode-Kodierungen in Webanwendungen. Vor allem aber schauen wir uns an, auf welche bekloppten fragwürdigen Workarounds dabei zwischenzeitlich etwa Datenbankhersteller gekommen waren.
Du fängst jede HTML-Seite immer mit <meta charset="UTF-8">
an? Dann bewirb dich bei uns!
Wir freuen uns, wenn Ihr diesen Beitrag teilt.
Kommentare
Keine Kommentare gefunden.