通用XML到CSV转换

时间:2013-09-23 03:45:03

标签: xml xslt csv export-to-csv

我正在尝试将动态XML转换为CSV。我搜索了各种选项来实现这一目标,但没有找到合适的答案。

XML的结构是动态的 - 它可以是产品数据,地理数据或任何此类事物。所以,我无法使用预定义的XSL或castor转换。

标签名称应构成CSV的标头。 例如:

<Ctry>
  <datarow>
     <CtryName>Ctry1</CtryName>
     <CtryID>12361</CtryID>
    <State>
      <datarow>
         <StateName>State1</StateName>
         <StateID>12361</StateID>
        <City>
           <datarow>
              <CityName>City1</CityName>
               <CityID>12361</CityID>
           </datarow>
        </City>
      </datarow>
      <datarow>
         <StateName>State2</StateName>
         <StateID>12361</StateID>
      </datarow>
      </State>
  </datarow>
</Ctry>

CSV应如下所示:

Header: CtryName   CtryId     StateName  StateId     CityName   CityID
Row1:   Ctry1       12361     State1     12361       City1      12361
Row2:   Ctry1       12361     State2     12361  

你能否推荐一些适合用来解决这个问题的东西?

1 个答案:

答案 0 :(得分:6)

下面是一个说明执行此类转换的通用样式表执行情况的脚本。样式表唯一的假设是元素<datarows>。给出的结构意味着根据请求的结果使用子元素:

数据:

  T:\ftemp>type xml2csv.xml 
  <Ctry>
    <datarow>
       <CtryName>Ctry1</CtryName>
       <CtryID>12361</CtryID>
      <State>
        <datarow>
           <StateName>State1</StateName>
           <StateID>12361</StateID>
          <City>
             <datarow>
                <CityName>City1</CityName>
                 <CityID>12361</CityID>
             </datarow>
          </City>
        </datarow>
        <datarow>
           <StateName>State2</StateName>
           <StateID>12361</StateID>
        </datarow>
        </State>
    </datarow>
  </Ctry>

执行:

  T:\ftemp>call xslt2 xml2csv.xml xml2csv.xsl 
  CtryName,CtryID,StateName,StateID,CityName,CityID
  Ctry1,12361,State1,12361,City1,12361
  Ctry1,12361,State2,12361

样式表:

  T:\ftemp>type xml2csv.xsl 
  <?xml version="1.0" encoding="US-ASCII"?>
  <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                  version="2.0">

  <xsl:output method="text"/>

  <xsl:variable name="fields" 
                select="distinct-values(//datarow/*[not(*)]/name(.))"/>

  <xsl:template match="/">
    <!--header row-->
    <xsl:value-of select="$fields" separator=","/>

    <!--body-->
    <xsl:apply-templates select="*"/>

    <!--final line terminator-->
    <xsl:text>&#xa;</xsl:text>
  </xsl:template>

  <!--elements only process elements, not text-->
  <xsl:template match="*">
    <xsl:apply-templates select="*"/>
  </xsl:template>

  <!--these elements are CSV fields-->
  <xsl:template match="datarow/*[not(*)]">
    <!--replicate ancestors if necessary-->
    <xsl:if test="position()=1 and ../preceding-sibling::datarow">
      <xsl:for-each select="ancestor::datarow[position()>1]/*[not(*)]">
        <xsl:call-template name="doThisField"/>
      </xsl:for-each>
    </xsl:if>
    <xsl:call-template name="doThisField"/>
  </xsl:template>

  <!--put out a field ending the previous field and escaping content-->
  <xsl:template name="doThisField">
    <xsl:choose>
      <xsl:when test="name(.)=$fields[1]">
        <!--previous line terminator-->
        <xsl:text>&#xa;</xsl:text>
      </xsl:when>
      <xsl:otherwise>
        <!--previous field terminator-->
        <xsl:text>,</xsl:text>
      </xsl:otherwise>
    </xsl:choose>
    <!--field value escaped per RFC4180-->
    <xsl:choose>
      <xsl:when test="contains(.,'&#x22;') or 
                      contains(.,',') or
                      contains(.,'&#xa;')">
        <xsl:text>"</xsl:text>
        <xsl:value-of select="replace(.,'&#x22;','&#x22;&#x22;')"/>
        <xsl:text>"</xsl:text>
      </xsl:when>
      <xsl:otherwise><xsl:value-of select="."/></xsl:otherwise>
    </xsl:choose>
  </xsl:template>

  </xsl:stylesheet>

请注意,上面的代码按RFC4180转义了各个字段。

我的个人资料有一个指向我的网站的链接,您可以在其中找到免费XML资源的目录,其中包括一个XSLT样式表,用于将RFC4180 CSV文件转换为XML文件。

根据原始海报的要求,这是答案的XSLT 1.0解决方案:

t:\ftemp>type xml2csv1.xsl 
<?xml version="1.0" encoding="US-ASCII"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                version="1.0">

<xsl:output method="text"/>

<xsl:variable name="firstFieldName" 
              select="name((//datarow/*[not(*)])[1])"/>

<xsl:key name="names" match="datarow/*[not(*)]" use="name(.)"/>

<xsl:template match="/">
  <!--header row-->
  <xsl:for-each select="//datarow/*[not(*)]
                        [generate-id(.)=
                         generate-id(key('names',name(.))[1])]">
    <xsl:if test="position()>1">,</xsl:if>
    <xsl:value-of select="name(.)"/>
  </xsl:for-each>

  <!--body-->
  <xsl:apply-templates select="*"/>

  <!--final line terminator-->
  <xsl:text>&#xa;</xsl:text>
</xsl:template>

<!--elements only process elements, not text-->
<xsl:template match="*">
  <xsl:apply-templates select="*"/>
</xsl:template>

<!--these elements are CSV fields-->
<xsl:template match="datarow/*[not(*)]">
  <!--replicate ancestors if necessary-->
  <xsl:if test="position()=1 and ../preceding-sibling::datarow">
    <xsl:for-each select="ancestor::datarow[position()>1]/*[not(*)]">
      <xsl:call-template name="doThisField"/>
    </xsl:for-each>
  </xsl:if>
  <xsl:call-template name="doThisField"/>
</xsl:template>

<!--put out a field ending the previous field and escaping content-->
<xsl:template name="doThisField">
  <xsl:choose>
    <xsl:when test="name(.)=$firstFieldName">
      <!--previous line terminator-->
      <xsl:text>&#xa;</xsl:text>
    </xsl:when>
    <xsl:otherwise>
      <!--previous field terminator-->
      <xsl:text>,</xsl:text>
    </xsl:otherwise>
  </xsl:choose>
  <!--field value escaped per RFC4180-->
  <xsl:choose>
    <xsl:when test="contains(.,'&#x22;') or 
                    contains(.,',') or
                    contains(.,'&#xa;')">
      <xsl:text>"</xsl:text>
      <xsl:call-template name="escapeQuote"/>
      <xsl:text>"</xsl:text>
    </xsl:when>
    <xsl:otherwise><xsl:value-of select="."/></xsl:otherwise>
  </xsl:choose>
</xsl:template>

<!--escape a double quote in the current node value with two double quotes-->
<xsl:template name="escapeQuote">
  <xsl:param name="rest" select="."/>
  <xsl:choose>
    <xsl:when test="contains($rest,'&#x22;')">
      <xsl:value-of select="substring-before($rest,'&#x22;')"/>
      <xsl:text>""</xsl:text>
      <xsl:call-template name="escapeQuote">
        <xsl:with-param name="rest" select="substring-after($rest,'&#x22;')"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="$rest"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

</xsl:stylesheet>