XML in Haskell - Datentypen und Serialisierung
Die Frage „Wie parse oder erzeuge ich ein XML-Dokument mit Haskell?“ ist mir schon oft gestellt worden und ich stelle sie mir auch selbst immer wieder. Es gibt viele XML-Libraries für Haskell, so dass man oft gar nicht weiss wo man als erstes schauen sollte. In dieser Serie von Blogartikeln möchten wir zeigen, wie man XML-Dokumente erzeugt, wie man vorhandene Dokumente in eigene Datenstrukturen einliest und wie man in Dokumenten suchen und diese transformieren kann.
In der Welt des World Wide Web werden Daten fast nur noch im JSON-Format ausgetauscht. In der Industrie spielt XML jedoch weiterhin eine sehr wichtige Rolle, so dass sich wahrscheinlich jeder irgendwann mit dem Erzeugen oder Verarbeiten von XML-Dokumenten beschäftigen wird. Da wir nicht davon ausgehen, dass jeder bereits Erfahrung mit XML hat, stellen wir in jedem Artikel auch XML-Technologien unabhängig von Haskell vor.
Dieser erste Artikel der Serie erklärt die Grundbegriffe von XML, zeigt auf wie die zugehörigen Haskell Datentypen aus dem Paket xml-conduit aussehen und veranschaulicht die Nutzung durch Erzeugung und Serialisierung eines XML-Dokument mit Hilfe dieser Datentypen.
Was ist XML und warum sollte ich XML überhaupt verwenden?
Die eXtensible Markup Language ist ein Regelwerk, das genutzt werden kann, um textbasierte Datenaustauschformate zu beschreiben. XML kann also genutzt werden um zum Beispiel Blogartikel, Rechnungen oder Arztbriefe maschinenlesbar zu beschreiben. Wenn man Daten speichern oder austauschen möchte ist es vorteilhaft XML zu verwenden, weil es für jede Programmiersprache gute Bibliotheken zum Einlesen und Ausgeben von XML-Dokumenten gibt. Was der Inhalt eines XML-Dokuments bedeutet und wie die Daten strukturiert sind, wird durch die XML-Spezifikation nicht festgelegt. Jede Anwendung, die XML benutzen möchte, muss also selbst festlegen wie ein XML-Dokument strukturiert sein darf. XML gibt vor wie die Kodierung der Struktur erfolgen muss. Hier ein Beispiel für ein XML-Dokument aus unserer digitalen Krankenakte Checkpad MED:
Ein Beispieldokument
Dieses Dokument enthält Daten zu unserer Demo-Patientin Jana Braun. Zu
diesen Daten gehören der Name, Identifikationsnummern für die Patientin
und den Krankenhausaufenthalt, Geburtsdatum, Geschlecht und Aufnahmedatum.
Aus Sicht des XML-Parsers sind die eigentlichen Daten irrelevant - es
zählt nur, dass das Dokument syntaktisch korrekt ist. Man sagt auch: Das
XML-Dokument ist well-formed. Ein Dokument darf (aber muss nicht) mit
einem Prolog <?xml version="1.0" encoding="UTF-8"?>
beginnen, der
festlegt welcher XML-Spezifikation das Dokument entspricht und in welcher
Zeichenkodierung (hier UTF-8) es abgelegt ist. Was jedes XML-Dokument
aber braucht ist ein Wurzelelement, das hier patient
heisst.
Elemente und Attribute
Ein Element wird durch ein Start-Tag in spitzen Klammern (hier <patient
...>
) geöffnet und durch ein End-Tag (hier </patient>
) wieder
geschlossen. Durch diese beiden Tags wird der Inhalt des Elements, die
sogenannten Kindknoten, umschlossen. Kindknoten eines Elements können
weitere Elemente oder Textknoten sein. Das Start-Tag kann zwischen dem
Namen und der schliessenden, spitzen Klammer noch Attribute enthalten, die
durch beliebig viele Leerzeichen getrennt werden. Das Start-Tag des
patient
Element hat hier die Attribute mit dem Namen xmlns
und id
deren Wert nach dem Gleichheitszeichen in einfachen oder doppelten
Anführungszeichen angegeben werden muss.
In Haskell sieht die Datentypdefinition zur Beschreibung von XML so aus:
Das Element name
aus unserem Beispiel hat drei Kindelemente, nämlich
given-name
, family-name
und display-name
. Das sind aber nicht alle
Kindknoten des name
Element. Es sind nur die Elemente, denn zwischen
der schließenden, spitzen Klammer des Start-Tag <name>
und der
öffnenden, spitzen Klammer des Start-Tag <given-name>
ist auch noch ein
Textknoten (NodeContent
), der einen Zeilenumbruch und einige Leerzeichen
enthält. Für unsere Anwendung Checkpad MED sind diese Textknoten
irrelevant und könnten ignoriert werden. Da der XML-Parser aber die Daten
nicht versteht wird er diese Textknoten nicht ignorieren. Es könnte beim
Inhalt ja auch um den Programmcode einer Programmiersprache handeln, wo
solches Whitespace zur Einrückung wichtig ist.
Was ist aber mit dem Zeilenumbruch innerhalb des End-Tag des Elements `admission‘? Hier ist sogar ein Zeilenumbruch enthalten. Whitespace innerhalb von Tags wird vom XML-Parser ignoriert und nicht an die XML verarbeitende Anwendung zurückgeliefert. Welche Informationen genau von einem Parser an die Anwendung zur Weiterverarbeitung geliefert werden müssen und was ignoriert werden darf, ist in der Spezifkation des XML Information Set definiert.
Namen
Jedes Element und jedes Attribut hat einen XML-Namen, der aus drei Bestandteilen besteht:
- dem
local name
(hier z.B.patient
) - einem optionalen
namespace name
(hierhttp://www.checkpad.de/ns/checkpad
) und - einem optionalen
namespace prefix
(im Beispiel gibt es keine)
Ein Präfix wird vor den Namen des Elements geschrieben und durch einen
Doppelpunkt abgetrennt. Zu jedem Präfix muss mit Hilfe eines Attributs
ein Namespace definiert werden. Ein Attribut zur Zuordnung eines
Namespace zu einem Präfix trägt das Präfix xmlns
und hat den lokalen
Namen des Präfix das definiert werden soll. xmlns:foo=...
bindet also
das Namespacepräfix foo
. Ein Namespace, der keinem Präfix zugeordnet
ist, definiert den Default-Namespace der für alle Elemente gilt, die kein
Präfix haben. Er wird mit xmlns=...
festgelegt. Ein Namespace gilt
immer ab dem Element in dessen Start-Tag er definiert wird für das Element
und alle enthaltenen Knoten. Wenn kein Default-Namespace definiert ist,
dann ist das Element keinem Namespace zugeordnet. Das Präfix wird
normalerweise von XML verarbeitenden Anwendungen ignoriert und es wird nur
der Name des Namespace zur Identifizierung verwendet. Das folgende
Dokument würde daher von unserer Anwendung gleich verarbeitet werden:
Als Haskell Datentyp sieht ein Name dann so aus:
Das Beispieldokument als Haskell-Wert
Wir können nun das Beispieldokument mit den definierten Haskell-Datentypen repräsentieren. Wir definieren dazu aber zunächst einige Hilfsfunktionen.
Mit diesen Hilfsfunktionen ist es nun einfach das Dokument aufzubauen.
Beginnen wir mit dem Element name
:
Nun können wir schon das Wurzelelement definieren:
Jetzt haben wir nur das id
Attribut von patient
noch nicht
hinzugefügt. Dafür können wir eine Hilfsfunktion definieren und beim
Erzeugen des gesamten Dokuments aufrufen:
Serialisierung des Dokuments
Das so definierte Dokument kann man nur einfach als Datei speichern. Dazu
stellt das Modul Text.XML
die Funktion writeFile :: RenderSettings ->
FilePath -> Document -> IO ()
zur Verfügung:
Da wir die Textknoten zwischen dem End-Tag von patient
und dem
Start-Tag von name
in Haskell nicht eingefügt haben sieht die Ausgabe
nicht exakt so aus wie das oben stehende Beispiel, sondern alles steht in
einer Zeile.
So, das war‘s für heute. Das nächste Mal schauen wir uns an, wie man XML-Dateien einlesen und verarbeiten kann. Ich freue mich über Feedback!