18. Unicode

Programme bekommen Daten in allen möglichen Encodings, z.B. aus dem Web oder in Koropra aus verschiedenen Quellen. Python kennt die üblichen Encodings und kann sie interpretieren, und zwar mit der decode-Methode von Strings. Hier habe ich einen String im Encoding CP473:

>>> origStr = 'fr\x84\xe1e'
>>> uStr = origStr.decode("cp437")
>>> print uStr
fräße
>>> uStr
u'fr\xe4\xdfe'

Was herauskommt, wenn ich origStr ausgebe, hängt von eurem System ab – bei mir ist es ziemlicher Quatsch, weswegen ich hier ein print origStr gelassen habe. Probiert es bei euch.

Ergebnis der decode-Methode ist ein Unicode-String. Unsere bisherigen Strings werden im Gegensatz dazu häufig als Bytestrings bezeichnet. Dass es ein Unicode-String ist, sieht man an dem “u” vor dem Anführungszeichen. Toll an Unicode-Strings ist, dass ihre Interpretation über alle Systeme hinweg identisch ist – in einem Unicode-String bedeutet 228 immer ein ä, während 951 immer ein griechisches η ist. Insbesondere sind die Codes von Zeichen in Unicode-Strings nicht wie bei Bytestrings auf den Bereich 0 bis 255 beschränkt.

Welche Zahl welchem Zeichen entspricht, könnt ihr den Unicode Code Charts entnehmen. Bevor ihr anfangt, eurem Rechner Tagalog-Zeichen entlocken zu wollen: Nur weil er Unicode verarbeiten kann, muss er noch lange nicht alle Unicode-Zeichen auch anzeigen können

Bei der Ein- und Ausgabe müssen Unicode-Strings fast immer kodiert werden. Dazu dient die encode-Methode:

>>> uStr.encode("iso-8859-1")
'fr\xe4\xdfe'
>>> uStr.encode("utf-8")
'fr\xc3\xa4\xc3\x9fe'

UTF-8 ist dabei ein Encoding für Unicode selbst. Dass man hier kodieren muss, hat vor allem historische Gründe – traditionell waren Zeichen im Rechner “klein”, repräsentiert durch ein “Byte” (eine Gruppe von acht bit) oder manchmal auch durch etwas größere oder kleinere Einheiten, jedenfalls durch eine Speicherzelle. Es gibt aber im Augenblick rund 100000 Unicode-Zeichen, und so viele verschiedene Werte lassen sich nicht in einer Speicherzelle repräsentieren.

Nun hätte man sagen können: Was solls? Warum soll man die Computer nicht so machen, dass in ihre Speicherzellen 100000 verschiedene Werte passen. So funktioniert aber die Welt nicht – selbst wenn jemand in der Lage wäre, die alten, byteorientierten Architekturen zu ersetzen, möchte niemand die Terabytes von byteorientierten Daten wegwerfen, schon gar nicht, wenn der Leidensdruck nur außerhalb des “Westens” erheblich ist. Abgesehen davon ist das das Internet weitgehend byteorientiert. Also: “Grundeinheit” von Rechnerspeichern, ob Platte oder Hauptspeicher, bleibt das Byte.

Nun kann man einfach ein paar Byte zusammenschnüren und so ausreichend viele Werte darstellen; in der Anfangszeit von Unicode war die Hoffnung, 65536 Zeichen seien genug, und so wollte man für die Zeichen einfach zwei Byte pro Zeichen verwenden. Es hat sich aber gezeigt, dass man doch mehr Platz haben will und auch, dass die Verwendung von zwei Bytes pro Zeichen gerade mit bestehenden Anwendungen recht mühsam ist (mal ganz zu schweigen von den Schwierigkeiten, sich zu einigen, ob das “höherwertige” Byte vorne oder hinten steht).

