XML in Daten mit Trennzeichen exportieren

(Auszug aus "XSLT Kochbuch" von Sal Mangano)

Problem

Sie müssen XML in ein Datenformat umwandeln, das sich für den Import in eine andere Anwendung, etwa eine Tabellenkalkulation, eignet.

Lösung

Viele Anwendungen importieren Daten, die mit Trennzeichen versehen sind. Das gebräuchlichste Format wird als CSV (Comma Separated Values; durch Kommas getrennte Werte) bezeichnet. Viele Tabellenkalkulationen und Datenbanken können mit CSV und anderen Formen solcherart getrennter Daten umgehen. Die Zuordnung von XML auf abgetrennte Daten kann einfach oder kompliziert sein, je nach dem Schwierigkeitsgrad der Zuordnung. Dieser Abschnitt beginnt mit einfachen Fällen und fährt dann mit komplizierteren Szenarien fort.

Eine CSV-Datei aus einfachen, mit Attributen kodierten Elementen erzeugen

In diesem Szenario haben Sie eine einfache XML-Datei mit Elementen, die auf Zeilen abgebildet werden, und Attributen, die auf Spalten abgebildet werden.

Dieses Problem ist für jede angegebene XML-Datei des passenden Formats trivial. Beispielsweise gibt das folgende Stylesheet, das in den folgenden drei Beispielen gezeigt wird, auf der Grundlage der Eingabe people.xml eine CSV-Datei aus.

Beispiel: people.xml

<?xml version="1.0" encoding="UTF-8"?>
<people>
  <person name="Al Zehtooney" age="33" sex="m" smoker="nein"/>
  <person name="Brad York" age="38" sex="m" smoker="ja"/>
  <person name="Charles Xavier" age="32" sex="m" smoker="nein"/>
  <person name="David Williams" age="33" sex="m" smoker="nein"/>
  <person name="Edward Ulster" age="33" sex="m" smoker="ja"/>
  <person name="Frank Townsend" age="35" sex="m" smoker="nein"/>
  <person name="Greg Sutter" age="40" sex="m" smoker="nein"/>
  <person name="Harry Rogers" age="37" sex="m" smoker="nein"/>
  <person name="John Quincy" age="43" sex="m" smoker="ja"/>
  <person name="Kent Peterson" age="31" sex="m" smoker="nein"/>
  <person name="Larry Newell" age="23" sex="m" smoker="nein"/>
  <person name="Max Milton" age="22" sex="m" smoker="nein"/>
  <person name="Norman Lamagna" age="30" sex="m" smoker="nein"/>
  <person name="Ollie Kensington" age="44" sex="m" smoker="nein"/>
  <person name="John Frank" age="24" sex="m" smoker="nein"/>
  <person name="Mary Williams" age="33" sex="w" smoker="nein"/>
  <person name="Jane Frank" age="38" sex="w" smoker="ja"/>
  <person name="Jo Peterson" age="32" sex="w" smoker="nein"/>
  <person name="Angie Frost" age="33" sex="w" smoker="nein"/>
  <person name="Betty Bates" age="33" sex="w" smoker="nein"/>
  <person name="Connie Date" age="35" sex="w" smoker="nein"/>
  <person name="Donna Finster" age="20" sex="w" smoker="nein"/>
  <person name="Esther Gates" age="37" sex="w" smoker="nein"/>
  <person name="Fanny Hill" age="33" sex="w" smoker="ja"/>
  <person name="Geta Iota" age="27" sex="w" smoker="nein"/>
  <person name="Hillary Johnson" age="22" sex="w" smoker="nein"/>
  <person name="Ingrid Kent" age="21" sex="w" smoker="nein"/>
  <person name="Jill Larson" age="20" sex="w" smoker="nein"/>
  <person name="Kim Mulrooney" age="41" sex="w" smoker="nein"/>
  <person name="Lisa Nevins" age="21" sex="w" smoker="nein"/>
</people>

Beispiel: Eine einfache, aber eingabespezifische CSV-Transformation.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text"/>
  <xsl:strip-space elements="*"/>
  <xsl:template match="person">
    <xsl:value-of select="@name"/>,<xsl:text/>
    <xsl:value-of select="@age"/>,<xsl:text/>
    <xsl:value-of select="@sex"/>,<xsl:text/>
    <xsl:value-of select="@smoker"/>
    <xsl:text>&#xa;</xsl:text>
  </xsl:template>
</xsl:stylesheet>

Beispiel: Ausgabe.

Al Zehtooney,33,m,nein
Brad York,38,m,ja
Charles Xavier,32,m,nein
David Williams,33,m,nein
Edward Ulster,33,m,ja
Frank Townsend,35,m,nein
Greg Sutter,40,m,nein

...

Die Lösung ist zwar einfach, dennoch wäre es schön, wenn man ein generisches Stylesheet anlegen könnte, das einfach an diese Art von Konvertierung angepasst werden kann. Die folgenden Beispiele zeigen eine generische Lösung und wie sie im Fall von people.xml eingesetzt werden könnte.

