Zeichenkodierungen oder „Warum funktionieren meine Umlaute nicht?”

Dieser Artikel beschreibt die verschiedenen Zeichenkodierungen, wie sie zu Problemen führen können, und wie man sie in Perl-Programmen korrekt berücksichtigt.

Inhaltsverzeichnis

Einführung

Jeder hat es schon mindestens einmal erlebt: Ein Programm, das mit Text arbeitet, funktioniert wunderbar, solange man keine Umlaute eingibt. Sonst kommt nur noch Zeichenmüll heraus und ein bis zwei nicht korrekt dargestellte Zeichen pro Umlaut.

ASCII

Um zu verstehen, warum es dazu kommt, muss man sich anschauen, wie „normaler” Text und wie Umlaute binär abgespeichert werden.

Angefangen hat es 1963 mit ASCII, einem Standard, der 128 Zeichen je eine Zahl von 0 bis 127 zuweist, die mit 7 bit kodiert werden können.

Festgelegt sind die Zahlenwerte für lateinische Buchstaben, Ziffern, Satzzeichen und Kontrollzeichen wie „Carriage Return” und „Line Feed”, also Zeilenumbrüche.

Zeichen, die im Alltag eines Amerikaners nicht vorkommen, wie die deutschen Umlaute, kyrillische Zeichen und vieles mehr, wurden ausser Acht gelassen.

Da ein Byte aus 8 Bits besteht, ist bei ASCII das erste, „most significant” Bit immer 0.

Andere Zeichenkodierungen

Als man in Europa anfing Computer zu benutzen, mussten die benötigten Zeichen irgendwie im Computer gespeichert werden und dazu benutzte man die verbleibenden 128 Zeichen pro Byte. So entstanden die Kodierungen Latin-1 für den westeuropäischen Raum, Latin-2 für Mitteleuropa und so weiter, auch bekannt als ISO-8859-1 und ISO-8859-2.

Diese Zeichensätze stimmen in den ersten 128 Zeichen mit ASCII überein, die zweiten 128 Zeichen, also die mit 1 als erstem Bit, unterscheiden sich jeweils untereinander.

Die Grenzen dieser Zeichensätze werden einem schnell anhand eines gar nicht so alten Beispieles klar: Mit der Einführung des Euros hatten viele Länder eine neue Währung und damit ein Währungssymbol, das sich nicht in den traditionellen Zeichensätzen ausdrücken ließ! (Dieses Problem wurde durch das Einführen des Zeichensatzes ISO-8859-15 behoben, der sich nur wenig von Latin-1 unterscheidet und das -Zeichen enthält).

Unicode

Die bisherigen Zeichenkodierungen konnten jeweils nur einen kleinen, lokal sinnvollen Bereich aller möglicher Zeichen darstellen - sobald man Texte mit gemischten Zeichensätzen verfassen wollte, ging das heillose Chaos los.

Um etwas Ordnung in das Chaos zu bekommen, hat das Unicode-Konsortium damit angefangen, jedem Zeichen, das in irgend einer Schrift in irgend einer Sprache vorkommt, eine eindeutige, ganze Zahl und einen Namen zuzuordnen.

Die Zahl heißt „Codepoint” und wird üblicherweise als vier- oder sechsstellige, hexadezimale Zahl in der Form U+0041 notiert; der dazugehörige Name wäre LATIN SMALL LETTER A.

Neben Buchstaben und anderen „Basiszeichen” gibt es auch Akzentuierungen wie den z. B. ACCENT, COMBINING ACUTE, die auf den vorherigen Buchstaben einen Akzent setzen.

Wenn auf ein Basiszeichen eine Akzentuierung oder andere kombinierende Zeichen folgen, bilden mehrer Codepoints ein logischen Buchstaben, ein sogenanntes Grapheme.

Unicode Transformation Formats

Die bisher vorgestellten Unicode-Konzepte stehen vollständig unabhängig davon, wie die Unicode-Zeichen kodiert werden.

Dafür wurden die „Unicode Transformation Formats” definiert, Zeichenkodierungen, die alle möglichen Unicode-Zeichen darstellen können. Der bekannteste Vertreter ist UTF-8, das für die bisher vergebenen Codepoints 1 bis 4 Bytes benötigt.

Auch in UTF-8 stimmen die ersten 128 Zeichen mit denen von ASCII überein.

Von UTF-8 gibt es auch eine laxe Variante, UTF8 (ohne Bindestrich geschrieben), die mehrere mögliche Kodierungen für ein Zeichen zulässt. Das Perl-Modul Encode unterscheidet diese Varianten.

UTF-16 dagegen benutzt für jedes Zeichen mindestens zwei Byte, für sehr hohe Unicode-Codepoints werden auch hier mehr Bytes benötigt.

UTF-32 kodiert jedes mögliche Zeichen mit vier Bytes.

CodepointZeichenASCIIUTF-8Latin-1ISO-8859-15UTF-16
U+0041A0x410x410x410x410x00 0x41
U+00c4 Ä - 0xc3 0x84 0xc4 0xc4 0x00 0xc4
U+20AC - 0xe2 0x82 0xac - 0xa4 0x20 0xac
U+c218 - 0xec 0x88 0x98 - - 0xc2 0x18

(Das Zeichen in der letzten Zeile ist das Hangul-Zeichen für die Silbe SU, und wird von Ihrem Browser nur dargestellt, wenn Sie entsprechende asiatische Schriftarten installiert haben.)

Was sind „Charsets”?

Das Wort charset wird mit zwei verschiedenen, zum Teil widersprüchlichen Bedeutungen verwendet.

Es kommt aus dem Englischen, und ist eigentlich eine Abkürzung für character set. Set ist Englisch für Menge, wenn man es wörtlich nimmt ist also ein Repertoir von Zeichen gemeint, wie Unicode eines definiert. Allerdings schränken auch Kodierungen wie ASCII oder Latin-1 den Zeichensatz ein, stellen also neben Kodierungen auch ein Repertoire oder charset da.

Häufig wird charset auch einfach als Synonym für Zeichenkodierung benutzt, wie zum Beispiel in den HTTP-Headern, die weiter unten noch einmal erwähnt werden.

Perl und Zeichenkodierungen

Perl unterscheidet bei Operationen auf Strings zwischen solchen, die die Strings als Text betrachten (wie z.B. uc, lc und substr sowie reguläre Ausdrücke), und solche, die Strings als Binärdaten betrachten, wie etwa print und das Lesen aus Dateihandles.

Damit Perl für die Textoperationen die Strings richtig interpretieren kann, muss man sie dekodieren. Das kann man mit dem der Funktion decode in dem Core-Modul Encode machen, oder mit den unten beschriebenen IO-Layern.

Umgekehrt muss man Strings mit Encode::encode kodieren, um binäre Operationen wie print auszuführen.

Alle Textoperationen sollte man auf nur auf Strings ausführen, die vorher dekodiert wurden, weil dann auch Nicht-ASCII-Zeichen korrekt behandelt werden: lc und uc funktionieren wie erwartet, und \w in regulären Ausdrücken passt auf jeden Buchstaben, auch auf Umlaute, ß und allen möglichen Zeichen in allen möglichen Sprachen, die dort als Bestandteil eines Wortes angesehen werden.

cmp vergleicht Nicht-ASCII-Zeichen allerdings nach Unicode-Codepoint, was nicht immer das ist, was man z.B. in deutschem Text erwartet. Nur wenn use locale aktiv ist, werden sprachspezifische Vergleichsregeln benutzt. Da das Verhalten von sort durch cmp definiert ist, gilt dies auch für das Sortieren von Listen.

#!/usr/bin/perl
use warnings;
use strict;
use Encode qw(encode decode);

my $enc = 'utf-8'; # in dieser Kodierung ist das Script gespeichert
my $byte_str = \n";

# Bytestrings:
print lc $byte_str; # gibt 'Ä' aus, lc hat nichts verändert

# Textstrings:
my $text_str = decode($enc, $byte_str);
$text_str = lc $text_str;
print encode($enc, $text_str); # gibt 'ä' aus, lc hat gewirkt

Es empfiehlt sich, alle Eingaben direkt zu dekodieren, dann mit diesen Strings zu arbeiten, und sie erst bei der Ausgabe (oder beim Speichern) wieder in Bytestrings zu kodieren. Wenn man sich nicht an diese Regel hält, verliert man im Programm schnell den Überblick welcher String ein bereits dekodiert wurde und welcher nicht.

Perl bietet mit den IO-Layern Mechanismen, mit denen man das kodieren und dekodieren an Dateihandles oder global an allen Handles durchführen lassen kann.

# IO-Layer: $handle liefert beim Lesen jetzt Textstrings:
open my $handle, '<:encoding(UTF-8)', $datei;

# das Gleiche:
open my $handle, '<', $datei;
binmode $handle, ':encoding(UTF-8)';

