使用XSLT样式表将XML文档转换为逗号分隔(CSV)文件

4
我需要帮助将一个xml文档使用xslt样式表转换为CSV文件。我尝试使用以下xsl,但似乎无法正确转换。我想要生成包含列标题和数据的逗号分隔文件。我的主要问题是如何去除最后一项后面的逗号,并插入回车符,使得每组数据都显示在单独的一行上。我一直在使用XML Notepad。
  <xsl:template match="/">
        <xsl:element name="table">
              <xsl:apply-templates select="/*/*[1]" mode="header" />
              <xsl:apply-templates select="/*/*" mode="row" />
        </xsl:element>
  </xsl:template>

  <xsl:template match="*" mode="header">
        <xsl:element name="tr">
              <xsl:apply-templates select="./*" mode="column" />
        </xsl:element>
  </xsl:template>

  <xsl:template match="*" mode="row">
        <xsl:element name="tr">
              <xsl:apply-templates select="./*" mode="node" />
        </xsl:element>
  </xsl:template>

  <xsl:template match="*" mode="column">
        <xsl:element name="th">
              <xsl:value-of select="translate(name(.),'qwertyuiopasdfghjklzxcvbnm_','QWERTYUIOPASDFGHJKLZXCVBNM ')" />
        </xsl:element>,
  </xsl:template>

  <xsl:template match="*" mode="node">
        <xsl:element name="td">
              <xsl:value-of select="." />
        </xsl:element>,
  </xsl:template> 


我检查了上面给出的链接,但那并没有帮助到我。谢谢。 - Brad H_KC
2
请提供一个XML文档样例以及您想要从中获得的确切结果。 - Dimitre Novatchev
还请注意,您正在创建结果树中的元素。我无法想象这如何被解释为 CSV。 - user357812
2个回答

5
我使用这个简单的XSLT将XML转换为CSV;它假设根节点的所有子节点都是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"/>

  <xsl:template match="/">
    <xsl:for-each select="*/*[1]/*">
      <xsl:value-of select="name()" />
      <xsl:if test="not(position() = last())">,</xsl:if>
    </xsl:for-each>
    <xsl:text>&#10;</xsl:text>
    <xsl:apply-templates select="*/*" mode="row"/>
  </xsl:template>

  <xsl:template match="*" mode="row">
    <xsl:apply-templates select="*" mode="data" />
    <xsl:text>&#10;</xsl:text>
  </xsl:template>

  <xsl:template match="*" mode="data">
    <xsl:choose>
      <xsl:when test="contains(text(),',')">
        <xsl:text>&quot;</xsl:text>
        <xsl:call-template name="doublequotes">
          <xsl:with-param name="text" select="text()" />
        </xsl:call-template>
        <xsl:text>&quot;</xsl:text>
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="." />
      </xsl:otherwise>
    </xsl:choose>
    <xsl:if test="position() != last()">,</xsl:if>
  </xsl:template>

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

所以这是一个XML格式的文件:
<csv>
  <row>
    <field1>foo</field1>
    <field2>ba"r</field2>
  </row>
  <row>
    <field1>foo,2</field1>
    <field2>bar,"2</field2>
  </row>
</csv>

转换为:

field1,field2
foo,ba"r
"foo,2","bar,""2"

我不确定这是否有帮助,这取决于您的XML布局。

编辑:这是一个更详细的转换:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:key name="field" match="/*/*/*" use="name()" />
  <xsl:output method="text"/>

  <xsl:template match="/">
    <xsl:for-each select="*/*/*[generate-id() = generate-id(key('field',name())[1])]">
      <xsl:value-of select="name()" />
      <xsl:if test="position() != last()">,</xsl:if>
    </xsl:for-each>
    <xsl:text>&#10;</xsl:text>
    <xsl:apply-templates select="*/*" mode="row"/>
  </xsl:template>

  <xsl:template match="*" mode="row">
    <xsl:variable name="row" select="*" />
    <xsl:for-each select="/*/*/*[generate-id() = generate-id(key('field',name())[1])]">
      <xsl:variable name="name" select="name()" />
      <xsl:apply-templates select="$row[name()=$name]" mode="data" />
      <xsl:if test="position() != last()">,</xsl:if>
    </xsl:for-each>
    <xsl:text>&#10;</xsl:text>
  </xsl:template>

  <xsl:template match="*" mode="data">
    <xsl:choose>
      <xsl:when test="contains(text(),',')">
        <xsl:text>&quot;</xsl:text>
        <xsl:call-template name="doublequotes">
          <xsl:with-param name="text" select="text()" />
        </xsl:call-template>
        <xsl:text>&quot;</xsl:text>
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="." />
      </xsl:otherwise>
    </xsl:choose>
    <xsl:if test="position() != last()">,</xsl:if>
  </xsl:template>

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

