XMLReader

Mit dem XMLReader Step kann man eine oder mehrere XML-Dateien einlesen und als Spreadsheet ausgeben. Oft wird dieser Prozess auch als parsen oder parsing bezeichnet.  
Die Funktionsweise verdeutlicht sich am besten an einem Beispiel.

XML-Einlesen ohne Skripting

Für einfach strukturierte XML-Dateien kann man auch den Step XMLReaderVisual verwenden. Dieser macht die Verarbeitung durch eine graphische Oberfläche noch einfacher.

Tutorial: REST-API Anbindung

Lernen Sie in unserem Tutorial REST API Anbindung mit Synesty, wie man jede beliebige HTTP-API mit JSON oder XML anbinden kann - auch ohne Add-Ons von Synesty und ohne Programmierung in PHP, Java oder Javascript.

Beispiel XML-Datei

Nehmen wir folgende XML-Datei an (Beispieldatei herunterladenopen in new window). Diese Datei enthält Daten für 2 Artikel, mit allerlei Artikelnummern und weiteren typischen Feldern.

<?xml version="1.0" encoding="ISO-8859-1"?>
<ARTIKELLISTE>
    <ARTIKEL>
        <LFSN>ZEG</LFSN>
        <LFKURZBEZ>ZEG</LFKURZBEZ>
        <INTARTNR>8592</INTARTNR>
        <ARTNR>010-12983</ARTNR>
        <REFARTNR>010-12983</REFARTNR>
        <HERSTNR>870310 / 10870310</HERSTNR>
        <MARKE>Schwalbe</MARKE>
        <MARKENLOGO></MARKENLOGO>
        <MODELL>High Pressure</MODELL>
        <MODELLJAHR>0</MODELLJAHR>
        <FARBE>blau</FARBE>
    </ARTIKEL>
    <ARTIKEL>
        <LFSN>ZEG</LFSN>
        <LFKURZBEZ>ZEG</LFKURZBEZ>
        <INTARTNR>8593</INTARTNR>
        <ARTNR>010-12984</ARTNR>
        <REFARTNR>010-12984</REFARTNR>
        <HERSTNR>870311 / 10870311</HERSTNR>
        <MARKE>Simson</MARKE>
        <MARKENLOGO></MARKENLOGO>
        <MODELL>High Pressure</MODELL>
        <MODELLJAHR>0</MODELLJAHR>
        <FARBE>rot</FARBE>
    </ARTIKEL>
</ARTIKELLISTE>

Ziel Spreadsheet

Ziel ist es, diese Datei einzulesen und einige der Felder als tabellarisches Spreadsheet auszugeben, um damit weiter zu arbeiten.

Beispiel-Flow

Folgender Beispiel-Flow liest die XML-Datei ein.

Die eigentliche Einlese-Logik steckt im Input transformationTemplate.

XML Parsing Variante 1

Die Einlese-Logik nutzt hierbei die XML-Parsing-Möglichkeiten der Skriptsprache Freemarkeropen in new window sowie eine Synesty-Hilfsfunktion addColumns().

<#assign row = target.addRow()>

<#list xml["ARTIKELLISTE"]["ARTIKEL"] as art>
    <#assign row = target.addRow()>
    ${addColumns(row, art)}
</#list>

Sehen wir uns nun diese Logik im Detail an:

<#assign row = target.addRow()>

Die Variable target repräsentiert unser Ziel-Spreadsheet. (Hinweis: Diese Variable ist fest vergeben und muss zwingend so heißen.) Über target.addRow(), wird dem Spreadsheet eine Zeile hinzugefügt. In diesem Fall eine leere Kopfzeile.

<#list xml["ARTIKELLISTE"]["ARTIKEL"] as art>
...
</#list>

Die im Hintergrund geparste XML-Datei steht als Variable xml im transformationTemplate zur Verfügung und beinhaltet die komplette XML-Struktur als DOM-Tree (oder sog. Node-Modell). Man kann nun auf einzelnen Felder der XML-Datei zugreifen.
 (Hinweis: Diese Variable ist fest vergeben und muss zwingend so heißen.)

Mit der list-Anweisungopen in new window wird über alle ARTIKEL der ARTIKELLISTE iteriert. Auf die Daten eines Artikels wird dann über den Alias art zugegriffen. (Hinweis: Anstatt art, kann auch ein beliebiges anderes Wort genutzt werden. )

