我正在尝试将动态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
你能否推荐一些适合用来解决这个问题的东西?
答案 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>
</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>
</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(.,'"') or
contains(.,',') or
contains(.,'
')">
<xsl:text>"</xsl:text>
<xsl:value-of select="replace(.,'"','""')"/>
<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>
</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>
</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(.,'"') or
contains(.,',') or
contains(.,'
')">
<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,'"')">
<xsl:value-of select="substring-before($rest,'"')"/>
<xsl:text>""</xsl:text>
<xsl:call-template name="escapeQuote">
<xsl:with-param name="rest" select="substring-after($rest,'"')"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$rest"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>