使用XSLT对XML进行排序 - 未知整个XML模式

7
我想知道是否可以使用XSLT对XML文件进行排序,即使我不知道整个XML模式。
例如,我想对以下XML文件进行排序。
按/CATALOG/CD/TITLE对/CATALOG/CD元素进行排序
<CATALOG attrib1="value1">
  <DVD2>
    <TITLE>The Godfather2</TITLE>
  </DVD2>
  <CD>
    <TITLE>Hide your heart</TITLE>
    <ARTIST>Bonnie Tyler</ARTIST>
    <COUNTRY>UK</COUNTRY>
    <COMPANY>CBS Records</COMPANY>
    <PRICE>9.90</PRICE>
    <YEAR>1988</YEAR>
  </CD>
  <CD attrib4="value4">
    <TITLE>Empire Burlesque</TITLE>
    <ARTIST>Bob Dylan</ARTIST>
    <COUNTRY>USA</COUNTRY>
    <COMPANY>Columbia</COMPANY>
    <PRICE>
      <CATALOG>
        <CD><TITLE>E</TITLE></CD>
        <CD><TITLE>I</TITLE></CD>
        <CD><TITLE>D</TITLE></CD>
      </CATALOG>
    </PRICE>
    <YEAR>1985</YEAR>
  </CD>
  <CD attrib2="value2">
    <TITLE attrib3="value3">Greatest Hits</TITLE>
    <ARTIST>Dolly Parton</ARTIST>
    <COUNTRY>USA</COUNTRY>
    <COMPANY>RCA</COMPANY>
    <PRICE>9.90</PRICE>
    <YEAR>1982</YEAR>
  </CD>
  <DVD>
    <TITLE>The Godfather1</TITLE>
  </DVD>
</CATALOG>

输出结果应该是:
<CATALOG attrib1="value1">
  <CD attrib4="value4">
    <TITLE>Empire Burlesque</TITLE>
    <ARTIST>Bob Dylan</ARTIST>
    <COUNTRY>USA</COUNTRY>
    <COMPANY>Columbia</COMPANY>
    <PRICE>
      <CATALOG>
        <CD><TITLE>E</TITLE></CD>
        <CD><TITLE>I</TITLE></CD>
        <CD><TITLE>D</TITLE></CD>
      </CATALOG>
    </PRICE>
    <YEAR>1985</YEAR>
  </CD>
  <CD attrib2="value2">
    <TITLE attrib3="value3">Greatest Hits</TITLE>
    <ARTIST>Dolly Parton</ARTIST>
    <COUNTRY>USA</COUNTRY>
    <COMPANY>RCA</COMPANY>
    <PRICE>9.90</PRICE>
    <YEAR>1982</YEAR>
  </CD>
  <CD>
    <TITLE>Hide your heart</TITLE>
    <ARTIST>Bonnie Tyler</ARTIST>
    <COUNTRY>UK</COUNTRY>
    <COMPANY>CBS Records</COMPANY>
    <PRICE>9.90</PRICE>
    <YEAR>1988</YEAR>
  </CD>
  <DVD2>
    <TITLE>The Godfather2</TITLE>
  </DVD2>
  <DVD>
    <TITLE>The Godfather1</TITLE>
  </DVD>
</CATALOG>

以下是我尝试过的其中一种方法:

下面是我所做的众多尝试之一:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/">
    <!--<CATALOG>-->
    <xsl:for-each select="CATALOG/CD">
      <xsl:sort select="TITLE" />
      <xsl:copy-of select="."/>
    </xsl:for-each>
    <!--</CATALOG>-->
  </xsl:template>
</xsl:stylesheet>

问题在于,使用这个XSLT时,CD列表外的XML部分不会显示。
我可以取消注释代码的两个部分,但这正是我想避免的。
在这种情况下,如果将任何属性添加到CATALOG元素中,则不会将它们复制到输出XML中。
我不想重新构建XML文件:我只想进行排序,仅知道一些XML模式的确切信息。
例如,使用.NET(使用XmlDocument和XmlNode对象)或Python的lxmx库很容易实现此功能,但是使用XSLT是否可能?
谢谢!
注意:很难找到一个样本输入XML,以避免在所有情况下误解问题。但我会尽力详细说明问题:
- 仅应对CATALOG下面的CD元素进行排序(例如,在Bob Dylan部分下的CD元素应保持不变) - 无论除CD以外的元素(例如DVD和DVD2)是在列表的开头还是结尾都是一样的 - 输出XML中不应缺少任何元素、属性、值、注释等内容 - 非CD元素(例如DVD和DVD2)不应按TITLE子元素排序

