XSLT添加元素层次结构标记

时间:2019-04-27 23:28:07

标签: xml xslt

我必须通过根据条件将XML元素替换为element start标记来将XML转换为另一个XML。输入可能具有多个级别元素,而级别元素可能具有另一个级别作为子级或同级。

下面是我的输入

<?xml version="1.0" encoding="UTF-8"?>
<Data>
  <Collection>
    <Primary>
      <PrimaryName>1238</PrimaryName>
      <Content>1</Content>
      <Modifier>81</Modifier>
    </Primary>
  </Collection>
  <CModifier>55</CModifier>
  <LEVEL>BEGIN</LEVEL>
  <Collection>
    <Primary>
      <PrimaryName>1023</PrimaryName>
      <Content>1</Content>
      <Modifier>81</Modifier>
    </Primary>
  </Collection>
  <CModifier>99</CModifier>
  <LEVEL>BEGIN</LEVEL>
  <LEVEL>BEGIN</LEVEL>
  <Collection>
    <Primary>
      <PrimaryName>5754</PrimaryName>
      <Content>Testing%</Content>
      <Modifier>11</Modifier>
    </Primary>
  </Collection>
  <LEVEL>END</LEVEL>
  <LEVEL>END</LEVEL>
  <LEVEL>END</LEVEL>
</Data>

我正在尝试将其转换为以下XML

<?xml version="1.0" encoding="UTF-8"?>
<Data>
  <Collection>
    <Primary>
      <PrimaryName>1238</PrimaryName>
      <Content>1</Content>
      <Modifier>81</Modifier>
    </Primary>
  </Collection>
  <CModifier>55</CModifier>
  <LEVEL>
  <Collection>
    <Primary>
      <PrimaryName>1023</PrimaryName>
      <Content>1</Content>
      <Modifier>81</Modifier>
    </Primary>
  </Collection>
  <CModifier>99</CModifier>
  <LEVEL>
  <LEVEL>
  <Collection>
    <Primary>
      <PrimaryName>5754</PrimaryName>
      <Content>Testing%</Content>
      <Modifier>11</Modifier>
    </Primary>
  </Collection>
  </LEVEL>
  </LEVEL>
  </LEVEL>
</Data>

我尝试使用XSLT,但看起来我不能仅根据条件添加开始标签或结束标签。

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


    <xsl:output method="xml" encoding="utf-8" indent="yes"/>

    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()" />
        </xsl:copy>
    </xsl:template>

    <xsl:template match="LEVEL">
        <xsl:if test="LEVEL='BEGIN'">
            <level>
        </xsl:if>
        <xsl:if test="LEVEL='END'">
            </level>
        </xsl:if>
    </xsl:template>
</xsl:stylesheet>

4 个答案:

答案 0 :(得分:2)

这是一个难题。您的尝试无法成功,因为XSLT样式表也必须是格式正确的XML文档。

以下样式表适用于给定的示例。希望您所有的输入文档都能满足此处的假设。

XSLT 1.0

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>

<xsl:key name="node-by-level" match="node()" use="generate-id(preceding-sibling::LEVEL[.='BEGIN'][1])" />

<!-- identity transform -->
<xsl:template match="@*|node()">
    <xsl:copy>
        <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
</xsl:template>

<xsl:template match="/Data">
    <xsl:copy>
        <xsl:apply-templates select="LEVEL[.='BEGIN'][1]"/>
    </xsl:copy>
</xsl:template>

<xsl:template match="LEVEL[.='BEGIN']">
    <LEVEL>
        <xsl:apply-templates select="key('node-by-level', generate-id())"/>
    </LEVEL>
</xsl:template>

<xsl:template match="LEVEL[.='END']"/>

</xsl:stylesheet>

已添加:

为应对您所编辑问题中增加的复杂性,我将分两步进行转换:

XSLT 1.0(+ EXSLT节点集功能)

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>

