XML zu CSV mit XSLT

74

Ich habe das folgende XML-Dokument:

<projects>
  <project>
   <name>Shockwave</name> 
   <language>Ruby</language> 
   <owner>Brian May</owner> 
   <state>New</state> 
   <startDate>31/10/2008 0:00:00</startDate> 
  </project>
  <project>
   <name>Other</name> 
   <language>Erlang</language> 
   <owner>Takashi Miike</owner> 
   <state> Canceled </state> 
   <startDate>07/11/2008 0:00:00</startDate> 
  </project>
...

Und ich möchte dies aus dem Ergebnis der Transformation (XSLT) erhalten:

Shockwave,Ruby,Brian May,New,31/10/2008 0:00:00
Other,Erlang,Takashi Miike,Cancelled,07/11/2008 0:00:00

Kennt jemand das XSLT, um dies zu erreichen? Ich benutze .net für den Fall, dass es darauf ankommt.

Pablo Fernandez
quelle
.NET ist nur wichtig, wenn Sie die XslTransform-Klasse verwenden , die nur xslt 1.0 unterstützt. Ist das eine Einschränkung? In diesem Fall sollte es erneut mit xslt-1.0 markiert werden .
Ryan Gates
Eine gute Antwort wird auch hier auf eine ähnliche Frage gegeben, wenn Sie Linux verwenden askubuntu.com/questions/174143/…
Leonard Saers
Es gibt das Tool xml2csv . Vielleicht ist das auch in Ihrem Fall nützlich?
Koppor

Antworten:

47

Hier wurde ein XML-Transformations-Stylesheet gefunden (Wayback-Maschinenlink, Site selbst ist auf Deutsch)

Das hier hinzugefügte Stylesheet könnte hilfreich sein:

<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="iso-8859-1"/>

<xsl:strip-space elements="*" />

<xsl:template match="/*/child::*">
<xsl:for-each select="child::*">
<xsl:if test="position() != last()">"<xsl:value-of select="normalize-space(.)"/>",    </xsl:if>
<xsl:if test="position()  = last()">"<xsl:value-of select="normalize-space(.)"/>"<xsl:text>&#xD;</xsl:text>
</xsl:if>
</xsl:for-each>
</xsl:template>

</xsl:stylesheet>

Vielleicht möchten Sie die Anführungszeichen in xsl entfernen: if-Tags, damit Ihre Werte nicht in Anführungszeichen gesetzt werden, je nachdem, wo Sie die CSV-Datei verwenden möchten.

