XML в CSV за допомогою XSLT


75

У мене є такий документ XML:

<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>
...

І я хотів би отримати це з результату перетворення (XSLT):

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

Хтось знає XSLT для досягнення цього? Я використовую .net на випадок, якщо це важливо.


.NET має значення лише в тому випадку, якщо ви використовуєте клас XslTransform, який підтримує лише xslt 1.0. Це обмеження? Якщо це так, його слід повторно позначити як xslt-1.0 .
Райан Гейтс

Тут також дається хороша відповідь на подібне запитання, якщо ви використовуєте Linux askubuntu.com/questions/174143/…
Леонард

Існує інструмент xml2csv . Може, це корисно і у вашому випадку?
koppor

xml2csv у мене спрацював чудово. Я виявив, що документація порушена, але знайшов хвилину, щоб задокументувати обхід, який приніс
Х'ю Еско

Відповіді:


47

Знайшов тут таблицю стилів перетворення XML (посилання на машину зворотного зв'язку, сам сайт - німецькою)

Додана тут таблиця стилів може бути корисною:

<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>

Можливо, ви хочете видалити лапки всередині xsl: if теги, тому він не поміщає ваші значення в лапки, залежно від того, де ви хочете використовувати файл CSV.


5
Будьте обережні, якщо в вихідних даних є кома, це не захищено. Можливо, ви захочете додати тест із вмістом () та втечу з перекладом ().
bortzmeyer

2
Я не думаю, що це обробляє подвійні лапки в даних. Щоб уникнути подвійних лапок, ви повинні замінити його двома подвійними лапками.
Сарел Бота

1
Зазвичай потрібно вкладати значення в лапки, лише якщо воно містить щось із наступного: роздільник (' ,'), лапка (' "'), новий рядок ( &#xD;). Якщо потрібно цитування, будь-які внутрішні лапки спочатку потрібно подвоїти (' ""').
mousio

2
Правильним новим рядком на unix є &#10;(\ n). &#xD;є шістнадцятковою \ r
igo

1
@ BotMaster3000: дякую, замінено на зворотну машину посиланням
schnaader

57

Ось версія із налаштовуваними параметрами, яку ви можете встановити програмно:

<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>

1
Мені подобається обов’язкове цитування. Принаймні, під час імпорту в Excel, він бере на себе випадок, коли в вихідних даних є $ delim.
bortzmeyer

Що нам потрібно зробити, якщо ми хочемо також включити імена стовпців ??
Омер Халід

1
@omer Існує кілька способів зробити це, залежно від вашого XML. Найкраще, якщо ви задасте нове запитання, оскільки розділ коментарів не є гарним місцем для обговорення подібних речей, і оскільки в цій темі це не було частиною питання, тому я не буду редагувати відповідь.
Томалак

19

Це xsl:stylesheetможе використовувати вказаний список заголовків стовпців і забезпечить правильне впорядкування рядків.

<?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>

2
Це приємно, але це не спрацювало б. replace()є функцією XPath 2.0. У XSLT 1.0 вам доведеться використовувати шаблон рекурсивної заміни рядка.
Томалак

1
Працював у мене з xsltproc / libxslt - це було досить добре. Дякую, що вказали на вимоги.
ioquatix

@ hd1, я все ще використовую цей сценарій у виробництві, тому, можливо, ви робите щось не так?
ioquatix

@MrSamuel цілком можливо, я, зрештою, найбільший дебіл у SO
hd1

2
Взагалі не використовував xsl, і вирішив використовувати SAX
hd1

2

Ця CsvEscapeфункція XSLT 1.0 і збігає стовпці значень ,, "і новий рядок , як RFC 4180 або Excel. Він використовує той факт, що ви можете рекурсивно викликати шаблони XSLT:

  • Шаблон EscapeQuotesзамінює всі подвійні лапки на 2 подвійні лапки, рекурсивно від початку рядка.
  • Шаблон CsvEscapeперевіряє, чи містить текст кому або подвійну лапку, і якщо це оточує весь рядок парою подвійних лапок і викликає EscapeQuotesрядок.

Приклад використання: 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>
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.