【问题标题】:Sort XML based on name, attribute name, attribute value and node value根据名称、属性名称、属性值和节点值对 XML 进行排序
【发布时间】:2017-11-13 12:53:30
【问题描述】:

我正在尝试基于 4 个维度对 xml 文件进行排序 - 节点名、属性名、属性值,最后基于节点值。

我的 XML

<NodeRoot>
    <NodeA class="3">
        <NodeB>
            <NodeC abc="1">103</NodeC>
            <NodeD>103</NodeD>
            <NodeC pqr="2">101</NodeC>
            <NodeC pqr="1">102</NodeC>
            <NodeD>101</NodeD>
        </NodeB>
    </NodeA>
    <NodeA class="1">
        <NodeGroup>
            <NodeC name="z" asc="2">103</NodeC>
            <NodeC name="b">101</NodeC>
            <NodeC name="a">102</NodeC>
        </NodeGroup>
    </NodeA>
</NodeRoot>

我的 XSL

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output encoding="utf-8" method="xml" omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="@*|node()">
    <xsl:copy>
        <xsl:apply-templates select="@*">
            <xsl:sort select="local-name()"/>
            <xsl:sort select="."/>
        </xsl:apply-templates>
        <xsl:apply-templates select="node()">
            <xsl:sort select="local-name()"/>
            <xsl:sort select="."/>
        </xsl:apply-templates>
    </xsl:copy>
</xsl:template>
</xsl:stylesheet>

电流输出

<NodeRoot>
   <NodeA class="1">
      <NodeGroup>
         <NodeC name="b">101</NodeC>
         <NodeC name="a">102</NodeC>
         <NodeC asc="2" name="z">103</NodeC>
      </NodeGroup>
   </NodeA>
   <NodeA class="3">
      <NodeB>
         <NodeC pqr="2">101</NodeC>
         <NodeC pqr="1">102</NodeC>
         <NodeC abc="1">103</NodeC>
         <NodeD>101</NodeD>
         <NodeD>103</NodeD>
      </NodeB>
   </NodeA>
</NodeRoot>

预期结果

<NodeRoot>
   <NodeA class="1">
      <NodeGroup>
         <NodeC asc="2" name="z">103</NodeC>
         <NodeC name="a">102</NodeC>
         <NodeC name="b">101</NodeC>
      </NodeGroup>
   </NodeA>
   <NodeA class="3">
      <NodeB>
         <NodeC abc="1">103</NodeC>
         <NodeC pqr="1">102</NodeC>
         <NodeC pqr="2">101</NodeC>
         <NodeD>101</NodeD>
         <NodeD>103</NodeD>
      </NodeB>
   </NodeA>
</NodeRoot>

测试 XSLT --> http://xsltransform.net/naZXpY7

【问题讨论】:

  • 我可以看到您的预期结果是如何按属性值排序的,因为 NodeGroup/NodeC 元素按 name 属性的 abz 值排序。此外,NodeA 元素按class 属性的13 值排序。我还可以看到它是如何按节点值排序的,因为NodeB/NodeC 元素(没有属性)是按101103 值排序的。但是,我看不到输出是如何按节点名称或属性名称排序的。
  • 如果一个元素有多个属性,你会发生什么?
  • @BenL - 用更好的示例 xml 更新了问题
  • @TimC - 用更好的示例 xml 更新了问题

标签: xml xslt xslt-1.0


【解决方案1】:

您目前正在按本地名称和值对元素的所有属性进行排序,然后是所有子项(再次按本地名称和字符串值)。

到目前为止,一切都很好。

您面临的一个困难是按“属性名称”排序的确切含义。从您的示例中,您似乎希望元素按字母顺序按属性名称列表排序,以便 NodeGroup 元素的子元素的排序键是

'NodeC', 'asc name', '2 z', 103
'NodeC', 'name', 'a', 102
'NodeC', 'name', 'b', 201

下一个困难是没有明显的方法可以从 XPath 1.0 表达式中获取值“asc name”,其中 NodeGroup 元素的第一个 NodeC 作为上下文节点。当然,可以生成字符串,但它需要调用命名模板。 (或者,更准确地说:如果没有这样的调用,我不知道如何生成它。)

XSLT 2.0 解决方案

XSLT 2.0 中的问题相对简单;以下片段显示了关键部分:

<xsl:template match="@*|node()">
  <xsl:copy>
    <xsl:apply-templates select="@*">
      <xsl:sort select="local-name()"/>
      <xsl:sort select="."/>
    </xsl:apply-templates>
    <xsl:apply-templates select="node()">
      <xsl:sort select="local-name()"/>
      <xsl:sort select="string-join(local:key2(.), ' ')"/>
      <xsl:sort select="string-join(local:key3(.), ' ')"/>
      <xsl:sort select="." data-type="number"/>
    </xsl:apply-templates>
  </xsl:copy>
