Home > Artikel > Ausgabe 3/2019 > XML-Dokumente transformieren mit XSL

XML-Dokumente transformieren mit XSL

Achtung: Sie sind nicht angemeldet. Wenn Sie Abonnent sind und sich anmelden, lesen Sie den kompletten Artikel, laden das PDF herunter oder probieren die Beispieldatenbank aus (sofern vorhanden).

Im Artikel Kategorie-XML-Export mit Transformation haben wir an einem Praxisbeispiel gezeigt, wie Sie die Daten aus den beiden Tabellen Kategorien und Artikel exportieren und in eine andere Form bringen, als es der reine Export getan hätte. Im vorliegenden Artikel schauen wir uns nun an, welche Möglichkeiten die Transformation mit XSL grundsätzlich bietet und welche Rolle dabei die Sprache XPath spielt.

Beispieldatenbank

Die Beispiele dieses Artikels finden Sie in der Datenbank 1903_XMLTransformieren.zip.

Eine tragende Rolle für die Beispiele der folgenden Seiten spielt die Funktion Transformieren aus Listing 1. Diese Funktion erwartet drei per Parameter übergebene Werte: Den Pfad zur Quelldatei, den Pfad zur XSL-Datei und den Pfad der zu erstellenden Datei. Der vierte Parameter ist optional und liefert einen Text zu einem eventuell beim Parsen der beteiligten XML-Dokumente gefundenen Fehler zurück.

Public Function Transformieren(strQuelle As String, strXSL As String, strZiel As String, Optional strFehler As String) As Long

      Dim objQuelle As MSXML2.DOMDocument

      Dim objXSL As MSXML2.DOMDocument

      Dim objZiel As MSXML2.DOMDocument

      Set objQuelle = New MSXML2.DOMDocument

      objQuelle.Load strQuelle

      If objQuelle.parseError = 0 Then

          Set objXSL = New MSXML2.DOMDocument

          objXSL.Load strXSL

          If objXSL.parseError = 0 Then

              Set objZiel = New MSXML2.DOMDocument

              objQuelle.transformNodeToObject objXSL, objZiel

              objZiel.Save strZiel

          Else

              Transformieren = objXSL.parseError.errorCode

              strFehler = ".xsl-datei: " & vbCrLf & strXSL & vbCrLf & objXSL.parseError.reason

          End If

      Else

          Transformieren = objQuelle.parseError.errorCode

          strFehler = "Quelldatei: " & vbCrLf & strQuelle & vbCrLf & objQuelle.parseError.reason

      End If

End Function

Listing 1: Die Prozedur Transformieren

Mit dieser Funktion werden wir die folgenden Beispiele dieses Artikels durchführen. Sie lädt als erstes das XML-Dokument aus strQuelle mit der Load-Methode in das DOMDocument-Element objQuelle. Dann prüft sie, ob das Dokument einen Fehler enthält. Falls nicht, lädt sie das XSL-Dokument in die Variable objXSL.

Auch hier folgt eine Prüfung auf eventuelle Fehler beim Parsen der Datei. Erfolgt auch dies fehlerfrei, ruft die Prozedur die transformNodeToObject-Methode von objQuelle auf und übergibt objXSL als ersten und objZiel als zweiten Parameter. Danach speichert sie das neu erstellte Objekt mit der Save-Methode des Objekts objZiel, und zwar unter dem Namen, der in der Variablen strZiel angegeben ist.

Sollte an irgendeiner Stelle ein Fehler beim Parsen aufgetreten sein, liefert

Für all dies ist ein Verweis auf die Bibliothek Microsoft XML, v3.0 erforderlich. Es gibt zwar moderne Versionen dieser Bibliothek, aber wir verwenden diese, weil die XPath-Befehle damit besser funktionieren – mehr dazu im Artikel XML-Dokumente mit XPath durchsuchen.

Diesen Verweis fügen wir über den Verweise-Dialog hinzu, den Sie im VBA-Editor mit dem Menüeintrag Extras|Verweise aktivieren (siehe Bild 1).