<#assign row = target.addRow()>
${addColumns(row, art)}

Über target.addRow(), wird dem Spreadsheet pro Iteration eine weitere Zeile hinzugefügt. Diese Zeile wird einer neuen Variable row zugewiesen, damit wir anschließend dieser Zeile Spalten hinzufügen können.

Wir erinnern uns dass die Felder des Artikels, über den wir gerade iterieren, in der Variable art zur Verfügung stehen. 
Um nun die Zeile (row) mit Spalten (Columns) zu füllen, rufen wir folgendes auf:

${addColumns(row, art)}

Damit wird die Funktion addColumns für die row aufgerufen und mit Spalten gefüllt. Die Spaltennamen und Inhalte stammen aus dem dem aktuellen XML-Knoten der sich hinter der Variable art verbirgt.

Parameter von addColumns()

Syntax:

${addColumns(row, node, [prefix], [options])}
Parameter
Beschreibung
row
Die Zeile (das Row-Objekt des Spreadsheets), der die Spalten hinzugefügt werden sollen.
node
Der Ober-Knoten (Node) im XML-Baum, dessen Unterknoten als Spalten hinzugefügt werden sollen.
prefix
optional: Dieser optionale Prefix wird allen hinzugefügten Spaltennamen vorangestellt.

Beispiel:
${addColumns(row, order['shipping_address'], "shipto_address_")}
1
${addColumns(row, order['billing_address'], "billto_address_")}
1

Hier werden Spalten eines XML-Unterknotens shipping_address und billing_address hinzugefügt. Da beide Adressen Spalten wie Vorname, Nachname enthalten können, ist es sinnvoll diese durch ein Prefix (hier shipping_address_ und billto_address_) von einander abzugrenzen und unterscheidbar zu machen.  

options(optional)

Ein Hash/Map (Schlüssel/Wert-Paare) mit optionalen Konfigurationsparametern. Die Notation der options ist technisch gesehen ein JSON-Objekt.
Einige Parameter gehören zusammen (z.B. columns und mode), einige stehen für sich allein oder greifen nur unter bestimmten Bedingungen. Es wird versucht in der Beschreibung deutlich zu machen, wann ein Paramter anwendbar ist.

Beispiel:Hier werden die beiden Parameter columns und mode dargestellt, die etwas weiter unten noch genauer erklärt werden.

2 Parameter: columns und mode
{"columns":["zip", "city"], "mode":"include"}
# Etwas lesbarer formatiert, sieht das so aus: { "columns":[ "zip", "city" ], "mode":"include" }
1
2
3
4
5
6
7
8
9
10

D.h. ein kompletter addColumns-Befehl inkl. options sieht wie folgt aus:

${addColumns(row, order['shipping_address'], "", {"columns":["zip", "city"], "mode":"include"})}
1

Wichtig

Wird options ohne den prefix Parameter verwendet, dann ist für prefix ein Platzhalter in Form von "" einzufügen.

Beispiel: ${addColumns(row, order['shipping_address'], "", {"columns":["zip", "city"], "mode":"include"})}


columns (optional)&mode (optional)

columns: Ein String-Array von Feldnamen, die im Spreadsheet hinzugefügt (include) oder ausgeschlossen (exclude) werden sollen.

mode: Über mode kann man steuern, ob die im Parameter columns definierten Spalten ausgegeben oder ignoriert werden.

  • include (default) - die Spalten in columnsArray werden ausgegeben
  • exclude - die Spalten in columnsArray werden ignoriert / ausgeschlossen

Beispiel: Nur bestimmte Feld ausgeben (include)

${addColumns(row, order['shipping_address'], "shipto_address_", {"columns":["zip", "city"]} )}
1

Diese Zeile sorgt dafür, dass nur die Felder zip und city des Knotens shipping_address im Spreadsheet landen. Alle anderen Address-Felder wie z.B. street oder country werden nicht ausgegeben.
Den optionalen Parameter mode kann man weglassen, da dieser per Default auf "include" steht. D.h. man hätte obige Zeile auch so schreiben können:

${addColumns(row, order['shipping_address'], "shipto_address_", {"columns":["zip", "city"], "mode":"include"} )}
1

Beispiel: Bestimmte Felder ausschließen (exclude)

${addColumns(row, order['shipping_address'], "shipto_address_", {"columns":["zip", "city"], "mode":"exclude"} )}
1

