Von Umlauten, Unicode und Encodings

Immer wieder tauchen Fragen zu Umlauten und Unicode auf. Mit dieser kurzen Anleitung wird versucht dieses Thema so einfach wie möglich zu erklären. Dabei geht es nicht so sehr um die korrekte Darstellung dessen, was wirklich im Hintergrund passiert.

Umlaute im Python-Code

Python verwendet zwei verschiedene Objekttypen zum Speichern von Text: str und unicode. Ersteres ist ein sogenannter Binär- oder Bytestring, zweiteres ein sogenannter Unicodestring (eine Abfolge von Unicode-Codepoints).

Wichtig ist es deswegen, zwischen Unicode und UTF-8 (oder UTF-16, UTF-32) zu unterscheiden. Es ist nicht möglich, Unicode-Daten direkt zu speichern oder anzuzeigen, man muss sie vorher in einen Bytestring enkodieren (also beispielsweise in UTF-8 oder Latin-9), denn Ein- und Ausgabegeräte können nur mit Bytes umgehen und nicht mit Unicodezeichen. Dies ist die Quelle vieler Verwirrungen, wenn man das allerdings verstanden hat, ist Unicode kaum noch "schwer".

Wenn man Python keine besonderen Anweisungen gibt, dann wird aus Text, den man einfach nur in Anführungszeichen schreibt, ein Objekt vom Typ str (s = "Hallo Welt"). Dieser Typ kann normalerweise nur mit den Zeichen etwas anfangen, die auch im amerikanischen Englisch Gebrauch finden. Da es in der englischen Sprache keine Umlaute wie Ö oder Ä gibt, können diese, ohne besondere Vorkehrungen, nicht in ein Literal vom Typ str geschrieben werden.

Um trotzdem Umlaute verwenden zu können, hat man etwas eingeführt, das Python anzeigt in welchem Encoding das Python-Modul geschrieben wurde, also der Code selbst. Mit Hilfe eines sogenannten encoding cookie zeigt man Python das Encoding des Python-Modules.

So könnte ein solches Cookie aussehen:

# -*- coding: iso-8859-1 -*-

Dieses Cookie muss als erste oder zweite Zeile im Modul stehen. Wichtig dabei ist allerdings, dass in dieser Magic-Line auch wirklich das Encoding angegeben wird, in dem die Datei abgespeichert wird. Es funktioniert nicht, wenn man dem Python-Interpreter mit # -*- coding: utf-8 -*- signalisiert, dass die Datei im UTF-8-Encoding abgespeichert wurde, wenn in Wirklichkeit die Datei als ISO-8859-1-Datei abgespeichert wird.

Viele einfache Editoren lassen einem keine Wahl -- besonders unter Windows ist das gerne der Fall. Wenn in einem deutschsprachigen Windows im Editor keine Auswahl getroffen werden kann, dann ist man mit dem Encoding "cp1252" nicht schlecht bedient. (cp1252 ist eine Windows-Erweiterung des ISO-8859-15 Charactersets, auch häufig als latin-9 bezeichnet. Das ist im Prinzip latin-1 mit einigen Ersetzungen, z.B. dem Euro-Zeichen).

Unter Linux können viele Editoren mit verschiedenen Encodings umgehen. Wenn man keine Auswahl trifft, dann wird die Datei im Standard-Encoding abgespeichert. Dieses Standard-Encoding findet man heraus, wenn man in einer Kommandozeilen-Konsole (Shell) den Befehl /usr/bin/locale eingibt:

de_DE       -->  # -*- coding: iso-8859-1 -*-
de_DE@euro  -->  # -*- coding: iso-8859-15 -*-
de_DE.utf8  -->  # -*- coding: utf-8 -*-

de_AT       -->  # -*- coding: iso-8859-1 -*-
de_AT@euro  -->  # -*- coding: iso-8859-15 -*-
de_AT.utf8  -->  # -*- coding: utf-8 -*-

Ab dem Moment, in dem das Encoding definiert wurde, können auch Umlaute und andere Sonderzeichen, die vom definierten Encoding abgedeckt werden, als String und in den Kommentaren Verwendung finden.

Vor der Definition des Encodings hätte Python einen Fehler angezeigt, wenn man einen String mit Umlauten eingegeben hätte:

s = "Hallo Welt" # OK
s = "Hallo Österreich" # Fehler

Nach der Definition des Encodings:

s = "Hallo Welt" # OK
s = "Hallo Österreich" # OK

Umwandeln zwischen den verschiedenen Encodings

Im Zusammenhang mit Unicode in Python muss man verstehen, dass "Unicode" kein Encoding ist! Unicodestrings sind eben nicht kodiert (intern sind sie dies natürlich, weil der Computer nur mit Bytes umgehen kann, aber für den Programmier ist das unsichtbar).

Deswegen bedeutet encode(): Unicodestring -> Bytestring, und decode(): Bytestring -> Unicodestring.