Hinzufügen eines Verweises auf die XML-Bibliothek

Bild 1: Hinzufügen eines Verweises auf die XML-Bibliothek

Wir gehen an dieser Stelle davon aus, dass Sie bereits eine Beispiel-XML-Datei erstellt haben, die im gleichen Verzeichnis wie die Datenbank liegt.

Fehler ausgeben

Um eventuell auftretende Fehler auszugeben, rufen wir die Prozedur Transformieren wie folgt auf und übergeben dabei noch die Variable strFehler:

Public Sub TransformierenMitMeldungen()

Dim strFehler As String

Transformieren CurrentProject.Path & "\KategorienUndArtikel.xml", CurrentProject.Path & "\KategorienUndArtikel.xsl", CurrentProject.Path & "\Transformiert.xml", strFehler

Debug.Print strFehler

End Sub

Tritt hier ein Fehler auf, wird dieser mit dem Parameter strFehler an die aufrufende Prozedur zurückgegeben. Diese gibt ihn dann im Direktbereich des VBA-Editors aus.

Beispieldokument als Quelle

Als Beispiel verwenden wir wie in einigen anderen Artikeln den Export der Tabelle tblKategorien, für die wir zusätzlich die Tabelle tblArtikel hinzugefügt haben. Diese sieht in stark gekürzter Form wie folgt aus:

<?xml version="1.0" encoding="UTF-8"?>

<dataroot xmlns:od="urn:schemas-microsoft-com:officedata" generated="2019-08-01T21:45:19">

<tblKategorien>

<KategorieID>1</KategorieID>

<Kategoriename>Getränke</Kategoriename>

<Beschreibung>Alkoholfreie Getränke...</Beschreibung>

<Abbildung>FRwvAAIAAAANAA4AFAAhAP...</Abbildung>

<tblArtikel>

<ArtikelID>1</ArtikelID>

<Artikelname>Chai</Artikelname>

<LieferantID>1</LieferantID>

<KategorieID>1</KategorieID>

<Liefereinheit>10 Kartons x 20 Beutel</Liefereinheit>

<Einzelpreis>9</Einzelpreis>

<Lagerbestand>39</Lagerbestand>

<BestellteEinheiten>0</BestellteEinheiten>

<Mindestbestand>10</Mindestbestand>

<Auslaufartikel>0</Auslaufartikel>

</tblArtikel>

...

</tblKategorien>

...

</dataroot>

Im einführenden Beispiel des Artikels Kategorie-XML-Export mit Transformation haben wir aus diesem Dokument ein Dokument gemacht, das so aussieht, wie wir uns ein solches Dokument vorgestellt hätten, also etwa so:

<?xml version="1.0" encoding="UTF-16"?>

<Bestellverwaltung xmlns="http://www.w3.org/TR/REC-html40">

<Kategorien>

<Kategorie ID="1">

<Kategoriename>Getränke</Kategoriename>

<Beschreibung>Alkoholfreie Getränke, Kaffee, Tee, Bier</Beschreibung>

<Artikelliste>

<Artikel ID="1">

<Artikelname>Chai</Artikelname>

</Artikel>

...

</Artikelliste>

</Kategorie>

...

</Kategorien>

</Bestellverwaltung>

Für die komplette XSL-Datei, die wir hier verwendet haben, verweisen wir Sie auf den Artikel Kategorie-XML-Export mit Transformation.

Kopf einer XSL-Datei

Die erste Zeile einer XSL-Datei sieht immer wie folgt aus:

<xsl:stylesheet version="1.0"

xmlns:xsl="http://www.w3.org/1999/XSL/Transform"

xmlns="http://www.w3.org/TR/REC-html40">

Dynamische und statische Anweisungen

