34 Punkte von GN⁺ 2025-09-13 | 1 Kommentare | Auf WhatsApp teilen
  • UTF-8 ist eine Encoding mit variabler Länge, die Millionen von Zeichen darstellen kann und zugleich abwärtskompatibel zu ASCII bleibt
  • Der gleiche 7-Bit-Bereich wie bei ASCII (U+0000~U+007F) wird unverändert als 1 Byte verwendet, sodass eine ASCII-Datei zugleich eine gültige UTF-8-Datei ist
  • Alle anderen Zeichen werden als 2- bis 4-Byte-Sequenzen dargestellt; das Bitmuster des führenden Bytes definiert die Länge, und die folgenden Bytes beginnen mit 10, um sie als Fortsetzungsbytes zu kennzeichnen
  • Dank dieses Designs kann UTF-8 einen universellen Zeichensatz verarbeiten und bleibt gleichzeitig vollständig kompatibel mit bestehenden ASCII-Systemen, weshalb es die am weitesten verbreitete Zeichenkodierung wurde
  • Andere Unicode-Kodierungen wie UTF-16 und UTF-32 bieten diese ASCII-Kompatibilität nicht

Die Genialität des UTF-8-Designs

  • Als ich UTF-8 zum ersten Mal kennengelernt habe, hat mich die Struktur, die mit bestehendem ASCII kompatibel bleibt, obwohl sie Millionen unterschiedlicher Zeichen aus verschiedenen Sprachen in einem System zusammenführt, tief beeindruckt
  • Grundsätzlich nutzt UTF-8 bis zu 32 Bit, während ASCII nur 7 Bit verwendet
  • Die Designprinzipien von UTF-8 sind wie folgt
    • Jede in ASCII kodierte Datei ist eine gültige UTF-8-Datei
    • Jede UTF-8-Datei, die nur ASCII-Zeichen enthält, ist eine gültige ASCII-Datei
  • Die Idee, ein veraltetes System mit nur 128 Zeichen mit einem System für Millionen von Zeichen zu verbinden, ist ausgesprochen innovativ

Grundkonzept von UTF-8

  • UTF-8 ist eine Zeichenkodierung mit variabler Länge (variable-width encoding), die dafür entworfen wurde, alle Zeichen des Unicode-Zeichensatzes darzustellen
  • Jedes Zeichen wird mit 1 bis 4 Bytes kodiert
  • Die ersten 128 Zeichen (U+0000~U+007F) werden als einzelnes Byte gespeichert und sichern damit die Abwärtskompatibilität zu ASCII
  • Alle anderen Zeichen werden mit zwei, drei oder vier Bytes kodiert
  • Die führenden Bits des ersten Bytes bestimmen die insgesamt benötigte Anzahl an Bytes für die Kodierung
1-Byte-Muster Anzahl Bytes Muster der gesamten Byte-Sequenz
0xxxxxxx 1 0xxxxxxx (normales ASCII)
110xxxxx 2 110xxxxx 10xxxxxx
1110xxxx 3 1110xxxx 10xxxxxx 10xxxxxx
11110xxx 4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
  • Das 2., 3. und 4. Byte einer Mehrbyte-Sequenz beginnt immer mit 10, was Fortsetzungsbytes eindeutig markiert
  • Die restlichen Bits von Hauptbyte und Fortsetzungsbytes werden kombiniert, um einen Code Point zu bilden
    • Ein Code Point ist eine eindeutige Unicode-Zeichenkennung und wird mit dem Präfix "U+" sowie hexadezimal dargestellt
    • Beispiel: Der Code Point von "A" ist U+0041
  • Der Ablauf zur Interpretation von Zeichen aus UTF-8-kodierten Bytes ist wie folgt
    • 1. Ein Byte lesen; wenn es mit 0 beginnt, wird es als Einzelbyte-Zeichen (ASCII) behandelt, das Zeichen mit den restlichen 7 Bits dargestellt und dann zum nächsten Byte übergegangen
    • 2. Falls nicht 0, dann
      • bei 110: ein 2-Byte-Zeichen, also ein weiteres Byte lesen
      • bei 1110: ein 3-Byte-Zeichen, also die nächsten 2 Bytes lesen
      • bei 11110: ein 4-Byte-Zeichen, also 3 weitere Bytes lesen
    • 3. Aus den bestimmten Bytes die restlichen Bits ohne die führenden Bits kombinieren und als Binärwert des Code Points verwenden
    • 4. Den Code Point im Unicode-Zeichensatz nachschlagen und auf dem Bildschirm darstellen
    • 5. Mit dem nächsten Byte wiederholen