Beispiel: generic-attr-to-csv.xslt

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:csv="http://www.ora.com/XSLTCookbook/namespaces/csv">
  <xsl:param name="delimiter" select=" ',' "/>
  <xsl:output method="text" />
  <xsl:strip-space elements="*"/>
  <xsl:template match="/">
    <xsl:for-each select="$columns">
      <xsl:value-of select="@name"/>
      <xsl:if test="position() != last()">
       <xsl:value-of select="$delimiter"/>
      </xsl:if>
    </xsl:for-each>
    <xsl:text>&#xa;</xsl:text>
    <xsl:apply-templates/>
  </xsl:template>
  <xsl:template match="/*/*">
    <xsl:variable name="row" select="."/>
    <xsl:for-each select="$columns">
      <xsl:apply-templates select="$row/@*[local-name(.)=current()/@attr]" mode="csv:map-value"/>
      <xsl:if test="position() != last()">
        <xsl:value-of select="$delimiter"/>
      </xsl:if>
    </xsl:for-each>
    <xsl:text>&#xa;</xsl:text>
  </xsl:template>
  <xsl:template match="@*" mode="map-value">
    <xsl:value-of select="."/>
  </xsl:template>
</xsl:stylesheet>

Beispiel: Verwendung der generischen Lösung, um people.xml zu verarbeiten.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:csv="http://www.ora.com/XSLTCookbook/namespaces/csv">
  <xsl:import href="generic-attr-to-csv.xslt"/>
  <!--Definiert die Zuordnung von Attributen auf Spalten -->
  <xsl:variable name="columns" select="document('')/*/csv:column"/>
  <csv:column name="Name" attr="name"/>
  <csv:column name="Alter" attr="age"/>
  <csv:column name="Geschlecht" attr="sex"/>
  <csv:column name="Raucher" attr="smoker"/>
  <!-- Verarbeitet eigene Attributzuordnungen -->
  <xsl:template match="@sex" mode="csv:map-value">
    <xsl:choose>
      <xsl:when test=".='m'">männlich</xsl:when>
      <xsl:when test=".='w'">weiblich</xsl:when>
      <xsl:otherwise>error</xsl:otherwise>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>

Diese Lösung ist an Tabellen ausgerichtet. Das Stylesheet generic-attr-to-csv.xslt verwendet eine Variable, die csv:column-Elemente enthält, die in der importierenden Tabellenkalkulation definiert sind. Die importierende Tabellenkalkulation muss nur die csv:column-Elemente in der Reihenfolge anordnen, in der die resultierenden Spalten in der Ausgabe auftauchen sollen. Die csv:column-Elemente definieren die Zuordnung zwischen einer benannten Spalte und einem Attributnamen im eingegebenen XML. Optional kann die importierende Tabellenkalkulation die Werte bestimmter Attribute übersetzen, indem sie ein Template angibt, das das angegebene Attribut mit Hilfe des Modus csv:map-value filtert. Hier verwenden Sie ein solches Template, um die abgekürzten @sex-Werte in people.xml zu übersetzen. Einige gebräuchliche Zuordnungen können in ein drittes Stylesheet gesetzt und ebenfalls importiert werden. Das Schöne an dieser Lösung ist, dass es für jemanden mit beschränkten XSLT-Kenntnissen einfach ist, eine neue CSV-Zuordnung zu definieren. Als weiteren Vorteil definiert das generische Stylesheet einen Top-Level-Parameter, mit dessen Hilfe man das vorgegebene Trennzeichen von einem Komma auf einen anderen Wert ändern kann.

Eine CVS-Datei aus einfachen, mit Elementen kodierten Daten erzeugen

In diesem Szenario haben Sie eine einfache XML-Datei mit Elementen, die auf Zeilen abgebildet werden, und Kindelementen, die auf Spalten abgebildet werden.

Dieses Problem ist mit dem vorherigen vergleichbar, allerdings haben Sie hier XML, das Elemente anstelle von Attributen benutzt, um die Spalten zu kodieren. Sie können nun ebenfalls eine generische Lösung angeben, wie in den folgenden Beispielen gezeigt.

Beispiel: Das People-Beispiel unter Verwendung von Elementen.

<people>
  <person>
    <name>Al Zehtooney</name>
    <age>33</age>
    <sex>m</sex>
    <smoker>nein</smoker>
  </person>
  <person>
    <name>Brad York</name>
    <age>38</age>
    <sex>m</sex>
    <smoker>ja</smoker>
  </person>
  <person>
    <name>Charles Xavier</name>
    <age>32</age>
    <sex>m</sex>
    <smoker>nein</smoker>
  </person>
  <person>
    <name>David Williams</name>
    <age>33</age>
    <sex>m</sex>
    <smoker>nein</smoker>
  </person>
  ...
</people>