Danach folgen einige weitere Anweisungen, die sich vor allem durch ein Merkmal unterscheiden: Sie beginnen entweder mit <xsl: oder eben nicht. Wenn Sie schon einmal eine PHP- oder ASP-Seite programmiert haben, kennen Sie das Prinzip: Sie mischen dort HTML-Code und dynamische Skriptbestandteile. Der HTML-Code wird 1:1 in das resultierende HTML-Dokument übernommen, der PHP- oder ASP-Code fügt weitere, dynamisch ermittelte Inhalte etwa aus einer Datenbank hinzu oder strukturiert die Inhalte etwa durch den Einsatz von Schleifen oder Bedingungen. Genau das Gleiche finden Sie auch in XSL-Dokumenten vor. Der Unterschied ist, dass die Daten, die im Zieldokument abgebildet werden sollen, aus dem Quelldokument stammen und nicht etwa aus einer Datenbank.

Das template-Element als Wurzelelement

Genau wie ein XML-Dokument hat auch das XSL-Dokument ein Wurzelelement, nämlich das Element template. Dieses verlangt zwingend nach der Angabe des Parameters match. match gibt an, auf welches Element der Originaldatei sich die nachfolgenden Anweisungen beziehen beziehungsweise welches Element samt Unterelementen die Daten für das Zielelement liefern soll. Hier stellen wir match mit "/" auf das Wurzelelement ein. Außerdem wollen wir als Basis ein öffnendes und schließendes Element namens Bestellverwaltung anlegen:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:template match="/">

<Bestellverwaltung>

</Bestellverwaltung>

</xsl:template>

</xsl:stylesheet>

Das Ergebnis sieht dann so aus:

<?xml version="1.0" encoding="UTF-16"?>

<Bestellverwaltung></Bestellverwaltung>

Zeilenumbrüche

Hier wollen wir nun zunächst einen Zeilenumbruch einfügen, denn obwohl wir das öffnende und schließende Bestellverwaltung-Element in zwei Zeilen geschrieben haben, wird der Zeilenumbruch nicht in das Ergebnis übernommen. Das gelingt nur, wenn das entsprechende Zeichen innerhalb eines öffnenden und schließenden Elements xsl:text eingefasst wird, also etwa so:

<Bestellverwaltung>

<xsl:text>

</xsl:text>

</Bestellverwaltung>

Einzelne Elemente des Quelldokuments ausgeben

Sie können auf einzelne Elemente des Quelldocuments mit dem Element xsl:value-of zugreifen und dabei den Pfad zu dem Element mit dem Attribut select angeben.

Wenn sie etwa den Wert des ersten KategorieID-Elements unterhalb der Elemente dataroot/tblKategorien ausgeben wollen, verwenden Sie:

<xsl:value-of select="/dataroot/tblKategorien/KategorieID"/>

Für das Feld Kategoriename nutzen Sie:

<xsl:value-of select="/dataroot/tblKategorien/Kategoriename"/>

Sie müssen also den kompletten Pfad vom Wurzelelement aus angeben.

<xsl:value-of select="/dataroot/tblKategorien/KategorieID"/>

Wenn es Elemente des Typs KategorieID nur an einer Stelle gibt, können Sie auch gezielt auf das erste überhaupt verfügbare Element KategorieID zugreifen:

<xsl:value-of select="//KategorieID"/>

In diesem Fall bedeutet //, dass an beliebiger Stelle unabhängig von der Ebene gesucht wird.

Leerzeichen zwischen Elemente

Übrigens können Sie auch Leerzeichen zwischen zwei zusammengesetzte Textelemente nur mit xsl:text einfügen. Einfach ein Leerzeichen wie hier reicht nicht:

<xsl:value-of select="//KategorieID"/>

Stattdessen fügen Sie das Leerzeichen in xsl:text eingefasst hinzu:

<xsl:value-of select="//KategorieID"/><xsl:text> </xsl:text><xsl:value-of select="//Kategoriename"/>

Schleifen mit for-each