Beispiel: das Hindi-Zeichen "अ"

  • UTF-8-Darstellung: 11100000 10100100 10000101 (3 Bytes)
  • Erstes Byte (11100000) → zeigt an, dass es sich um ein 3-Byte-Zeichen handelt
  • Kombination der Nutzbits aus den drei Bytes → 00001001 00000101 = hexadezimal 0x0905
  • Der Code Point U+0905 steht für das Devanagari-Zeichen "अ"

Dateibeispiele

  • 1. Hey👋 Buddy

    • Besteht aus insgesamt 13 Bytes
      • ASCII-Zeichen (H, e, y, B, u, d, d, y, Leerzeichen) → jeweils 1 Byte
      • 👋 (U+1F44B) → 4 Bytes 11110000 10011111 10010001 10001011
    • Diese Datei ist eine gültige UTF-8-Datei, aber da sie ein Nicht-ASCII-Zeichen (Emoji) enthält, ist sie nicht ASCII-kompatibel
  • 2. Hey Buddy

    • Insgesamt 9 Bytes, alle im ASCII-Bereich
    • Daher ist diese Datei zugleich eine gültige ASCII-Datei und eine gültige UTF-8-Datei

Vergleich mit anderen Kodierungen

  • Es gibt einige Kodierungen, die Kompatibilität zu ASCII bieten, sie sind aber nicht so weit verbreitet wie UTF-8
  • Auch GB18030 (chinesischer Standard) bietet ASCII-Kompatibilität, wird jedoch nicht so breit genutzt
  • Die ISO/IEC-8859-Familie ist auf Erweiterungen mit einem einzelnen Byte (maximal 256 Zeichen) beschränkt und hat daher Grenzen
  • UTF-16/UTF-32 sind nicht ASCII-kompatibel
    • 'A' (U+0041): In UTF-16 00 41, in UTF-32 00 00 00 41

Bonus: UTF-8 Playground