Beispiel: generic-elem-to-csv.xslt

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:csv="http://www.ora.com/XSLTCookbook/namespaces/csv">
  <xsl:param name="delimiter" select=" ',' "/>
  <xsl:output method="text" />
  <xsl:strip-space elements="*"/>
  <xsl:template match="/">
    <xsl:for-each select="$columns">
      <xsl:value-of select="@name"/>
      <xsl:if test="position() != last()">
        <xsl:value-of select="$delimiter"/>
      </xsl:if>
    </xsl:for-each>
    <xsl:text>&#xa;</xsl:text>
    <xsl:apply-templates/>
  </xsl:template>
  <xsl:template match="/*/*">
    <xsl:variable name="row" select="."/>
    <xsl:for-each select="$columns">
      <xsl:apply-templates select="$row/*[local-name(.)=current()/@elem]" mode="csv:map-value"/>
      <xsl:if test="position() != last()">
        <xsl:value-of select="$delimiter"/>
      </xsl:if>
    </xsl:for-each>
    <xsl:text>&#xa;</xsl:text>
  </xsl:template>
  <xsl:template match="node()" mode="map-value">
    <xsl:value-of select="."/>
  </xsl:template>
</xsl:stylesheet>

Beispiel: people-elem-to-csv.xslt

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:csv="http://www.ora.com/XSLTCookbook/namespaces/csv">
  <xsl:import href="generic-elem-to-csv.xslt"/>
  <!--Definiert die Zuordnung von Elementen zu Spalten -->
  <xsl:variable name="columns" select="document('')/*/csv:column"/>
  <csv:column name="Name" elem="name"/>
  <csv:column name="Alter" elem="age"/>
  <csv:column name="Geschlecht" elem="sex"/>
  <csv:column name="Raucher" elem="smoker"/>
</xsl:stylesheet>

Beispiel: Ausgabe.

Name,Alter,Geschlecht,Raucher
Al Zehtooney,33,m,nein
Brad York,38,m,ja
Charles Xavier,32,m,nein
David Williams,33,m,nein
...

Komplexere Zuordnungen verarbeiten

In diesem Szenario haben Sie es mit einer willkürlichen Zuordnung sowohl von Attributen als auch von Elementen zu Zeilen und Spalten zu tun. Hier lässt sich die Dokumentenreihenfolge nicht so hübsch auf die Zeilen- oder Spaltenreihenfolge abbilden. Darüber hinaus könnte die Zuordnung in dem Sinn knapp ausfallen, dass viele leere Werte in den CSV-Daten generiert werden müssen.

Betrachten Sie beispielsweise das folgende XML, das eine Spesenabrechnung eines bald zu entlassenden Angestellten zeigt:

<ExpenseReport statementNum="123">
  <Employee>
    <Name>Salvatore Mangano</Name>
    <SSN>999-99-9999</SSN>
    <Dept>XSLT Hacking</Dept>
    <EmpNo>1</EmpNo>
    <Position>Koch</Position>
    <Manager>Großer Chef O'Reilly</Manager>
  </Employee>
  <PayPeriod>
    <From>1/1/02</From>
    <To>1/31/02</To>
  </PayPeriod>
  <Expenses>
    <Expense>
      <Date>12/20/01</Date>
      <Account>12345</Account>
      <Desc>Herumgehangen, anstatt zur Konferenz zu fahren.</Desc>
      <Lodging>500.00</Lodging>
      <Transport>50.00</Transport>
      <Fuel>0</Fuel>
      <Meals>300.00</Meals>
      <Phone>100</Phone>
      <Entertainment>1000.00</Entertainment>
      <Other>300.00</Other>
   </Expense>
   <Expense>
      <Date>12/20/01</Date>
      <Account>12345</Account>
      <Desc>Am Strand</Desc>
      <Lodging>500.00</Lodging>
      <Transport>50.00</Transport>
      <Fuel>0</Fuel>
      <Meals>200.00</Meals>
      <Phone>20</Phone>
      <Entertainment>300.00</Entertainment>
      <Other>100.00</Other>
   </Expense>
 </Expenses>
</ExpenseReport>

Stellen Sie sich nun vor, dass Sie dieses XML in eine Tabellenkalkulation importieren müssten. Wenn die entsprechenden Stile der Tabellenkalkulation angewandt wurden, sehen die Ergebnisse daher aus wie in der folgenden Abbildung dargestellt.

Tabellenblatt mit der Spesenabrechnung

Abbildung: Tabellenblatt mit der Spesenabrechnung.

Um die Daten korrekt in alle Zellen zu setzen, sodass die Zuweisung der Stile die einzig erforderliche Weiterverarbeitung ist, muss folgende durch Kommas getrennte Datei erzeugt werden:

,,,,,,,,,,,,Abrechnung Nr.,123,


,,,,,,,,,,,Spesenabrechnung,

 


,,,Angestellter,,,,,,,,,Zahlungszeitraum,


,,,Name,Salvatore Mangano,,Ang. #,1,,,,,Von,1/1/02,
,,,SSN,999-99-9999,,Position,Koch,
,,,Abteilung,XSLT Hacking,,,,,,,,Bis,1/31/02,
,,,Datum,Kostenstelle,Beschreibung,Unterkunft,Transport,Benzin,Essen,Telefon,Unterhaltung,
Sonstige,Gesamt,

,,,12/20/01,12345,Herumgehangen, anstatt zur Konferenz zu fahren.,500.00,50.00,0,300.