Möchte man einen Bytestring von einem Encoding in ein anderes umwandeln, dann geschieht das in zwei Schritten

iso-8859-1  -->  Unicodestring  -->  utf-8
utf-8       -->  Unicodestring  -->  iso-8859-1
...

Die Umwandlung nach Unicode wird von decode() und die Umwandlung von Unicode in ein anderes Encoding wird von encode() erledigt.

#!/usr/bin/env python
# -*- coding: iso-8859-1 -*-

# Text definieren
s_iso88591 = "Hallo Österreich"

# Text nach Unicode umwandeln
s_unicode = s_iso88591.decode("iso-8859-1")

# Text nach UTF-8 umwandeln
s_utf8 = s_unicode.encode("utf-8")

print type(s_iso88591)
print type(s_unicode)
print type(s_utf8)

try:
    print s_iso88591
except:
    print "Nicht darstellbar..."

try:
    print s_unicode
except:
    print "Nicht darstellbar..."

try:
    print s_utf8
except:
    print "Nicht darstellbar..."

Aus diesem Beispiel lassen sich schon ein paar Dinge herauslesen.

  • Wird das Encoding im Cookie angegeben, dann wird jedes Stringliteral in dem mit der Magic-Line definierten Encoding gespeichert.
  • Man muss beim Umwandeln nach Unicode das Quell-Encoding angeben.
  • Man muss beim Umwandeln von Unicode das Ziel-Encoding angeben.
  • Der Typ des Objektes sagt nichts über das verwendete Encoding aus. Ein Bytestring weiß nichts über sein Encoding (er könnte genausogut auch binäre Daten enthalten).
  • Manche Encodings sind nicht in einer Konsole (Shell) darstellbar. Welche Encodings funktionieren, ist abhängig vom Betriebssystem und den Einstellungen der jeweiligen Konsole, mit der das Programm gestartet wurde. (Deshalb wurden die print-Anweisungen in try und except eingeschlossen.)

Neben Bytestringliteralen gibt es in Python auch Unicodestring-Literale. Diese sehen so aus:

s_unicode = u"Hallo Österreich"

Durch das Encoding Cookie weiß Python, dass der String innerhalb der Anführungszeichen iso-8859-1 ist und dekodiert daher den Bytestring, der in der Quellendatei steht, von diesem Encoding zu Unicode. Es muss vor den Anführungszeichen einfach nur ein u geschrieben werden.

Umlaute in die Konsole schreiben (print)

Python läuft unter den verschiedensten Betriebssystemen. Deshalb ist es ein wenig schwieriger, sich auf ein einheitliches Encoding für die Kommandozeile zu einigen. Es läuft derzeit ein großer Umbruch von den verschiedenen Encodings, hin zu UTF-8. Mit UTF-8 als gemeinsames Encoding gäbe es keine Probleme mit den Umlauten. Leider kann es noch Jahre dauern, bis die meisten populären Betriebssysteme UTF-8 als Standard-Encoding verwenden.

Bis es so weit ist, müssen wir beim Programmieren auf dieses Thema eingehen und uns selbst darum kümmern, dass der Text den wir an die Konsole schicken einem Encoding entspricht mit dem die Konsole etwas anfangen kann. Mit Konsole meine ich die Dos-Eingabeaufforderung sowie die verschiedensten Shells die man unter den anderen gängigen Betriebssystemen so verwendet.

Das Encoding der Standardausgabe (STDOUT) kann man so abfragen:

stdout_encoding = sys.stdout.encoding

Das funktioniert aber nicht, wenn man ein Skript in einigen der üblichen IDEs startet, da diese meist STDOUT umleiten, so dass die Ausgabe innerhalb der IDE angezeigt wird. Das kann man meist so umgehen:

stdout_encoding = sys.stdout.encoding or sys.getfilesystemencoding()

Wenn man einen Text mit Umlauten an die Konsole (in unserem Fall STDOUT) schicken möchte, dann muss man diesen Text in das Encoding der Konsole umwandeln. Wie ich bereits weiter oben schon beschrieben habe, muss diese Umwandlung über Unicode laufen. Hier ein Beispiel:

#!/usr/bin/env python
# -*- coding: iso-8859-1 -*-

import sys

# Encoding der Standardausgabe herausfinden
stdout_encoding = sys.stdout.encoding or sys.getfilesystemencoding()

# Nachricht definieren (durch die Magic-Line als iso-8859-1)
message = "Hallo Österreich"

# Nachricht umwandeln (iso-8859-1 -> unicode -> Ausgabe-Encoding) und
# mit print an die Standardausgabe übergeben.
print message.decode("iso-8859-1").encode(stdout_encoding)

Das gleiche Beispiel ein wenig kürzer (es entfällt die Umwandlung nach Unicode):

#!/usr/bin/env python
# -*- coding: iso-8859-1 -*-

import sys

