Zeichencodierung/Unicode

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Der universale Code: Unicode

Mit Unicode werden so ziemlich alle Zeichencodierungsprobleme dieser Welt gelöst. Und weil Webseiten potentiell mit allen Schriften dieser Welt genutzt werden, ist seit HTML 4.0 und XML (und damit auch XHTML) festgelegt, dass grundsätzlich alle in Unicode definierten Zeichen vorkommen dürfen.

Mit dieser Festlegung kann man also jedes der 149.186 Unicode-Zeichen[1] in seinem Text verwenden. Aber Unicode regelt nicht nur die Codierung von immer mehr Schriftzeichen dieser Welt, sondern kennt zu jedem Zeichen dutzende definierte Eigenschaften. Ein Zeichen besitzt mindestens folgende Informationen:

  • der eindeutige Unicode-Name des Zeichens
  • den zugeordneten Codepoint (die „Nummer“ des Zeichens)
  • eine generelle Kategorisierung (Buchstabe, Zahl, Symbol, Interpunktion, ...)
  • weitere grundsätzliche Charakteristiken (Whitespace, Bindestrich, alphabetisch, ideografisch, Nicht-Zeichen, veraltet, ...)
  • anzeigerelevante Informationen (Schreibrichtung, Form, Spiegelung, Breite)
  • Groß-/Kleinschreibung
  • Zahlenwert und -typ bei Zahlzeichen
  • Trennfunktionen (zwischen Wörtern, Zeilen, Sätzen,...)

Die meisten Eigenschaften eines Zeichens sind nur für die interne Behandlung durch den Computer relevant. Beispielsweise werden arabische Schriftzeichen automatisch korrekt von rechts nach links geschrieben, oder der Zeilenumbruch nutzt die Stellen, an denen eine Wortgrenze z. B. durch ein normales Leerzeichen existiert. Dieser kleine Exkurs soll aber verdeutlichen, dass die Bytewerte der Buchstaben für den Computer viel mehr bedeuten können, als nur den Buchstaben an sich.

Über die Definition von Zeichen und ihren Codepoints hinaus liefert das Unicode-Konsortium noch eine Fülle von weiteren Standardisierungen, die sich auf die linguistisch korrekten Behandlung von schriftlicher menschlicher Sprache beziehen. Darunter sind Algorithmen zur sprachspezifischen Sortierung (Beispiel: Im Deutschen gibt es zwei Sortierungsvarianten, Wörterbücher sortieren mit ä=a, ö=o, ü=u, Telefonbücher mit ä=ae, ö=oe, ü=ue) und zur Normalisierung (Wie prüft man, ob zwei Zeichenketten identisch sind).

Codepoints und Schriftzeichen

Ein Unicode Codepoint entspricht oft einem bestimmten Schriftzeichen oder Leerraum. Das muss aber nicht immer so sein. Einige Codepoints stellen Steuerzeichen dar, wie zum Beispiel U+0009 und U+000A für Tabulator und Zeilenvorschub. Andere werden nur fallweise dargestellt, beispielsweise der bedingte Trennstrich ­ mit dem Codepoint U+00AD.

Um beliebige Kombinationen von Akzenten (diakritische Zeichen) mit anderen Schriftzeichen zu ermöglichen, gibt es Codepoints für kombinierende Zeichen. Sie werden nicht eigenständig dargestellt, sondern mit dem vorangehenden Zeichen zu einem Schriftzeichen verschmolzen. Die deutschen Umlautpunkte  ̈ existieren beispielsweise als Codepoint U+0308 COMBINING DIAERESIS. Ein ä lässt sich dadurch einerseits als Codepoint U+00E4 LATIN SMALL LETTER A WITH DIAERESIS darstellen, andererseits aber auch als die Abfolge U+0061 U+0308.

Weil es dadurch erschwert wird, Zeichenketten zu verarbeiten und zu vergleichen, sieht Unicode einen Normalisierungsalgorithmus vor, der in JavaScript durch die Methode normalize von String.prototype und in PHP durch die Normalizer-Klasse bereitgestellt wird.