00,100,1000.00,300.00,

,,,12/20/01,12345,Am Strand,500.00,50.00,0,200.00,20,300.00,100.00,Zwischensumme Gesamt,
,,,Bestätigt,,Anmerkungen,,,,,,,Vorschüsse,
,,,,,,,,,,,,Gesamt,

Wie Sie sehen können, fehlt der Zuordnung von XML auf die abgetrennten Daten die Gleichförmigkeit, die in den vorherigen Beispielen für eine einfache Implementierung sorgte. Das soll nicht heißen, dass kein Stylesheet erzeugt werden kann, um die notwendige Zuordnung vorzunehmen. Falls Sie jedoch das Problem direkt angehen, wird wahrscheinlich ein nur für diesen Zweck verwendbares und komplexes Stylesheet herauskommen.

Wenn Sie sich mit komplexen Transformationen konfrontiert sehen, dann stellen Sie fest, ob das Problem vereinfacht werden könnte, indem das Quelldokument zuerst in eine Zwischenform transformiert und anschließend diese Zwischenform in das gewünschte Ergebnis überführt wird. Versuchen Sie, mit anderen Worten, komplexe Transformationsprobleme auf zwei oder mehr weniger komplizierte Probleme herunterzubrechen.

Beim weiteren Nachdenken über diese Zeilen werden Sie merken, dass das Problem der Zuordnung von XML zu der Tabellenkalkulation tatsächlich ein Problem der Zuweisung von XML-Inhalt an die Zellen in der Tabellenkalkulation ist. Sie können daher eine Zwischenform erfinden, die aus Zellelementen besteht. Beispielsweise würde ein Zellelement, das den Wert »foo« in Zelle A1 setzt, <cell col="A" row="1" value="foo"/> sein. Unser Ziel besteht darin, ein Stylesheet zu erzeugen, das jedes wichtige Element in der Quelle einem Zellelement zuordnet. Da Sie sich nun keine Gedanken mehr über die Anordnung machen müssen, ist die Zuordnung einfach:

<xsl:template match="ExpenseReport">
  <c:cell col="M" row="3" value="Abrechnung Nr."/>
  <c:cell col="N" row="3" value="{@statementNum}"/>
  <c:cell col="L" row="6" value="Spesenabrechnung"/>
  <xsl:apply-templates/>
  <xsl:variable name="offset" select="count(Expenses/Expense)+18"/>
  <c:cell col="M" row="{$offset}" value="Zwischensumme Gesamt"/>
  <c:cell col="D" row="{$offset + 1}" value="Bestätigt"/>
  <c:cell col="F" row="{$offset + 1}" value="Anmerkungen"/>
  <c:cell col="M" row="{$offset + 1}" value="Vorschüsse"/>
  <c:cell col="M" row="{$offset + 2}" value="Gesamt"/>
</xsl:template>
<xsl:template match="Employee">
  <c:cell col="D" row="10" value="Angestellter"/>
  <xsl:apply-templates/>
</xsl:template>
<xsl:template match="Employee/Name">
  <c:cell col="D" row="12" value="Name"/>
  <c:cell col="E" row="12" value="{.}"/>
</xsl:template>
<xsl:template match="Employee/SSN">
  <c:cell col="D" row="13" value="SSN"/>
  <c:cell col="E" row="13" value="{.}"/>
</xsl:template>
<xsl:template match="Employee/Dept">
  <c:cell col="D" row="14" value="Abteilung"/>
  <c:cell col="E" row="14" value="{.}"/>
</xsl:template>
<xsl:template match="Employee/EmpNo">
  <c:cell col="G" row="12" value="Ang. #"/>
  <c:cell col="H" row="12" value="{.}"/>
</xsl:template>
<xsl:template match="Employee/Position">
  <c:cell col="G" row="13" value="Position"/>
  <c:cell col="H" row="13" value="{.}"/>
</xsl:template>
<xsl:template match="Employee/Manager">
  <c:cell col="G" row="14" value="Manager"/>
  <c:cell col="H" row="14" value="{.}"/>
</xsl:template>
<xsl:template match="PayPeriod">
  <c:cell col="M" row="10" value="Zahlungszeitraum"/>
  <xsl:apply-templates/>
</xsl:template>
<xsl:template match="PayPeriod/From">
  <c:cell col="M" row="12" value="Von"/>
  <c:cell col="N" row="12" value="{.}"/>
</xsl:template>
<xsl:template match="PayPeriod/To">
  <c:cell col="M" row="14" value="Bis"/>
  <c:cell col="N" row="14" value="{.}"/>
</xsl:template>
<xsl:template match="Expenses">
  <c:cell col="D" row="16" value="Datum"/>
  <c:cell col="E" row="16" value="Kostenstelle"/>
  <c:cell col="F" row="16" value="Beschreibung"/>
  <c:cell col="G" row="16" value="Unterkunft"/>
  <c:cell col="H" row="16" value="Transport"/>
  <c:cell col="I" row="16" value="Benzin"/>
  <c:cell col="J" row="16" value="Essen"/>
  <c:cell col="K" row="16" value="Telefon"/>
  <c:cell col="L" row="16" value="Unterhaltung"/>
  <c:cell col="M" row="16" value="Sonstige"/>
  <c:cell col="N" row="16" value="Gesamt"/>
  <xsl:apply-templates/>
