【问题标题】:XSLT transform siblings to childrenXSLT 将兄弟姐妹转换为孩子
【发布时间】:2017-01-02 10:40:31
【问题描述】:

我正在尝试在 xslt 中做一些棘手的事情 我有一个平面 xml,其中包含大量兄弟姐妹,并且根据我想将它们转换为孩子的名称 我的转换的一般规则是:

  1. 如果标签/名称是“BLOCK”,则打开一个带有标签/值作为属性的“BLOCK”元素
  2. 如果标记/名称是“BLOCK_END”,则关闭“BLOCK”元素(
  3. 在所有其他情况下,创建元素标签/名称,放置标签/值并立即关闭它

所以对于下面的 xml:

<message>
    <tag>
        <name>BLOCK</name>
        <value>first</value>
    </tag>
    <tag>
        <name>FOO</name>
        <value>BAR</value>
    </tag>
    <tag>
        <name>BLOCK</name>
        <value>second</value>
    </tag>
    <tag>
        <name>FOO2</name>
        <value>BAR2</value>
    </tag>
    <tag>
        <name>BLOCK_END</name>
    </tag>
    <tag>
        <name>BLOCK_END</name>
    </tag>
    <tag>
        <name>BLOCK</name>
        <value>third</value>
    </tag>
    <tag>
        <name>FOO3</name>
        <value>BAR3</value>
    </tag>
    <tag>
        <name>BLOCK_END</name>
    </tag>
</message>

这是我希望的结果:

<message>
    <BLOCK id="first">
        <FOO>BAR</FOO>
        <BLOCK id="second">
            <FOO2>BAR2</FOO2>
        </BLOCK>
    </BLOCK>
    <BLOCK id="third">
        <FOO3>BAR3</FOO3>
    </BLOCK">    
</message>

我使用了以下 xslt。这工作正常,但遗憾的是它在遇到第一个 BLOCK_END 标记后完成了执行

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

<xsl:template match="tag">
    <xsl:variable name="tagName" select="name"/>
    <xsl:variable name="tagValue" select="value"/>
    <xsl:choose>
        <xsl:when test="$tagName = 'BLOCK'">
            <xsl:element name="{$tagName}">
                <xsl:attribute name="id">
                    <xsl:value-of select="$tagValue"/>
                </xsl:attribute>
                <xsl:apply-templates select="./following-sibling::*[1]" />
            </xsl:element>
        </xsl:when>
        <xsl:when test="$tagName = 'BLOCK_END'">
            <!-- DO NOTHING-->
        </xsl:when>
        <xsl:otherwise>
            <xsl:element name="{$tagName}">
                <xsl:value-of select="$tagValue"/>
            </xsl:element>
            <xsl:apply-templates select="./following-sibling::*[1]" />
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>

更新:感谢BitTickler,我离我越来越近了,但还不是很远。

【问题讨论】:

  • 你能使用像 Saxon 9、Altova、XmlPrime 这样的 XSLT 2.0 处理器吗?
  • @MartinHonnen 遗憾的是,我坚持使用核心 XSLT 1.0
  • 另外,我对最初的&lt;xsl:for-each ...&gt; 声明表示怀疑,因为这是非常必要的,并且已经暗示这种方法会引起麻烦。相反,您应该为第一个标签元素应用模板,然后让递归调用发挥它们的作用。

标签: xml xslt xpath


【解决方案1】:

可能有更短的方法,但在我看来这仍然是最直接的方法:

XSLT 1.0

<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="child-item" match="tag[not(name='BLOCK')]" use="generate-id(preceding-sibling::tag[name='BLOCK'][@level=current()/@level][1])" />

<xsl:key name="child-block" match="tag[name='BLOCK']" use="generate-id(preceding-sibling::tag[name='BLOCK'][@level=current()/@level - 1][1])" />

<xsl:template match="/message">
    <xsl:variable name="first-pass-rtf">
        <xsl:apply-templates select="tag[1]" mode="first-pass" />
    </xsl:variable>
    <xsl:variable name="first-pass" select="exsl:node-set($first-pass-rtf)/tag" />
    <!-- output -->
    <message>
        <xsl:apply-templates select="$first-pass[name='BLOCK'][@level=1]"/>
    </message>
</xsl:template>

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

<xsl:template match="tag" mode="first-pass">
    <xsl:param name="level"/>
    <tag level="{$level}">
        <xsl:copy-of select="*"/>
    </tag>
    <xsl:apply-templates select="following-sibling::tag[1]" mode="first-pass">
        <xsl:with-param name="level" select="$level"/>
    </xsl:apply-templates>
</xsl:template>

<xsl:template match="tag[name='BLOCK_END']" mode="first-pass">
    <xsl:param name="level"/>
    <xsl:apply-templates select="following-sibling::tag[1]" mode="first-pass">
        <xsl:with-param name="level" select="$level - 1"/>
    </xsl:apply-templates>
</xsl:template>

<!-- output templates -->
<xsl:template match="tag[name='BLOCK']">
    <BLOCK id="{value}">
        <xsl:apply-templates select="key('child-item', generate-id()) | key('child-block', generate-id())" />
    </BLOCK>
</xsl:template>

<xsl:template match="tag">
    <xsl:element name="{name}">
        <xsl:value-of select="value"/>
    </xsl:element>
</xsl:template>

</xsl:stylesheet>

首先将您的输入重写为:

<xsl:variable name="first-pass-rtf">
    <tag level="1">
        <name>BLOCK</name>
        <value>first</value>
    </tag>
    <tag level="1">
        <name>FOO</name>
        <value>BAR</value>
    </tag>
    <tag level="2">
        <name>BLOCK</name>
        <value>second</value>
    </tag>
    <tag level="2">
        <name>FOO2</name>
        <value>BAR2</value>
    </tag>
    <tag level="1">
        <name>BLOCK</name>
        <value>third</value>
    </tag>
    <tag level="1">
        <name>FOO3</name>
        <value>BAR3</value>
    </tag>
</xsl:variable>

然后将每个tag 关联到其父块变得简单(r)。

【讨论】:

    【解决方案2】:

    问题在于递归模板调用有两个目的(1 太多):

    1. 将光标移到输入标签元素的末尾。
    2. 处理输出的嵌套级别。

    为此,有必要从递归函数(模板)“返回”当前输出状态和“迭代”状态。

    在函数式语言中,这可以被证明,例如使用以下短代码,模拟情况。

    type Node = 
        | Simple of string * string
        | Nested of string * string * Node list
    
    let input =
        [ 
            Simple ("BLOCK","first")
            Simple ("FOO","BAR")
            Simple ("BLOCK","second")
            Simple ("FOO2","BAR2")
            Simple ("BLOCK_END","")
            Simple ("FOO3","BAR3")
            Simple ("BLOCK_END","")
        ]
    
    let rec transform (result,remaining) =
        match remaining with
        | [] -> result,remaining
        | x::xs -> 
            match x with
            | Simple (n,v) when n = "BLOCK" ->
                let below,remaining' = transform ([],xs)
                transform (result @ [Nested(n,v,below)],remaining')
            | Simple (n,v) when n = "BLOCK_END" ->
                result,xs
            | Simple (n,v) ->
                transform (result @[x],xs)
    
    transform ([],input)
    

    既然有 1 个有效的解决方案策略,剩下的唯一问题是,如何将此策略应用于 xslt 转换。

    要启动整个过程,可能应该转换第一个 &lt;tag&gt; 元素。在它的转换中,递归发生了。

    BLOCK_END 应该以某种方式从递归中返回,这样当前位置是已知的,因此 BLOCK 部分可以稍后在该点恢复。

    到目前为止,我的最佳猜测如下:

    <xsl:template match="/">
      <xsl:element name="message">
        <xsl:apply-templates select="/message/tag[1]" />
      </xsl:element>
    </xsl:template>
    
    <xsl:template name="nest" match="tag">
      <xsl:variable name="tagName" select="name"/>
      <xsl:variable name="tagValue" select="value"/>
    
      <xsl:choose>
        <xsl:when test="./name='BLOCK'">
          <xsl:element name="{$tagName}">
            <xsl:attribute name="id">
              <xsl:value-of select="$tagValue"/>
            </xsl:attribute>
            <xsl:apply-templates select="./following-sibling::tag[1]" />
          </xsl:element>
          <!--TODO: We must continue here with the remaining nodes. But we do not know  how many 
          Nodes the block contained... Our cursor (.) is unaffected by previous recursion. -->
          <!--<xsl:apply-templates select="./following-sibling::tag[1]" />-->
        </xsl:when>
        <xsl:when test="./name='BLOCK_END'">
          <!--No nothing-->
        </xsl:when>
        <xsl:otherwise>
          <xsl:element name="{$tagName}">
            <xsl:value-of select="$tagValue"/>
          </xsl:element>
          <xsl:apply-templates select="./following-sibling::tag[1]" />
        </xsl:otherwise>
      </xsl:choose>
    
    </xsl:template>
    

    产生输出:

    <message>
       <BLOCK id="first">
          <FOO>BAR</FOO>
          <BLOCK id="second">
             <FOO2>BAR2</FOO2>
          </BLOCK>
       </BLOCK>
    </message>
    

    【讨论】:

    • 许多公平点 - 特别是评论中的点。感谢您的建议,我觉得我越来越近了。我对在“END_BLOCK”之后调用模板有疑问。重点是“退出递归调用”,这样 就可以做到这一点。如果我在“END_BLOCK”之后再次进行递归调用,那么我会在关闭 元素之前遍历整个文档。请检查我更新的描述
    • @DannyBoy 是的,您不想输入另一个递归。但是你想以某种方式推进.。也许您需要完全切换到&lt;xsl:cal-template name="nest"&gt; &lt;xsl:with-param...&gt; ... 构造。如果没有其他方法可以做到这一点。然后参数包含例如以下兄弟姐妹左右。
    • 花费了我超过预期的时间,但我设法找到了解决方案。在 TODO 部分中,我寻找“关闭”标签并为关闭标签之后的下一个兄弟重新初始化循环。奇迹般有效。这是一个很酷的问题,没有你的帮助是不可能完成的!非常感谢!
    • @DannyBoy 将您的解决方案发布为答案,或编辑此解决方案。否则这对任何人都没有帮助。
    【解决方案3】:

    最终解决方案是对 BitTickler 想法的扩展:

    我必须对原始 xml 进行 twick 以便结束块标记也包含一个标识符(我们使用它来搜索相应的 END_BLOCK 标记以查找 BLOCK 标记)

    <message>
        <tag>
            <name>BLOCK</name>
            <value>first</value>
        </tag>
        <tag>
            <name>FOO</name>
            <value>BAR</value>
        </tag>
        <tag>
            <name>BLOCK</name>
            <value>second</value>
        </tag>
        <tag>
            <name>FOO2</name>
            <value>BAR2</value>
        </tag>
        <tag>
            <name>BLOCK_END</name>
            <value>second</value>
        </tag>
        <tag>
            <name>BLOCK_END</name>
            <value>first</value>
        </tag>
        <tag>
            <name>BLOCK</name>
            <value>third</value>
        </tag>
        <tag>
            <name>FOO3</name>
            <value>BAR3</value>
        </tag>
        <tag>
            <name>BLOCK_END</name>
            <value>third</value>
        </tag>
    </message>
    

    然后诀窍是对 END_BLOCK + 1 标记的模板进行循环调用(参见“魔术”注释)。执行“调用模板”没有任何好处(我这边只是一些剩余的试错),我会将其恢复为“应用模板”

    <?xml version="1.0"?>
    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    
        <xsl:template match="/">
            <message>
                <xsl:call-template name="transformTag">
                    <xsl:with-param name="tag" select="message/tag[1]"/>
                </xsl:call-template>
            </message>
        </xsl:template>
    
        <xsl:template name="transformTag">
            <xsl:param name="tag"/>
            <xsl:variable name="tagName" select="$tag/name"/>
            <xsl:variable name="tagValue" select="$tag/value"/>
            <xsl:choose>
                <xsl:when test="$tagName = 'BLOCK'">
                    <!-- OPEN A BLOCK, RECURENTLY CALL FOR THE NEXT ELEMENT AND THEN MAKE A RECURENT CALL ONCE AGAIN FOR THE NEXT BLOCK-->
                    <xsl:element name="{$tagName}">
                        <xsl:attribute name="id">
                            <xsl:value-of select="$tagValue"/>
                        </xsl:attribute>
                        <xsl:call-template name="transformTag">
                            <xsl:with-param name="tag" select="$tag/following-sibling::*[1]"/>
                        </xsl:call-template>
                    </xsl:element>
                    <!-- THIS IS WHERE THE MAGIC HAPPENS-->
                    <xsl:variable  name="closingTag" select="$tag/following-sibling::*[name='END_BLOCK' and substring-before(value,'@@')=$tagValue][1]"/>
    
                    <xsl:if test="$closingTag/name='END_BLOCK'">
                        <xsl:variable name="nextTag" select="$closingTag/following-sibling::*[1]"/>
                        <xsl:if test="$nextTag[name() = 'tag']">
                            <xsl:call-template name="transformTag">
                                <xsl:with-param name="tag" select="$nextTag"/>
                            </xsl:call-template>
                        </xsl:if>
                    </xsl:if>
                </xsl:when>
                <xsl:when test="$tagName = 'END_BLOCK'">
                    <!-- DO NOTHING AND EXIT THE RECURENT CALL (THIS CLOSES THE TAG)-->
                </xsl:when>
                <xsl:otherwise>
                    <!-- PRINT THE REGULAR TAG AND RECURENTLY CALL FOR THE NEXT TAG-->
                    <xsl:element name="_{$tagName}">
                        <xsl:value-of select="$tagValue"/>
                    </xsl:element>
                    <xsl:call-template name="transformTag">
                        <xsl:with-param name="tag" select="$tag/following-sibling::*[1]"/>
                    </xsl:call-template>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:template>
    </xsl:stylesheet>
    

    【讨论】:

    • "不得不 twick 原始 xml" 这使它成为一个完全不同的问题。当我们在这里回答 XSLT 问题时,我们假设输入的 XML 模式已经给出。如果您确实有能力根据自己的喜好对其进行修改,为什么不让它变得易于处理呢?
    • “不得不 twick 原始 xml”并不意味着“我是生成 xml 并且可以随心所欲地制作结构的人”。我的意思是我的原始 xml 更复杂,并且有更多的数据。在这个 SO question 中,我过滤了 xml 以抓住问题的本质。幸运的是,“关闭块标识符”是我可以使用的,但我最初忽略了它,因为我(错误地)认为它无关紧要
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-08-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-05-06
    • 2023-03-22
    • 2018-11-15
    相关资源
    最近更新 更多