</xsl:template>

<xsl:function name="local:key2"  as="xs:string*">
  <xsl:param name="e" as="node()"/>
  <xsl:for-each select="$e/@*">
    <xsl:sort select="local-name()"/>
    <xsl:sort select="string()"/>
    <xsl:value-of select="local-name()"/>
  </xsl:for-each>
</xsl:function>

<xsl:function name="local:key3"  as="xs:string*">
  <xsl:param name="e" as="node()"/>
  <xsl:for-each select="$e/@*">
    <xsl:sort select="local-name()"/>
    <xsl:sort select="string()"/>
    <xsl:value-of select="string()"/>
  </xsl:for-each>
</xsl:function>

这种通用方法也可以在 XSLT 1.0 中用于用户定义函数的 EXSLT 扩展。

XSLT 1.0 中带有 EXSLT 函数的解决方案

如果您的 XSLT 1.0 处理器支持 EXSLT 样式的用户定义函数,您可以在 XSLT 1.0 中执行类似的操作。 (我最初的尝试失败了,但是当我记得将extension-element-prefixes 属性添加到样式表元素时,错误就消失了。)

<xsl:stylesheet version="1.0"
                extension-element-prefixes="func"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:xs="http://www.w3.org/2001/XMLSchema"
                xmlns:func="http://exslt.org/functions"
                xmlns:local="http://example.com/nss/dummy">

  <xsl:output encoding="utf-8" 
              method="xml" 
              omit-xml-declaration="yes" 
              indent="yes"/>
  <xsl:strip-space elements="*"/>
  <xsl:template match="@*|node()">
    <xsl:copy>
      <xsl:apply-templates select="@*">
        <xsl:sort select="local-name()"/>
        <xsl:sort select="."/>
      </xsl:apply-templates>
      <xsl:apply-templates select="node()">
        <xsl:sort select="local-name()"/>
        <xsl:sort select="local:key2(.)"/>
        <xsl:sort select="local:key3(.)"/>
        <xsl:sort select="." data-type="number"/>
      </xsl:apply-templates>
    </xsl:copy>
  </xsl:template>

  <func:function name="local:key2">
    <xsl:param name="e" select="."/>

    <func:result>
      <xsl:for-each select="$e/@*">
        <xsl:sort select="local-name()"/>
        <xsl:sort select="string()"/>
        <xsl:value-of select="concat(local-name(), ' ')"/>
      </xsl:for-each>
    </func:result>
  </func:function>

  <func:function name="local:key3">
    <xsl:param name="e" select="."/>
    <func:result>
      <xsl:for-each select="$e/@*">
        <xsl:sort select="local-name()"/>
        <xsl:sort select="string()"/>
        <xsl:value-of select="concat(string(), ' ')"/>
      </xsl:for-each>
    </func:result>
  </func:function>
</xsl:stylesheet>

当使用 xsltproc 在您的输入上运行时,这会产生所需的输出。

您还可以使用节点集扩展在 XSLT 1.0 中做一些聪明的事情。

未扩展的 XSLT 1.0 中的两阶段管道

但我认为在未扩展的 XSLT 1.0 中解决此问题的最简单方法是将两个样式表连接在一起。第一个为每个元素添加两个属性,以提供排序键 2 和 3。(调整命名模板以使它们执行您想要的操作。)

<xsl:stylesheet version="1.0" 
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:PJ="http://example.com/PankajJaju">
  <xsl:output encoding="utf-8" 
              method="xml" 
              omit-xml-declaration="yes" 
              indent="yes"/>
  <xsl:strip-space elements="*"/>
  <xsl:template match="@*|node()">
    <xsl:copy>
      <xsl:apply-templates select="@*"/>
      <xsl:if test="self::*">
        <xsl:attribute name="PJ:attribute-names" 
                       namespace="http://example.com/PankajJaju">
          <xsl:call-template name="attribute-name-list"/>
        </xsl:attribute>
        <xsl:attribute name="PJ:attribute-values" 
                       namespace="http://example.com/PankajJaju">
          <xsl:call-template name="attribute-value-list"/>
        </xsl:attribute>
      </xsl:if>
      <xsl:apply-templates select="node()"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template name="attribute-name-list">
    <xsl:for-each select="@*">
      <xsl:sort select="local-name()"/>
      <xsl:sort select="string()"/>
      <xsl:value-of select="concat(local-name(), ' ')"/>
    </xsl:for-each>
  </xsl:template>
  <xsl:template name="attribute-value-list">
    <xsl:for-each select="@*">
      <xsl:sort select="local-name()"/>
      <xsl:sort select="string()"/>
      <xsl:value-of select="concat(string(), ' ')"/>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

我已将它们放入命名空间以减少名称冲突的可能性。