# Jedes open() soll automatich :encoding(iso-8859-1) benutzen:
use open ':encoding(iso-8859-1)';

# Alle Stringkonstanten werden als utf-8 interpretiert
# und automatisch dekodiert
use utf8;

# Schreibe Text mit der aktuellen locale nach STDOUT:
use PerlIO::locale;
binmode STDOUT, ':locale';
# alle Lese-/Schreibeoperation mit aktueller locale:
use open ':locale';

Mit Vorsicht sollte man den Input Layer :utf8 genießen, der annimmt, dass die Eingabedatei gültiges UTF-8 ist. Sollte sie das nicht sein (und man hat keine Möglichkeit, das zu überprüfen), ist das eine potentielle Quelle für Sicherheitslücken (siehe diesen Artikel auf Perlmonks für Details).

Das Modul und Pragma utf8 erlaubt es auch, Nicht-ASCII-Zeichen in Variablennamen zu verwenden. In Namespaces und Modulenamen sollte man davon absehen, da es dort nicht zuverlässig funktioniert. Auch sollte man immer beachten, dass nicht jeder die Möglichkeit hat, beliebige Unicode-Zeichen mit der Tastatur einzugeben.

Die Arbeitsumgebung testen

Ausgestattet mit diesem Wissen kann man testen, ob das Terminal und locales auf die gleiche Kodierung eingestellt sind, und auf welche:

#!/usr/bin/perl
use warnings;
use strict;
use Encode;

my @charsets = qw(utf-8 latin1 iso-8859-15 utf-16);

my $test = 'Ue: ' . chr(220) .'; Euro: '. chr(8364) . "\n";

for (@charsets){
    print "$_: " . encode($_, $test);
}

Wenn man dieses Programm in einem Terminal ausführt, wird nur eine Textzeile korrekt angezeigt werden, die erste Spalte darin ist dann die Zeichenkodierung des Terminals.

Wie vorher gesagt ist das Eurozeichen nicht in Latin-1 vorhanden, das Ü sollte in einem Latin-1-Terminal trotzdem richtig angezeigt werden.

In Windowsterminals sind auch die Zeichenkodierungen cp850 und cp858 (die nur von neuen Encode-Versionen unterstützt wird) üblich, der Rest der Betriebsumgebung benutzt Windows-1252.

Um den obigen Test auf alle in Perl verfügbaren Zeichenkodierungen auszuweiten, kann man die Liste der Kodierungen durch Encode->encodings(':all') ersetzen.

Troubleshooting

"Wide Character" - Warnungen

Manchmal stolpert man über "Wide character in print" und ähnliche Warnungen. Sie bedeuten, dass ein vorher dekodierter String, der intern als UTF-8 gespeichert wurde, für eine Operation benutzt wurde, in der nur binäre Daten sinnvoll sind.

Abhilfe schafft es, den String vorher mit Encode::encode oder einem entsprechenden Output-Layer zu kodieren.

Strings Untersuchen

Leider dokumentieren viele Module nicht, welche Art von Daten sie zurückliefert, also ob sie bereits dekodiert wurden oder nicht.

Im Allgemeinen ist das auch nicht durch eine Analyse der Strings herauszufinden, da Perl 5 keine getrennten Datentypen für dekodierte und für binäre Strings hat.

Es gibt aber eine Heuristik, die manchmal hilft. Dazu benötigt man das Modul Devel::Peek:

use Devel::Peek;
use Encode;
my $str = "ä";
Dump $str;
$str = decode("utf-8", $str);
Dump $str;
Dump encode('latin1', $str);

__END__
SV = PV(0x814fb00) at 0x814f678
REFCNT = 1
FLAGS = (PADBUSY,PADMY,POK,pPOK)
PV = 0x81654f8 "\303\244"\0
CUR = 2
LEN = 4
SV = PV(0x814fb00) at 0x814f678
REFCNT = 1
FLAGS = (PADBUSY,PADMY,POK,pPOK,UTF8)
PV = 0x817fcf8 "\303\244"\0 [UTF8 "\x{e4}"]
CUR = 2
LEN = 4
SV = PV(0x814fb00) at 0x81b7f94
REFCNT = 1
FLAGS = (TEMP,POK,pPOK)
PV = 0x8203868 "\344"\0
CUR = 1
LEN = 4

Der String UTF8 in der Zeile FLAGS = zeigt, dass der String intern als UTF-8 gespeichert wird und bereits dekodiert wurde. In der Zeile PV = sieht man bei solchen Strings die Bytes und in eckigen Klammern die Codepoints.

