XML Umlaute in Python

Angenommen, du möchtest in deiner Eigenschaft als Berufscholeriker eine XML-Datei mit dem - im übrigen völlig berechtigten - Eintrag "XML ist blöd" erstellen, in Python, und das ganze später natürlich auch wieder einlesen.

Leichter gesagt als getan! Diese Aufgabe berührt einige Themenbereiche, die nicht völlig trivial sind, so zum Beispiel:

Wie werden Umlaute in XML dargestellt?

Grundsätzlich gibt es zwei Möglichkeiten:

Eine UNICODE XML-Datei erzeugen

In diesem Fall muß man einfach als encoding utf-8 angeben, und (natürlich) eine UTF-8 Datei erzeugen. Hört sich einfach an! Versuchen wir folgendes:

import codecs

output = codecs.open("test.xml","wb","utf-8")
print >>output, '''<?xml version="1.0" encoding="utf-8"?>
<root>
    <item>XML ist bl&ouml;d</item>
</root>
'''
output.close()

Das klappt leider nicht:

Traceback (most recent call last):
  File >>"test.py", line 4, in ?
    print output, '''<?xml version="1.0" encoding="utf-8"?>
  File "C:\Python22\lib\codecs.py", line 338, in write
    return self.writer.write(data)
  File "C:\Python22\lib\codecs.py", line 137, in write
    data, consumed = self.encode(object, self.errors)
UnicodeError: ASCII decoding error: ordinal not in range(128)

Die Begründung für dieses Verhalten steht in der Python-Unicode-Faq. Allerdings ist die Behebung nicht offensichtlich.

Man könnte zum Beispiel versuchen, den Quelltext des Programms in UTF-8 abzuspeichern. Dann sollte der String bereits Unicode sein, oder? Versuchen wir das. Der beste Texteditor der Welt (TM) - SciTE - kann über den Menüpunkt File/Encoding/UTF-8 die Datei in UTF-8 abspeichern. Versucht man jetzt, das Programm auszuführen, erscheint folgendes:

python -u utf8.py
  File "utf8.py", line 1
  import codecs
    ^
SyntaxError: invalid syntax

Offenbar kommt die aktuelle Pythonversion (2.2.2) nicht mit dem BOM zurecht. Für Python 2.3 ist eine Lösung vorgesehen: der Einsatz eines Encodings. Es gibt dazu ein PEP 263, in dem diese Lösung näher erläutert ist.

Für Python 2.2 müssen wir also eine andere Lösung suchen. Wenn man einen Quelltext mit beispielsweise Notepad (oder Scite im 8-Bit Mode) erfasst, so wird die Datei (in Deutschland) in der Microsoft Windows Codepage 1252 abgefasst. Der String ist also cp1252 codiert (er liegt ja als Bytebatzen vor), und muß deshalb decodiert werden. Unser Code schaut folglich so aus:

import codecs

output = codecs.open("test.xml","wb","utf-8")
print >>output, '''<?xml version="1.0" encoding="utf-8"?>
<root>
    <item>XML ist bl&ouml;d</item>
</root>
'''.decode("cp1252")
output.close()

Und, oh wunder, das klappt!

Eine XML-Datei mit Entities erzeugen

Wir wollen unseren String später sowieso auf einer Webseite anzeigen; es macht also Sinn, das ö als &ouml; zu codieren. Hört sich auch erstmal einfach an:

import codecs

output = open("test.xml","wb")
print >>output, '''<?xml version="1.0"?>
<root>
    <item>XML ist bl&ouml;d</item>
</root>
'''
output.close()

Aber, wenn man versucht die Datei einzulesen, erscheint beispielsweise im IE folgende Fehlermeldung:

Reference to undefined entity 'ouml'.
Error processing resource 'file:///test.xml'. Line 3, Position 22 

    <item>XML ist bl&ouml;d</item>
---------------------^

Die Ursache ist, daß die in HTML übliche Kodierung in XML nicht einfach so zur Verfügung steht, sondern explizit definiert werden muß, als Entity. Was aber ist eine XML Entity? Dazu gibt es eine Erklärung bei Selfhtml. Jetzt muß man noch wissen, wie ö definiert wird! Dazu gibt es eine einfache und eine umständliche Möglichkeit. Einfach ist folgendes:

>>> ord("&ouml;")
246

Daraus liest man ab, daß ö als &#246; codiert werden kann. Die Von-Hinten-Durch-Die-Brust-Ins-Auge-Schuß-Methode geht so: Guckst du http://www.unicode.org/charts/, klickst du alle PDFs durch, bis du dein ö findest. Als verantwortungsvoller Autor habe ich das natürlich getan, hier: http://www.unicode.org/charts/PDF/U0080.pdf findest du für ö 0xF6, was dezimal 246 ist.

Erschöpft aber froh schreibt man nun:

import codecs

output = open("test.xml","wb")
print >>output, '''<?xml version="1.0"?>
<!DOCTYPE book [
 <!ENTITY ouml "&#246;">
]>
<root>
    <item>XML ist bl&ouml;d</item>
</root>
'''
output.close()

Jetzt ist die XML-Datei korrekt.

Die XML-Datei einlesen

Das einlesen der XML-Datei ist in Python denkbar einfach:

import xml.sax

class handler(xml.sax.ContentHandler):
    
    def startElement(self, name, attrs):
        print "startElement:", name, attrs

    def endElement(self, name):
        print "endElement:", name

    def characters(self, content):
        print "characters:", content

xml.sax.parse("test.xml", handler())

Aber, leider leider, es gibt ein Problem:

startElement: root <xml.sax.xmlreader.AttributesImpl instance at 0x00816F80>
characters: 

characters:     
startElement: item <xml.sax.xmlreader.AttributesImpl instance at 0x00817050>
characters: XML ist bl
characters: 
Traceback (most recent call last):
  File "test.py", line 27, in ?
    xml.sax.parse("test.xml", handler())
  File "C:\Python22\lib\xml\sax\__init__.py", line 33, in parse
    parser.parse(source)
  File "C:\Python22\lib\xml\sax\expatreader.py", line 91, in parse
    xmlreader.IncrementalParser.parse(self, source)
  File "C:\Python22\lib\xml\sax\xmlreader.py", line 123, in parse
    self.feed(buffer)
  File "C:\Python22\lib\xml\sax\expatreader.py", line 144, in feed
    self._parser.Parse(data, isFinal)
  File "test.py", line 25, in characters
    print "characters:", content
UnicodeError: ASCII encoding error: ordinal not in range(128)

Was hier passiert ist folgendes: das ü wird eingelesen und soll per print ausgegeben werden. Das Zeichen ist aber nicht bytecodiert, sondern liegt in Unicodeform vor. Wir müssen den String also in cp1252 codieren:

import xml.sax

class handler(xml.sax.ContentHandler):
    
    def startElement(self, name, attrs):
        print "startElement:", name, attrs

    def endElement(self, name):
        print "endElement:", name

    def characters(self, content):
        print "characters:", content.encode("cp1252")

xml.sax.parse("test.xml", handler())

Und so klappt das dann auch.