需要XSLT转换以移除按属性排序的重复元素

6
我有一份糟糕的XML文件需要通过BizTalk进行处理,我已经将其归一化为下面的示例。虽然我不是XSLT专家,但在网络和VS2010调试器的帮助下,我可以找到自己的XSL解决方案。
现在我需要一个聪明的XSLT代码来“清除”重复元素,并仅保留最新的元素,这是由ValidFromDate属性中的日期决定的。
ValidFromDate属性是XSD:Date类型。
<SomeData>
  <A ValidFromDate="2011-12-01">A_1</A>
  <A ValidFromDate="2012-01-19">A_2</A>
  <B CalidFromDate="2011-12-03">B_1</B>
  <B ValidFromDate="2012-01-17">B_2</B>
  <B ValidFromDate="2012-01-19">B_3</B>
  <C ValidFromDate="2012-01-20">C_1</C>
  <C ValidFromDate="2011-01-20">C_2</C>
</SomeData>

经过转换后,我只想保留这些行:

<SomeData>
  <A ValidFromDate="2012-01-19">A_2</A>
  <B ValidFromDate="2012-01-19">B_3</B>
  <C ValidFromDate="2012-01-20">C_1</C>
</SomeData>

有什么线索可以告诉我如何编写这个XSL吗?我已经在网上寻找解决方案很久了,尝试了很多聪明的XSL排序脚本,但没有一个让我感觉朝着正确的方向前进。


另外...由于这将从BizTalk映射中调用,因此通过.NET我仅限于使用XSLT 1.0... - LarsWA
2
也许应该使用 C_1 而不是 C_2 - Kirill Polishchuk
当然可以...谢谢。我在我的探索中编辑了这个。 - LarsWA
首先...有很多非常好的解决方案。通过阅读它们,我在XSLT方面变得更加熟练了。我没有时间尝试所有的解决方案,还有其他的解决方案也可以达到目的。 - LarsWA
6个回答

3

使用Xslt 1.0解决这个问题的最佳方案是使用Muenchian分组。 (假设元素已经按ValidFromDate属性排序),以下样式表应该可以解决问题:

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

  <xsl:key name="element-key" match="/SomeData/*" use="name()" />

  <xsl:template match="/SomeData">
    <xsl:copy>
      <xsl:for-each select="*[generate-id() = generate-id(key('element-key', name()))]">
        <xsl:copy-of select="(. | following-sibling::*[name() = name(current())])[last()]" />
      </xsl:for-each>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

当我对你的示例Xml运行它时,这是我得到的结果:

<?xml version="1.0" encoding="utf-8"?>
<SomeData>
  <A ValidFromDate="2012-01-19">A_2</A>
  <B ValidFromDate="2012-01-19">B_3</B>
  <C ValidFromDate="2011-01-20">C_2</C>
</SomeData>

嗨,我对你的答案进行了修改。在给定OP的源XML的情况下,两者产生相同的输出,但我无法确定是否存在更微妙的差异。 - Zach Young
不应该有任何区别。你的版本更加简洁!谢谢! - Pawel
@lwburk:是的,谢谢你指出来。我会仔细看看你的答案。 - Zach Young
@Pawel:实际上,这些元素没有排序 - 因此这个解决方案是不正确的。 - Dimitre Novatchev

2
以下样式表不依赖于输入顺序即可产生正确结果:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:output omit-xml-declaration="yes" indent="yes"/>
    <xsl:key name="byName" match="/SomeData/*" use="name()"/>
    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="SomeData">
        <xsl:copy>
            <xsl:apply-templates select="@*"/>
            <xsl:for-each select="*[generate-id()=
                                    generate-id(key('byName', name())[1])]">
                <xsl:apply-templates select="key('byName', name())" mode="out">
                    <xsl:sort select="translate(@ValidFromDate, '-', '')" 
                              data-type="number" order="descending"/>
                </xsl:apply-templates>
            </xsl:for-each>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="SomeData/*" mode="out">
        <xsl:if test="position()=1">
            <xsl:apply-templates select="."/>
        </xsl:if>
    </xsl:template>
</xsl:stylesheet>

输出:

<SomeData>
   <A ValidFromDate="2012-01-19">A_2</A>
   <B ValidFromDate="2012-01-19">B_3</B>
   <C ValidFromDate="2012-01-20">C_1</C>
</SomeData>