Schnaader
quelle
5
Seien Sie vorsichtig, wenn die Originaldaten ein Komma enthalten, wird es nicht maskiert. Möglicherweise möchten Sie einen Test mit includes () und ein Escape mit translate () hinzufügen.
Bortzmeyer
2
Ich denke nicht, dass dies ein doppeltes Anführungszeichen in den Daten behandelt. Um einem doppelten Anführungszeichen zu entgehen, müssen Sie es durch zwei doppelte Anführungszeichen ersetzen.
Sarel Botha
1
Normalerweise muss ein Wert nur dann in Anführungszeichen gesetzt werden, wenn er eines der folgenden Elemente enthält: das Trennzeichen (' ,'), das Anführungszeichen (' "'), eine neue Zeile ( &#xD;). Wenn Anführungszeichen erforderlich sind, müssen alle inneren Anführungszeichen zuerst verdoppelt werden (' ""').
Mausio
2
Die richtige neue Zeile unter Unix ist &#10;(\ n). &#xD;ist hexadezimal \ r
igo
1
@ BotMaster3000: Danke, ersetzt durch einen Wayback-Maschinenlink
Schnaader
57

Hier ist eine Version mit konfigurierbaren Parametern, die Sie programmgesteuert einstellen können:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text" encoding="utf-8" />

  <xsl:param name="delim" select="','" />
  <xsl:param name="quote" select="'&quot;'" />
  <xsl:param name="break" select="'&#xA;'" />

  <xsl:template match="/">
    <xsl:apply-templates select="projects/project" />
  </xsl:template>

  <xsl:template match="project">
    <xsl:apply-templates />
    <xsl:if test="following-sibling::*">
      <xsl:value-of select="$break" />
    </xsl:if>
  </xsl:template>

  <xsl:template match="*">
    <!-- remove normalize-space() if you want keep white-space at it is --> 
    <xsl:value-of select="concat($quote, normalize-space(), $quote)" />
    <xsl:if test="following-sibling::*">
      <xsl:value-of select="$delim" />
    </xsl:if>
  </xsl:template>

  <xsl:template match="text()" />
</xsl:stylesheet>
Tomalak
quelle
1
Ich mag das obligatorische Zitat. Zumindest beim Importieren in Excel wird der Fall behoben, in dem die Originaldaten ein $ delim enthalten.
Bortzmeyer
Was müssen wir tun, wenn wir auch die Spaltennamen einschließen möchten?
Omer Khalid
1
@omer Abhängig von Ihrem XML gibt es verschiedene Möglichkeiten, dies zu tun. Es ist am besten, wenn Sie eine neue Frage stellen, da der Kommentarbereich kein guter Ort ist, um solche Dinge zu diskutieren, und weil er in diesem Thread nicht Teil der Frage war, sodass ich die Antwort nicht bearbeiten werde.
Tomalak
19

Dies xsl:stylesheetkann eine angegebene Liste von Spaltenüberschriften verwenden und stellt sicher, dass die Zeilen korrekt sortiert werden.

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:csv="csv:csv">
    <xsl:output method="text" encoding="utf-8" />
    <xsl:strip-space elements="*" />

    <xsl:variable name="delimiter" select="','" />

    <csv:columns>
        <column>name</column>
        <column>sublease</column>
        <column>addressBookID</column>
        <column>boundAmount</column>
        <column>rentalAmount</column>
        <column>rentalPeriod</column>
        <column>rentalBillingCycle</column>
        <column>tenureIncome</column>
        <column>tenureBalance</column>
        <column>totalIncome</column>
        <column>balance</column>
        <column>available</column>
    </csv:columns>

    <xsl:template match="/property-manager/properties">
        <!-- Output the CSV header -->
        <xsl:for-each select="document('')/*/csv:columns/*">
                <xsl:value-of select="."/>
                <xsl:if test="position() != last()">
                    <xsl:value-of select="$delimiter"/>
                </xsl:if>
        </xsl:for-each>
        <xsl:text>&#xa;</xsl:text>

        <!-- Output rows for each matched property -->
        <xsl:apply-templates select="property" />
    </xsl:template>

    <xsl:template match="property">
        <xsl:variable name="property" select="." />

        <!-- Loop through the columns in order -->
        <xsl:for-each select="document('')/*/csv:columns/*">
            <!-- Extract the column name and value -->
            <xsl:variable name="column" select="." />
            <xsl:variable name="value" select="$property/*[name() = $column]" />

            <!-- Quote the value if required -->
            <xsl:choose>
                <xsl:when test="contains($value, '&quot;')">
                    <xsl:variable name="x" select="replace($value, '&quot;',  '&quot;&quot;')"/>
                    <xsl:value-of select="concat('&quot;', $x, '&quot;')"/>
                </xsl:when>
                <xsl:when test="contains($value, $delimiter)">
                    <xsl:value-of select="concat('&quot;', $value, '&quot;')"/>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:value-of select="$value"/>
                </xsl:otherwise>
            </xsl:choose>

            <!-- Add the delimiter unless we are the last expression -->
            <xsl:if test="position() != last()">
                <xsl:value-of select="$delimiter"/>
            </xsl:if>
        </xsl:for-each>

        <!-- Add a newline at the end of the record -->
        <xsl:text>&#xa;</xsl:text>
    </xsl:template>

</xsl:stylesheet>
ioquatix
quelle
2
Das ist schön, aber es würde nicht funktionieren. replace()ist eine XPath 2.0-Funktion. In XSLT 1.0 müssten Sie eine rekursive Zeichenfolgenersetzungsvorlage verwenden.
Tomalak
1
Arbeitete für mich mit xsltproc / libxslt - es war gut genug. Vielen Dank, dass Sie auf die Anforderungen hingewiesen haben.
Ioquatix
@ hd1, ich verwende dieses Skript immer noch in der Produktion, also machst du wahrscheinlich etwas falsch?
Ioquatix
@ MrSamuel ziemlich wahrscheinlich, ich bin schließlich der größte Idiot auf SO
hd1
2
Ich habe xsl überhaupt nicht verwendet und mich für SAX entschieden
hd1
2

Diese CsvEscapeFunktion ist XSLT 1.0 und entweicht Spaltenwerte ,, "und neue Zeilen wie RFC 4180 oder Excel. Es nutzt die Tatsache, dass Sie XSLT-Vorlagen rekursiv aufrufen können:

  • Die Vorlage EscapeQuotesersetzt alle doppelten Anführungszeichen rekursiv ab dem Anfang der Zeichenfolge durch zwei doppelte Anführungszeichen.
  • Die Vorlage CsvEscapeprüft, ob der Text ein Komma oder ein doppeltes Anführungszeichen enthält, und umgibt in diesem Fall die gesamte Zeichenfolge mit zwei doppelten Anführungszeichen und fordert EscapeQuotesdie Zeichenfolge auf.

Anwendungsbeispiel: xsltproc xmltocsv.xslt file.xml > file.csv

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text" encoding="UTF-8"/>

  <xsl:template name="EscapeQuotes">
    <xsl:param name="value"/>
    <xsl:choose>
      <xsl:when test="contains($value,'&quot;')">
    <xsl:value-of select="substring-before($value,'&quot;')"/>
    <xsl:text>&quot;&quot;</xsl:text>
    <xsl:call-template name="EscapeQuotes">
      <xsl:with-param name="value" select="substring-after($value,'&quot;')"/>
    </xsl:call-template>
      </xsl:when>
      <xsl:otherwise>
    <xsl:value-of select="$value"/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>

  <xsl:template name="CsvEscape">
    <xsl:param name="value"/>
    <xsl:choose>
    <xsl:when test="contains($value,',')">
      <xsl:text>&quot;</xsl:text>
      <xsl:call-template name="EscapeQuotes">
    <xsl:with-param name="value" select="$value"/>
      </xsl:call-template>
      <xsl:text>&quot;</xsl:text>
    </xsl:when>
    <xsl:when test="contains($value,'&#xA;')">
      <xsl:text>&quot;</xsl:text>
      <xsl:call-template name="EscapeQuotes">
    <xsl:with-param name="value" select="$value"/>
      </xsl:call-template>
      <xsl:text>&quot;</xsl:text>
    </xsl:when>
    <xsl:when test="contains($value,'&quot;')">
      <xsl:text>&quot;</xsl:text>
      <xsl:call-template name="EscapeQuotes">
    <xsl:with-param name="value" select="$value"/>
      </xsl:call-template>
      <xsl:text>&quot;</xsl:text>
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="$value"/>
    </xsl:otherwise>
    </xsl:choose>
  </xsl:template>

  <xsl:template match="/">
    <xsl:text>project,name,language,owner,state,startDate</xsl:text>
    <xsl:text>&#xA;</xsl:text>
    <xsl:for-each select="projects/project">
      <xsl:call-template name="CsvEscape"><xsl:with-param name="value" select="normalize-space(name)"/></xsl:call-template>
      <xsl:text>,</xsl:text>
      <xsl:call-template name="CsvEscape"><xsl:with-param name="value" select="normalize-space(language)"/></xsl:call-template>
      <xsl:text>,</xsl:text>
      <xsl:call-template name="CsvEscape"><xsl:with-param name="value" select="normalize-space(owner)"/></xsl:call-template>
      <xsl:text>,</xsl:text>
      <xsl:call-template name="CsvEscape"><xsl:with-param name="value" select="normalize-space(state)"/></xsl:call-template>
      <xsl:text>,</xsl:text>
      <xsl:call-template name="CsvEscape"><xsl:with-param name="value" select="normalize-space(startDate)"/></xsl:call-template>
      <xsl:text>&#xA;</xsl:text>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>
jmiserez
quelle