Diese Zeile sorgt dafür, dass alle Felder außer zip und city des Knotens shipping_address im Spreadsheet landen.
In diesem Fall muss der Parameter mode angegeben werden.

Wichtig

Die eckigen Klammern bei ["zip", "city"] sind wichtig, da dies die Freemarker-Notation für ein Array ist. Bitte nicht vergessen!


skipEmptyValues (optional)

Über skipEmptyValues (true/false, default: false) kann man steuern, ob man eine Spalte nur hinzufügen möchte, wenn der Wert der XML-Node nicht leer ist. Das ist praktisch bei riesigen XML-Dateien, die haufenweise leere Tags enthalten, die man nicht im Output-Spreadsheet sehen möchte. Damit kann man das resultierende Spreadsheet auf die nötigsten nicht-leeren Spalten beschränken.

${addColumns(row, order['shipping_address'], "shipto_address_", {"columns":["zip","city"], "skipEmptyValues":true})}
1

Diese Zeile sorgt dafür, dass die Felder zip und city dem Spreadsheet hinzugefügt werden, allerdings nur wenn das Feld jeweils gefüllt ist. Wenn z.B. die Spalte zip immer leer wäre, dann würde im Output-Spreadsheet keine Spalte für zip auftauchen.


autoExpand (optional)

Der Parameter autoExpand steuert, ob und wie Unter-Nodes automatisch "aufgeklappt" werden, wenn deren Wert ebenfalls wieder Unter-Nodes enthält.
Ohne Angabe von autoExpand, werden diese komplexen Felder leer ausgegeben. Man könnte dieses komplexe Feld durch einen weiteren Aufruf der ${addColumns()} Funktion als Spalten hinzufügen. 
Durch die Option autoExpand, kann man sich diesen weiteren Aufruf sparen, und die Felder werden sofort hinzugefügt. Da dieses Verhalten nicht immer gewünscht ist, ist dies optional. Das spart unter Umständen einiges an Tipparbeit.

  • asColumns - fügt die Felder des Unterobjekts als neue Spalten hinzu. Dabei werden die Feldnamen des Unterobjekts automatisch mit dem Schlüssel (Key) des Felders ge-prefixt (z.B. billing_address_street, billing_address_city)

Beispiel asColumns:

XML Quelle
<?xml version="1.0" encoding="ISO-8859-1"?>
<ARTIKELLISTE>
  <ARTIKEL>
    <ARTNR>010-12983</ARTNR>
    <Category>
       <Name>Zubehör</Name>
       <Visible>YES</Visible>
    </Category>
  </ARTIKEL>      
</ARTIKELLISTE>
1
2
3
4
5
6
7
8
9
10

Das entscheidende hier ist der Category-Knoten, welcher wieder Unter-Knoten enthält.

Damit diese Unterknoten (Name und Visible) auch durch addColumns() mit ausgegeben werden, muss die Option "autoExpand": "asColumns" gesetzt werden.

<#assign row = target.addRow()>
<#list xml["ARTIKELLISTE"]["ARTIKEL"] as art>
  <#assign row = target.addRow()>

${addColumns(row, art, "myPrefix_", {"autoExpand": "asColumns"})}
</#list>
1
2
3
4
5
6
7

Ausgabe:

myPrefix_ARTNRmyPrefix_Category_NamemyPrefix_Category_Visible
010-12983ZubehörYES

XML Parsing Variante 2

Diese Einlese-Logik verzichtet auf die addColumns() Hilfsfunktion und ist somit länger. Sie zeigt quasi den manuellen Weg, wenn man volle Flexibilität braucht. Das ist in etwas das, was *addColumns() *im Hintergrund automatisch macht.

<#assign row = target.addRow()>

<#list xml["ARTIKELLISTE"]["ARTIKEL"] as art>
    <#assign row = target.addRow()>
    ${row.addCol("Artikelnummer",art["ARTNR"])}
    ${row.addCol("Marke",art["MARKE"])}
    ${row.addCol("Modell",art["MODELL"])}
</#list>

XML-Dateien mit XML-Namespace Deklaration (xmlns)

In einigen Fällen werden in XML-Dateien auch sogenannte Namespaces deklariert. Namespaces dienen der eindeutigen Identifizierung vom XML Elementen. Deklarierte Namespaces erkennt man am xmlns Attribut, z.B.

xmlns="http://meistensirgendeinelangeurl.de/" für einen Default Namespace oder xmlns:prefix="http://meistensirgendeinelangeurl.de/erweitert"

