Layer 8Ehrlicher Tech-Support

Zeichen sind keine Bytes – Namen brechen beim Kürzen

Der Nachname endet auf einem schwarzen Rautezeichen, das Feld meldet zu lang. In UTF-8 ist ein Zeichen ein bis vier Bytes – Länge ist zweideutig.

Aufwand:
ca. 20 Min.
Voraussetzung:
Python 3, Eine Kommandozeile, Die Feldlängen der eigenen Datenbank
Gilt für:
UTF-8 als Zeichenkodierung, PostgreSQL 18, Python 3, Node.js, Java, .NET (UTF-16 im Speicher)

Das Symptom

Im Kundenverzeichnis steht ein Name, der so nicht heißt:

Jörg M�

Oder das Formular verweigert die Eingabe mit „maximal 20 Zeichen“, obwohl der eingetippte Name sichtbar kürzer ist. Oder der nächtliche Export bricht ab, weil eine Zeile „ungültige Zeichenkodierung“ meldet – ausgerechnet bei den Datensätzen mit Umlaut, während zehntausend andere durchlaufen. Immer dasselbe Muster: Es funktioniert bei Müller nicht und bei Meier schon.

Die „Layer 8“-Wahrheit

Zwischen „Zeichen“ und „Byte“ liegt seit der Umstellung auf UTF-8 ein Unterschied, den ASCII jahrzehntelang versteckt hat. Die Unicode-FAQ sagt es nüchtern: Ein Zeichen wird je nach Kodierungsform als Folge von einem bis vier Bytes dargestellt. Ein „M“ ist ein Byte, ein „ü“ sind zwei, ein Emoji sind vier. Wer „zwanzig“ sagt, hat damit noch nichts gesagt.

Gefährlich wird das erst durch den zweiten Teil: Ein Schnitt an einer beliebigen Byte-Grenze zerreißt Zeichen. Das ist kein Schönheitsfehler, sondern ein Formatverstoß – in UTF-8 muss auf ein Anfangsbyte das passende Folgebyte kommen, und ein Programm, das die halbe Folge liest, muss laut Standard entweder einen Fehler melden oder das Ersetzungszeichen U+FFFD einsetzen. Beide Reaktionen sind richtig, und beide sind hässlich: Entweder bricht der Import ab oder der Name ist für immer verstümmelt.

Der Layer-8-Anteil steckt in den Testdaten. Solange mit Max Mustermann getestet wird, sind Zeichen und Bytes dasselbe, jede Grenze stimmt und jede Kürzung sieht sauber aus. Der Fehler wartet geduldig auf den ersten echten Nachnamen. Wer ihn finden will, legt sich einen Umlautnamen, einen Akzentnamen, einen nichtlateinischen Namen und ein Emoji dauerhaft in den Testbestand – und schreibt an jede Längengrenze dazu, worin sie gemessen wird.

Die echte, verifizierte Lösung

1000 % faktenbasiert. Jeder Befehl ist real, jeder Pfad existiert. Schritt für Schritt:

  1. 1.

    Die beiden Zahlen nebeneinander stellen

    Bevor irgendetwas gekürzt wird, lass dir Zeichenzahl und Bytezahl desselben Textes ausgeben. Der Unterschied ist der ganze Fehler. Python zählt in Zeichen, weil Zeichenketten dort laut Dokumentation unveränderliche Folgen von Unicode-Codepunkten sind; erst `encode` macht daraus Bytes.

    python3 -c "s='Müller'; print(len(s), len(s.encode('utf-8')))"
    # 6 7
    python3 -c "print(len('👍'), len('👍'.encode('utf-8')))"
    # 1 4
    node -e "console.log('👍'.length, [...'👍'].length)"
    # 2 1
    
  2. 2.

    Auf der Kommandozeile: wc -m gegen wc -c

    Dasselbe ohne Programmiersprache. Die beiden Schalter beantworten zwei verschiedene Fragen, und wer sie verwechselt, misst die falsche Grenze. Bei reinem ASCII sind sie gleich – das ist der Grund, warum der Fehler bei Testdaten nie auffällt und beim ersten echten Nachnamen zuschlägt.

    printf 'Müller' > /tmp/mu.txt
    wc -m /tmp/mu.txt   # 6 /tmp/mu.txt
    wc -c /tmp/mu.txt   # 7 /tmp/mu.txt
    
  3. 3.

    Ansehen, was ein Schnitt an der Byte-Grenze anrichtet

    `head -c` schneidet nach Bytes, ohne zu wissen, wo ein Zeichen anfängt. Landet der Schnitt zwischen den beiden Bytes eines Umlauts, bleibt ein halbes Zeichen übrig – und das ist keine gültige UTF-8-Datei mehr, sondern Datenmüll mit Ersetzungszeichen.

    printf 'Jörg Müller' | head -c 8
    # Jörg M�
    printf 'Jörg Müller' | head -c 8 | od -c | head -1
    # 0000000   J 303 266   r   g       M 303
    
  4. 4.

    In der Anwendung nur noch in Zeichen schneiden

    Die Regel ist einfach: Zerlegt wird auf der Ebene der Zeichenkette, nie auf der Ebene der Bytes. In Python heißt das `s[:10]` statt `s.encode()[:10]`; in jeder anderen Sprache heißt es, die Zeichenketten-Funktion zu nehmen und nicht die Byte-Puffer-Funktion. Auf der Kommandozeile ist es `cut -c` statt `cut -b`.

    python3 -c "s='Jörg Müller'; print(s[:8]); print(s.encode('utf-8')[:8].decode('utf-8','replace'))"
    # Jörg Mül
    # Jörg M�
    
  5. 5.

    Nachsehen, worin die Datenbank ihre Feldlänge zählt

    Bei PostgreSQL steht die Antwort in der Dokumentation: n zählt Zeichen, nicht Bytes. Wichtiger als die Antwort ist aber die zweite dort dokumentierte Eigenheit – ein zu langer Wert löst normalerweise einen Fehler aus, bei einer ausdrücklichen Typumwandlung wird er dagegen stillschweigend abgeschnitten. Der laute Fall ist der harmlose; der stille frisst Namen.

    INSERT INTO test2 VALUES ('too long');
    -- ERROR:  value too long for type character varying(5)
    INSERT INTO test2 VALUES ('too long'::varchar(5));  -- schneidet ohne Fehler ab
    
  6. 6.

    Die Grenze hinschreiben, statt sie zu vererben

    Jede Längengrenze bekommt eine Einheit: 20 Zeichen oder 20 Bytes, und zwar überall gleich – im Formular, in der Prüfung, in der Schnittstelle, in der Spalte. Wo eine Byte-Grenze technisch feststeht, etwa in einem Protokoll oder einem Dateiformat, wird die Eingabe in Zeichen gemessen und trotzdem in Bytes geprüft, bevor sie abgeschickt wird. Zwei Prüfungen sind billiger als ein Datensatz mit halbem Umlaut.

  7. 7.

    Mit echten Namen testen, nicht mit Testnamen

    Die Testdaten sind der eigentliche Grund, warum dieser Fehler bis in die Produktion kommt: Wer mit Max Mustermann testet, testet reines ASCII, und dort stimmen Zeichen und Bytes überein. In den Testbestand gehören deshalb dauerhaft ein Umlautname, ein Name mit Akzent, ein nichtlateinischer Name und ein Emoji. Das ist keine Spielerei, sondern der einzige Weg, die Verwechslung sichtbar zu machen, bevor ein Kunde sie meldet.