1 Kommentare

 
GN⁺ 2025-09-13
Hacker-News-Kommentare
  • Da in UTF-8 Fortsetzungsbytes immer mit 10 beginnen, kann man selbst dann, wenn man zu einem beliebigen Byte springt, sofort erkennen, ob diese Position der Anfang eines Zeichens oder ein Fortsetzungsbyte ist. Deshalb lässt sich der Anfang des nächsten oder vorherigen Zeichens leicht finden. Bei einer Kodierung wie der variablen Ganzzahlkodierung von EBML (mit invertiertem 1/0-Schema, um die ASCII-Kompatibilität einzelner Bytes zu erhalten) ist es schwieriger, an einer beliebigen Position direkt den Zeichenanfang zu erkennen. Siehe dazu RFC8794 Abschnitt 4.4

    • Stimmt, das ist ein großer Vorteil von UTF-8. Man kann sich in einer UTF-8-Zeichenkette frei vor- und zurückbewegen, ohne sie von Anfang an lesen zu müssen. Bei Python werden in CPython wide characters verwendet, um String-Indizes auf Zeichenbasis zu ermöglichen. Früher konnte man zwischen 2-Byte- oder 4-Byte-Zeichen wählen, später wurde zur Laufzeit automatisch umgeschaltet. Es bleiben aber wide characters, nicht UTF-8. Schon ein einzelnes Emoji kann zum Beispiel die Größe einer Zeichenkette vervierfachen. Ich habe eher darüber nachgedacht, intern UTF-8 zu verwenden und den Indextyp als opakes Objekt zu gestalten, sodass man durch Addieren oder Subtrahieren kleiner Ganzzahlen in der Zeichenkette vor- und zurückgehen kann. Erst bei einer tatsächlichen Umwandlung in eine Ganzzahl oder bei direkter Subskription würde dann der String-Index berechnet. Mit so einem Ansatz könnten auch reguläre Ausdrücke und Ähnliches opake Indexobjekte nutzen und auf der UTF-8-Repräsentation gut funktionieren

    • Ich halte LEB128/VLQ für besser als das variablenlange Ganzzahlformat von EBML. Es unterscheidet per MSB im Byte: 0 bedeutet Ende der Sequenz, das nächste Byte beginnt eine neue Sequenz, 1 bedeutet zurückspulen, bis ein MSB 0 auftaucht. Dafür gibt es auch SIMD-optimierte effiziente Implementierungen. Der Unterschied zwischen LEB128 und VLQ ist nur die Endianness. ASCII wäre 0xxxxxxx, erweiterte Zeichen 1xxxxxxx 0xxxxxxx, 1xxxxxxx 1xxxxxxx 0xxxxxxx usw., womit sich in 3 Bytes bis zu 0x1FFFFF kodieren ließe, also mehr als Unicode braucht. Selbstsynchronisierend ist das zwar nicht, aber kompakter. ASCII bleibt weiterhin 1 Byte, und Codepoints unter U+3FFF wie mathematische Symbole oder Japanisch lassen sich in 2 Bytes darstellen, was für kleinere Codegrößen vorteilhaft ist

    • Ich denke, das gilt nur unter der Annahme, dass der Text nicht beschädigt oder absichtlich manipuliert wurde. Beim Parsen oder Escapen fehlerhafter UTF-8-Sequenzen sind schon viele Sicherheitslücken entstanden. Beispiele dazu finden sich beim CVE-2025-1094-Problem in PostgreSQL und auch in der Liste UTF-8-bezogener CVEs

    • Ganz so stimmt das nicht. Bei ungültigem UTF-8 kann aus einem Zeichen ein Fortsetzungsbyte werden. Wenn zum Beispiel 0b01100001 0b10000000 0b01100001 hereinkommt, entstehen daraus drei Zeichen: a�a. Um festzustellen, ob ein ausgegebenes Zeichen an dieser Stelle beginnt, muss man die vorherigen 1 bis 3 Bytes betrachten

    • Bei einer maximalen Multibyte-Länge von 4 Bytes muss man höchstens 3 Bytes rückwärts prüfen, um festzustellen, ob die aktuelle Position ein Fortsetzungsbyte ist. Wenn kein Startbyte auftaucht, weiß man, dass es sich um ein Einzelbyte-Zeichen handelt. Ich vermute, das wurde für Wiederherstellungszwecke so entworfen: Selbst wenn eine Bibliothek UTF-8 nicht korrekt erkennt, kann sie in einem ausgeschnittenen Slice die fehlerhaften führenden und abschließenden Bytes ignorieren und immerhin noch eine halbwegs vernünftige Zeichenkette extrahieren

  • Ich finde UTF-8 wirklich großartig. Der Kern liegt in der Entscheidung, dass ASCII nur 7 Bits verwendet hat. Schon 1963 war die Wahl von 7 Bit etwas ungewöhnlich. Ich frage mich, ob das bloß ein historischer Zufall war oder ob die Entwickler von ASCII darüber nachgedacht haben, mit einem zusätzlichen Bit weitere Symbole unterzubringen, oder ob sie Codepages bzw. Erweiterbarkeit im Blick hatten

    • Den genauen Grund kenne ich nicht, aber früher waren 8 Bit nicht immer selbstverständlich verfügbar. 7 Bit plus 1 Paritäts- oder Flag-Bit waren üblich (deshalb kodiert E-Mail 8 Bit bis heute noch mit quoted-printable in 7 Bit). Wenn etwas alle 8 Bit unverändert übertragen kann, nennt man das 8-bit clean. In diesem Zusammenhang ist UTF-8 letztlich auch ein Beispiel dafür, wie das bei ASCII übrig gebliebene achte Bit gut genutzt wurde. Siehe auch die Erklärung zu 8-bit clean

    • Ich bin kein Experte, aber ich habe früher einmal etwas zur Geschichte von ASCII gelesen. ASCII hat seine Wurzeln in Teletype-Codes, die sich aus Telegraphencodes entwickelt haben. Morsecode war variabel lang, was die maschinelle Umsetzung umständlich machte. Deshalb entstand der 5-Bit-Baudot-Code. Mit einem Code fester Länge wollte man Maschinen vereinfachen und auch die Ermüdung der Operatoren verringern. Wegen des Baudot-Codes heißt die Symbolrate bis heute Baud. Später erhöhte sich die Flexibilität durch Lochstreifeneingabe mit Schreibmaschinen, wodurch Sonderzeichen wie Carriage Return und Line Feed hinzukamen. Die frühe Computerindustrie übernahm Lochkarten als Eingabe; IBM entwickelte dafür ein neues 8-Bit-System, um Karten schneller zu verarbeiten, und daraus entstand eine ASCII-basierte Lösung. Letztlich wurden Binärcodes mit dem technischen Fortschritt immer weiter ausgebaut. ASCII war ebenfalls ein Übergangsprodukt, das noch vor der Konvention des 8-Bit-Byte entstand

    • Das freie Bit war tatsächlich für die Wiederverwendung als Paritätsbit gedacht

    • Die 8-Bit-Erweiterungen von ASCII (die ISO-8859-x-Familie) waren jahrzehntelang weit verbreitet und werden auch in den Standard-Codepages von Windows bis heute verwendet. Selbst wenn ASCII von Anfang an 8 Bit gehabt hätte, wären die Kernzeichen vermutlich trotzdem in den ersten 128 Positionen konzentriert gewesen, sodass es für UTF-8 weiterhin gut gepasst hätte. Wenn man von historischem Zufall sprechen will, dann ist es weniger die 7-Bit-Natur von ASCII als vielmehr die Tatsache, dass sich Computer damals vor allem im englischsprachigen Raum entwickelten und Englisch sich eben mit 7 Bit ausreichend darstellen ließ

    • 7 Bit an sich sind nicht besonders seltsam. Baudot hatte 5 Bit, das reichte nicht, daraus entstanden 6-Bit-Codes, und später dann 7-Bit-ASCII. IBM standardisierte mit System/360 das 8-Bit-Byte (mit EBCDIC-Codierung), aber andere Computerhersteller hatten keine feste Bytelänge. Auch wenn 7 Bit heute merkwürdig wirken, passten Zeichen und Systemwortlängen damals eben nicht zwingend sauber zusammen

  • Ich stimme zu, dass UTF-8 ein Design ist, das die Erwartungen übertrifft. Aber bei Unicode gibt es das Problem, dass der Geltungsbereich zu breit wird. Dann stellt sich die Frage, was überhaupt in Unicode enthalten sein sollte. Intuitiv würde man sagen: „alle unterscheidbaren druckbaren Zeichen, die die Menschheit zur Kommunikation verwendet“, aber in der Praxis ist es nicht so einfach.

    • Die Abgrenzung ist nicht eindeutig. Manche Codepoints existieren nur zum Kombinieren

    • Es ist nicht konkret genug. Dasselbe Zeichen kann auf mehrere Arten geschrieben werden. Zeichen, die visuell gleich aussehen, können unterschiedliche Codepoints und Bedeutungen haben

    • Es geht nicht nur um Druckbares. Es gibt Steuerzeichen. Die wurden zwar aus ASCII-Kompatibilität übernommen, aber es kommen auch eigene Steuerzeichen dazu Animierte Unicode-Punkte scheint es noch nicht zu geben. Zumindest alles Druckbare lässt sich auf Papier bringen. Aber ob diese Unveränderlichkeit auch in Zukunft bestehen bleibt, weiß ich nicht. Nebenbei gibt es unter den UTF-Kodierungen auch UTF-7, das der Autor nicht erwähnt hat. Das ist UTF-8 ähnlich, wurde aber unter der Annahme entwickelt, dass in Netzwerken der 80er die Nutzung des letzten Bits nicht sicher war. Ich habe tatsächlich einmal eine E-Mail in UTF-7 bekommen. Wie die verschickt wurde, weiß ich bis heute nicht

    • UTF-7 wurde hauptsächlich für Übertragungsumgebungen geschaffen, die nicht 8-bit clean sind, etwa E-Mail. Heute ist es veraltet, und die Kodierung der Supplemental Planes geht damit auch nicht wirklich (nur über UTF-16-Surrogate-Paare). Es gibt auch UTF-9, aber das ist eine Parodie aus einem Aprilscherz-RFC (für 36-Bit-Umgebungen wie den PDP-10)

  • Ich habe mich immer über eines gewundert: Unicode-Codepoints lassen sich theoretisch auch als unnötig lange Bytefolgen kodieren. UTF-8 verbietet das und erlaubt nur die kürzeste Sequenz. Zum Beispiel gehen 00000001 und 11000000 10000001 beide auf denselben Wert. Könnte man es dann nicht gleich anders entwerfen, sodass es solche ungültigen Kodierungen gar nicht erst gibt? Man könnte zum Beispiel den Anfang eines 2-Byte-Sequenzraums auf den letzten gültigen Einzelbytewert folgen lassen, sodass 11000000 10000001 dann 128+1 bedeutet und 0–127 als 1 Byte bleiben. Dann gäbe es keine ungültigen Codes, und in Grenzfällen wären Zeichenketten vielleicht etwas kürzer. Ich frage mich, ob das damals wegen der Hardwarekosten nicht weiter verfolgt wurde. (Update: Die tatsächliche Bitsequenz sollte 10000001 sein, das habe ich korrigiert)

    • In mehreren Antworten wird auf Synchronisationsmarker hingewiesen, aber die eigentliche Frage ist, warum U+0080 als c2 80 kodiert wird und nicht als c0 80 (also der erste Wert oberhalb von 7f). Ich denke, die Gründe sind diese a) Wenn overlong encodings erlaubt würden, würde das eine Sicherheitslücke schaffen, weil manche Implementierungen nur auf kurze Sequenzen prüfen b) Standard-UTF-8-Kodierung und -Dekodierung lassen sich allein mit Maskierung (bitmask) und Schiebeoperationen (bitshift) umsetzen. Der vorgeschlagene Ansatz würde zusätzliche Subtraktionen erfordern In einer E-Mail-Diskussion von 1992 wurde darüber gesprochen, und FSS-UTF enthält additive constants (siehe unten)

    Eine 2-Byte-Sequenz kann 2^11 Codes tragen, aber 0-7f sind ungültig. Offenbar hielt man das für besser als additive constants ohne besonderen Ausgleich
    Mehr dazu steht am Ende von utf-8-history.txt

    • Entscheidend ist, die Self-Synchronicity der Bytemuster zu erhalten. Würden Fortsetzungsbytes nicht konsistent beibehalten wie bei 11000000 10000001, ginge die Eigenschaft verloren, in einem abgeschnittenen UTF-8-Stream immer die Codepoint-Grenzen finden zu können. Wenn dazu noch Additions- und Subtraktionsoperationen kommen, leidet auch die Decoder-Performance. Der aktuelle Ansatz braucht nur Bitoperationen

    • Wie im Kommentar von quectophoton gesagt: Fortsetzungsbytes müssen immer mit 10 beginnen, damit ein Parser von jeder Position aus die Codepoint-Grenzen finden kann. Das war bei der Entwicklung von UTF-8 Anfang der 90er besonders wichtig, weil viele Übertragungsumgebungen unzuverlässig waren

    • Mit dem vorgeschlagenen Verfahren würden Kodierung und Dekodierung rechnerisch komplexer und langsamer. Heute wäre das egaler, aber in den 90ern bei langsamerer Hardware war das ein wesentlicher Punkt

  • Wer mehr über das Design von UTF-8 lesen möchte, sollte sich den One-Pager von Russ Cox und die historische Zusammenfassung von Rob Pike ansehen

  • UTF-8 ist großartig, und es wäre schön, wenn es überall verwendet würde (ich schaue dich an, JavaScript). Der einzige echte Nachteil ist für mich, dass der Standard nicht eindeutig vorgibt, wie ungültige Bytefolgen zu interpretieren sind. Ein noch perfekteres Design hätte für jede mögliche Bytefolge eine festgelegte Interpretation definiert. So etwas wie in der HTML5-Spezifikation könnte erfolgreich funktionieren

    • Aus Sicherheitssicht sollte man fehlerhaftes UTF-8 nicht irgendwie weiterverarbeiten, sondern die Daten sofort als gefährlich verwerfen und einen Fehler auslösen. Sonst ist man Angriffen durch Umgehung von Validierungen schutzlos ausgesetzt
  • Ich habe ein gespaltenes Verhältnis zu Backwards Compatibility. Verwirrende Altlasten mag ich nicht, aber ich sehe es positiv, wenn Fortschritt auch einmal etwas kaputtmacht. Gleichzeitig finde ich Beispiele wie UTF-8 oder EAN angenehm, bei denen Kompatibilität erhalten bleibt und das Design trotzdem clever ist. Ehrlich gesagt scheint UTF-8 für die Kompatibilität fast nichts geopfert zu haben

    • UTF-8 scheint für die Kompatibilität fast nichts geopfert zu haben
      Die Kodierung oberhalb von 21 Bit ist gesperrt. Das liegt an der Kompatibilität zu UTF-16 (dessen Surrogatmechanismus bis 2^21 - 1 reicht). Vielleicht werden wir diese Grenze irgendwann bereuen. Einen anderen praktischen Grund, Codepoints über 21 Bit zu verbieten, scheint es kaum zu geben

    • Ich mag es, wenn Machthaber unter dem Banner des Fortschritts mutig etwas umkrempeln
      Aber wenn ein System, von dem man abhängt, nur deshalb kaputtgeht, weil jemand einen Parameternamen geändert hat oder Teile der Standardbibliothek „unsauber“ wirkten, dann ist das kein Spaß

    • Wenn ich überhaupt etwas ändern würde, dann vielleicht einige der Steuerzeichen durch häufigere Zeichen ersetzen, um wenigstens ein bisschen Platz zu sparen (wenn man dafür sogar Unicode-Kompatibilität opfern würde). Als Multibyte-Zeichenkodierungsformat ist es aber auch für sich genommen nahezu optimal

  • Ich mag den Link zum UTF-8-Playground (utf8-playground.netlify.app) sehr. Schön wäre noch, wenn man Codepoints direkt in der UI eingeben könnte (im Moment ging das nur per URL). (Update: Das ist inzwischen möglich, der PR wurde bereits gemergt)

    • Danke für den Beitrag, das ist jetzt gemergt und direkt live
  • Wer tiefer in das Thema einsteigen möchte und so etwas wie Advent of Code mag, findet auf i18n-puzzles mehrere Rätsel zu Textkodierungen. Das hilft dabei, die Funktionsweise von UTF-8, UTF-16 und ähnlichen Formaten wirklich zu verinnerlichen

  • Danke für den guten Artikel. Ich empfehle UTF-8 ebenfalls, finde es aber nur dann wirklich gut, wenn es mit BOM verwendet wird. Sonst kann eine Anwendung nicht wissen, dass es UTF-8 ist, und übersieht vielleicht, dass auch wieder als UTF-8 gespeichert werden sollte. Wenn man unter Windows zum Beispiel ein neues Textdokument anlegt und die leere Datei nur ein BOM enthält, erkennt jede App beim späteren Bearbeiten und Speichern automatisch, dass sie als UTF-8 gespeichert werden muss. Ohne BOM kann eine App zwar versuchen, die Kodierung automatisch zu erkennen, aber das ist nie vollkommen zuverlässig, und sobald Sonderzeichen wie Akzente dazukommen, wird es schnell chaotisch (etwa wenn der Editor die Sprache falsch errät oder Notepad nach einem Update die Standardkodierung ändert). Deshalb stimme ich zu, UTF-8 zu verwenden, aber BOM sollte dabei unbedingt die Standardeinstellung von OS und Apps sein