<xsl:key name="elem-by-level" match="*" use="generate-id(preceding-sibling::BEGIN[@level=current()/@level - 1][1])" />

<!-- identity transform -->
<xsl:template match="@*|node()">
    <xsl:copy>
        <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
</xsl:template>

<xsl:template match="/Data">
    <!-- first pass, using sibling recursion -->
    <xsl:variable name="first-pass">
        <xsl:apply-templates select="*[1]" mode="first-pass"/>
    </xsl:variable>
    <!-- output -->
    <xsl:copy>
        <xsl:apply-templates select="exsl:node-set($first-pass)/*[@level=0]" />
    </xsl:copy>
</xsl:template>

<!-- first pass templates -->

<xsl:template match="*" mode="first-pass">
    <xsl:param name="level" select="0"/>
    <xsl:copy>
        <xsl:attribute name="level">
            <xsl:value-of select="$level"/>
        </xsl:attribute>
        <xsl:copy-of select="@*|node()"/>
    </xsl:copy>
    <xsl:apply-templates select="following-sibling::*[1]" mode="first-pass">
        <xsl:with-param name="level" select="$level"/>
    </xsl:apply-templates>
</xsl:template>

<xsl:template match="LEVEL[.='BEGIN']" mode="first-pass">
    <xsl:param name="level" select="0"/>
    <BEGIN level="{$level}"/>
    <xsl:apply-templates select="following-sibling::*[1]" mode="first-pass">
        <xsl:with-param name="level" select="$level + 1"/>
    </xsl:apply-templates>  
</xsl:template>

<xsl:template match="LEVEL[.='END']" mode="first-pass">
    <xsl:param name="level" select="0"/>
    <xsl:apply-templates select="following-sibling::*[1]" mode="first-pass">
        <xsl:with-param name="level" select="$level - 1"/>
    </xsl:apply-templates>  
</xsl:template>

<!-- output templates -->

<xsl:template match="BEGIN">
    <LEVEL>
        <xsl:apply-templates select="key('elem-by-level', generate-id())"/>
    </LEVEL>
</xsl:template>

<xsl:template match="@level"/>

</xsl:stylesheet>

使用以下输入示例进行测试:

XML

<Data>
    <Item name="0A"/>
    <Item name="0B"/>
    <LEVEL>BEGIN</LEVEL>
    <Item name="1A"/>
    <LEVEL>BEGIN</LEVEL>
    <LEVEL>BEGIN</LEVEL>
    <Item name="3A"/>
    <LEVEL>END</LEVEL>
    <Item name="2A"/>
    <LEVEL>END</LEVEL>
    <Item name="1B"/>
    <LEVEL>END</LEVEL>
    <Item name="0C"/>
    <LEVEL>BEGIN</LEVEL>
    <Item name="1C"/>
    <LEVEL>END</LEVEL>
    <Item name="0D"/>
</Data>

产生:

结果

<?xml version="1.0" encoding="utf-16"?>
<Data>
  <Item name="0A" />
  <Item name="0B" />
  <LEVEL>
    <Item name="1A" />
    <LEVEL>
      <LEVEL>
        <Item name="3A" />
      </LEVEL>
      <Item name="2A" />
    </LEVEL>
    <Item name="1B" />
  </LEVEL>
  <Item name="0C" />
  <LEVEL>
    <Item name="1C" />
  </LEVEL>
  <Item name="0D" />
</Data>

演示https://xsltfiddle.liberty-development.net/3NJ38Zr

答案 1 :(得分:1)

