Doppelt auftretende Elemente ignorieren

(Auszug aus "XSLT Kochbuch" von Sal Mangano)

Problem

Sie wollen alle Knoten auswählen, die auf der Grundlage von Eindeutigkeitskriterien in einem bestimmten Kontext eindeutiger sind.

Lösung

XSLT 1.0

Das Auswählen eindeutiger Knoten ist eine gebräuchliche Anwendung der preceding- und preceding-sibling-Achsen. Wenn die Elemente, die Sie auswählen, nicht alle Geschwisterknoten sind, verwenden Sie preceding. Der folgende Code erzeugt eine eindeutige Liste der Produkte von SalesBySalesPerson.xml:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
  <xsl:template match="/">
    <products>
      <xsl:for-each select="//product[not(@sku=preceding::product/@sku)]">
        <xsl:copy-of select="."/>
      </xsl:for-each>
    </products>
  </xsl:template>
</xsl:stylesheet>

Wenn die Elemente alle Geschwister sind, verwenden Sie preceding-sibling:

<products>
  <product sku="10000" totalSales="10000.00"/>
  <product sku="10000" totalSales="990000.00"/>
  <product sku="10000" totalSales="1110000.00"/>
  <product sku="20000" totalSales="50000.00"/>
  <product sku="20000" totalSales="150000.00"/>
  <product sku="20000" totalSales="150000.00"/>
  <product sku="25000" totalSales="920000.00"/>
  <product sku="25000" totalSales="2920000.00"/>
  <product sku="30000" totalSales="5500.00"/>
  <product sku="30000" totalSales="115500.00"/>
  <product sku="70000" totalSales="10000.00"/>
</products>

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
  <xsl:template match="/products ">
    <products>
      <xsl:for-each select="product[not(@sku=preceding-sibling::product/@sku)]">
        <xsl:copy-of select="."/>
      </xsl:for-each>
    </products>
  </xsl:template>
</xsl:stylesheet>

Um preceding zu vermeiden, das ineffizient sein kann, gehen Sie bis zu den Vorfahren, die Geschwister sind. Dort benutzen Sie dann preceding-sibling und gehen anschließend wieder zu den Knoten, die Sie testen wollen:

<xsl:for-each select="//product[not(@sku=../preceding-sibling::*/product/@sku)]">
  <xsl:copy-of select="."/>
</xsl:for-each>

Falls Sie sicher sind, dass die Elemente sortiert sind, sodass doppelt auftretende Knoten nebeneinander stehen (wie bei den genannten Produkten), dann müssen Sie nur den unmittelbar vorangehenden Geschwisterknoten betrachten:

<xsl:for-each select="/salesperson/product[not(@name=preceding-sibling::product[1]/@name]">
  <!-- irgendetwas mit jedem eindeutig genannten Produkt tun -->
</xsl:for-each>

XSLT 2.0

Sie können dieses Problem in XSLT 2.0 als Gruppierungsproblem lösen. Verwenden Sie einfach for-each-group mit dem Eindeutigkeitskriterium als group-by-Wert. Setzen Sie den ersten Knoten in der aktuellen Gruppe als eindeutigen Knoten ein:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
  <xsl:template match="/">
    <products>
      <xsl:for-each-group select="//product" group-by="@sku">
        <xsl:copy-of select="current-group( )[1]"/>
      </xsl:for-each-group>
    </products>
 </xsl:template>
</xsl:stylesheet>

Diskussion

XSLT 1.0

Mit Hilfe der Erweiterungsfunktion node-set( ) können Sie auch Folgendes machen:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exslt=" http://exslt.org">
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
  <xsl:template match="/">
    <xsl:variable name="products">
      <xsl:for-each select="//product">
        <xsl:sort select="@sku"/>
        <xsl:copy-of select="."/>
      </xsl:for-each>
    </xsl:variable>
    <products>
      <xsl:for-each select="exslt:node-set($products)/product">
        <xsl:variable name="pos" select="position( )"/>
        <xsl:if test="$pos = 1 or not(@sku = $products/preceding-sibling::product[1]/@sku">
          <xsl:copy-of select="."/>
        </xsl:if>
      </xsl:for-each>
    </products>
  </xsl:template>
</xsl:stylesheet>

Ich habe jedoch noch nie bemerkt, dass diese Technik schneller wäre als die mit der preceding-Achse. Diese Technik ist in Situationen vorteilhaft, in denen der Duplikatstest nicht trivial ist. Stellen Sie sich beispielsweise einen Fall vor, bei dem Duplikate durch die Verkettung zweier Attribute ermittelt werden:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exslt=" http://exslt.org">
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
  <xsl:template match="/">
    <xsl:variable name="people">
      <xsl:for-each select="//person">
        <xsl:sort select="concat(@lastname,@firstname)"/>
        <xsl:copy-of select="."/>
      </xsl:for-each>
    </xsl:variable>
    <products>
      <xsl:for-each select="exslt:node-set($people/)person">
        <xsl:variable name="pos" select="position( )"/>
        <xsl:if test="$pos = 1 or concat(@lastname,@firstname) != concat(people/person[$pos - 1]/@lastname,people/person[$pos - 1]/@firstname)">
          <xsl:copy-of select="."/>
        </xsl:if>
      </xsl:for-each>
    </products>
  </xsl:template>
</xsl:stylesheet>

Wenn Sie versuchen, Duplikate zu entfernen, funktionieren die folgenden Beispiele nicht :

<xsl:template match="/">
  <products>
    <xsl:for-each select="//product[not(@sku=preceding::product[1]/@sku)]">
      <xsl:sort select="@sku"/>
      <xsl:copy-of select="."/>
    </xsl:for-each>
  </products>
</xsl:template>

Führen Sie keine Sortierung durch. So vermeiden Sie, dass Sie alle bis auf das unmittelbar vorhergehende Element betrachten müssen. Die Achse ist relativ zur Originalreihenfolge des Knotens im Dokument. Das gilt auch, wenn Sie preceding-sibling verwenden. Folgender Code ist ebenso fehlerhaft:

<xsl:template match="/">
  <xsl:variable name="products">
    <xsl:for-each select="//product">
      <!-- hier wurde sort entfernt -->
      <xsl:copy-of select="."/>
    </xsl:for-each>
  </xsl:variable>
  <products>
    <xsl:for-each select="exsl:node-set($products)/product">
      <xsl:sort select="@sku"/>
      <xsl:variable name="pos" select="position( )"/>
      <xsl:if test="$pos = 1 or @sku != $products/product[$pos - 1]/@sku">
        <xsl:copy-of select="."/>
      </xsl:if>
    </xsl:for-each>
  </products>
</xsl:template>

Dieser Code schlägt fehl, weil position( ) die Position nach der Sortierung zurückliefert, allerdings wurde gar nicht der Inhalt von $products sortiert, sondern stattdessen eine nicht erreichbare Kopie davon.

XSLT 2.0

Manchmal wollen Sie doppelt auftretende Elemente nur dann entfernen, wenn sie nebeneinander stehen. Stellen Sie sich beispielsweise eine Datenmenge vor, die aus einer Reihe von Messungen gewonnen wurde, die in vorgegebenen Zeitintervallen durchgeführt wurden. Wenn das System, das gemessen wurde, einigermaßen stabil ist, könnte es viele aufeinander folgende Messungen geben, die gleich sind. Man könnte diese aufeinander folgenden Duplikate entfernen, ohne andere identische Messwerte zu löschen, die weiter hinten in der Folge auftauchen.

Für dieses Problem würden Sie ebenfalls xsl:for-each-group verwenden, allerdings mit group-adjacent anstatt mit group-by:

<measurements>
  <data time="12:00:00" value="1.0"/>
  <data time="12:00:01" value="1.0"/>
  <data time="12:00:02" value="1.1"/>
  <data time="12:00:03" value="1.1"/>
  <data time="12:00:04" value="1.0"/>
  <data time="12:00:05" value="1.1"/>
  <data time="12:00:06" value="1.2"/>
  <data time="12:00:07" value="1.3"/>
  <data time="12:00:08" value="1.4"/>
  <data time="12:00:09" value="1.6"/>
  <data time="12:00:10" value="1.9"/>
  <data time="12:00:11" value="2.1"/>
  <data time="12:00:12" value="1.7"/>
  <data time="12:00:13" value="1.5"/>
</measurements>

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
  <xsl:template match="/measurements">
    <xsl:copy>
      <xsl:for-each-group select="data" group-adjacent="@value">
        <xsl:copy-of select="current-group( )[1]"/>
      </xsl:for-each-group>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

<!--Ausgabe -->
<measurements>
  <data time="12:00:00" value="1.0"/>
  <data time="12:00:02" value="1.1"/>
  <data time="12:00:04" value="1.0"/>
  <data time="12:00:05" value="1.1"/>
  <data time="12:00:06" value="1.2"/>
  <data time="12:00:07" value="1.3"/>
  <data time="12:00:08" value="1.4"/>
  <data time="12:00:09" value="1.6"/>
  <data time="12:00:10" value="1.9"/>
  <data time="12:00:11" value="2.1"/>
  <data time="12:00:12" value="1.7"/>
  <data time="12:00:13" value="1.5"/>
</measurements>

Siehe auch

Dave Parsons XSLT FAQ beschreibt eine Lösung, die Schlüssel verwendet. Außerdem finden Sie dort Lösungen für verwandte Probleme.

  

<< zurück vor >>

 

 

 

Tipp der data2type-Redaktion:
Zum Thema XSLT bieten wir auch folgende Schulungen zur Vertiefung und professionellen Fortbildung an:

Copyright © 2006 O'Reilly Verlag GmbH & Co. KG
Für Ihren privaten Gebrauch dürfen Sie die Online-Version ausdrucken.
Ansonsten unterliegt dieses Kapitel aus dem Buch "XSLT Kochbuch" denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

O'Reilly Verlag GmbH & Co. KG, Balthasarstraße 81, 50670 Köln, kommentar(at)oreilly.de