【问题标题】:How can I recursively nest XML nodes, based on an attribute value, using XSLT?如何使用 XSLT 基于属性值递归嵌套 XML 节点?
【发布时间】:2016-04-21 20:28:09
【问题描述】:

我需要在 Visual Studio 2013 中使用 XLST 1.0 转换一些 XML。

我有以下 XML:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <MessageTemplates>
    <MessageTemplate>
      <Segment name="Uno" cardinality="first">
        <value>something</value>
      </Segment>
      <Segment name="Dos" cardinality="second">
        <value>something</value>
      </Segment>
      <Segment name="Tres" cardinality="third">
        <value>something</value>
      </Segment>
      <Segment name="Quatro" cardinality="third">
        <value>something</value>
      </Segment>
      <Segment name="Cinco" cardinality="second">
        <value>something</value>
      </Segment>
      <Segment name="Seis" cardinality="third">
        <value>something</value>
      </Segment>
      <Segment name="Siete" cardinality="first">
        <value>something</value>
      </Segment>
    </MessageTemplate>
  </MessageTemplates>
</root>

Segment 节点的cardinality 属性是有序的,first 是最高的,third 是最低的。我需要创建嵌套级别,基于cardinality,如下:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <MessageTemplates>
    <MessageTemplate>
      <Cardinality type="first">
        <Segment name="Uno">
          <value>something</value>
        </Segment>
        <Cardinality type="second">
          <Segment name="Dos">
            <value>something</value>
          </Segment>
          <Cardinality type="third">
            <Segment name="Tres">
              <value>something</value>
            </Segment>
            <Segment name="Quatro">
              <value>something</value>
            </Segment>
          </Cardinality>
          <Segment name="Cinco">
            <value>something</value>
          </Segment>
          <Cardinality type="third">
            <Segment name="Seis">
              <value>something</value>
            </Segment>
          </Cardinality>
        </Cardinality>
        <Segment name="Siete">
          <value>something</value>
        </Segment>
      </Cardinality>
    </MessageTemplate>
  </MessageTemplates>
</root>

我尝试了几种不同的方法来转换这个文件,但都失败了。我搜索了 SO 并阅读了数十篇文章,但没有找到任何符合我想要做的事情的案例。我还尝试寻找增量方法来实现我的目标,例如使用递归模板调用一次只处理一个 Segment 等。我最接近的是使用以下 XSLT:

<xsl:template match="MessageTemplates/MessageTemplate">
  <MessageTemplate>
    <xsl:copy-of select="@*"/>
    <xsl:call-template name="cardinality"/>
  </MessageTemplate>
</xsl:template>

<xsl:template name="cardinality" match="MessageTemplates/MessageTemplate/Segment">
  <xsl:choose>
    <xsl:when test="position() = 1">
      <Cardinality type="{Segment/@cardinality}">
        <Segment>
          <xsl:apply-templates select="@*[name() != 'cardinality'] | node()" />
        </Segment>
      </Cardinality>
    </xsl:when>

    <xsl:when test="position() != last() and following-sibling::Segment/@cardinality != @cardinality">
      <Cardinality type="{@cardinality}">
        <Segment>
          <xsl:apply-templates select="@*[name() != 'cardinality'] | node()" />
        </Segment>
      </Cardinality>
    </xsl:when>

    <xsl:when test="position() = last()">
      <Segment>
        <xsl:apply-templates select="@*[name() != 'cardinality'] | node()" />
      </Segment>
    </xsl:when>
  </xsl:choose>
</xsl:template>

生成了以下 XML:

<?xml version="1.0" encoding="utf-8"?>
<root>
  <Version>1.0</Version>
  <MessageTemplates>
    <MessageTemplate>
      <Cardinality type="first">
        <Segment>
          <Cardinality type="">
            <Segment name="Uno">
              <value>something</value>
            </Segment>
          </Cardinality>
          <Cardinality type="second">
            <Segment name="Dos">
              <value>something</value>
            </Segment>
          </Cardinality>
          <Cardinality type="third">
            <Segment name="Tres">
              <value>something</value>
            </Segment>
          </Cardinality>
          <Cardinality type="third">
            <Segment name="Quatro">
              <value>something</value>
            </Segment>
          </Cardinality>
          <Cardinality type="second">
            <Segment name="Cinco">
              <value>something</value>
            </Segment>
          </Cardinality>
          <Cardinality type="third">
            <Segment name="Seis">
              <value>something</value>
            </Segment>
          </Cardinality>
          <Segment name="Siete">
            <value>something</value>
          </Segment>
        </Segment>
      </Cardinality>
    </MessageTemplate>
  </MessageTemplates>
</root>