# Encoding der Standardausgabe herausfinden
stdout_encoding = sys.stdout.encoding or sys.getfilesystemencoding()

# Nachricht definieren (durch die Magic-Line als iso-8859-1)
message = u"Hallo Österreich"

# Nachricht umwandeln (unicode -> Ausgabe-Encoding) und
# mit print an die Standardausgabe übergeben.
print message.encode(stdout_encoding)

Umlaute von der Kommandozeile übernehmen

Wenn ein Programm über die Kommandozeile gestartet wird, dann kann man Parameter an das Programm übergeben. Diese können mit sys.argv abgefragt und verwendet werden.

Solch ein Aufruf könnte so aussehen:

python mein_skript.py "Hallo Österreich" "Über den Wolken"

Um mit diesen Argumenten etwas anfangen zu können, muss man den Text vom Encoding der Kommandozeile nach Unicode oder in das Encoding des Python-Skriptes umwandeln.

VORSICHT! Das Encoding der Kommandozeile ist nicht das Encoding von STDIN.

Das Encoding der Kommandozeile lässt sich mit sys.getfilesystemencoding() feststellen. Es ist also das Encoding, mit dem die Dateinamen der Dateien gespeichert werden.

#!/usr/bin/env python
# -*- coding: iso-8859-1 -*-

import sys

# Encoding der Standardausgabe und des Dateisystems herausfinden
stdout_encoding = sys.stdout.encoding or sys.getfilesystemencoding()
fs_encoding = sys.getfilesystemencoding()

# Kompletten Kommandozeilenaufruf übernehmen und nach Unicode umwandeln
cmd_original = " ".join(sys.argv)
cmd_unicode = cmd_original.decode(fs_encoding)

# String verändern und wieder an die Kommandozeile übergeben
cmd_unicode += u" (öäü)"
print cmd_unicode.encode(stdout_encoding)

Umlaute von Datenbanken oder Dateien

Die String-Daten, die man aus einer Datenbank liest, sind ziemlich sicher nicht im gleichen Encoding wie unser Skript. (durch Encoding Cookie definiert)

Meist kümmert sich die Schnittstelle darum, dass die Daten von der Datenbank nach Unicode umgewandelt werden. Das lässt sich leicht feststellen, indem man mit type() einen der Strings aus der Datenbank überprüft. Das hat den Vorteil, dass man weiß woran man ist und nicht mehr nach Unicode umwandeln muß, um zum gewünschten Ziel-Encoding zu gelangen.

Ist das nicht der Fall, muss man erst einmal feststellen, in welchem Encoding die Werte an das Python-Programm übergeben werden. Vielleicht gibt es einen Hinweis in der Beschreibung der Datenbank oder der Datenbankschnittstelle. Aber auch mit "Trial and Error" lässt sich herausfinden, welches Encoding von der Datenbank verwendet wird. Hat man das Encoding herausgefunden, dann sollte man sich im Programm darum kümmern, dass die Strings vor der Verwendung nach Unicode oder in das Encoding des Skriptes umgewandelt werden.

Alles hier geschriebene gilt natürlich auch in umgekehrter Reihenfolge. Beim Schreiben in die Datenbank muss man sich natürlich auch darum kümmern, dass das richtige Encoding verwendet wird. Auch hier ist es wieder am Einfachsten, wenn sich bereits die Datenbankschnittstelle darum kümmert und man einfach mit Unicode arbeiten kann.

Lesen und Schreiben von Dateien

Normalerweise braucht man sich nicht um das Encoding der Dateien zu kümmern. Meist liest man die Datei mit dem gleichen Betriebssystem, mit dem die Datei geschrieben wurde.

Natürlich gibt es Ausnahmen und für solche Fälle gibt es das Modul codecs. Damit lassen sich Dateien mit den verschiedensten Encodings öffnen, lesen und schreiben. Man kann mit dem Befehl codecs.open() eine Datei öffnen und dabei gleich auch noch das Encoding angeben, mit dem in die Datei geschrieben wird.

Wird beim Schreiben in diese Datei ein Unicode-String übergeben, dann wird dieser automatisch in das vorher definierte Ziel-Encoding umgewandelt.

#!/usr/bin/env python
# -*- coding: iso-8859-1 -*-

import sys
import codecs

# Encoding der Standardausgabe herausfinden
stdout_encoding = sys.stdout.encoding or sys.getfilesystemencoding()

# Datei schreiben
f = codecs.open("dateiname.txt", "w", "utf-8")
message = u"Über den Wolken"
f.write(message)
f.close()

# Datei lesen
f = codecs.open("dateiname.txt", "r", "utf-8")
s_unicode = f.read()
f.close()

# In die Konsole schreiben
print s_unicode.encode(stdout_encoding)

<<Tags(unicode,tipps,string)>>

Von Umlauten, Unicode und Encodings (last edited 2009-06-17 16:14:14 by localhost)