Häufige Fragen (FAQ)

?Warum ist „Müller“ mal sechs und mal sieben lang?

Weil zwei verschiedene Dinge gezählt werden. Sechs ist die Zahl der Zeichen, sieben die Zahl der Bytes in UTF-8. Die Unicode-FAQ nennt den Grund in einem Satz: Je nach gewählter Kodierungsform wird jedes Zeichen entweder als „a sequence of one to four 8-bit bytes“, als ein oder zwei 16-Bit-Codeeinheiten oder als eine einzige 32-Bit-Codeeinheit dargestellt. In UTF-8 belegen die 128 ASCII-Zeichen ein Byte, Umlaute und die meisten europäischen Sonderzeichen zwei, viele asiatische Schriftzeichen drei und Emoji vier. Die Frage „wie lang ist dieser Text“ hat deshalb keine Antwort, solange niemand sagt, worin gemessen wird.

?Woher kommt das schwarze Rautezeichen am Ende des Namens?

Das ist U+FFFD, das Ersetzungszeichen, und es steht dort, weil jemand mitten in einem Zeichen geschnitten hat. Die Unicode-FAQ beschreibt genau diesen Fall: In UTF-8 muss auf ein Byte der Form 110xxxxx ein Byte der Form 10xxxxxx folgen, eine Folge wie 110xxxxx 0xxxxxxx sei „illegal, and must never be generated“. Trifft ein Programm auf so eine Folge, muss es das erste Byte als Abbruchfehler behandeln – „either signaling an error, filtering the byte out, or representing the byte with a marker such as U+FFFD REPLACEMENT CHARACTER“. Das Rautezeichen ist also keine Anzeigepanne, sondern die korrekte Meldung, dass die Daten selbst kaputt sind.

?Mein Feld ist varchar(20) – zählt das Zeichen oder Bytes?

Das musst du nachsehen, denn beides kommt vor. Die PostgreSQL-Dokumentation ist eindeutig: Beide SQL-Zeichentypen speichern Zeichenketten „up to n characters (not bytes) in length“. Genau deshalb warnt dieselbe Seite davor, aus der Byte-Obergrenze eine Feldlänge abzuleiten: „with multibyte character encodings the number of characters and bytes can be quite different“. Andere Systeme und vor allem selbstgebaute Prüfungen in Formularen, Schnittstellen und Exportdateien zählen dagegen gern Bytes. Solange das nicht geklärt ist, ist jede Angabe „maximal 20“ eine Vermutung.

?Und warum ist ein Emoji plötzlich zwei Zeichen lang?

Weil dann in UTF-16 gezählt wird, und dort passen nicht alle Zeichen in eine Codeeinheit. Die Unicode-FAQ nennt UTF-16 als die Form, die Java und Windows beziehungsweise .NET benutzen; JavaScript zählt ebenso. Selbst gemessen: Die Längenangabe des Daumen-hoch-Emoji ist in Node.js 2, in Python 1, und in UTF-8 belegt dasselbe Zeichen vier Bytes. Ein Feld mit der Beschriftung „maximal 280 Zeichen“ bedeutet also je nach Programmiersprache etwas anderes – und wer die Grenze in der einen Sprache prüft und in der anderen speichert, produziert genau die Fehler, um die es hier geht. Der Schritt mit den Zählbefehlen unten zeigt beide Zahlen nebeneinander.

Belege

Diese Seite ist gegen die folgenden Primärquellen geprüft – Hersteller, Standard oder Gesetzestext. Wer es nicht glaubt, muss es nicht: Da steht, wo es nachzulesen ist.

Zuletzt geprüft am . Dokumentation altert – ein Datum daneben ist der einzige ehrliche Umgang damit.

Auch zu finden unter

Verwandte Fehler