基本上,我想要的是将 all Segment 节点包装在单个 Cardinality 节点中。然后,如果下一个Segmentcardinality 值低于当前Segmentcardinality 值,我想将所有以下 Segment 节点包装在@987654338 中@node,只要cardinality的值相同。我希望每个cardinality 级别都发生这种情况。最后,我想将Segmentcardinality 值移动到Cardinality 节点的type 属性。 Segment 节点的顺序必须保持。

任何帮助将不胜感激。

【问题讨论】:

  • 您希望计算机能够理解“second”是“first”的子代,“third”是“second”的子代吗?
  • 这些值只是举例,但我确实明白你的意思。唯一基数值在 XML 中出现的顺序最终将决定嵌套顺序。
  • 这需要一些工作。顺便说一句,为什么结果中没有UnoSiete 兄弟姐妹?它们都具有相同的“第一”基数。
  • 是的,我自己也注意到了这一点,并且正在寻找“编辑”按钮。哈哈
  • 您将使用哪种 XSLT 1.0 处理器?如果您的处理器支持,您可能会利用一些扩展。

标签: xml xslt recursion


【解决方案1】:

这是一种递归方法。至少对于给定的示例,它确实产生了所需的输出。我对此并不满意。它不是很可靠,也不是很快,也不是可维护的,但至少它给了你基本的想法。 (如果没有更好的)

<xsl:stylesheet version="1.0"
     xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" indent="yes" />


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

    <xsl:template match="Segment/@cardinality"  />

    <xsl:template match="MessageTemplate">
        <xsl:copy>
            <Cardinality type="first">
                <xsl:apply-templates select="Segment[1]" mode="nested" >
                    <xsl:with-param name="currentcardinality" select="'first'" />
                </xsl:apply-templates>
            </Cardinality>
        </xsl:copy>
    </xsl:template>

    <xsl:template name="comapreNext">
        <xsl:variable name="this" select="@cardinality" />
        <xsl:variable name="next" select="following-sibling::Segment[1]/@cardinality" />
        <xsl:choose>
            <xsl:when test="$this= $next" >
                <xsl:text>eq</xsl:text>
            </xsl:when>
            <xsl:when test="($this='first' and ($next = 'second' or $next = 'third') ) or
                                ($this='second' and ( $next = 'third') )" >
                <xsl:text>lt</xsl:text>
            </xsl:when>
            <xsl:otherwise>
                <xsl:text>gt</xsl:text>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>

    <xsl:template match="Segment"  mode="nested">
        <xsl:param name="currentcardinality"/>
        <xsl:variable name="this" select="." />
        <xsl:variable name="next">
            <xsl:call-template name="comapreNext"/>
        </xsl:variable>
        <xsl:variable name="next_le" select="$next='lt' or $next = 'eq'" />
        <xsl:choose>
            <xsl:when test="@cardinality = $currentcardinality  ">
                <!-- copy Segment without cardinality -->
                <xsl:apply-templates select="."  />
                <xsl:apply-templates select="following-sibling::Segment[1][$next_le]" mode="nested" >
                    <xsl:with-param name="currentcardinality" select="@cardinality" />
                </xsl:apply-templates>
            </xsl:when>
            <xsl:otherwise>
                <Cardinality type="{@cardinality}" >
                    <xsl:apply-templates select="."  />
                    <xsl:apply-templates select="following-sibling::Segment[1][$next_le]" mode="nested" >
                        <xsl:with-param name="currentcardinality" select="@cardinality" />
                    </xsl:apply-templates>
                    <xsl:if test="@cardinality = 'second'  ">
                        <!-- find same cardinality but not next -->
                        <xsl:apply-templates select="(following-sibling::Segment[position() != 1][not(@cardinality ='third')])[1][@cardinality = $this/@cardinality]" mode="nested" >
                            <xsl:with-param name="currentcardinality" select="@cardinality" />
                        </xsl:apply-templates>
                    </xsl:if>
                </Cardinality>
            </xsl:otherwise>
        </xsl:choose>
        <xsl:if test="@cardinality = 'first'  ">
            <!-- find same cardinality but not next -->
            <xsl:apply-templates select="(following-sibling::Segment[position() != 1])[@cardinality = $this/@cardinality][1]" mode="nested" >
                <xsl:with-param name="currentcardinality" select="@cardinality" />
            </xsl:apply-templates>
        </xsl:if>
    </xsl:template>
</xsl:stylesheet>

生成以下输出:

<MessageTemplates>
  <MessageTemplate>
  <Cardinality type="first">
    <Segment name="Uno">
      <value>something</value>
    </Segment>
    <Cardinality type="second">
      <Segment name="Dos">
        <value>something</value>
      </Segment>
      <Cardinality type="third">
        <Segment name="Tres">
          <value>something</value>
        </Segment>
        <Segment name="Quatro">
          <value>something</value>
        </Segment>
      </Cardinality>
      <Segment name="Cinco">
        <value>something</value>
      </Segment>
      <Cardinality type="third">
        <Segment name="Seis">
          <value>something</value>
        </Segment>
      </Cardinality>
    </Cardinality>
    <Segment name="Siete">
      <value>something</value>
    </Segment>
  </Cardinality>
 </MessageTemplate>
