# XSLT nach CSV, Tags liegen aber ungeordnet vor



## sad3 (13. Mai 2013)

Hallo,
ich habe XML-Dateien, die folgendermaßen aufgebaut sind:


```
<company id="0">
<data name="a" section="1">text</data>
<data name="a" section="2">text</data>
<data name="b" section="1">text</data>
<data name="b" section="3">text</data>
...
</company>
<company id="1">
...
</company>
...
```

Die Attribute "name" und "section" können beliebige Strings enthalten. Die Data-Tags sind allerdings nicht geordnet (Tags mit den gleichen Attributwerten kommen bei verschiedenen companies an unterschiedlichen Stellen) und es können auch Tags fehlen (z.B. hat eine Company ein '<data name="a" section="1">' -Tag, eine andere aber nicht.)

Das Problem ist: ich möchte das ganze nun in eine CSV-Datei konvertieren (pro Company eine Zeile, in jeder Zelle der Inhalt eines Data-Tags), dabei sollten aber logischerweise in einer Spalte nur die Inhalte von Data-Tags mit gleicher name/section Kombination sein.
So wie ich es momentan habe, werden die data-Inhalte in der Reihenfolge, in der sie im company-Tag vorkommen, in die CSV-Datei geschrieben. Das bringt mir natürlich nichts.

Ein Versuch von mir war, die data-Tags nach den Attributen zu sortieren, aber das hilft auch nichts, wenn Tags fehlen (s.o.).

Nun habe ich keinerlei Ansatzpunkt für dieses Problem.

Kann mir bitte jemand helfen?

Gruß,
Sad3

PS: aufrufende Technologie ist Java.


----------



## Martin Honnen (14. Mai 2013)

Erkläre mal genauer, wie das XML aussehen kann und welches Format du haben willst. Sind die Werte des "name"-Attributes immer die Buchstaben "a", "b", "c", ... und die Werte des "section"-Attributes immer positive, ganze Zahlen 1,  2, 3, ...? Wenn für eine "company" ein "data" mit "section" als z.b. 10 auftaucht, bedeutet das dann, dass für alle Zeilen die Spalten 1 ... 10 generiert werden müssen? Dann könnte man einfach das Maximum der "section"-Attribute bestimmen.

Und wenn du Java benutzt, um XSLT aufzurufen, benutzt du dann Saxon 9 und XSLT 2.0? Oder nur XSLT 1.0?


----------



## sad3 (14. Mai 2013)

Danke schonmal für die Antwort.

Wie schon gesagt, die Attribute "name" und "section" können beliebige Strings enthalten. (In der Praxis gibt es ca. 900 Kombinationen.) Man muss also nicht beim Auftauchen einer 10 auch die Spalten 1-9 generieren.

Der Parser ist für XSLT 2.0, von Saxon habe ich keine Ahnung, kann man das irgendwo nachgucken?


----------



## sad3 (14. Mai 2013)

Das ist eine Beispiel-XML:

```
<company id="0">
<data name="a" section="q">text1</data>
<data name="a" section="w">text2</data>
<data name="b" section="r">text3</data>
<data name="b" section="e">text4</data>
<data name="a" section="e">text5</data>
</company>

<company id="1">
<data name="b" section="r">text6</data>
<data name="a" section="q">text7</data>
<data name="b" section="e">text8</data>
<data name="a" section="e">text9</data>
<data name="b" section="s">text0</data>
</company>
```

So wird es momentan ausgegeben:

```
text1,text2,text3,text4,text5
text6,text7,text8,text9,text10
```

So soll es ausgegeben werden:

```
text1,text2,text3,text4,text5,
text7,,text6,text8,text9,text0
```


Habe mal nachgefragt, Saxon 8.


----------



## Martin Honnen (14. Mai 2013)

Saxon 8 ist veraltet und existiert in zahlreichen Unterversionen, die verschiedene Stationen auf dem Weg zum XSLT 2.0 Standard implementiert haben. Die aktuelle Version von Saxon ist 9.5.

Mit XSLT 2.0 könnte man das eventuell so lösen:


```
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  xmlns:mf="http://example.org/mf"
  exclude-result-prefixes="xs mf">

<xsl:param name="lf" as="xs:string" select="'
'"/>
<xsl:param name="cs" as="xs:string" select="','"/>
<xsl:param name="rs" as="xs:string" select="'|'"/>

<xsl:output method="text"/>

<xsl:key name="k1" match="data" use="concat(@name, $rs, @section)"/>

<xsl:variable name="main-input" select="/"/>

<xsl:variable name="cols" as="element(data)+">
  <xsl:for-each-group select="/root/company/data" group-by="concat(@name, $rs, @section)">
    <!--
    <xsl:sort select="@name"/>
    <xsl:sort select="@section"/>
    -->
    <data key="{current-grouping-key()}"/>
  </xsl:for-each-group>
</xsl:variable>

<xsl:template match="root">
  <xsl:value-of select="$cols/@key" separator="{$cs}"/>
  <xsl:value-of select="$lf"/>
  <xsl:apply-templates select="company"/>
</xsl:template>

<xsl:template match="company">
  <xsl:value-of select="for $col in $cols
                        return
                          (if (key('k1', $col/@key, current())) then key('k1', $col/@key, current()) else '')" separator="{$cs}"/>
  <xsl:value-of select="$lf"/>
</xsl:template>

</xsl:stylesheet>
```

Das macht dann mit Saxon 9 aus der Eingabe

```
<root>
<company id="0">
<data name="a" section="q">text1</data>
<data name="a" section="w">text2</data>
<data name="b" section="r">text3</data>
<data name="b" section="e">text4</data>
<data name="a" section="e">text5</data>
</company>
 
<company id="1">
<data name="b" section="r">text6</data>
<data name="a" section="q">text7</data>
<data name="b" section="e">text8</data>
<data name="a" section="e">text9</data>
<data name="b" section="s">text0</data>
</company>
</root>
```
das Resultat

```
a|q,a|w,b|r,b|e,a|e,b|s
text1,text2,text3,text4,text5,
text7,,text6,text8,text9,text0
```

Die erste Zeile mit den Kombinationen muss man nicht ausgeben. Das Stylesheet hat drei Parameter, der Parameter "rs" sollte so gewählt werden, dass es ein Zeichen ist, das in data/@name und data/@section nicht vorkommt, damit es beim Gruppieren als Trennzeichen funktionieren kann.


----------



## sad3 (14. Mai 2013)

Hallo,
einen ganz großes Dankeschön schonmal an dich, ich hatte nicht gedacht, dass ich das nur mit XLST noch hinbekomme.
Ich versuche das jetzt bei mir zu übernehmen, wenn ich Probleme habe, melde ich mich nochmal.
Viele Grüße,
Sad3


----------



## sad3 (16. Mai 2013)

Ich muss jetzt nochmal nachfragen, ich komme einfach nicht weiter (ich bin halt auch Anfänger).

Hier ist ein inhaltlich gekürztes, aber von der Form her vollständiges XML-Dokument:


```
<calibrationDataFile>
<account xmlns="http://#####/#####/#####/calibrationdata" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" companyID="1" schemaCountry="DE" schemaAccountingType="IFRS_ISF" schemaVersion="2" accountID="51" identNumber="DE000911" status="RATING_DESIRED" certificationDate="2012-08-15T11:35:30.890000" referenceDate="2011-03-31">
   <column name="generalJ" scenario="JahrGeneral">2011-03-31</column>
   <column name="note11_q1" scenario="Vorjahr1Notes">2</column>
   <column name="note11_q1" scenario="JahrNotes">1</column>
</account>
</calibrationDataFile>
```

Die XSLT sieht so aus:


```
<xsl:stylesheet version="2.0"
	xmlns:bcx="http://#####/#####/#####/calibrationdata"
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:xs="http://www.w3.org/2001/XMLSchema">
 
<xsl:param name="lf" as="xs:string" select="'
'"/>
<xsl:param name="cs" as="xs:string" select="';'"/>
<xsl:param name="rs" as="xs:string" select="'|'"/>
 
<xsl:output method="text"/>
 
<xsl:key name="k1" match="column" use="concat(@name, $rs, @scenario)"/>
 
<xsl:variable name="main-input" select="/"/>
 
<xsl:variable name="cols" as="element(column)+">
  <xsl:for-each-group select="/calibrationDataFile/account/column" group-by="concat(@name, $rs, @scenario)">
    <!--
    <xsl:sort select="@name"/>
    <xsl:sort select="@scenario"/>
    -->
    <column key="{current-grouping-key()}"/>
  </xsl:for-each-group>
</xsl:variable>
 
<xsl:template match="calibrationDataFile">
  <xsl:value-of select="$cols/@key" separator="{$cs}"/>
  <xsl:value-of select="$lf"/>
  <xsl:apply-templates select="account"/>
</xsl:template>
 
<xsl:template match="account">
  <xsl:value-of select="for $col in $cols
                        return
                          (if (key('k1', $col/@key, current())) then key('k1', $col/@key, current()) else '')" separator="{$cs}"/>
  <xsl:value-of select="$lf"/>
</xsl:template>
 
</xsl:stylesheet>
```

Dabei bekomme ich immer den Fehler:


> Error on line 16 of file:///C:/temp/KalibrierungsDatenExport/test.xslt:
> XTTE0570: An empty sequence is not allowed as the value of variable $cols
> ; SystemID: file:///C:/temp/KalibrierungsDatenExport/test.xslt; Line#: 16; Column#: -1
> net.sf.saxon.trans.DynamicError: An empty sequence is not allowed as the value of variable $cols



Ich denke, ich weiß, was das bedeutet, aber ich bekomme es einfach nicht hin, die XSLT so zu ändern, dass die column-Tags erkannt werden. Was geändert werden muss ist doch bestimmt das select in Zeile 18, aber ich habe einfach zu wenig Ahnung von XSLT und Namespaces. Ich habe schon alles probiert, ich bitte um Hilfe!


----------



## Martin Honnen (16. Mai 2013)

Ich habe das XSLT angepasst:

```
<xsl:stylesheet version="2.0"
    xmlns:bcx="http://#####/#####/#####/calibrationdata"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:xs="http://www.w3.org/2001/XMLSchema">
 
<xsl:param name="lf" as="xs:string" select="'
'"/>
<xsl:param name="cs" as="xs:string" select="';'"/>
<xsl:param name="rs" as="xs:string" select="'|'"/>
 
<xsl:output method="text"/>
 
<xsl:key name="k1" match="bcx:column" use="concat(@name, $rs, @scenario)"/>
 
<xsl:variable name="main-input" select="/"/>
 
<xsl:variable name="cols" as="element(column)+">
  <xsl:for-each-group select="/calibrationDataFile/bcx:account/bcx:column" group-by="concat(@name, $rs, @scenario)">
    <!--
    <xsl:sort select="@name"/>
    <xsl:sort select="@scenario"/>
    -->
    <column key="{current-grouping-key()}"/>
  </xsl:for-each-group>
</xsl:variable>
 
<xsl:template match="calibrationDataFile">
  <xsl:value-of select="$cols/@key" separator="{$cs}"/>
  <xsl:value-of select="$lf"/>
  <xsl:apply-templates select="bcx:account/bcx:column"/>
</xsl:template>
 
<xsl:template match="bcx:column">
  <xsl:value-of select="for $col in $cols
                        return
                          (if (key('k1', $col/@key, current())) then key('k1', $col/@key, current()) else '')" separator="{$cs}"/>
  <xsl:value-of select="$lf"/>
</xsl:template>
 
</xsl:stylesheet>
```
Mit dem Beispiel

```
<calibrationDataFile>
<account xmlns="http://#####/#####/#####/calibrationdata" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" companyID="1" schemaCountry="DE" schemaAccountingType="IFRS_ISF" schemaVersion="2" accountID="51" identNumber="DE000911" status="RATING_DESIRED" certificationDate="2012-08-15T11:35:30.890000" referenceDate="2011-03-31">
   <column name="generalJ" scenario="JahrGeneral">2011-03-31</column>
   <column name="note11_q1" scenario="Vorjahr1Notes">2</column>
   <column name="note11_q1" scenario="JahrNotes">1</column>
</account>
</calibrationDataFile>
```
gibt Saxon 9.5 dann

```
generalJ|JahrGeneral;note11_q1|Vorjahr1Notes;note11_q1|JahrNotes
2011-03-31;;
;2;
;;1
```
aus.


----------



## Martin Honnen (16. Mai 2013)

Eine Anmerkung noch, das Forum gibt numerische Zeichenreferenzen in Codebeispielen nicht korrekt aus, der Parameter "lf" wird als "& # 1 0;" (nur ohne die Leerzeichen zwischen den Zeichen) definiert.


----------



## sad3 (17. Mai 2013)

Hallo,
vielen Dank nochmal für deine Hilfe, es läuft jetzt alles super.

Ich habe noch ein Problem, vielleicht kannst du mir auch dabei helfen. Der Inhalt der column-Tags kann Zeilenumbrüche enthalten, die aber logischerweise für eine csv-Datei nicht hilfreich sind. Wie kann man die entfernen?
Strip-space ist nur für Whitespace-Nodes zuständig, und normalize-Space nur für Whitespaces generell.


----------



## Martin Honnen (17. Mai 2013)

Eventuell reicht "translate", sonst gibt es mit XSLT 2.0 auch "replace"::

```
<xsl:value-of select="for $col in $cols
                        return
                          (if (key('k1', $col/@key, current())) then replace(key('k1', $col/@key, current()), '\n', '') else '')" separator="{$cs}"/>
```


----------



## sad3 (24. Mai 2013)

Hallo,
vielen Dank nochmal für deine Hilfe, das Projekt ist jetzt abgeschlossen und alles läuft wunderbar.
Viele Grüße,
sad3


----------