</xsl:template>
<xsl:template match="Expenses/Expense">
  <xsl:apply-templates>
   <xsl:with-param name="row" select="position( )+16"/>
  </xsl:apply-templates>
</xsl:template>
<xsl:template match="Expense/Date">
  <xsl:param name="row"/>
  <c:cell col="D" row="{$row}" value="{.}"/>
</xsl:template>
<xsl:template match="Expense/Account">
  <xsl:param name="row"/>
  <c:cell col="E" row="{$row}" value="{.}"/>
</xsl:template>
<xsl:template match="Expense/Desc">
  <xsl:param name="row"/>
  <c:cell col="F" row="{$row}" value="{.}"/>
</xsl:template>
<xsl:template match="Expense/Lodging">
  <xsl:param name="row"/>
  <c:cell col="G" row="{$row}" value="{.}"/>
</xsl:template>
<xsl:template match="Expense/Transport">
  <xsl:param name="row"/>
  <c:cell col="H" row="{$row}" value="{.}"/>
</xsl:template>
<xsl:template match="Expense/Fuel">
  <xsl:param name="row"/>
  <c:cell col="I" row="{$row}" value="{.}"/>
</xsl:template>
<xsl:template match="Expense/Meals">
  <xsl:param name="row"/>
  <c:cell col="J" row="{$row}" value="{.}"/>
</xsl:template>
<xsl:template match="Expense/Phone">
  <xsl:param name="row"/>
  <c:cell col="K" row="{$row}" value="{.}"/>
</xsl:template>
<xsl:template match="Expense/Entertainment">
  <xsl:param name="row"/>
  <c:cell col="L" row="{$row}" value="{.}"/>
</xsl:template>
<xsl:template match="Expense/Other">
  <xsl:param name="row"/>
  <c:cell col="M" row="{$row}" value="{.}"/>
</xsl:template>

Ein großer Vorteil bei der Verwendung eines Attributs zum Kodieren eines Zellwertes besteht darin, dass Sie auf diese Weise Attributwert-Templates benutzen können, wodurch Sie ein sehr präzises Übersetzungsschema erhalten. In diesem Stylesheet begegnen uns zwei Arten der Zuordnung. Die erste Art ist absolut. Beispielsweise wollen Sie, dass der Name des Angestellten auf Zelle E12 abgebildet wird. Die zweite Art ist relativ. Sie wollen, dass jedes Spesenobjekt relativ zu Zeile 16 zugeordnet wird, basierend auf seiner Position im Quelldokument.

Wenn Sie dieses Stylesheet auf das Quelldokument anwenden, erhalten Sie folgende Ausgabe:

<c:cells xmlns:c="http://www.ora.com/XSLTCookbook/namespaces/cells" >
  <c:cell col="M" row="3" value="Abrechnung Nr."/>
  <c:cell col="N" row="3" value="123"/>
  <c:cell col="L" row="6" value="Spesenabrechnung"/>
  <c:cell col="D" row="10" value="Angestellter"/>
  <c:cell col="D" row="12" value="Name"/>
  <c:cell col="E" row="12" value="Salvatore Mangano"/>
  <c:cell col="D" row="13" value="SSN"/>
  <c:cell col="E" row="13" value="999-99-9999"/>
  <c:cell col="D" row="14" value="Abteilung"/>
  <c:cell col="E" row="14" value="XSLT Hacking"/>
  <c:cell col="G" row="12" value="Ang. #"/>
  <c:cell col="H" row="12" value="1"/>
  <c:cell col="G" row="13" value="Position"/>
  <c:cell col="H" row="13" value="Koch"/>
  <c:cell col="G" row="14" value="Manager"/>
  <c:cell col="H" row="14" value="Großer Chef O'Reilly"/>
  <c:cell col="M" row="10" value="Zahlungszeitraum"/>
  <c:cell col="M" row="12" value="Von"/>
  <c:cell col="N" row="12" value="1/1/02"/>
  <c:cell col="M" row="14" value="Bis"/>
  <c:cell col="N" row="14" value="1/31/02"/>
  <c:cell col="D" row="16" value="Datum"/>
  <c:cell col="E" row="16" value="Kostenstelle"/>
  <c:cell col="F" row="16" value="Beschreibung"/>
  <c:cell col="G" row="16" value="Unterkunft"/>
  <c:cell col="H" row="16" value="Transport"/>
  <c:cell col="I" row="16" value="Benzin"/>
  <c:cell col="J" row="16" value="Essen"/>
  <c:cell col="K" row="16" value="Telefon"/>
  <c:cell col="L" row="16" value="Unterhaltung"/>
  <c:cell col="M" row="16" value="Sonstige"/>
  <c:cell col="N" row="16" value="Gesamt"/>
  <c:cell col="D" row="18" value="12/20/01"/>
  <c:cell col="E" row="18" value="12345"/>
  <c:cell col="F" row="18" value="Herumgehangen, anstatt zur Konferenz zu fahren."/>
  <c:cell col="G" row="18" value="500.00"/>
  <c:cell col="H" row="18" value="50.00"/>
  <c:cell col="I" row="18" value="0"/>
  <c:cell col="J" row="18" value="300.00"/>
  <c:cell col="K" row="18" value="100"/>
  <c:cell col="L" row="18" value="1000.00"/>
  <c:cell col="M" row="18" value="300.00"/>
  <c:cell col="D" row="20" value="12/20/01"/>
  <c:cell col="E" row="20" value="12345"/>
  <c:cell col="F" row="20" value="Am Strand"/>
  <c:cell col="G" row="20" value="500.00"/>
  <c:cell col="H" row="20" value="50.00"/>
  <c:cell col="I" row="20" value="0"/>
  <c:cell col="J" row="20" value="200.00"/>
  <c:cell col="K" row="20" value="20"/>
  <c:cell col="L" row="20" value="300.00"/>
  <c:cell col="M" row="20" value="100.00"/>
  <c:cell col="M" row="20" value="Zwischensumme Gesamt"/>
  <c:cell col="D" row="21" value="Bestätigt"/>
  <c:cell col="F" row="21" value="Anmerkungen"/>
  <c:cell col="M" row="21" value="Vorschüsse"/>
  <c:cell col="M" row="22" value="Gesamt"/>