</MessageTemplates>

【讨论】:

  • 我已经测试了您的解决方案,它确实适用于给定的示例,所以我接受这个作为答案。但是,我发现当我有一个像“第一”、“第一”、“第三”和“第一”这样的文件时,最后一个“第一”段会在输出文件中重复。 (是的,我很遗憾将我的基数称为“第一”、“第二”和“第三”。)您能否建议我应该关注match="Segment" 模板的哪个部分来解决这个问题?
  • 尝试将最后一个if改为&lt;xsl:if test="@cardinality = 'first' and not(following-sibling::Segment[1][@cardinality = 'first']) "&gt;
【解决方案2】:

以下是您可以用作起点的内容。

它使用Muenchian grouping method 生成不同的基数列表,按照它们在源 XML 文档中出现的顺序排列。

从列表中的第一个基数开始,每个基数获取匹配的段,然后递归到列表中的下一个基数 - 从而实现所需的嵌套。

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:strip-space elements="*"/>
<xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes"/>

<xsl:key name="segment-by-cardinality" match="Segment" use="@cardinality" />

<xsl:variable name="cardinalities">
    <!-- generate a distinct list of cardinalities -->
    <xsl:for-each select="root/MessageTemplates/MessageTemplate/Segment[count(. | key('segment-by-cardinality', @cardinality)[1]) = 1]">
        <Cardinality type="{@cardinality}"/>
    </xsl:for-each>
</xsl:variable>
<xsl:variable name="cardinalities-set" select="exsl:node-set($cardinalities)/Cardinality" />

<xsl:variable name="source-doc" select="/" />

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

<xsl:template match="MessageTemplate">
    <xsl:copy>
        <!-- start with the top-level cardinality -->
        <xsl:apply-templates select="$cardinalities-set[1]"/>
    </xsl:copy>
</xsl:template>

<xsl:template match="Cardinality">
    <xsl:variable name="type" select="@type" />
    <xsl:copy>  
        <xsl:copy-of select="@*"/>
        <!-- switch the context back to the XML source in order to use key -->
        <xsl:for-each select="$source-doc">
            <xsl:apply-templates select="key('segment-by-cardinality', $type)"/>
        </xsl:for-each>
        <!-- proceed to the next cardinality in the list -->
        <xsl:apply-templates select="following-sibling::Cardinality[1]"/>
    </xsl:copy>
</xsl:template>

</xsl:stylesheet> 

应用于您的示例输入,结果将是:

<?xml version="1.0" encoding="utf-8"?>
<root>
   <MessageTemplates>
      <MessageTemplate>
         <Cardinality type="first">
            <Segment name="Uno" cardinality="first">
               <value>something</value>
            </Segment>
            <Segment name="Siete" cardinality="first">
               <value>something</value>
            </Segment>
            <Cardinality type="second">
               <Segment name="Dos" cardinality="second">
                  <value>something</value>
               </Segment>
               <Segment name="Cinco" cardinality="second">
                  <value>something</value>
               </Segment>
               <Cardinality type="third">
                  <Segment name="Tres" cardinality="third">
                     <value>something</value>
                  </Segment>
                  <Segment name="Quatro" cardinality="third">
                     <value>something</value>
                  </Segment>
                  <Segment name="Seis" cardinality="third">
                     <value>something</value>
                  </Segment>
               </Cardinality>
            </Cardinality>
         </Cardinality>
      </MessageTemplate>
   </MessageTemplates>
</root>

请注意,这不符合您“Segment 节点的顺序必须保持”的要求。我不完全理解这个要求。如果您有一些标准应该对 Cardinality 的孩子(即它的 Segments 和下一个更高的 Cardinality)进行排序,您可以在另一遍中执行此操作。但是由于下一个更高的基数可以包含多个段,其中一些可能在当前的一些段之前,而有些则不是,我不太明白“正确”的顺序是什么。

【讨论】:

  • 谢谢@Michael.hor257k。我很感激你的努力。这与我尝试 Muenchian 分组时的结果相似。但是,我尝试转换的 XML 用于驱动消息生成引擎,因此段的顺序至关重要。我在本例中命名为“first”的基数用于消息页眉和页脚。 'second',标识可以在消息中重复的段。 “第三”也可以重复,但只能在“第二”之内。我知道我的示例名称并未表明它们的功能,但这就是订单要求背后的原因。
  • 这里需要应用的逻辑恐怕还是逃不过我。如果我手动执行此操作,我不知道该怎么做 - 所以我也无法建议算法。
猜你喜欢
  • 1970-01-01
  • 2021-05-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-06-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多