抱歉,由于措辞不够清晰,我只想对CD元素进行排序并没有表达明确。因此我不得不稍微编辑一下问题。 - user834929
4个回答

2

这需要用到身份转换吗?它可以用于复制未知模式的XML。

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="@*|node()">
    <xsl:copy>
      <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

我认为你只需要添加一个新模板来匹配CATALOG元素,然后可以在其中采取一些覆盖操作(在您的情况下,对CD元素进行排序)。

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   <xsl:template match="@*|node()">
      <xsl:copy>
         <xsl:apply-templates select="@*|node()"/>
      </xsl:copy>
   </xsl:template>

   <xsl:template match="CATALOG">
      <xsl:copy>
         <xsl:apply-templates select="@*" />
         <xsl:apply-templates select="CD">
            <xsl:sort select="TITLE"/>
         </xsl:apply-templates>
         <xsl:apply-templates select="*[local-name() != 'CD']" />
      </xsl:copy>
   </xsl:template>
</xsl:stylesheet>

因此,当匹配CATALOG时,您仍然可以复制任何属性以及模式中的任何非CD子元素,而无需明确知道它们的名称。请注意,如果存在DVD元素直到CATALOG,例如,在这种情况下,所有这些元素将在排序后的CD元素之后移动。


乍一看,这个解决方案是合适的。我正在进行更多的测试和分析。谢谢! - user834929
在所有其他元素之前,在CD元素之后的CATALOG元素中,有很多空行。你认为有可能去掉它们吗?我不太理解Kragen的XSLT代码,但我认为他已经解决了这个特定的问题。 - user834929
我已经用 select="*[..." 替换了 select="node()[..." 子句,如果你想再试一次。 - Tim C
我认为你最后的修改使得解决方案非常完整。谢谢! - user834929

1
为获取CATALOG元素中的所有属性,您可以编写以下代码:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>

    <xsl:template match="CATALOG">
        <xsl:copy>
            <xsl:copy-of select="@*"/>

            <xsl:copy-of select="CD[1]/preceding-sibling::*"/>
            <xsl:for-each select="CD">
                <xsl:sort select="TITLE"/>
                <xsl:copy-of select="."/>
            </xsl:for-each>
            <xsl:copy-of select="CD[last()]/following-sibling::*"/>
        </xsl:copy>
    </xsl:template>

</xsl:stylesheet>

结果:

<CATALOG atr1="value1" atr2="value2">
    <DVD>
        <FORMAT>DVD-9</FORMAT>
    </DVD>
    <CD>
        <TITLE>1999 Grammy Nominees</TITLE>
        <ARTIST>Many</ARTIST>
        <COUNTRY>USA</COUNTRY>
        <COMPANY>Grammy</COMPANY>
        <PRICE>10.20</PRICE>
        <YEAR>1999</YEAR>
    </CD>
    <CD>
        <TITLE>Big Willie style</TITLE>
        <ARTIST>Will Smith</ARTIST>
        <COUNTRY>USA</COUNTRY>
        <COMPANY>Columbia</COMPANY>
        <PRICE>9.90</PRICE>
        <YEAR>1997</YEAR>
    </CD>
    ...
    <BLUERAY>
        <TITLE>Contact</TITLE>
        <YEAR>1997</YEAR>
    </BLUERAY>
</CATALOG>