这里是使用“同级递归”技术的XSLT 2.0解决方案:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet exclude-result-prefixes="#all" version="3.0" xmlns:f="http://local/"
    xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    expand-text="yes">

    <xsl:strip-space elements="*"/>
    <xsl:output method="xml" indent="yes"/>

    <xsl:function name="f:depth" as="xs:integer">
        <xsl:param name="n" as="element()"/>
        <xsl:sequence select="count($n/preceding-sibling::LEVEL[.='BEGIN']) - count($n/preceding-sibling::LEVEL[.='END'])"/>
    </xsl:function>

    <xsl:template match="Data">
        <Data>
            <xsl:apply-templates select="*[1]"/>
        </Data>
    </xsl:template>

    <xsl:template match="*">
        <xsl:copy-of select="."/>
        <xsl:apply-templates select="following-sibling::*[1]"/>
    </xsl:template>

    <xsl:template match="LEVEL[.='BEGIN']">
        <LEVEL>
        <xsl:apply-templates select="following-sibling::*[1]"/>
        </LEVEL>
        <xsl:apply-templates select="following-sibling::*[f:depth(.) = f:depth(current())][1]"/>
    </xsl:template>

    <xsl:template match="LEVEL[.='END']"/>

</xsl:stylesheet>

“同级递归”的一般思想是编写模板规则来处理单个元素,然后从那里决定如何处理下一个同级元素。在这种情况下,挑战在于BEGIN模板必须在匹配的END之后继续处理,而我通过编写一个函数来完成此任务,该函数将每个元素的深度计算为先前BEGIN和先前END之间的差。

在XSLT 1.0中,我认为您可以通过直接扩展此f:depth函数来实现相同的功能。

一次计算所有元素的深度,并将该值作为属性附加到每个元素,可能会更有效。或者在XSLT 3.0中,可以使用记事函数或累加器或xsl:iterate计算深度。也许也可以使用xsl:number来完成。

答案 2 :(得分:1)

如果不限于XSLT 1,那么我建议进行两步转换,在XSLT 3中,您可以使用累加器来确保在第一个转换步骤中然后在第二个转换步骤中用嵌套级别值装饰LEVEL元素步骤变成直接的递归xsl:for-each-group group-starting-with/group-ending-with分组问题:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:mf="http://example.com/mf"
    exclude-result-prefixes="#all"
    version="3.0">

  <xsl:output indent="yes"/>

  <xsl:mode on-no-match="shallow-copy" use-accumulators="level"/>

  <xsl:mode name="add-levels" on-no-match="shallow-copy" use-accumulators="level"/>

  <xsl:accumulator name="level" as="xs:integer" initial-value="0">
      <xsl:accumulator-rule match="LEVEL[. = 'BEGIN']" phase="start" select="$value + 1"/>
      <xsl:accumulator-rule match="LEVEL[. = 'END']" phase="end" select="$value - 1"/>
  </xsl:accumulator>

  <xsl:template match="LEVEL" mode="add-levels">
      <LEVEL level="{accumulator-before('level')}">
          <xsl:apply-templates select="@* , node()" mode="#current"/>
      </LEVEL>
  </xsl:template>

  <xsl:variable name="indexed-levels">
      <xsl:apply-templates select="/" mode="add-levels"/>
  </xsl:variable>

    <xsl:function name="mf:nest" as="node()*">
        <xsl:param name="nodes" as="node()*"/>
        <xsl:param name="level" as="xs:integer"/>
        <xsl:for-each-group select="$nodes" group-starting-with="LEVEL[. = 'BEGIN' and accumulator-before('level') = $level]">
            <xsl:choose>
                <xsl:when test="self::LEVEL[. = 'BEGIN' and accumulator-before('level') = $level]">
                    <xsl:for-each-group select="current-group() except ." group-ending-with="LEVEL[. = 'END' and accumulator-before('level') = $level]">
                        <xsl:choose>
                            <xsl:when test="current-group()[last()][self::LEVEL[. = 'END' and accumulator-before('level') = $level]]">
                                <LEVEL>
                                    <xsl:apply-templates select="mf:nest(current-group()[position() lt last()], $level + 1)"/>
                                </LEVEL>
                            </xsl:when>
                            <xsl:otherwise>
                                <xsl:apply-templates select="current-group()"/>
                            </xsl:otherwise>
                        </xsl:choose>
                    </xsl:for-each-group>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:apply-templates select="current-group()"/>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:for-each-group>
    </xsl:function>

  <xsl:template match="/">
      <xsl:apply-templates select="$indexed-levels/node()"/>
  </xsl:template>

  <xsl:template match="/*">
      <xsl:copy>
          <xsl:apply-templates select="mf:nest(*, 1)"/>
      </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/bnnZWp/8