Mit xsl:value-of greifen Sie nur auf den ersten gefundenen Wert zu. Wenn Sie alle Werte mit diesem Pfad zugreifen wollen, etwa in einer Schleife, können Sie xsl:for-each verwenden.

Um beispielsweise alle Elemente des Typs tblKategorie des Beispieldokuments zu durchlaufen und wie oben einfach die KategorieID und den Kategorienamen auszugeben, verwenden Sie innerhalb des template-Elements zunächst ein Wurzelelement (Kategorien – ohne dieses Element hätte das Dokument mehrere Wurzelelemente, was nicht zulässig ist). Dann folgt ein Zeilenumbruch und das öffnende Element xsl:for-each. Der Wert für das Attribut select gibt an, welches Element durchlaufen werden soll, in diesem Fall die dataroot/tblKategorien-Elemente. Innerhalb der Schleife geben wir das öffnende <Kategorie>-Element, die Werte der Elemente KategorieID und Kategoriename und das schließende </Kategorie>-Element sowie einen Zeilenumbruch aus:

<Kategorien>

<xsl:text>

</xsl:text>

<xsl:for-each select="dataroot/tblKategorien">

<Kategorie>

<xsl:value-of select="KategorieID"/> <xsl:text> </xsl:text> <xsl:value-of select="Kategoriename"/>

</Kategorie>

<xsl:text>

</xsl:text>

</xsl:for-each>

</Kategorien>

Das ist das Ergebnis:

<?xml version="1.0" encoding="UTF-16"?>

<Kategorien>

<Kategorie>1 Getränke</Kategorie>

<Kategorie>2 Gewürze</Kategorie>

<Kategorie>3 Süßwaren</Kategorie>

...

</Kategorien>

Wir haben vorher probiert, nur die Felder KategorieID und Kategoriename auszugeben, aber dann funktioniert der Zeilenumbruch nicht – dieser lässt sich offensichtlich nur zwischen zwei in spitze Klammern eingefasste Elemente einfügen.

Verschachtelte for-each-Schleifen

Innerhalb der for-each-Schleife können Sie beliebige Anweisungen einfügen, sogar weitere for-each-Schleifen. Im folgenden Beispiel haben wir zu jedem Kategorie-Element zunächst ein Artikelliste-Element hinzugefügt. Darin durchlaufen wir eine weitere for-each-Schleife, diesmal mit dem select-Attribut tblArtikel. Warum nicht dataroot/tblKategorien/tblArtikel?

Weil das Wurzelelement der for-each-Schleife immer das Element ist, in dem die Schleife ausgeführt wird – das ist in diesem Fall bereits das Element tblKategorien. Wir können uns also direkt auf die darin enthaltenen Elemente des Typs tblArtikel beziehen. Die Werte der darin enthaltenen Elemente ermitteln wir dann wie im vorherigen Beispiel mit der value-of-Funktion. Im folgenden Code haben wir aus Gründen der Übersicht die Code-Elemente für die Zeilenumbrüche entfernt:

<Kategorien>

<xsl:for-each select="dataroot/tblKategorien">

<Kategorie>

<Kategoriedaten>

<xsl:value-of select="KategorieID"/> <xsl:text> </xsl:text> <xsl:value-of select="Kategoriename"/>

</Kategoriedaten>

<Artikelliste>

<xsl:for-each select="tblArtikel">

<Artikel><xsl:value-of select="ArtikelID"/> <xsl:text> </xsl:text> <xsl:value-of select="Artikelname"/></Artikel>

</xsl:for-each>

</Artikelliste>

</Kategorie>

</xsl:for-each>

</Kategorien>

Das Ergebnis sieht dann etwa wie folgt aus:

<?xml version="1.0" encoding="UTF-16"?>

<Kategorien>

<Kategorie>

<Kategoriedaten>1 Getränke</Kategoriedaten>

<Artikelliste>

<Artikel>1 Chai</Artikel>

<Artikel>2 Chang</Artikel>

<Artikel>24 Guaraná Fantástica</Artikel>

...

</Artikelliste>