你的一行代码加上几个修改可能会让我们更接近解决方案。但是,如果我在“目录”下添加一个名为“DVD”的元素,这个新元素将被忽略。 - user834929
“忽略”指的是“排除”。无论 DVD 元素是添加到列表的开头还是结尾,都没有关系。但它应该包含在输出 XML 中。 - user834929
@user834929:我认为你可以使用深拷贝和preceding-sibling以及following-sibling轴来获取这些元素。但是我不确定这是否是最有效的方法(对于大型XML文档)。 - Grzegorz Szpetkowski
如果我做了和你一模一样的修改,它仍然无法工作。结果 XML 将包含蓝光和 DVD 元素,但 CD 元素未排序。 - user834929
你是对的,抱歉。尝试更改为CD[1]/preceding-sibling::*CD[last()]/following-sibling::* - Grzegorz Szpetkowski
问题在于,如果CD元素中存在非CD元素,则这些非CD元素将被排除在输出之外。只有当非CD元素存在于列表的开头或结尾时,此XSLT才有效。 - user834929

1

在只修改身份转换的基础上(这可能并不真正安全),我认为以下内容应该等同于@Tim的回答。

注意,我根本不提倡这种技术,除非你理解身份转换的一般行为。

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

    <xsl:template match="@* | node()">
        <xsl:copy>
            <xsl:apply-templates select="@* 
                | node()[not(self::CD[parent::CATALOG])]"/>
            <xsl:apply-templates select="CD[parent::CATALOG]">
                <xsl:sort select="TITLE"/>
            </xsl:apply-templates>
        </xsl:copy>
    </xsl:template>

</xsl:stylesheet>

或者,如果您关心其他元素DVDDVD2,您可以这样做:

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

    <xsl:template match="@* | node()">
        <xsl:copy>
            <xsl:apply-templates select="@*"/>
            <xsl:apply-templates select="CD[parent::CATALOG]">
                <xsl:sort select="TITLE"/>
            </xsl:apply-templates>
            <xsl:apply-templates select="node()
                [not(self::CD[parent::CATALOG])]"/>
        </xsl:copy>
    </xsl:template>

</xsl:stylesheet>

你能最终解释一下这个转换的问题吗? - Emiliano Poggi
嗯,那正是我的转换所做的。有什么问题吗? - Emiliano Poggi
如果在目录下没有CD元素,它就不会起作用。 - user834929
我明白了,只是因为我对我的答案的正确性感兴趣,所以我已经更改了它,即使你已经满意于@Tim的答案。 - Emiliano Poggi
你的代码在功能上是适当的。空行只是一个缺陷。我总是担心这些缺陷是否隐藏了其他未知的错误。无论如何,我不明白为什么你说strip-space和output-indent-yes不冲突。你XSLT中的xsl:output元素只是被简单地忽略了(因为xsl:strip-space)。即使我改变这两个元素的顺序。 - user834929
显示剩余24条评论

0

试试这个:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
  <xsl:output method="xml" indent="yes" />

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

  <xsl:template match="CATALOG">
    <xsl:copy>
      <xsl:apply-templates select="@* | *">
        <xsl:sort select="TITLE" />
      </xsl:apply-templates>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

这是标准的复制模板match="@* | node()",加上一个特殊情况,即对CATALOG节点指定排序条件。

请注意,我已经将第二个模板中apply-templates规则的选择器与标准复制模板略有不同(@* | *)。这是因为标准复制模板选择器还包括文本节点,然而文本节点没有TITLE元素,因此排序指令最终会将它们全部放在前面,看起来有点奇怪(试一下就知道了)。


这个XSLT将目录下的所有节点按照标题进行排序。没有标题的元素会放在列表的开头。实际上,这对我来说有点混乱,因为我的目标有点不同。无论如何,你提到了“文本节点”。这是关于从输出XML中删除空行吗? - user834929
你的目标是什么?文本节点是XML DOM中仅包含文本的节点。例如,1999 Grammy Nominees(在Title元素内部)是一个文本节点,但是节点之间的空格也是文本节点。使用node()(其中包括文本节点)意味着所有文本节点都会被复制到CATALOG元素的开头 - 试一下就知道了。 - Justin
我的目标是仅对 CATALOG 下的 CD 元素进行排序。(其他元素可以放置在列表的开头或结尾。) - user834929
基本上,Tim C的解决方案是合适的。唯一的问题是,我认为你可以解决的是输出XML包含太多的空行(空格)。 - user834929
@user834929:尝试使用 <xsl:strip-space elements="*"/> - Grzegorz Szpetkowski
@Grzegorz Szpetkowski:谢谢。这个想法有一半是好的。我无法成功地将缩进与这个元素结合使用。 - user834929

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