Andererseits gibt es auch Zeichen, die den gleichen Glyphen (die visuelle Darstellung des Zeichens) nutzen, aber unterschiedliche Bedeutung haben. Vergleichen Sie beispielsweise p und р - sie sehen gleich aus, aber eins von den beiden ist das kyrillische Zeichen Er. Da die kyrillischen Zeichen trotz gleichen Aussehens eine andere Bedeutung haben, nutzen sie andere Codepoints. Die dadurch entstehende visuelle Verwechselungsgefahr ist bei Phishing-Attacken beliebt: eine URL wie https://рost.example.org kann Sie schneller nach Fernost schicken als Sie „Transsibirische Eisenbahn“ sagen können. Wenn Sie diesen Link kopieren und einfügen, sehen Sie https://xn--ost-zed.example.org/, die für nicht-ASCII-Zeichen erforderliche Punycode-Darstellung des Domain-Namens, woran Sie die Täuschung erkennen können.

Unicode und Fonts

Die gängigen Font-Formate TTF (TrueType) und OTF (OpenType) lassen maximal 65535 Glyphen zu. Die Darstellung sämtlicher Unicode-Zeichen mit einer Schriftdatei (und damit font-family in CSS) ist damit technisch unmöglich. Projekte wie Googles Noto bestehen daher aus vielen Schriftdateien, um das Problem durch Modularisierung zu lösen. In einer CSS @font-face Definition lässt sich der Unicode-Zeichenbereich angeben, für den eine Schriftart Glyphen enthält. Wenn ein HTML Element nun beispielsweise Codepoints für ägyptische Hieroglyphen und lateinische Schriftzeichen enthält, könnte man im CSS die entsprechenden Noto-Module so angeben:

  p.ägyptisch {
    font-family: "Noto Sans Egyptian Hieroglyphs", "Noto Sans", sans-serif;
  }

und auf diese Weise beide Noto-Module gemeinsam nutzen.

Codierungsformen von Unicode-Zeichen

Der oben erwähnte Codepoint eines Unicode-Zeichens ist nur eine abstrakte Nummer. Die Schreibweise dieser Nummer im Unicode-Standard erfolgt in hexadezimalen Zahlen mit vorangestelltem „U+“. Der Codepoint legt noch keinerlei computerkompatible Darstellung fest, dies ist Aufgabe des Codierschemas. Da die Unicode-Codepoints von U+0000 bis U+10FFFF (hexadezimale Zahlendarstellung), mit einer beabsichtigten Lücke zwischen U+D7FF und U+E000, reichen, sind für eine vollständige Codierung des gesamten Codepoint-Bereichs als Binärzahl mindestens 3 Byte erforderlich.

Im Folgenden werden die verbreiteten Speichervarianten von Unicode-Zeichen kurz vorgestellt.

UTF-32: Simpel, aber verschwenderisch

Da Computer viel besser mit Vielfachen von 2 umgehen können, hat man kein Codierschema mit 3 Bytes definiert, sondern als simpelste Variante UTF-32 festgelegt, bei dem jede Codeeinheit 32 Bit = 4 Bytes groß ist. Der Codepoint jedes Zeichens wird hierbei einfach als 32-Bit-Zahlwert gespeichert.

Es gibt von UTF-32 zwei Varianten, UTF-32LE (Little Endian) und UTF-32BE (Big Endian), die sich einfach nur in der Reihenfolge unterscheiden, mit denen einzelne Bytes einer größeren Zahl in den Datenstrom gegeben werden. Big Endian beginnt mit der „Tausenderstelle“, Little Endian mit der „Einerstelle“. Damit eindeutig festgestellt werden kann, welche Variante genutzt wird, gibt es ein spezielles Zeichen namens „BOM“ (Byte Order Mark) mit dem Codepoint U+FEFF, welches dem Datenstrom vorangestellt wird. Sind die ersten vier Bytes eines Datenstroms 0x00, 0x00, 0xFE und 0xFF, so wird Big Endian verwendet. Sind es hingegen 0xFF, 0xFE, 0x00 und 0x00, wird Little Endian verwendet.