</c:cells>

Das ist natürlich nicht das endgültige Ergebnis, das Sie haben wollen. Allerdings ist unschwer zu erkennen, dass durch die Sortierung dieser Zellen zuerst nach @row und dann nach @col die Zuordnung der Zellen in eine durch Kommas getrennte Form einfach ist. Falls Sie die node-set-Erweiterung von EXSLT verwenden wollen, können Sie Ihr Ergebnis sogar in einem einzigen Durchlauf erzielen. Beachten Sie außerdem, dass die Zuordnung von Zellen auf durch Kommas getrennte Werte vollständig generisch ist, sodass Sie sie in Zukunft auch für andere Zuordnungen von XML auf durch Kommas getrennte Werte wiederverwenden können (siehe die folgenden beiden Beispiele).

Beispiel: Generisches cells-to-comma-delimited.xslt.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:c="http://www.ora.com/XSLTCookbook/namespaces/cells" xmlns:exsl="http://exslt.org/common" extension-element-prefixes="exsl">
  <xsl:output method="text"/>
  <!-- Wird verwendet, um Spaltenbuchstaben auf Zahlen abzubilden -->
  <xsl:variable name="columns" select=" '_ABCDEFGHIJKLMNOPQRSTUVWXYZ' "/>
  <xsl:template match="/">
    <!-- Erfasst Zellen in einer Variablen -->
    <xsl:variable name="cells">
      <xsl:apply-templates/>
    </xsl:variable>
    <!-- Sortiert in Zeile-Spalte-Reihenfolge -->
    <xsl:variable name="cells-sorted">
      <xsl:for-each select="exsl:node-set($cells)/c:cell">
        <xsl:sort select="@row" data-type="number"/>
        <xsl:sort select="@col" data-type="text"/>
        <xsl:copy-of select="."/>
      </xsl:for-each>
    </xsl:variable>
    <xsl:apply-templates select="exsl:node-set($cells-sorted)/c:cell"/>
  </xsl:template>
  <xsl:template match="c:cell">
    <xsl:choose>
      <!-- Erkennen eines Zeilenwechsels -->
      <xsl:when test="preceding-sibling::c:cell[1]/@row != @row">
        <!-- Berechnen, wie viele Zeilen eventuell übersprungen werden müssten -->
        <xsl:variable name="skip-rows">
          <xsl:choose>
            <xsl:when test="preceding-sibling::c:cell[1]/@row">
              <xsl:value-of select="@row - preceding-sibling::c:cell[1]/@row"/>
            </xsl:when>
            <xsl:otherwise>
              <xsl:value-of select="@row - 1"/>
            </xsl:otherwise>
          </xsl:choose>
        </xsl:variable>
        <xsl:call-template name="skip-rows">
          <xsl:with-param name="skip" select="$skip-rows"/>
        </xsl:call-template>
        <xsl:variable name="current-col" select="string-length(substring-before($columns,@col))"/>
        <xsl:call-template name="skip-cols">
          <xsl:with-param name="skip" select="$current-col - 1"/>
        </xsl:call-template>
        <xsl:value-of select="@value"/>,<xsl:text/>
      </xsl:when>
      <xsl:otherwise>
        <!-- Berechnen, wie viele Spalten eventuell übersprungen werden müssten -->
        <xsl:variable name="skip-cols">
          <xsl:variable name="current-col" select="string-length(substring-before($columns,@col))"/>
          <xsl:choose>
            <xsl:when test="preceding-sibling::c:cell[1]/@col">
              <xsl:variable name="prev-col" select="string-length(substring-before($columns,@col))"/>
              <xsl:choose>
                <xsl:when test="preceding-sibling::c:cell[1]/@col))"/>
                <xsl:value-of select="$current-col - $prev-col - 1"/>
              </xsl:choose>
              <xsl:otherwise>
                <xsl:value-of select="$current-col - 1"/>
              </xsl:otherwise>
            </xsl:when>
          </xsl:choose>
        </xsl:variable>
        <xsl:call-template name="skip-cols">
          <xsl:with-param name="skip" select="$skip-cols"/>
        </xsl:call-template>
        <!--Ausgabe des Wertes der Zelle und eines Kommas -->
        <xsl:value-of select="@value"/>,<xsl:text/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
  <!-- Wird verwendet, um leere Zeilen für unzusammenhängende Reihen einzufügen -->
  <xsl:template name="skip-rows">
    <xsl:param name="skip"/>
    <xsl:choose>
      <xsl:when test="$skip &gt; 0">
        <xsl:text>&#xa;</xsl:text>
        <xsl:call-template name="skip-rows">
          <xsl:with-param name="skip" select="$skip - 1"/>
        </xsl:call-template>
      </xsl:when>
      <xsl:otherwise/>
    </xsl:choose>
  </xsl:template>
  <!-- Wird verwendet, um zusätzliche Kommas für unzusammenhängende Spalten einzufügen -->
  <xsl:template name="skip-cols">
    <xsl:param name="skip"/>
    <xsl:choose>
      <xsl:when test="$skip &gt; 0">
        <xsl:text>,</xsl:text>
        <xsl:call-template name="skip-cols">
          <xsl:with-param name="skip" select="$skip - 1"/>
        </xsl:call-template>
      </xsl:when>
      <xsl:otherwise/>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>

