简体   繁体   中英

Convert XML to CSV using XSLT - dynamic columns

I have to convert a XML file to a CSV file. The input XML file is something like that:

<Person>
    <Name>John</Name>
    <FamilyMembers>
        <FamilyMember>
            <Name>Lisa</Name>
            <Type>Sister</Type>
        </FamilyMember>
        <FamilyMember>
            <Name>Tom</Name>
            <Type>Brother</Type>
        </FamilyMember>
    </FamilyMembers>
</Person>
<Person>
    <Name>Daniel</Name>
    <FamilyMembers>
        <FamilyMember>
            <Name>Peter</Name>
            <Type>Father</Type>
        </FamilyMember>
    </FamilyMembers>
</Person>

The final CSV file should look like the following:

Name;Sister;Brother;Father
John;Lisa;Tom
Daniel;;;Peter

What I basically want is one column for every "Type" node with a different content. There is no limitation of "Type".

EDIT: My actual XSLT parse it to a CSV which looks like that:

Name;Name;Type
John;Lisa;Sister
John;Tom;Brother
Daniel;Peter;Father 

Have anyone any idea how to solve my problem?

André

Here is an XSLT1.0 solution (Thanks Martin!) that makes use of an xsl:key which is usually the most efficient way of solving problems. Essentially you are trying to group by Type , so to get the distinct family member types you could define a key like so

<xsl:key name="Type" match="Type" use="." />

Then for your header rows, to actually get the distinct types, you iterate over all types, but only select the records that first occur in the key for their given value

<xsl:apply-templates 
   select="//Type[generate-id() = generate-id(key('Type', .)[1])]" 
   mode="header" />

(The mode of header is because the Type records will be matched in a separate place for the family members in a moment, and so you need to distinguish between matching templates)

Next, you would select each Person record, and for each such record you would select the distinct types again, but this time passing in the current Person record as a parameter so you can extract the relevant family member

<xsl:apply-templates 
   select="//Type[generate-id() = generate-id(key('Type', .)[1])]" 
   mode="family">
   <xsl:with-param name="Person" select="." />
 </xsl:apply-templates>

And in the matching template for this (with mode of family) you could then output the relevant family member of the type

Here is the full XSLT

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   <xsl:output method="xml" indent="yes"/>
   <xsl:key name="Type" match="Type" use="." />

   <xsl:template match="/*">
      <xsl:text>Name</xsl:text>
      <xsl:apply-templates select="//Type[generate-id() = generate-id(key('Type', .)[1])]" mode="header" />
      <xsl:text>&#10;</xsl:text>
      <xsl:apply-templates select="Person" />
   </xsl:template>

   <xsl:template match="Person">
      <xsl:value-of select="Name" />
      <xsl:apply-templates select="//Type[generate-id() = generate-id(key('Type', .)[1])]" mode="family">
         <xsl:with-param name="Person" select="." />
      </xsl:apply-templates>
      <xsl:text>&#10;</xsl:text>
   </xsl:template>

   <xsl:template match="Type" mode="header">
      <xsl:text>;</xsl:text>
      <xsl:value-of select="." />
   </xsl:template>

   <xsl:template match="Type" mode="family">
      <xsl:param name="Person" />
      <xsl:text>;</xsl:text>
      <xsl:value-of select="$Person/FamilyMembers/FamilyMember[Type=current()]/Name" />
   </xsl:template>
</xsl:stylesheet>

When applied to your XML (assuming a single root element), the following is output

Name;Sister;Brother;Father
John;Lisa;Tom;
Daniel;;;Peter

This does assume you can't have more than one brother, or sister, etc, per Person.

The stylesheet

<?xml version='1.0'?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:output method="text"/>

<xsl:template match="/">
    <xsl:text>Name;Sister;Brother;Father&#xd;</xsl:text>
    <xsl:for-each select="Persons/Person">
        <xsl:variable name="name" select="Name"/>
        <xsl:variable name="others">
            <xsl:value-of select="FamilyMembers/FamilyMember[Type/text()='Sister']/Name/text()"/>
            <xsl:text>;</xsl:text>
            <xsl:value-of select="FamilyMembers/FamilyMember[Type/text()='Brother']/Name/text()"/>
            <xsl:text>;</xsl:text>
            <xsl:value-of select="FamilyMembers/FamilyMember[Type/text()='Father']/Name/text()"/>
            <xsl:text>&#xd;</xsl:text>          
        </xsl:variable>
        <xsl:value-of select="concat($name,';',$others)"/>
    </xsl:for-each> 
</xsl:template>

</xsl:stylesheet>

When applied to the XML(modified to make well-formed):

<Persons>
    <Person>
        <Name>John</Name>
        <FamilyMembers>
            <FamilyMember>
                <Name>Lisa</Name>
                <Type>Sister</Type>
            </FamilyMember>
            <FamilyMember>
                <Name>Tom</Name>
                <Type>Brother</Type>
            </FamilyMember>
        </FamilyMembers>
    </Person>
    <Person>
        <Name>Daniel</Name>
        <FamilyMembers>
            <FamilyMember>
                <Name>Peter</Name>
                <Type>Father</Type>
            </FamilyMember>
        </FamilyMembers>
    </Person>
</Persons>

Results

Name;Sister;Brother;Father
John;Lisa;Tom;
Daniel;;;Peter

Hope this helps.

I had a go with XSLT 2.0:

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

<xsl:param name="sep" as="xs:string" select="';'"/>

<xsl:key name="k1" match="FamilyMember" use="Type"/>

<xsl:output method="text"/>

<xsl:variable name="cols" as="xs:string*" select="('Name', distinct-values(//Person/FamilyMembers/FamilyMember/Type))"/>

<xsl:template match="/">
  <xsl:value-of select="$cols" separator="{$sep}"/>
  <xsl:text>&#10;</xsl:text>
  <xsl:apply-templates select="//Person"/>
</xsl:template>

<xsl:template match="Person">
  <xsl:value-of select="Name"/>
  <xsl:variable name="cells" as="xs:string*" select="
    for $col in $cols[position() gt 1] return (key('k1', $col, current())/Name, '')[1]"/>
  <xsl:sequence select="if (not(empty($cells))) then concat($sep, string-join($cells, $sep)) else ()"/>  
  <xsl:text>&#10;</xsl:text>
</xsl:template>

</xsl:stylesheet>

Transforms

<Persons>
<Person>
    <Name>John</Name>
    <FamilyMembers>
        <FamilyMember>
            <Name>Lisa</Name>
            <Type>Sister</Type>
        </FamilyMember>
        <FamilyMember>
            <Name>Tom</Name>
            <Type>Brother</Type>
        </FamilyMember>
    </FamilyMembers>
</Person>
<Person>
    <Name>Daniel</Name>
    <FamilyMembers>
        <FamilyMember>
            <Name>Peter</Name>
            <Type>Father</Type>
        </FamilyMember>
    </FamilyMembers>
</Person>
</Persons>

into

Name;Sister;Brother;Father
John;Lisa;Tom;
Daniel;;;Peter

I might later try to convert that into an XSLT 1.0 solution but I guess if Tim C is already trying to solve it in XSLT 1.0 he will be faster.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM