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.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.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.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.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.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.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.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.