XML Beispiel mit xmlns Namespace Deklaration

<?xml version="1.0" encoding="ISO-8859-1"?>
<ARTIKELLISTE xmlns="http://meistensirgendeinelangeurl.de/" xmlns:prefix="http://meistensirgendeinelangeurl.de/erweitert">
    <ARTIKEL>
        <ARTNR>010-12983</ARTNR>
        <MARKE>Schwalbe</MARKE>
        <MODELL>High Pressure</MODELL>
        <prefix:DETAILS>
            <IMG>image1.jpg</IMG>
        </prefix:DETAILS>
    </ARTIKEL>
    <ARTIKEL>
        ...
    </ARTIKEL>
</ARTIKELLISTE>

Wenn das der Fall ist, dann würde das obige Freemarker-Skript zum Einlesen nicht funktionieren. 
Man muss Freemarker erst mitteilenopen in new window, dass dieser Namespace verwendet werden soll.  Der Default-Namespace wird immer mit "D" angegeben, alle anderen über den Namespace-Präfix, z.B. "prefix"

Dazu fügt man im transformationTemplatefolgende Zeile an den Anfang ein:

<#ftl ns_prefixes={"D":"http://meistensirgendeinelangeurl.de/", "prefix":"http://meistensirgendeinelangeurl.de/erweitert"}>

Danach funktioniert das Skript wieder so wie oben. 
Das komplette Skript sieht dann so aus.

<#ftl ns_prefixes={"D":"http://meistensirgendeinelangeurl.de/", "prefix":"http://meistensirgendeinelangeurl.de/erweitert"}>

<#assign row = target.addRow()>

<#list xml["ARTIKELLISTE"]["ARTIKEL"] as art>
    <#assign row = target.addRow()>
    ${row.addCol("Artikelnummer",art["ARTNR"])}
    ${row.addCol("Marke",art["MARKE"])}
    ${row.addCol("Modell",art["MODELL"])}
    ${row.addCol("Bild1", art["prefix:DETAILS"][0]["IMG"])}
</#list>

Details zu Freemarker und XML Namespaces:

Komplexere XML-Strukturen

Das obige Beispiel repräsentiert eine sehr einfache XML-Datei. 
XML-Dateien können aber wesentlich komplexere und verschachtelte Strukturen beinhalten.

Da dies immer von der konkreten XML-Struktur abhängt empfehlen wir sich ausgiebig mit folgenden Seiten aus der Freemarker Dokumentation auseinander zu setzen:

XML-Dateien mit Attributen

In manchen Fällen besitzen XML-Elemente sog. Attribute. Ein Attribut gibt weitere Informationen über das XML-Element an. In dem folgenden Beispiel (Beispieldatei herunterladenopen in new window) besitzt das Element <PREIS> die Attribute  "typ" und "wert".

<?xml version="1.0" encoding="ISO-8859-1"?>
<ARTIKELLISTE>
    <ARTIKEL>
        <LFSN>ZEG</LFSN>
        <LFKURZBEZ>ZEG</LFKURZBEZ>
        <INTARTNR>8592</INTARTNR>
        <ARTNR>010-12983</ARTNR>
        <REFARTNR>010-12983</REFARTNR>
        <HERSTNR>870310 / 10870310</HERSTNR>
        <MARKE>Schwalbe</MARKE>
        <MARKENLOGO></MARKENLOGO>
        <MODELL>High Pressure</MODELL>
        <MODELLJAHR>0</MODELLJAHR>
        <FARBE>blau</FARBE>
        <PREISE>
            <PREIS typ="EKN" wert="1000"/>
            <PREIS typ="EKB" wert="1190"/>
            <PREIS typ="VKN" wert="1400"/>
            <PREIS typ="VKB" wert="1666"/>
        </PREISE>
    </ARTIKEL>
</ARTIKELLISTE>

In der Beispiel XML-Datei sind mehrere Preise aufgelistet. Diese Preise sollen in einen Spreadsheet ausgegeben werden. Für eine bessere Unterscheidung wird der Typ mit angegeben. Am besten lässt sich dies bewerkstelligen wenn jeder Typ in eine separate Spalte im Spreadsheet geschrieben wird. Das hat außerdem noch den Vorteil das man später die Preise besser filtern und bearbeiten kann.

Ziel Spreadsheet