Beispiel: Anwendungsspezifisches expense-to-delimited.xslt.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:c="http://www.ora.com/XSLTCookbook/namespaces/cells" xmlns:exsl="http://exslt.org/common" extension-element-prefixes="exsl">
  <xsl:include href="cells-to-comma-delimited.xslt"/>
  <xsl:template match="ExpenseReport">
    <c:cell col="M" row="3" value="Abrechnung Nr."/>
    <c:cell col="N" row="3" value="{@statementNum}"/>
    <c:cell col="L" row="6" value="Spesenabrechnung"/>
    <xsl:apply-templates/>
    <xsl:variable name="offset" select="count(Expenses/Expense)+18"/>
    <c:cell col="M" row="{$offset}" value="Zwischensumme Gesamt"/>
    <c:cell col="D" row="{$offset + 1}" value="Bestätigt"/>
    <c:cell col="F" row="{$offset + 1}" value="Anmerkungen"/>
    <c:cell col="M" row="{$offset + 1}" value="Vorschüsse"/>
    <c:cell col="M" row="{$offset + 2}" value="Gesamt"/>
  </xsl:template>
  <xsl:template match="Employee">
    <c:cell col="D" row="10" value="Angestellter"/>
    <xsl:apply-templates/>
  </xsl:template>
  <xsl:template match="Employee/Name">
    <c:cell col="D" row="12" value="Name"/>
    <c:cell col="E" row="12" value="{.}"/>
  </xsl:template>
  <!-- ... -->
  <!-- Rest weggelassen; ist identisch mit dem oben gezeigten Original-Stylesheet -->
  <!-- ... -->
</xsl:stylesheet>

Das wiederverwendbare cells-to-comma-delimited.xslt erfasst die Zellen, die von dem anwendungsspezifischen Stylesheet erfasst wurden, in einer Variablen und sortiert sie. Anschließend transformiert es diese Zellen in eine durch Kommas getrennte Ausgabe. Dies wird erledigt, indem jede Zelle relativ zu ihrem Vorgänger in der sortierten Anordnung betrachtet wird. Liegt der Vorgänger in einer anderen Zeile, dann müssen ein oder mehrere Newlines ausgegeben werden. Befindet sich andererseits der Vorgänger nicht in einer benachbarten Spalte, dann müssen ein oder mehrere zusätzliche Kommas ausgegeben werden. Sie müssen auch den Fall abfangen, bei dem die erste Zeile oder Spalte innerhalb einer Zeile nicht die erste Zeile oder Spalte in der Tabelle ist. Sobald diese Details behandelt wurden, müssen Sie nur noch den Wert der Zelle, gefolgt von einem Komma, ausgeben.

XSLT 2.0

Eine nette Erweiterung von xsl:value-of, mit deren Hilfe sich abgetrennter Text leichter erzeugen lässt, ist das Attribut separator. Wenn xsl:value-of eine Sequenz übergeben wird, serialisiert es diese. Wurde auch das Attribut separator angegeben, fügt es nach jedem Objekt mit Ausnahme des letzten ein Trennzeichen ein. Das Trennzeichen kann ein Literal oder ein Attributwert-Template sein. Im nächsten Beispiel setze ich diese Eigenschaft ein, um den Code zu vereinfachen, der die Spaltennamen ausgibt. Ich nutze außerdem XPath 2.0, um die Funktionalität zu verallgemeinern, sodass das gleiche zugrunde liegende Stylesheet mit XML verwendet werden kann, das Elemente oder Attribute einsetzt. Außerdem verwende ich anstelle von eingebettetem Stylesheet-XML literale Sequenzen, um die CSV-Zuordnungen zu kodieren. Dies dient dazu, die größere Flexibilität von XSLT 2.0 zu verdeutlichen, und nicht dazu, eine Technik über die andere zu stellen (siehe die beiden folgenden Beispiele).