请注意,结果与您列出的期望输出略有不同,因为C_1实际上是最新的C元素(即输入未经过排序)。通过依赖初始排序顺序(并盲目地遵循列出的期望输出),现有答案实际上是不正确的。
解释:
- xsl:key将所有/SomeData/*name()分组 - 外部的for-each选择每个组中的第一项 - 然后对该组的所有成员应用模板,这些成员按@ValidFromDate排序 - 单个附加模板处理从每个排序组中选择第一个元素 - 一个身份转换模板处理其余部分

嗨lwburk...感谢指出C_1的问题。我犯了错。解决方案还是很好用的。我选择了Dimitre的解决方案,因为它更简短、更易于概述和维护,这样那些需要对其进行维护的人就可以轻松地进行操作了。 - LarsWA

2
基于@ValidFromDate 的排序:
XSLT:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" indent="yes"/>

  <xsl:key name="k" match="*" use="name()"/>

  <xsl:template match="SomeData">
    <xsl:copy>
      <xsl:apply-templates select="*[generate-id() = 
                           generate-id(key('k', name()))]"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="*">
    <xsl:apply-templates select="key('k', name())" mode="a">
      <xsl:sort select="@ValidFromDate" order="descending"/>
    </xsl:apply-templates>
  </xsl:template>

  <xsl:template match="*" mode="a">
    <xsl:if test="position() = 1">
      <xsl:copy-of select="."/>
    </xsl:if>
  </xsl:template>

</xsl:stylesheet>

应用在:

<SomeData>
  <A ValidFromDate="2011-12-01">A_1</A>
  <A ValidFromDate="2012-01-19">A_2</A>
  <B CalidFromDate="2011-12-03">B_1</B>
  <B ValidFromDate="2012-01-17">B_2</B>
  <B ValidFromDate="2012-01-19">B_3</B>
  <C ValidFromDate="2012-01-20">C_1</C>
  <C ValidFromDate="2011-01-20">C_2</C>
</SomeData>

产生:

<SomeData>
  <A ValidFromDate="2012-01-19">A_2</A>
  <B ValidFromDate="2012-01-19">B_3</B>
  <C ValidFromDate="2012-01-20">C_1</C>
</SomeData>

2

相对于@lwburk提供的XSLT 1.0解决方案,以下是一个简化且更短的解决方案:

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

 <xsl:key name="kName" match="*/*" use="name()"/>

 <xsl:template match="/">
  <xsl:apply-templates select=
   "*/*[generate-id()
       =
        generate-id(key('kName', name())[1])
       ]
   "/>
 </xsl:template>

 <xsl:template match="*/*">
  <xsl:for-each select="key('kName', name())">
   <xsl:sort select="@ValidFromDate" order="descending"/>
   <xsl:if test="position() = 1">
    <xsl:copy-of select="."/>
   </xsl:if>
  </xsl:for-each>
 </xsl:template>
</xsl:stylesheet>

当应用此转换到提供的XML文档时

<SomeData>
    <A ValidFromDate="2011-12-01">A_1</A>
    <A ValidFromDate="2012-01-19">A_2</A>
    <B CalidFromDate="2011-12-03">B_1</B>
    <B ValidFromDate="2012-01-17">B_2</B>
    <B ValidFromDate="2012-01-19">B_3</B>
    <C ValidFromDate="2012-01-20">C_1</C>
    <C ValidFromDate="2011-01-20">C_2</C>
</SomeData>

想要的、正确的结果已经生成:

<A ValidFromDate="2012-01-19">A_2</A>
<B ValidFromDate="2012-01-19">B_3</B>
<C ValidFromDate="2012-01-20">C_1</C>

1

根据Pawel的回答,我进行了以下修改,得到了相同的结果:

<xsl:template match="/SomeData">
  <xsl:copy>
    <xsl:copy-of select="*[generate-id() = generate-id(key('element-key', name())[last()])]"/>
  </xsl:copy>
</xsl:template>

如果它们每次都产生相同的结果,我喜欢这样做,因为它会更加简洁。

1

无需依赖输入顺序的XLST 2.0解决方案。

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:fn="http://www.w3.org/2005/xpath-functions">
    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
    <xsl:template match="/">
        <SomeData>
            <xsl:for-each-group select="/SomeData/*" group-by="name()">
                    <xsl:for-each select="current-group()">
                        <xsl:sort select="number(substring(attribute(),1,4))" order="descending" data-type="number"/> <!-- year-->
                        <xsl:sort select="number(substring(attribute(),6,2))" order="descending" data-type="number"/> <!-- month-->
                        <xsl:sort select="number(substring(attribute(),9,2))" order="descending" data-type="number"/> <!-- date-->
                        <xsl:if test="position()=1">
                                <xsl:sequence select="."/>
                        </xsl:if>
                    </xsl:for-each>
            </xsl:for-each-group>
        </SomeData>
</xsl:template>
</xsl:stylesheet>

是的,XSLT 2.0使得这个过程简单了一些,但是微软暂时还没有实现它... 不过还是谢谢。 - LarsWA

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