鉴于累加器的性质,甚至只使用一个直接使用累加器值的分组步骤就足够了,而不是先将其插入临时树中即可:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:mf="http://example.com/mf"
    exclude-result-prefixes="#all"
    version="3.0">

    <xsl:output indent="yes"/>

    <xsl:mode on-no-match="shallow-copy" use-accumulators="level"/>

    <xsl:accumulator name="level" as="xs:integer" initial-value="0">
        <xsl:accumulator-rule match="LEVEL[. = 'BEGIN']" phase="start" select="$value + 1"/>
        <xsl:accumulator-rule match="LEVEL[. = 'END']" phase="end" select="$value - 1"/>
    </xsl:accumulator>

    <xsl:template match="LEVEL" mode="add-levels">
        <LEVEL level="{accumulator-before('level')}">
            <xsl:apply-templates select="@* , node()" mode="#current"/>
        </LEVEL>
    </xsl:template>

    <xsl:function name="mf:nest" as="node()*">
        <xsl:param name="nodes" as="node()*"/>
        <xsl:param name="level" as="xs:integer"/>
        <xsl:for-each-group select="$nodes" group-starting-with="LEVEL[. = 'BEGIN' and accumulator-before('level') = $level]">
            <xsl:choose>
                <xsl:when test="self::LEVEL[. = 'BEGIN' and accumulator-before('level') = $level]">
                    <xsl:for-each-group select="current-group() except ." group-ending-with="LEVEL[. = 'END' and accumulator-before('level') = $level]">
                        <xsl:choose>
                            <xsl:when test="current-group()[last()][self::LEVEL[. = 'END' and accumulator-before('level') = $level]]">
                                <LEVEL>
                                    <xsl:apply-templates select="mf:nest(current-group()[position() lt last()], $level + 1)"/>
                                </LEVEL>
                            </xsl:when>
                            <xsl:otherwise>
                                <xsl:apply-templates select="current-group()"/>
                            </xsl:otherwise>
                        </xsl:choose>
                    </xsl:for-each-group>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:apply-templates select="current-group()"/>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:for-each-group>
    </xsl:function>

    <xsl:template match="/*">
        <xsl:copy>
            <xsl:apply-templates select="mf:nest(*, 1)"/>
        </xsl:copy>
    </xsl:template>

</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/bnnZWp/7

答案 3 :(得分:-1)

对于查询,

  

我不能仅根据条件添加开始标签或结束标签。

其中一种方法如下:

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

<xsl:output method="xml" encoding="utf-8" indent="yes" />

<xsl:template match="@*|node()">
    <xsl:copy>
        <xsl:apply-templates select="@*|node()" />
    </xsl:copy>
</xsl:template>

<xsl:template match="LEVEL">
    <xsl:if test=".='BEGIN'">
        <xsl:variable name="startTag">
            <xsl:text>&lt;</xsl:text>
            <xsl:value-of select="'level'" />
            <xsl:text>&gt;</xsl:text>
        </xsl:variable>
        <xsl:value-of select="$startTag" disable-output-escaping="yes" />
    </xsl:if>
    <xsl:if test=".='END'">
        <xsl:variable name="endTag">
            <xsl:text>&lt;/</xsl:text>
            <xsl:value-of select="'level'" />
            <xsl:text>&gt;</xsl:text>
        </xsl:variable>
        <xsl:value-of select="$endTag" disable-output-escaping="yes" />
    </xsl:if>
</xsl:template>
</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/gWvjQfA