Beispiel: Generisches cells-to-comma-delimited.xslt.

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:fn="http://www.w3.org/2004/10/xpath-functions" xmlns:csv="http://www.ora.com/XSLTCookbook/namespaces/csv">
  <xsl:param name="delimiter" select=" ',' "/>
  <!--Diese sollten im importierenden Stylesheet außer Kraft gesetzt werden -->
  <xsl:variable name="columns" select="()" as="xs:string*"/>
  <xsl:variable name="nodeNames" select="$columns" as="xs:string*"/>
  <xsl:output method="text" />
  <xsl:strip-space elements="*"/>
  <xsl:template match="/">
    <!-- Hier benutzen wir die neue Fähigkeit des value-of -->
    <xsl:value-of select="$columns" separator="{$delimiter}" />
    <xsl:text>&#xa;</xsl:text>
    <xsl:apply-templates mode="csv:map-row"/>
  </xsl:template>
  <xsl:template match="/*/*" mode="csv:map-row" name="csv:map-row">
    <xsl:param name="elemOrAttr" select=" 'elem' " as="xs:string"/>
    <xsl:variable name="row" select="." as="node()"/>
    <xsl:for-each select="$nodeNames">
      <xsl:apply-templates select="if ($elemOrAttr eq 'elem') then $row/*[local-name(.) eq current()] else $row/@*[local-name(.) eq current()]" mode="csv:map-value"/>
      <xsl:value-of select="if (position() ne last()) then $delimiter else ()"/>
    </xsl:for-each>
    <xsl:text>&#xa;</xsl:text>
  </xsl:template>
  <xsl:template match="node()" mode="csv:map-value">
    <xsl:value-of select="."/>
  </xsl:template>
</xsl:stylesheet>

Beispiel: Anwendungsspezifisches expense-to-delimited.xslt.

<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:csv="http://www.ora.com/XSLTCookbook/namespaces/csv">
  <xsl:import href="toCSV.xslt"/>
  <!--Definiert die Zuordnung von Knoten auf Spalten -->
  <xsl:variable name="columns" select="'Name', 'Alter', 'Geschlecht', 'Raucher'" as="xs:string*"/>
  <xsl:variable name="nodeNames" select="'name', 'age', 'sex', 'smoker'" as="xs:string*"/>
  <!-- Schaltet die vorgegebene Verarbeitung von Elementen auf Attribute um -->
  <xsl:template match="/*/*" mode="csv:map-row">
    <xsl:call-template name="csv:map-row">
      <xsl:with-param name="elemOrAttr" select=" 'attr' "/>
    </xsl:call-template>
  </xsl:template>
  <!-- Verarbeitet eigene Attributzuordnungen -->
  <xsl:template match="@sex" mode="csv:map-value">
    <xsl:choose>
      <xsl:when test=".='m'">männlich</xsl:when>
      <xsl:when test=".='w'">weiblich</xsl:when>
      <xsl:otherwise>error</xsl:otherwise>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>

Diskussion

Die meisten Transformationen von XML auf abgetrennte Daten, die Ihnen wahrscheinlich begegnen werden, sind für jemanden mit einer gewissen Erfahrung in XSLT recht einfach. Der Wert der gezeigten Beispiele liegt darin, dass sich an ihnen demonstrieren lässt, dass Probleme in zwei Teile aufgeteilt werden können: in einen wiederverwendbaren Teil, der XSLT-Kompetenz erfordert, und in einen anwendungsspezifischen Teil, der nicht so viel XSLT-Kenntnis verlangt, sobald seine Konventionen verstanden wurden.

Der wahre Wert dieser Technik besteht darin, dass sie es Leuten, die weniger bewandert in XSLT sind, erlaubt, nützliche Arbeit zu verrichten. Nehmen Sie beispielsweise an, Sie müssen eine große XML-Datenbasis in durch Kommas getrennte Daten umwandeln; natürlich soll das am besten schon gestern erledigt werden. Jemandem zu zeigen, wie er diese generischen Lösungen wiederverwenden kann, ist viel einfacher, als ihm so viel XSLT beizubringen, dass er eigene Skripten schreiben kann.

  

zum Seitenanfang

<< zurück vor >>

 

 

 

Tipp der data2type-Redaktion:
Zum Thema XSLT bieten wir auch folgende Schulungen zur Vertiefung und professionellen Fortbildung an:

Copyright © 2006 O'Reilly Verlag GmbH & Co. KG
Für Ihren privaten Gebrauch dürfen Sie die Online-Version ausdrucken.
Ansonsten unterliegt dieses Kapitel aus dem Buch "XSLT Kochbuch" denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

O'Reilly Verlag GmbH & Co. KG, Balthasarstraße 81, 50670 Köln, kommentar(at)oreilly.de