Auch hier wird wieder eine  list-Anweisungopen in new window verwendet um über jedes PREIS-Element zu iteriern. Auf die Daten eines Attributs wird dann mit der freemarker Anweisung  attr("Attributename", Element) zugegriffen.

<#assign row = target.addRow()>
<#list xml["ARTIKELLISTE"]["ARTIKEL"] as item>

    <#assign row = target.addRow()>
 
    <#list item["PREISE"]["PREIS"] as preis>
        ${row.addCol(attr("typ", preis), attr("wert", preis)!)}
    </#list>
</#list>

Zugriff auf XML-Attribute per attr()

Die Funktion attr() ist eine Synesty-eigene Funktion, weil der Zugriff auf XML-Attribute direkt per Freemarker starke Performance-Probleme hat.

Details zu Freemarker und XML Attributes:

  • http://freemarker.incubator.apache.org/docs/xgui_imperative_learn.html#autoid_133

XML in Spreadsheet-Spalten einlesen

Der XMLReader Step kann außer FILE und FILELIST Inputs auch mit dem Typ SPREADSHEET umgehen - d.h. ein Spreadsheet, in dem in einer Spalte ein XML-String steht.

Wann braucht man das?

Meistens wird das gebraucht wenn man vorher mit dem Step SpreadsheetURLDownload arbeitet und z.B. eine Webservice- oder REST-API anbindet. Die Ausgabe des SpreadsheetURLDownload ist ein Spreadsheet, in dem in einer Spalte die XML-Antwort des API-Calls steht (so ist das zumindest bei einigen-APIs der Fall).

Anwendung

Sobald Sie dem XMLReader Step das Input-Spreadsheet übergeben, erscheint eine weitere Option spreadsheetXMLColumn, mit der man bestimmt, in welcher Spalte dieses Spreadsheets der XML-String steht, der geparst werden soll.

Mit anderen Worten: statt einer Liste von XML-Dateien (FILELIST) wird eine eine Liste von Spalten verarbeitet, in denen XML drin steht.

Zugriff auf die Spreadsheet-Spalten

Man kann im transformationTemplate auch auf die anderen Spalten des Input-Spreadsheets zuzugreifen. Meistens wird das genutzt in Verbindung mit dem SpreadsheetURLDownload und der dortigen Möglichkeit Spalten über die Option outputSourceColumns durchzuschleifen (z.B. Artikelnummer / SKU).

Dafür existiert eine Variable inputRow
Damit haben Sie  immer die aktuelle Zeile des Input-Spreadsheets verfügbar und können auf die Spaltenwerte zugreifen.

Beispiel:

${inputRow.get("meinSKUSpalte")!}

Angenommen im Input-Spreadsheet gibt es eine Spalte *meineSKUSpalte, *die eine Artikelnummer enthält, und die auch wieder in der Ausgabe des XMLReader Steps auftauchen soll.

Sie können im XMLReader dann grob sowas machen, um eine Spalte mit dieser Artikelnummer hinzuzufügen:

${row.addCol("SKU", inputRow.get("meinSKUSpalte") )}

Fehler im Parsing Code

Das Einlesen von XML Dateien ist nicht ganz einfach und es passieren auch schnell einmal Tippfehler, man verschreibt sich bei einem XML-Tag, welche es dann nicht gibt.

In diesem Fall gibt der Step Fehlermeldungen wie folgt aus:

Error in XML parsing code: No compatible overloaded variation was found; can't convert (unwrap) the 2nd argument to the desired Java type.The FTL type of the argument values were: string (wrapper: f.t.SimpleScalar), sequence+hash (wrapper: f.e.dom.NodeListModel).
--------FTL stack trace ("~" means nesting-related): - Failed at: ${row.addCol("Modell", art["MODELLa"])} [in template "xmlToSpreadsheet" at line 6, column 9]---- (responsible code: - Failed at: ${row.addCol("Modell", art["MODELLa"])} [in template "xmlToSpreadsheet" at line 6, column 9])

Dieser Fehler wird genauer in diesem Knowledge Base Artikelopen in new window erklärt.

Beispiel Flow zum Download

Das komplette Beispiel von oben gibt es als installierbare Vorlageopen in new window oder

Download Beispiel Flow

(Rechtsklick und Speichern unter) und im Studio als Flow importieren.

Weitere Beispiele

Weitere auch etwas komplexere Beispiele finden Sie auf der Seite XML und JSON Parsing am Beispiel.