Allerdings ist der Umkehrschluss nicht zulässig: das Fehlen des UTF8 Flags bedeutet keineswegs, dass der String vorher nicht dekodiert wurde. Es bedeutet lediglich, dass perl für Textoperationen die Kodierung Latin-1 annimmt.

Fehlerhafte Module

Weitere Probleme können durch fehlerhafte Module entstehen. So ist die Funktionalität des Pragmas encoding sehr verlockend:

# automatische Konvertierungen:
use encoding ':locale';

Allerdings funktionieren unter dem Einfluss von use encoding AUTOLOAD-Funktionen nicht mehr, und das Modul funktioniert nicht im Zusammenspiel mit Threads.

Kodierungen im WWW

Beim Schreiben von CGI-Scripten muss man sich überlegen in welcher Kodierung die Daten ausgegeben werden sollen und das entsprechend im HTTP-Header vermerken.

Für die meisten Anwendungen empfiehlt sich UTF-8, da man damit einerseits beliebge Unicode-Zeichen kodieren kann, andererseits auch deutschen Text platzsparend darstellen kann.

HTTP bietet zwar mit dem Accept-Charset-Header eine Möglichkeit herauszufinden, ob ein Browser mit einer Zeichenkodierung etwas anfangen kann, aber wenn man sich an die gängigen Kodierungen hält, ist es in der Praxis nicht nötig, diesen Header zu prüfen.

Für HTML-Dateien sieht ein Header typischerweise so aus: Content-Type: text/html; charset=UTF-8. Wenn man einen solchen Header sendet, muss man im HTML-Code nur die Zeichen escapen, die in HTML eine Sonderbedeutung haben (<, >, & und innerhalb von Attributen auch ").

Zu beachten ist auch, dass der HTTP-Header (und damit auch URLs) nur ASCII-Zeichen enthalten darf, d.h. URLs und Cookies müssen nach ASCII kodiert werden. Üblich ist es, die Daten in UTF-8 umzuwandeln, und alle Bytewerte größer als 127 (und solche die dort nicht erlaubt sind, z.B. Leerzeichen in URLs) als Prozentzeichen gefolgt vom zweistelligen Hexadezimalwert des Bytes zu kodieren. Aus einem Leerzeichen wird dabei %20, aus einem Ä wird %c4%84.

Beim Einlesen von POST- oder GET-Parametern mit dem Modul CGI muss man darauf achten, welche Version man benutzt: In älteren Versionen liefert die param-Methode immer Bytestrings zurück, in neueren Versionen (ab 3.29) werden die Strings dekodiert zurückgegeben, wenn vorher mit charset die Zeichenkodierung UTF-8 eingestellt wurde - andere Kodierungen werden von CGI nicht unterstützt.

Damit Formularinhalte vom Browser mit bekanntem Zeichensatz abgeschickt werden, gibt man im Formular das accept-charset-Attribut mit an:

<form method="post" accept-charset="utf-8" action="/script.pl">

Bei Verwendung eines Template-Systems sollte man darauf achten, dass es mit Zeichenkodierungen umgehen kann. Beispiele sind Template::Alloy, HTML::Template::Compiled (seit Version 0.90 mit der Option open_mode) oder Template Toolkit in Verbindung mit Template::Provider::Encoding.

Weiterführende Themen

Mit den Grundlagen zu den Themen Zeichenkodierungen und Perl kommt man schon sehr weit, zum Beispiel kann man Webanwendunen „Unicode-Safe” machen, also dafür sorgen, dass alle möglichen Zeichen vom Benutzer eingegeben und dargestellt werden können.

Damit ist aber noch längst nicht alles auf diesem Gebiet gesagt. Der Unicode-Standard erlaubt es beispielsweise, bestimmte Zeichen auf verschiedene Arten zu kodieren. Um Strings korrekt miteinander zu vergleichen, muss man sie vorher „normalisieren”. Mehr dazu gibt es in der Unicode-Normalisierungs-FAQ.

Um landesspezifisches Verhalten für Programme zu implementieren, lohnt es, die locales genauer anzusehen. Im Türkischen z.B. wird lc 'I' zu ı, U+0131 LATIN SMALL LETTER DOTLESS I, während uc 'i' zu İ, U+0130 LATIN CAPITAL LETTER I WITH DOT ABOVE wird.. Ein guter Einstiegspunkt in die Locales ist das Dokument perllocale.


Dieser Artikel wurde für das $foo-Magazin geschrieben und in der 5. Ausgabe (Anfang 2008) veröffentlicht.