</Kategorie>

...

</Kategorien>

Die Informationen, die bisher im Element Kategoriedaten landen, sollen nun in zwei Elementen namens KategorieID und Kategoriename abgelegt werden. Dazu ersetzen wir das Element Kategoriedaten durch den folgenden Ausdruck:

<KategorieID><xsl:value-of select="KategorieID"/></KategorieID>

... Leerzeile ...

<Kategoriename><xsl:value-of select="Kategoriename"/></Kategoriename>

Das liefert dann diese Ausgabe:

<Kategorie>

<KategorieID>2</KategorieID>

<Kategoriename>Gewürze</Kategoriename>

</Kategorie>

Schleife über untergeordnete Elemente

Wir haben nun die tblKategorie-Elemente durchlaufen und die tblKategorie- und die tblArtikel-Elemente in zwei verschachtelten for-each-Schleifen. Sie können auch direkt nur die tblArtikel-Elemente durchlaufen. Das gelingt etwa so:

<xsl:template match="/">

<Artikelliste>

<xsl:text>

</xsl:text>

<xsl:for-each select="dataroot/tblKategorien/tblArtikel">

<Artikel>

<xsl:value-of select="ArtikelID"/>

<xsl:text> </xsl:text>

<xsl:value-of select="Artikelname"/>

</Artikel>

<xsl:text>

</xsl:text>

</xsl:for-each>

</Artikelliste>

<xsl:text>

</xsl:text>

</xsl:template>

Wichtig hierbei ist, dass Sie im select-Attribut der for-each-Schleife nicht direkt tblArtikel referenzieren, sondern dataroot/tblKategorien/tblArtikel.

Das liegt daran, dass wir uns aktuell im Kontext des Wurzelelements befinden und nicht mehr, wie innerhalb der Schleife über die Kategorien aus dem vorherigen Beispiel, im Kontext eines tblKategorie-Elements.

Das Ergebnis ist dann eine solche Artikelliste:

<?xml version="1.0" encoding="UTF-16"?>

<Artikelliste>

<Artikel>1 Chai</Artikel>

<Artikel>2 Chang</Artikel>

<Artikel>24 Guaraná Fantástica</Artikel>

...

<Artikel>7 Uncle Bob's Organic Dried Pears</Artikel>

</Artikelliste>

Durch die Sortierung nach den übergeordneten Kategorien im Quelldokument sind die Artikel nicht nach der ID sortiert. Wie Sie das hinbekommen, sehen wir uns weitern unten an.

Kommentare

Kommentare im XSL-Code können Sie auf zwei Arten herstellen – der Unterschied ist, ob der Kommentar im XSL-Code gelten oder in der erstellten Datei landen soll. Die erste Methode ist die unter HTML übliche Vorgehensweise, bei der Sie die auszukommentierenden Elemente in <!-- und --> einfassen. Damit sorgen Sie dafür, dass die so eingefassten Anweisungen nicht berücksichtigt werden.

Die zweite Methode ist die Verwendung des xsl:comment-Elements:

<xsl:comment>

... auskommentierter Bereich

</xsl:comment>

Dies sorgt dafür, dass die Zeichen <!-- und --> im resultierenden Dokument landen.

Attribute hinzufügen

Grundsätzlich können Sie alle Daten in Form von Elementen zu einem übergeordneten Element hinzufügen. Es gibt aber auch die Gelegenheit, Attribute zu verwenden.

Zuletzt haben wir die beiden interessanten Felder der Kategorie-Elemente wie folgt abgebildet:

<Kategorie>

<KategorieID>2</KategorieID>

<Kategoriename>Gewürze</Kategoriename>

</Kategorie>

Sie haben das Ende des frei verfügbaren Teil dieses Artikels erreicht!

Wenn Sie mehr lesen und auf viele weitere Artikel zugreifen möchten, melden Sie sich als Abonnent unter Login an. Falls nicht, bestellen Sie doch einfach ein Jahresabonnement!