这个功能会在你的CSV中创建一列,包含所有“行”中存在的标签名称,并在每行中填充相应的列。

你写道:“它假设根节点的所有子节点都是CSV中的行”。我认为你做了更多的假设。以你的示例输入文档为例:在你的样式表中,你假设第一行中的字段是所有可能存在的字段,并且进一步假设各个行的字段顺序相同且没有遗漏。 - user357812
当然,这是一个相当简单的例程。我最初编写它是为了将xhtml中的<table>标签转换为CSV,除非您有colspan属性,否则它会非常有效。 XML显然比CSV格式更灵活,因此如果不符合这些假设,它就无法很好地转换。当然,您可以首先使用XSD验证它是否符合要求。 - Flynn1179
思考一下,适应转换以正确执行这个并不太难;我编辑了一个更彻底的XSLT,它不会做出这些假设;但它仍然假定您的XML只有两个级别(在根节点下面),并且每个一级深度的元素都被视为一行。它有一个限制,就是当“行”具有相同的标记名称时,就像<table>标记一样具有讽刺意味。我的原始转换实际上将<th>标记的内容作为列名而不是节点名称;我上面的第一个转换是一个更通用的版本。 - Flynn1179

1
我使用了@flynn1179的“更彻底的转换”xslt,但发现随着行数的增加,所需时间异常长。(这是使用Oracle dbms_xmlgen xslt transform)。
我发现,因为我知道我的两个数据级别叫什么(在这种情况下是ROWSET和ROW),所以我能够优化xslt并获得显著的性能提升。
我的修改版本(专门针对Oracle的dbms_xmlgen包)如下。它还包括我在互联网上找到的其他CSV转换的部分。
<xsl:stylesheet  version="1.0"  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output method="text" encoding="utf-8"/>
 <xsl:strip-space elements="*"/>

 <!-- some variables for unprintable charcaters -->
<xsl:variable name="CRLF">  
  <xsl:text>&#13;&#10;</xsl:text>  
</xsl:variable>  
<xsl:variable name="CR">  
  <xsl:text>&#13;</xsl:text>  
</xsl:variable>  
<xsl:variable name="LF">  
  <xsl:text>&#10;</xsl:text>  
</xsl:variable>  
<xsl:variable name="apos">'</xsl:variable>

<xsl:template match="/ROWSET">
<xsl:for-each select="ROW[1]/*">
  <xsl:value-of select="local-name()" />
  <xsl:if test="position() != last()">,</xsl:if>
</xsl:for-each>
<xsl:value-of select="$LF"/>
<xsl:apply-templates />
</xsl:template>

<xsl:template match="ROW">
  <xsl:apply-templates />
  <xsl:value-of select="$LF"/>
</xsl:template>

<xsl:template match="ROW/*">
  <xsl:choose>
  <xsl:when test="contains( text(), ',' ) or   
                  contains( text(), $apos ) or  
                  contains( text(), $CRLF ) or  
                  contains( text(), $CR ) or  
                  contains( text(), $LF )">
    <!-- Field contains a comma, apostrophe and/or a linefeed, so quote --> 
    <xsl:text>&quot;</xsl:text>
    <xsl:call-template name="doublequotes">
      <xsl:with-param name="text" select="text()" />
    </xsl:call-template>
    <xsl:text>&quot;</xsl:text>
  </xsl:when>
  <xsl:otherwise>
    <xsl:value-of select="." />
  </xsl:otherwise>
</xsl:choose>
<xsl:if test="position() != last()">,</xsl:if>
</xsl:template>

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接