Der größte Nachteil ist, dass UTF-32 extrem verschwenderisch mit Speicherplatz umgeht. Da Unicode insgesamt pro Zeichen nur 3 Byte benötigt, ist pro UTF-32-Zeichen schon mindestens ein Byte ungenutzt verschwendet. Die am häufigsten benutzten Zeichen lassen sich sogar allesamt mit nur 2 Bytes darstellen, da sie sich im Bereich U+0000 bis U+FFFF befinden. Lateinische Buchstaben ohne diakritische Zeichen, wie sie in (westlichen) Fließtexten häufig vorkommen, lassen sich gar nur mit einem Byte codieren, da sie im Bereich U+0000 bis U+00FF vorkommen.

UTF-16: Praxisformat im RAM

UTF-16 nutzt Codiereinheiten, die 16 Bit = 2 Bytes groß sind. Dies halbiert bei üblichen Texten den Speicherverbrauch im Vergleich zu UTF-32. Auch für UTF-16 gibt es wieder die zwei Varianten UTF-16LE und UTF-16BE. Die BOM erscheint bei Big Endian als 0xFE, 0xFF, und bei Little Endian als 0xFF, 0xFE.

Allerdings lassen sich mit nur 2 Bytes nicht mehr alle Unicode-Zeichen direkt darstellen. An dieser Stelle kommt die eingangs erwähnte beabsichtigte Lücke ins Spiel. Zeichen, die sich nicht direkt codieren lassen, weil sie aus dem Codepoint-Bereich zwischen U+10000 und U+10FFFF kommen, werden als Kombination zweier Codiereinheiten dargestellt, den so genannten Surrogatzeichen aus dem Bereich U+D800 bis U+DFFF. Da in diesem Bereich keine gültigen Unicode-Zeichen liegen können, kann ein Programm eindeutig erkennen, dass hier Zeichen aus dem höheren Bereich codiert wurden.

Um ein Unicode-Zeichen mit einer Codepoint-Nummer über U+FFFF in UTF-16 zu codieren, wird von der Codepoint-Nummer zunächst 65536 (Hexadezimal 10000) abgezogen. Weil die höchste zulässige Codepoint-Nummer U+10FFFF ist, entsteht so eine Zahl im Bereich 0x00000 bis 0xFFFFF, die sich mit 20 Bits darstellen lässt. Der reservierte Bereich der Surrogatzeichen ist zweigeteilt, zum einen U+D800 bis U+DBFF für die so genannten high surrogates und U+DC00 bis U+DFFF für die low surrogates. Wenn Sie nachrechnen, werden Sie feststellen, dass beide Teilbereiche einen Umfang von 1024 Zeichen haben, wodurch sich jeweils 10 Bits codieren lassen. Der Codiervorgang soll an Hand des Unicodezeichens U+1F642 (leicht lächelndes Gesicht 🙂) vorgestellt werden.

Codepoint 1F642 0001 1111 0110 0100 0010
0x10000 abziehen 0F642 0000 1111 0110 0100 0010
Aufteilen
Umgruppieren
0000 1111 01
.... ..00 0111 1101
10 0100 0010
.... ..10 0100 0010
Surrogate D800 / DC00 1101 1000 0000 0000 1101 1100 0000 0000
Zusammenfügen 1101 1000 0011 1101
D83D
1101 1110 0100 0010
DE42

Die UTF-16 Darstellung von U+1F642 erfolgt durch die beiden Surrogatzeichen U+D83D (high surrogate) und U+DE42 (low surrogate). Die Aufteilung der Surrogatzeichen in zwei Bereiche stellt sicher, dass auch bei einer Verwechslung von UTF-16LE und UTF-16BE die richtige Zuordnung der höherwertigen und niederwertigen Bitgruppe möglich ist.

UTF-16 wird von vielen Programmen für die interne Repräsentation von Zeichenketten verwendet. Da es aber gerade bei westeuropäischen Sprachen immer noch relativ viele Bytes verschwendet, und außerdem Probleme mit Unicode-unfähigen Programmen wie z. B. Editoren macht, wird es zum Datenaustausch zwischen Systemen nicht sehr häufig verwendet.