第二个使用排序键执行实际排序并抑制临时属性。

<xsl:stylesheet version="1.0" 
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:PJ="http://example.com/PankajJaju">
  <xsl:output encoding="utf-8" 
              method="xml" 
              omit-xml-declaration="yes" 
              indent="yes"/>
  <xsl:strip-space elements="*"/>
  <xsl:template match="@*|node()">
    <xsl:copy>
      <xsl:apply-templates select="@*">
        <xsl:sort select="local-name()"/>
        <xsl:sort select="."/>
      </xsl:apply-templates>
      <xsl:apply-templates select="node()">
        <xsl:sort select="local-name()"/>
        <xsl:sort select="@PJ:attribute-names"/>
        <xsl:sort select="@PJ:attribute-values"/>
        <xsl:sort select="."/>
      </xsl:apply-templates>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="@PJ:attribute-names | @PJ:attribute-values"/>
</xsl:stylesheet>

可以使用您喜欢的任何技术将这些流水线连接在一起。例如,从 bash 命令行使用 xsltproc,并将名称 p1.xsl 和 p2.xsl 分配给管道样式表 1 和 2 ...

xsltproc p1.xsl input.xml | xsltproc p2.xsl -

这会产生你想要的输出。

【讨论】:

  • XSLT 2.0 抛出错误,但 XSLT 1.0 有效。您的两部分解决方案很巧妙。一个小问题,我添加了exclude-result-prefixes="PJ" 来抑制根节点中的命名空间,但它不起作用。
  • exclude-result-prefixes 仅适用于文字结果元素;它不会影响 xsl:copy 指令产生的输出中包含的命名空间。
  • 这是一个小问题,根本不会打扰我。感谢您的解决方案。
  • 您还可以在 XSLT 1.0 中使用节点集扩展做一些聪明的事情。 - 我已根据您的建议创建了一个,它会返回预期的结果。不确定它的效率如何,但它在大多数情况下都有效。
【解决方案2】:

基于@C。 M. Sperberg-McQueen 关于节点集扩展的建议以及在https://www.xml.com/pub/a/2003/07/16/nodeset.html 的示例的帮助下,我想出了一个合并 McQueen 的 2 个 xsls 的 xsl。

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exslt="http://exslt.org/common" exclude-result-prefixes="exslt" xmlns:PJ="http://example.com/PankajJaju">
    <xsl:output encoding="utf-8" method="xml" omit-xml-declaration="yes" indent="yes"/>
    <xsl:strip-space elements="*"/>

    <xsl:template match="node()|@*" name="first-pass">
        <xsl:copy>
            <xsl:apply-templates select="@*"/>
            <xsl:if test="self::*">
                <xsl:attribute name="PJ:attribute-names" namespace="http://example.com/PankajJaju">
                    <xsl:call-template name="attribute-name-list"/>
                </xsl:attribute>
                <xsl:attribute name="PJ:attribute-values" namespace="http://example.com/PankajJaju">
                    <xsl:call-template name="attribute-value-list"/>
                </xsl:attribute>
            </xsl:if>
            <xsl:apply-templates select="node()"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template name="attribute-name-list">
        <xsl:for-each select="@*">
            <xsl:sort select="local-name()"/>
            <xsl:sort select="string()"/>
            <xsl:value-of select="concat(local-name(), ' ')"/>
        </xsl:for-each>
    </xsl:template>
    <xsl:template name="attribute-value-list">
        <xsl:for-each select="@*">
            <xsl:sort select="local-name()"/>
            <xsl:sort select="string()"/>
            <xsl:value-of select="concat(string(), ' ')"/>
        </xsl:for-each>
    </xsl:template>

    <xsl:template match="/">
        <xsl:variable name="process-one">
            <xsl:call-template name="first-pass"/>
        </xsl:variable>
        <xsl:apply-templates select="exslt:node-set($process-one)" mode="second-pass"/>
    </xsl:template>

    <xsl:template match="@*|node()" mode="second-pass">
        <xsl:copy>
            <xsl:apply-templates select="node()|@*" mode="second-pass"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="*" mode="second-pass">
        <xsl:copy>
            <xsl:apply-templates select="@*" mode="second-pass">
                <xsl:sort select="local-name()"/>
                <xsl:sort select="."/>
            </xsl:apply-templates>
            <xsl:apply-templates select="node()" mode="second-pass">
                <xsl:sort select="local-name()"/>
                <xsl:sort select="@PJ:attribute-names"/>
                <xsl:sort select="@PJ:attribute-values"/>
                <xsl:sort select="."/>
            </xsl:apply-templates>
        </xsl:copy>
    </xsl:template>        
    <xsl:template match="@PJ:attribute-names | @PJ:attribute-values" mode="second-pass"/>
</xsl:stylesheet>

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多