XSLT对属性进行三级分组

4

好的,我知道这个问题的变体已经被问答过了;我一整天都在阅读它们,但我仍然卡住了。所以,让我们开始:

我需要从一些XML中创建一个HTML摘要列表。

给定以下XML:

<Root><!-- yes, I know I don't need a 'Root' element! Legacy code... -->
  <Plans>
    <Plan AreaID="1" UnitID="83">
      <Part ID="9122" Name="foo" />
      <Part ID="9126" Name="bar" />
    </Plan>
    <Plan AreaID="1" UnitID="86">
      <Part ID="8650" Name="baz" />
    </Plan>
    <Plan AreaID="2" UnitID="26">
      <Part ID="215" Name="quux" />
    </Plan>
    <Plan AreaID="1" UnitID="95">
      <Part ID="7350" Name="meh" />
    </Plan>
  </Plans>
</Root>

我需要发射:
<ol>
  <li>Area 1: 
    <ol><!-- units in Area 1 -->
      <li>Unit 83: 
        <ol>
          <li>Part 9122 (foo)</li>
          <li>Part 9126 (bar)</li>
        </ol>
      </li>
      <li>Unit 86: 
        <ol>
          <li>Part 8650 (baz)</li>
        </ol>
      <li>Unit 95: 
        <ol>
          <li>Part 7350 (meh)</li>
        </ol>
      </li>
    </ol><!-- /units in Area 1-->
  </li>
  <li>Area 2: 
    <ol><!-- units in Area 2 -->
      <li>Unit 26: 
        <ol>
          <li>Part 215 (quux)</li>
        </ol>
      </li>
    </ol><!-- /units in Area 2-->
  </li>
</ol>

我已经将外部分组处理好了——我可以获取区域1和2的顶级列表元素。但是我无法获取区域中单位的序列,要么没有输出,要么重复相同的值。我甚至还没开始处理零件层面 :-(
我一直在使用这样的样式表:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
<xsl:output method="html" indent="yes"/>

<xsl:key name="kAreaID" match="Plan" use="@AreaID" />
<xsl:key name="kUnitID" match="Plan" use="@UnitID" />

<xsl:template match="/Root/Plans">
<html><head><title>test grouping</title></head>
<body>
  <ol>
    <xsl:for-each select="./Plan[generate-id(.) = 
                      generate-id( key( 'kAreaID', @AreaID )[1] )]"
    >
      <xsl:sort order="ascending" select="./@AreaID" />
      <li>Area <xsl:value-of select="@AreaID"/>: 
        <ol>
          <xsl:for-each select="key( 'kUnitID', @UnitID )">
            <li>Unit <xsl:value-of select="@UnitID"/>: 
              <ol>
                <li>(Parts go here...)</li>
              </ol>
            </li>
          </xsl:for-each>
        </ol>
      </li>
    </xsl:for-each>
  </ol>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

非常感谢您的帮助!


谢谢,这让我更接近了。我还有一点困难——第二级元素重复而不是分组,所以我得到区域1 单元83 零件9122 单元83 零件9126 单元86 零件8650 而不是 区域1 单元83 零件9122 零件9126 单元86 零件8650 但比之前更接近了! - Val
哎呀,我发现评论的格式与帖子不一样 :( - Val
请检查我的修改方案。 :) - Tomalak
4个回答

18

这里是您正在寻找的 Muenchian 分组解决方案。

从您提供的原始 XML 开始,我认为按 AreaID 进行分组就足够了,但事实证明还需要通过 UnitID 进行第二次分组。

这是我修改过的 XSLT 1.0 解决方案。它并不比原始解决方案复杂很多:

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

  <xsl:key name="kPlanByArea" match="Plan" 
           use="@AreaID" />
  <xsl:key name="kPlanByAreaAndUnit" match="Plan" 
           use="concat(@AreaID, ',', @UnitID)" />

  <xsl:template match="/">
    <xsl:apply-templates select="Root/Plans" />
  </xsl:template>

  <!-- main template -->
  <xsl:template match="Plans">
    <ol>
      <!-- group by '{@AreaID}' (note the template mode!) -->
      <xsl:apply-templates mode="area-group" select="
        Plan[
          generate-id()
          =
          generate-id(
            key('kPlanByArea', @AreaID)[1]
          )
        ]
      ">
        <xsl:sort select="@AreaID" data-type="number" />
      </xsl:apply-templates>
    </ol>
  </xsl:template>

  <!-- template to output each '{@AreaID}' group -->
  <xsl:template match="Plan" mode="area-group">
    <li>
      <xsl:value-of select="concat('Area ', @AreaID)" />
      <ol>
        <!-- group by '{@AreaID},{@UnitID}' -->
        <xsl:apply-templates mode="unit-group" select="
          key('kPlanByArea', @AreaID)[
            generate-id()
            =
            generate-id(
              key('kPlanByAreaAndUnit', concat(@AreaID, ',', @UnitID))[1]
            )
          ]
        ">
          <xsl:sort select="@UnitID" data-type="number" />
        </xsl:apply-templates>
      </ol>
    </li>
  </xsl:template>

  <!-- template to output each '{@AreaID},{@UnitID}' group -->
  <xsl:template match="Plan" mode="unit-group">
    <li>
      <xsl:value-of select="concat('Unit ', @UnitID)" />
      <ol>
        <xsl:apply-templates select="
          key('kPlanByAreaAndUnit', concat(@AreaID, ',', @UnitID))/Part
        ">
          <xsl:sort select="@UnitID" data-type="number" />
        </xsl:apply-templates>
      </ol>
    </li>
  </xsl:template>

  <!-- template to output Parts into a list -->
  <xsl:template match="Part">
    <li>
      <xsl:value-of select="concat('Part ', @ID, ' (', @Name ,')')" />
    </li>
  </xsl:template>

</xsl:stylesheet>

由于您的XML缺少它,我添加了一个UnitID进行分组:

<Plan AreaID="1" UnitID="86">
  <Part ID="8651" Name="zzz" />
</Plan>

以下是输出结果:

<ol>
  <li>Area 1
    <ol>
      <li>Unit 83
        <ol>
          <li>Part 9122 (foo)</li>
          <li>Part 9126 (bar)</li>
        </ol>
      </li>
      <li>Unit 86
        <ol>
          <li>Part 8650 (baz)</li>
          <li>Part 8651 (zzz)</li>
        </ol>
      </li>
      <li>Unit 95
        <ol>
          <li>Part 7350 (meh)</li>
        </ol>
      </li>
    </ol>
  </li>
  <li>Area 2
    <ol>
      <li>Unit 26
        <ol>
          <li>Part 215 (quux)</li>
        </ol>
      </li>
    </ol>
  </li>
</ol>

由于您似乎对XSL键有困难,这里是我的尝试解释:

<xsl:key>与许多编程语言中的关联数组(映射、哈希或任何其他称呼)完全等效。 下面是一个例子:

<xsl:key name="kPlanByAreaAndUnit" match="Plan" 
         use="concat(@AreaID, ',', @UnitID)" />

生成一个数据结构,可以用 JavaScript 表示为:

var kPlanByAreaAndUnit = {
  "1,83": ['array of all <Plan> nodes with @AreaID="1" and @UnitID="83"'],
  "1,86": ['array of all <Plan> nodes with @AreaID="1" and @UnitID="86"'],
  /* ... */
  "1,95": ['array of all <Plan> nodes with @AreaID="1" and @UnitID="95"']
};

访问数据结构的函数被称为 key()。因此,这个XPath表达式是:

key('kPlanByAreaAndUnit', concat(@AreaID, ',', @UnitID))

等同于逻辑操作符(在JavaScript中同样如此):

kPlanByAreaAndUnit[this.AreaID + ',' + this.UnitID];

返回匹配给定键字符串的所有节点(更正确地说,是一个节点集合)。这个节点集可以像XSLT中的任何其他节点集一样使用,例如像通过“传统”的XPath检索到的节点集。这意味着您可以对其应用条件(谓词):

<!-- first node only... -->
key('kPlanByAreaAndUnit', concat(@AreaID, ',', @UnitID))[1]

<!-- nodes that have <Part> children only... -->
key('kPlanByAreaAndUnit', concat(@AreaID, ',', @UnitID))[Part]

或将其用作XPath导航的基础:

<!-- the actual <Part> children of matched nodes... -->
key('kPlanByAreaAndUnit', concat(@AreaID, ',', @UnitID))/Part

等等。 这也意味着我们可以将它用作<xsl:apply-templates>的“select”表达式,并且我们可以将其用作分组的基础。 这就引导我们进入上述样式表的核心(如果你理解了这个,那么你也理解了解决方案的其余部分):

key('kPlanByArea', @AreaID)[
  generate-id()
  =
  generate-id(
    key('kPlanByAreaAndUnit', concat(@AreaID, ',', @UnitID))[1]
  )
]

在 JavaScript 中,这可以表达为:

// the result will be a node-set, so we prepare an array
var selectedNodes = [];

// "key('kPlanByArea', @AreaID)"
var nodeSet = kPlanByArea[this.AreaID];

// "[...]" - the [] actually triggers a loop that applies 
// the predicate expression to all nodes in the set, so we do:
for (var i = 0; i < nodeSet.length; i++) {
   // use the current node for any calculations
   var c = nodeSet[i];
   if (
     // if the current node === the *first* node in kPlanByAreaAndUnit...
     generateId(c)
     ==
     generateId(kPlanByAreaAndUnit[c.AreaID + ',' + c.UnitID][0])
   ) {
     // ...include it in the resulting selection
     selectedNodes.push(c)
   }
}

表达式完成后,只选择具有给定“AreaID, UnitID”组合的相应第一节点 - 实际上我们已经按照它们的“AreaID, UnitID”组合进行了分组。

将模板应用于此节点集会导致每个组合仅出现一次。我的<xsl:template match="Plan" mode="unit-group">然后再次检索完整列表,以实现每个组的完整输出。

我希望使用JavaScript来解释这个概念是一个有用的想法。


嗨,我希望你能出现!(在研究这个问题时,我读了很多你的东西。)示例不清楚;它是一个3级目标,但只在2级上进行分组。我没有展示相同的UnitID也可以出现在单独的Plan元素中,就像AreaID一样。因此,我需要将这些UnitIDs聚合到单个组中,就像AreaIDs一样。此外,你的构造承担了大部分工作,<xsl:apply-templates select="key('kPlanByArea', @AreaID)">,仍然让我头疼。我的递归解决方案有效,所以我倾向于暂时不去管它。谢谢!(仍然讨厌XSL!) - Val
1
我已经修改了解决方案,使其进行双向分组,并添加了一些解释,这可能会让你的头痛消失。由于我认为代码比文字更清晰地传达了含义,所以我尝试使用JavaScript来模拟解释,而不是用英语写出来。我知道xsl:key概念需要一些时间才能理解,但一旦理解了,你甚至可能会想知道为什么要花这么长时间。;-) - Tomalak
哦,顺便说一下:如果您的文档中有多个<Plans>元素,则上述解决方案将失败。如果可能发生这种情况,我们需要引入一个额外的检查,请告诉我是否有必要。 - Tomalak
3
同意,这是我在研究了几天后看到的关于xsl:key的最好解释!其他的都要么是手摇(“只需写这个,然后就有魔法!”),要么是学术委员会成员为规范撰写的过度冗长的规范术语。你提供的JavaScript类比非常棒,我终于可以阅读key()语句并理解它们的含义了。虽然我还没有足够的声望来为你点赞,但我一定会将其标记为答案,尽管我仍然坚持我自己的方法(已通过用户验收,在生产环境中:-)。感谢你的帮助! - Val
关于计划,没有其他的计划元素,这就是为什么我不需要前任腐败政权留下的根元素。总有一天我会找到用途;与此同时,我已经开始解决下一个问题了。 - Val

1

我认为你根本不需要使用kUnitID键。相反,替换以下行...

<xsl:for-each select="key( 'kUnitID', @UnitID )">

使用这行代码代替,它应该循环遍历所有与当前AreaID匹配的部分

<xsl:for-each select="key( 'kAreaID', @AreaID )">

在这个循环内,对于你的(部分放在这里...)代码,你可以简单地循环遍历这些部分。
<xsl:for-each select="Part">
   <li>Part (<xsl:value-of select="@ID" />)</li>
</xsl:for-each>

非常好!但是,我得到了与上面类似的结果——第二级元素重复而不是分组,所以我得到了区域1 单元83 零件9122 单元83 零件9126 单元86 零件8650 而不是 区域1 单元83 零件9122 零件9126 单元86 零件8650 我会继续努力解决它。感谢您的帮助! - Val

1

好吧,我暂时放弃了键和Muenchian分组。我几乎不理解它,而且对它进行破解并没有产生期望的结果。我虽然理解递归,但是我选择了这种递归方法来产生所需的输出。我在http://www.biglist.com/lists/xsl-list/archives/200412/msg00865.html上找到了答案。