Wenn man nun ohnehin schon raffiniertere Möglichkeiten ersinnt, Unicode in Bytestrings darzustellen, kann man ja für “Abwärtskompatibilität” sorgen – das heißt allgemein, dass ein neues Verfahren mit einem alten Verfahren in irgendeinem Sinn verträglich ist. UTF-8 ist dazu so angelegt, dass alle legalen ASCII-Strings auch legales UTF-8 sind, dass also Probleme erst dann auftauchen, wenn man nationale Sonderzeichen (dazu zählen etwa die deutschen Umlaute) verwendet.

Wenn die alten US-Zeichen nur jeweils ein Byte verwenden, ist klar, dass andere Zeichen mehr, unter Umständen deutlich mehr, als zwei Bytes brauchen. Im Beispiel oben wurden ü und ß zu zwei Zeichen; die (nicht gezeigte) Hiragana Iteration Mark auf Codeposition 12445 wird zur drei Zeichen, während die altgriechische Notation für die Note e auf Codeposition 119296 zu vier Zeichen wird.

Fehlerbehandlung

Bei En- und Dekodieren kann einiges schiefgehen:

>>> uStr.encode("ascii")
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
UnicodeEncodeError: 'ascii' codec can't encode
characters in position 2-3: ordinal not in
range(128)
>>> "ärger".decode("utf-8")
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
UnicodeDecodeError: 'utf8' codec can't decode
bytes in position 0-2: invalid data

Im ersten Fall hat sich der ASCII-Codec (Codec ist dabei kurz für Kodierer/Dekodierer, also das Programmstück, das tatsächlich die Übersetzungen besorgt) beschwert, dass er das “ä” aus dem fräße nicht darstellen kann – in ASCII ist halt kein “ä” enthalten, ebensowenig wie, meinetwegen, altgriechische Musiknotation im üblichen iso-8859-1.

Der zweite Fall ist subtiler: UTF-8 lässt nur bestimmte Folgen von Bytes zu – so muss z.B. nach irgendeinem Nicht-ASCII-Zeichen (in diesem Fall das “ä”) mindestens ein weiteres kommen, was hier nicht der Fall ist. Der String ist also nicht in UTF-8 kodiert, sondern in etwas anderem.

In der Regel braucht man in solchen Fällen eine Fehleranalyse und -behebung. Wenn man aber in einer Situation ist, in der das nicht machbar oder wünschenswert ist– beispielsweise will man dringend irgendwas anzeigen oder auf Fehler in anderen Programmen (z.B. Outlook Express) nicht mit Absturz reagieren –, kann man encode zusätzlich “ignore” oder “replace” übergeben:

>>> uStr.encode("ascii", "ignore")
'fre'
>>> uStr.encode("ascii", "replace")
'fr??e'

Es gibt noch ein paar andere Möglichkeiten zur Fehlerbehandlung – wenn ihr sie braucht, findet ihr in der Dokumentation zum Codecs-Modul etwas genauere Informationen.

(Weiterführend:) Dort befindet sich auch eine Liste der standardmäßig unterstützten Codecs, und amusanterweise sind darunter auch auch encoder für zlib (das ist die Kompression, die gzip verwendet) oder base64 (so werden binäre Attachments kodiert). Auf diese Weise kann man sehr einfach packen:

>>> s = open("/usr/bin/python").read()
>>> len(s)
2179185
>>> len(s.encode("zlib"))
699045

(hier haben wir eine Datei gelesen – was das bedeutet, sehen wir erst später) oder E-Mail-Attachments ein- und auspacken:

>>> "Bla".encode("base64")
'Qmxh\n'

Übungen zu diesem Abschnitt

Ihr solltet euch wenigstens an den rötlich unterlegten Aufgaben versuchen

(1)

Besorgt euch den “nackten” Text eines base64-enkodieren Attachments (dazu könnt ihr z.B. in euren Mailspool gucken und den String mit einem Texteditor herauspröbeln). Schickt ihn durch decode("base64").


Markus Demleitner

Copyright Notice