UTF-8: Die Codierungsform der Wahl

UTF-8 nutzt Codiereinheiten, die 8 Bit = 1 Byte groß sind. Genau wie bei UTF-16 ist der Gesamtbereich der Unicode-Zeichen mit nur einem Byte nicht abzudecken, also werden bei Bedarf mehrere Bytes zusammengefasst, um einen Codepoint zu codieren.

UTF-8 hat im Vergleich zu den anderen UTF-Varianten mehrere praktische Vorteile:

  • Die ersten 127 Zeichen und Bytes sind identisch mit ASCII, d. h. alle Texte, die in der Hauptsache Unicode-Zeichen mit Codepoints zwischen U+0000 und U+007F verwenden, bleiben problemlos lesbar.
  • Auch Systeme, die UTF-8 nicht verstehen, können die Bytes trotzdem (eingeschränkt) verarbeiten, weil es sich um – wenn auch eher abstruse – „normale“ Zeichen handelt.
  • Selbst das byteorientierte Sortieren von UTF-8-Texten funktioniert und sortiert die Strings geordnet nach dem Zahlenwert der enthaltenen Codepoints.
  • Bei Verwendung von westeuropäischen Sprachen wird im Vergleich zu UTF-16 viel Speicherplatz gespart, da die meisten Zeichen nur ein Byte benötigen.

Ein Unicode-Zeichen kann in UTF-8 zwischen einem und vier Bytes groß sein. Um einen Codepoint in UTF-8 zu codieren, wird dessen binäre Zahldarstellung benötigt.

Unicode-Bereich Binäre Zahldarstellung 1. Byte 2. Byte 3. Byte 4. Byte
U+0000–U+007F          00000000 0xxxxxxx 0xxxxxxx
U+0080–U+07FF          00000yyy yyxxxxxx 110yyyyy 10xxxxxx
U+0800–U+D7FF, U+E000–U+FFFF          zzzzyyyy yyxxxxxx 1110zzzz 10yyyyyy 10xxxxxx
U+10000–U+10FFFF 000uuuuu zzzzyyyy yyxxxxxx 11110uuu 10uuzzzz 10yyyyyy 10xxxxxx

In der Tabelle steht u für die Angabe der Unicode-Ebene (Plane) falls nicht BMP. Was die Position des Zeichens innerhalb der Ebene betrifft, steht x steht für den Anteil im letzten, y für den Anteil im vorletzten und z für den Anteil im drittletzten Zeichen. Die Angabe anderer Unicode-Ebenen als der BMP muss auf die genannte Art erfolgen. Die UTF-16-Surrogates U+D800–U+DFFF dürfen nicht in UTF-8 kodiert werden. Einige Datenbanksysteme tun dies trotzdem. Diese Fehlfunktion wurde nachträglich als CESU-8 standardisiert. Ebenso ist die Codierung der Zeichen U+110000–U+1FFFFF oder die Erweiterung auf mehr als 4 Zeichen nicht erlaubt, da Unicode nur 17 Ebenen definiert und UTF-16 auch nur diese kodieren kann. Außerdem dürfen keine Sequenzen kodiert werden, die länger als nötig sind.

Ein Beispiel: Das Eurozeichen hat den Codepoint U+20AC. Die binäre Darstellung dieser Zahl ist 00100000 10101100. Setzt man diese Bits in das Schema der dritten Zeile, so erhält man 11100010 10000010 10101100 oder als Bytes 0xE2, 0x82 und 0xAC.

UTF8Kodierung.png

UTF-8 basiert zwar prinzipiell auf Big Endian, kennt aber offiziell keine Varianten für Big Endian oder Little Endian. Die für diesen Zweck benutzte Byte Order Mark sieht immer gleich aus: 0xEF, 0xBB und 0xBF. Da sie somit ihren Zweck der Erkennung der Byte-Reihenfolge nicht erfüllt, darf man sie ohne Probleme weglassen. Allerdings kann sie auch zur Erkennung, dass es sich um UTF-8-Text handelt, herangezogen werden.

  1. Wikipedia: Unicode, Version 15 vom September 2022