讨论线程警告说性能在大输入方面会受到影响,与Muenchian方法相比,下面的解决方案冗长而重复(我可能可以重构它,使其更小,更难理解;-),但是1)它实际上对我有用,2)对于我的目前问题,输入集相当小,最多只有十几个底层零件节点。

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <!-- recursive grouping http://www.biglist.com/lists/xsl-list/archives/200412/msg00865.html -->

  <xsl:template match="//Plans">
    <html>
      <head>
        <title>test grouping</title>
      </head>
      <body>
        <ol>
          <xsl:call-template name="PlanGrouping">
            <xsl:with-param name="list" select="Plan"/>
          </xsl:call-template>
        </ol>
      </body>
    </html>
  </xsl:template>

  <xsl:template name="PlanGrouping">
    <xsl:param name="list"/>
    <!-- Selecting the first Area ID as group identifier and the group itself-->
    <xsl:variable name="group-identifier" select="$list[1]/@AreaID"/>
    <xsl:variable name="group" select="$list[@AreaID = $group-identifier]"/>
    <!-- Do some work for the group -->
    <li>
      Area <xsl:value-of select="$group-identifier"/>:
      <ol>
        <xsl:call-template name="AreaGrouping">
          <xsl:with-param name="list" select="$list[(@AreaID = $group-identifier)]"/>
        </xsl:call-template>
      </ol>
    </li>
    <!-- If there are other groups left, calls itself -->
    <xsl:if test="count($list)>count($group)">
      <xsl:call-template name="PlanGrouping">
        <xsl:with-param name="list" select="$list[not(@AreaID = $group-identifier)]"/>
      </xsl:call-template>
    </xsl:if>
  </xsl:template>

  <xsl:template name="AreaGrouping">
    <xsl:param name="list"/>
    <!-- Selecting the first Unit ID as group identifier and the group itself-->
    <xsl:variable name="group-identifier" select="$list[1]/@UnitID"/>
    <xsl:variable name="group" select="$list[@UnitID = $group-identifier]"/>
    <!-- Do some work for the group -->
    <li>
      Unit <xsl:value-of select="$group-identifier"/>:
      <ol>
        <xsl:call-template name="Parts">
          <xsl:with-param name="list" select="$list[(@UnitID = $group-identifier)]"/>
        </xsl:call-template>
      </ol>
    </li>
    <!-- If there are other groups left, calls itself -->
    <xsl:if test="count($list)>count($group)">
      <xsl:call-template name="AreaGrouping">
        <xsl:with-param name="list" select="$list[not(@UnitID = $group-identifier)]"/>
      </xsl:call-template>
    </xsl:if>
  </xsl:template>

  <xsl:template name="Parts">
    <xsl:param name="list"/>
    <xsl:for-each select="$list/Part">
      <li>
        Part <xsl:value-of select="@ID"/> (<xsl:value-of select="@Name"/>)
      </li>
    </xsl:for-each>
  </xsl:template>

</xsl:stylesheet>

+1 实际上,这不是一个糟糕的解决方案。虽然不太优雅,但没有理由将其从生产中删除。 ;) 然而,在“<xsl:if test="count($list)>count($group)">”下面一行似乎存在复制粘贴错误。 - Tomalak
为什么我看不到复制/粘贴错误呢?有两个这样的部分--在每个分组级别的模板中,测试会检查该级别是否还有剩余项目,如果有,则用列表减去当前元素调用它本身。我错过了什么吗? - Val

0

这个代码可以实现你想要的功能,但是使用的是递归而不是分组。抱歉我还在学习如何使用分组:

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

  <xsl:key name="kAreaID" match="Plan" use="@AreaID" />
  <xsl:key name="kUnitID" match="Plan" use="@UnitID" />

  <xsl:template match="/Root/Plans">
    <html>
      <head>
        <title>test grouping</title>
      </head>
      <body>
        <ol>
          <xsl:for-each select="./Plan[generate-id(.) = 
                      generate-id( key( 'kAreaID', @AreaID )[1] )]"    >
            <xsl:sort order="ascending" select="./@AreaID" />
            <xsl:variable name="curArea" select="@AreaID"/>

            <li>
              Area <xsl:value-of select="$curArea"/>:
              <ol>
                <xsl:for-each select="ancestor::Root/Plans/Plan[@AreaID = $curArea]">
                  <xsl:variable name="curUnit" select="@UnitID"/>
                  <li>
                    Unit <xsl:value-of select="$curUnit"/>:
                    <ol>
                        <xsl:for-each select="ancestor::Root/Plans/Plan[@AreaID = $curArea and @UnitID = $curUnit]/Part">
                          <li>
                            Part <xsl:value-of select="concat(@ID, '  (', @Name, ')')"/>
                          </li>
                        </xsl:for-each>
                    </ol>
                  </li>
                </xsl:for-each>
              </ol>
            </li>
          </xsl:for-each>
        </ol>
      </body>
    </html>
  </xsl:template>
</xsl:stylesheet>

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