XSLT 1.0 - 将具有子节点的兄弟节点合并为新的复合节点

4

我很难准确地描述问题的标题,也许通过示例更容易理解。

假设我有一个来自系统A的XML文档,它看起来像这样:

<root>
    <phone_numbers>
        <phone_number type="work">123-WORK</phone_number>
        <phone_number type="home">456-HOME</phone_number>
        <phone_number type="work">789-WORK</phone_number>
        <phone_number type="other">012-OTHER</phone_number>
    </phone_numbers>
    <email_addresses>
        <email_address type="home">a@home</email_address>
        <email_address type="other">b@other</email_address>
        <email_address type="home">c@home</email_address>
        <email_address type="work">d@work</email_address>
        <email_address type="other">e@other</email_address>
        <email_address type="other">f@other</email_address>
    </email_addresses>
</root>

我需要将这些内容放入以下结构中,以便在系统B中使用:

<root>
    <addresses>
        <address name="work1">
            <phone_number>123-WORK</phone_number>
            <email_address>d@work</email_address>
        </address>
        <address name="work2">
            <phone_number>789-WORK</phone_number>
        </address>
        <address name="other1">
            <phone_number>012-OTHER</phone_number>
            <email_address>b@other</email_address>
        </address>
        <address name="other2">
            <email_address>e@other</email_address>
        </address>
        <address name="other3">
            <email_address>f@other</email_address>
        </address>
        <address name="home1">
            <phone_number>456-HOME</phone_number>
            <email_address>a@home</email_address>
        </address>
        <address name="home2">
            <email_address>c@home</email_address>
        </address>
    </addresses>
</root>

每种类型可以有任意数量的电子邮件地址(据我所知,从0到无穷大)。每种类型也可以有任意数量的电话号码,而一种类型的电话号码数量不必与同一类型的电子邮件地址数量匹配。
除了它们按照添加到系统A中的顺序输入之外,第一个文档中的电子邮件地址和电话号码实际上并没有关联。
我必须按类型将电子邮件地址和电话号码配对以适应系统B,并且我想将它们配对,以便类型X的第一个电话号码与类型X的第一个电子邮件地址配对,并且不会将类型X的任何电话号码与类型不同的电子邮件配对。
由于我必须将它们配对,而且由于它们被输入到系统的顺序是我找到双方关系的最接近方式,因此我希望以这种方式对它们进行排序。我将不得不告诉用户检查结果是否合理,但我必须将它们配对 - 没有选择。
复杂化问题的是,我的实际XML文档具有更多的节点,我需要将其与phone_numbers和email_addresses合并,并且我有超过两个@types。
另一个注意事项:我已经计算了任何给定类型的最大节点数,因此对于我的示例文档,我知道单个@type的最大
节点数为三(具有@type = other的三个节点=具有@name = otherX的三个
节点)。

类型的顺序重要吗?类型“other”直到第二个“email_address”或第四个“phone_number”才出现,但该类型在您的输出中排名第二。 - Wayne
类型的顺序无关紧要。 - tex
好问题,+1。看看我的解决方案,它更简单 :) - Dimitre Novatchev
还添加了详细的解释。希望更容易理解。 - Dimitre Novatchev
2个回答

1

这个样式表:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:key name="byType" match="/root/*/*" use="@type" />
    <xsl:key name="phoneByType" match="phone_numbers/phone_number"
        use="@type" />
    <xsl:key name="emailByType" match="email_addresses/email_address"
        use="@type" />
    <xsl:template match="/">
        <root>
            <addresses>
                <xsl:apply-templates />
            </addresses>
        </root>
    </xsl:template>
    <xsl:template match="/root/*/*" />
    <xsl:template
        match="/root/*/*[generate-id()=generate-id(key('byType', @type)[1])]">
        <xsl:apply-templates select="key('phoneByType', @type)"
            mode="wrap" />
        <xsl:apply-templates
            select="key('emailByType', @type)
                [position() > count(key('phoneByType', @type))]"
            mode="wrap" />
    </xsl:template>
    <xsl:template match="phone_numbers/phone_number" mode="wrap">
        <xsl:variable name="pos" select="position()" />
        <address name="{concat(@type, $pos)}">
            <xsl:apply-templates select="." mode="out" />
            <xsl:apply-templates select="key('emailByType', @type)[$pos]"
                mode="out" />
        </address>
    </xsl:template>
    <xsl:template match="email_addresses/email_address" mode="wrap">
        <address
            name="{concat(@type, 
                          position() + count(key('phoneByType', @type)))}">
            <xsl:apply-templates select="." mode="out" />
        </address>
    </xsl:template>
    <xsl:template match="/root/*/*" mode="out">
        <xsl:copy>
            <xsl:apply-templates />
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>

在这个输入上:

<root>
    <phone_numbers>
        <phone_number type="work">123-WORK</phone_number>
        <phone_number type="home">456-HOME</phone_number>
        <phone_number type="work">789-WORK</phone_number>
        <phone_number type="other">012-OTHER</phone_number>
    </phone_numbers>
    <email_addresses>
        <email_address type="home">a@home</email_address>
        <email_address type="other">b@other</email_address>
        <email_address type="home">c@home</email_address>
        <email_address type="work">d@work</email_address>
        <email_address type="other">e@other</email_address>
        <email_address type="other">f@other</email_address>
        <email_address type="test">g@other</email_address>
    </email_addresses>
</root>

生成:

<root>
    <addresses>
        <address name="work1">
            <phone_number>123-WORK</phone_number>
            <email_address>d@work</email_address>
        </address>
        <address name="work2">
            <phone_number>789-WORK</phone_number>
        </address>
        <address name="home1">
            <phone_number>456-HOME</phone_number>
            <email_address>a@home</email_address>
        </address>
        <address name="home2">
            <email_address>c@home</email_address>
        </address>
        <address name="other1">
            <phone_number>012-OTHER</phone_number>
            <email_address>b@other</email_address>
        </address>
        <address name="other2">
            <email_address>e@other</email_address>
        </address>
        <address name="other3">
            <email_address>f@other</email_address>
        </address>
        <address name="test1">
            <email_address>g@other</email_address>
        </address>
    </addresses>
</root>

说明:

  • 有三个组:1)按类型列出所有联系信息;2)按类型列出所有电话号码;3)按类型列出所有电子邮件地址
  • 第一组用于获取每种类型的第一个出现
  • 然后我们逐个处理每个电话号码,与同一位置的任何电子邮件地址配对
  • 最后,我们考虑所有没有相应电话号码的电子邮件地址

看起来非常有前途。我会在这个周末试用一下并告诉你结果。谢谢! - tex
如果您在那之前路过,我想再次强调我有超过两个节点(确切地说是4个),并且我有超过三种类型(4种)。我仍在努力理解您的样式表,所以我还不确定这会产生多大的差异。 - tex
@tex - 这将处理新的“type”属性而不需要任何更改--请注意,我添加了一个名为“test”的属性来演示这一点--但是它需要更改才能支持除电话号码和电子邮件地址之外的新联系元素。 - Wayne
再次感谢您的帮助。如果我可以选择两个答案,我会这样做,但是我选择了Dimitre的答案,因为对我来说更清晰一些。给你加1分。 - tex

1

这个转换非常简单(只有3个模板,没有模式):

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

 <xsl:key name="kTypeByVal" match="@type" use="."/>

 <xsl:key name="kPhNumByType" match="phone_number"
  use="@type"/>

 <xsl:key name="kAddrByType" match="email_address"
  use="@type"/>

 <xsl:variable name="vallTypes" select=
 "/*/*/*/@type
          [generate-id()
          =
           generate-id(key('kTypeByVal',.)[1])
          ]"/>

 <xsl:template match="/">
  <root>
   <addresses>
    <xsl:apply-templates select="$vallTypes"/>
   </addresses>
  </root>
 </xsl:template>

 <xsl:template match="@type">
  <xsl:variable name="vcurType" select="."/>
  <xsl:variable name="vPhoneNums" select="key('kPhNumByType',.)"/>
  <xsl:variable name="vAddresses" select="key('kAddrByType',.)"/>

  <xsl:variable name="vLonger" select=
  "$vPhoneNums[count($vPhoneNums) > count($vAddresses)]
  |
   $vAddresses[not(count($vPhoneNums) > count($vAddresses))]
  "/>

  <xsl:for-each select="$vLonger">
   <xsl:variable name="vPos" select="position()"/>
   <address name="{$vcurType}{$vPos}">
    <xsl:apply-templates select="$vPhoneNums[position()=$vPos]"/>
    <xsl:apply-templates select="$vAddresses[position()=$vPos]"/>
   </address>
  </xsl:for-each>
 </xsl:template>

 <xsl:template match="phone_number|email_address">
  <xsl:copy>
   <xsl:copy-of select="node()"/>
  </xsl:copy>
 </xsl:template>
</xsl:stylesheet>

当应用于提供的XML文档(以及具有所述属性的任何文档)时:

<root>
    <phone_numbers>
        <phone_number type="work">123-WORK</phone_number>
        <phone_number type="home">456-HOME</phone_number>
        <phone_number type="work">789-WORK</phone_number>
        <phone_number type="other">012-OTHER</phone_number>
    </phone_numbers>
    <email_addresses>
        <email_address type="home">a@home</email_address>
        <email_address type="other">b@other</email_address>
        <email_address type="home">c@home</email_address>
        <email_address type="work">d@work</email_address>
        <email_address type="other">e@other</email_address>
        <email_address type="other">f@other</email_address>
    </email_addresses>
</root>

得到了想要的、正确的结果

<root>
   <addresses>
      <address name="work1">
         <phone_number>123-WORK</phone_number>
         <email_address>d@work</email_address>
      </address>
      <address name="work2">
         <phone_number>789-WORK</phone_number>
      </address>
      <address name="home1">
         <phone_number>456-HOME</phone_number>
         <email_address>a@home</email_address>
      </address>
      <address name="home2">
         <email_address>c@home</email_address>
      </address>
      <address name="other1">
         <phone_number>012-OTHER</phone_number>
         <email_address>b@other</email_address>
      </address>
      <address name="other2">
         <email_address>e@other</email_address>
      </address>
      <address name="other3">
         <email_address>f@other</email_address>
      </address>
   </addresses>
</root>

解释:

  1. 使用 Muenchian 方法对 type 属性的所有不同值进行收集,并将其存储在变量 $vallTypes 中。

  2. 对于在步骤 1 中找到的每个不同值,输出一个 <address> 元素,如下所示。

  3. 生成一个 name 属性,其值为当前 type 和当前 position() 的连接。

  4. 捕获两个节点集并存储在变量中:一个包含具有此特定值的 type 属性的所有 phone_number 元素,另一个包含具有此特定值的 type 属性的所有 email_address 元素。

  5. 对于这两个节点集中较长的一个元素或(如果可能)一对元素用于生成(省略 type 属性)最终输出。


这个工作得非常好。正如你所想象的那样,我的示例文档非常简化,但我能够修改您的工作样式表以转换实际(更为复杂)的数据。 - tex

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