Teilstrings vom Ende eines Strings suchen

(Auszug aus "XSLT Kochbuch" von Sal Mangano)

Problem

XSLT besitzt keine Funktionen, um Strings in umgekehrter Richtung zu durchsuchen.

Reguläre Ausdrücke verwenden

Dieses Kapitel stellt eines der bevorzugten Werkzeuge gestandener Programmierer für aufwändigere Stringmanipulationen vor: reguläre Ausdrücke (auch als Regex – von englisch: regular expressions – bezeichnet). Die Aufnahme von Regex-Fähigkeiten in XSLT gehörte zur Top-10-Liste praktisch jedes mir bekannten XSLT-Entwicklers. Dieser Abschnitt ist für solche Entwickler gedacht, die noch nicht das Vergnügen hatten, mit regulären Ausdrücken zu arbeiten, oder die sich davon eingeschüchtert fühlen. Dies ist keine umfassende Referenz, sollte aber für den Anfang genügen.

Ein regulärer Ausdruck ist ein String, der ein Muster kodiert, das in einem anderen String gefiltert werden kann. Das einfachste Muster ist der String selbst – das bedeutet, der String »foo« kann als regulärer Ausdruck verwendet werden. Er filtert den String »foobar«, beginnend mit dem ersten Zeichen. Die wahre Stärke von regulären Ausdrücken zeigt sich jedoch erst dann, wenn Sie damit beginnen, die besonderen Metazeichen einzusetzen, die von der Sprache erkannt werden.

Die wichtigsten Metazeichen sind diejenigen, mit denen Platzhalter oder Jokerzeichen konstruiert werden.

  • Ein Punkt (.) entspricht einem einzelnen Zeichen.
  • Eine Zeichenklasse ([aeiou], [a-z] oder [a-zA-Z]) filtert eine Liste, einen Bereich oder eine Kombination aus Listen und Bereichen von Zeichen.
  • Manche gebräuchlichen Zeichenklassen tragen besondere Abkürzungen. Zum Beispiel ist \s eine Abkürzung für Whitespace-Zeichen, einschließlich Leerzeichen, Tabulator, Newline und Carriage Return, \d ist die Kurzform von [0–9]. Wenn es eine Backslash-Abkürzung für eine Zeichenklasse gibt, dann ist es häufig der Fall, dass die großgeschriebene Version eine Umkehrung bedeutet. So filtert zum Beispiel \S Nicht-Whitespace und \D eine Nicht-Ziffer. Dies ist allerdings nicht allgemein gültig. Beispielsweise filtert \n zwar ein Newline, allerdings bedeutet \N im Gegenzug nicht, dass es Nicht-Newline filtert (das gilt auch für \t – Tabulator und \r – Carriage Return).
  • Man kann eine Zeichenklasse negieren, indem man sie mit einem ^ beginnt. Zum Beispiel filtert [^aeiou] alle Zeichen mit Ausnahme dieser kleingeschriebenen Vokale. Das gilt auch für Bereiche; [^0–9] ist das Gleiche wie \D.
  • Literale und Jokerzeichen werden oft gemischt. Zum Beispiel filtert d[aeiou]g »dag«, »deg«, »dig«, »dog« und »dug« sowie jeden längeren String, der diese Teilstrings enthält.
  • Gleichermaßen wichtig sind die Wiederholungsmetazeichen, die es erlauben, vorhergehende Zeichen, Jokerzeichen oder Kombinationen daraus mehrfach zu filtern.
  • Das Metazeichen * bedeutet, dass das vorhergehende Zeichen 0 oder mehrere Male gefiltert wird. Daher filtert be* Strings, die »b«, »be«, »bee«, »beee« und so weiter enthalten. (10)* filtert Strings, die »10«, »1010«, »101010« und so weiter enthalten. Die Klammer dient hier der Gruppierung. Wenn Sie die Klammer entfernen, erhalten Sie 10*, die Wiederholung gilt dann nur für die 0.
  • Das Metazeichen + bedeutet, dass das vorhergehende Zeichen einmal oder mehrere Male gefiltert wird. Daher filtert be+ Strings, die »be«, »bee«, »beee« und so weiter enthalten, nicht jedoch »b«.
  • Das Metazeichen ? bedeutet, dass der vorhergehende Ausdruck null oder einmal gefiltert wird. Daher filtert be? Strings, die »b« und »be« enthalten.
  • Häufig muss man sich genau darüber auslassen, wo der reguläre Ausdruck angewandt werden soll. Vor allem werden Sie oft ein Muster am Anfang (^) oder am Ende ($) eines Strings filtern, und manchmal wollen Sie nur filtern, wenn das Muster sowohl am Anfang als auch am Ende verankert ist. Zum Beispiel filtert »^be+« den Ausdruck »bee keeper«, nicht jedoch »has been«. Der reguläre Ausdruck »be+$« filtert »to be or not to be«, aber nicht »be he alive or be he dead«. »be+$« filtert »be« und »bee«, aber nicht »been« oder »Abe«.
  • Die bisher präsentierte Regex-Maschinerie kann die meisten Filteraufgaben bewältigen, die Ihnen wahrscheinlich begegnen werden. Allerdings gibt es einige sogenannte kontextsensitive Suchen, die nicht durch einfache Regex-Muster abgehandelt werden können. Stellen Sie sich vor, Sie wollen Zahlen filtern, die mit derselben Ziffer beginnen und enden (11, 909, 3233 usw.). Reine reguläre Ausdrücke sind dazu nicht in der Lage; die meisten Regex-Engines, einschließlich derjenigen für XPath 2.0, bieten aber Erweiterungen, die dies ermöglichen.
  • Es sind zwei Konventionen erforderlich. Die erste verlangt von Ihnen, dass Sie den Teil des Musters, den Sie später referenzieren wollen, mit Hilfe von Klammern als eine Captured Group markieren, und die zweite verlangt, dass Sie die Gruppe über eine Indexvariable referenzieren. Zum Beispiel ist (\d)\d*\1 ein regulärer Ausdruck, der alle Zahlen erfasst, die mit derselben Ziffer anfangen und enden. Die Gruppe ist die erste Ziffer (\d ) und die Referenz ist \1, die bedeutet »was immer die erste Gruppe erfasst hat«. Wie Sie sich vorstellen können, können Sie mehrere Gruppen haben, wie etwa (\d)(\d)\1\2, wodurch Zahlen wie »1212« und »9999«, nicht jedoch »1213« oder »1221« erfasst werden. Rückverweise wie \1, \2 usw. werden zusammen mit der XPath 2.0-Funktion matches() benutzt. Eine ähnliche Notation, die ein $ anstelle von \ verwendet, ist für Fälle reserviert, in denen die Referenz außerhalb des regulären Ausdrucks selbst auftaucht. Dies tritt in der Funktion replace() auf, bei der Sie von dem Ersatz-Regex auf Gruppen im erfassten Regex verweisen wollen. Beispielsweise ersetzt replace($einText, `(\d)\d*', `$1') die erste Sequenz von 1 oder mehr Ziffern in $einText durch die erste Ziffer in dieser Sequenz. Diese Einrichtung steht auch in der Anweisung xsl:analyze-string zur Verfügung. Wir besprechen dies ausführlicher in den Rezepten Einen String umkehren und Ohne regulare Ausdrücke auskommen.

Falls Sie tiefer in die Welt der regulären Ausdrücke eindringen wollen, sollten Sie das Buch Reguläre Ausdrücke von Jeffrey E. F. Friedl (O'Reilly-Verlag) lesen. Interessiert Sie der XSLT 2.0-Ansatz für reguläre Ausdrücke, dann schauen Sie sich XPath 2.0 von Michael Kay (Wrox, 2004) oder die W3C-Empfehlung unter String Functions that Use Pattern Matching und Regular Expressions an.

Lösung

XSLT 1.0

Mittels Rekursion können Sie eine umgekehrte Suche mit der Suche nach dem letzten Vorkommen von substr emulieren. Mit Hilfe dieser Technik erzeugen Sie ein substring-before-last und ein substring-after-last:

<xsl:template name="substring-before-last">
  <xsl:param name="input" />
  <xsl:param name="substr" />
  <xsl:if test="$substr and contains($input, $substr)">
    <xsl:variable name="temp" select="substring-after($input, $substr)" />
    <xsl:value-of select="substring-before($input, $substr)" />
    <xsl:if test="contains($temp, $substr)">
      <xsl:value-of select="$substr" />
      <xsl:call-template name="substring-before-last">
        <xsl:with-param name="input" select="$temp" />
        <xsl:with-param name="substr" select="$substr" />
      </xsl:call-template>
    </xsl:if>
  </xsl:if>
</xsl:template>

<xsl:template name="substring-after-last">
  <xsl:param name="input"/>
  <xsl:param name="substr"/>
    
  <!-- Extrahiert den String, der nach dem ersten Vorkommen kommt -->
  <xsl:variable name="temp" select="substring-after($input,$substr)"/>
    
  <xsl:choose>
    <!-- Falls der Suchstring immer noch enthalten ist, erfolgt rekursive Verarbeitung -->
    <xsl:when test="$substr and contains($temp,$substr)">
      <xsl:call-template name="substring-after-last">
        <xsl:with-param name="input" select="$temp"/>
        <xsl:with-param name="substr" select="$substr"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="$temp"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

XSLT 2.0

XSLT 2.0 fügt keine umgekehrten Versionen von substring-before/after hinzu, man kann aber die gewünschte Wirkung erzielen, indem man die vielseitige tokenize()-Funktion einsetzt, die reguläre Ausdrücke benutzt:

<xsl:function name="ckbk:substring-before-last">
  <xsl:param name="input" as="xs:string"/>
  <xsl:param name="substr" as="xs:string"/>
  <xsl:sequence select="if ($substr) then if (contains($input, $substr)) then string-join(tokenize($input, $substr)[position( ) ne last( )],$substr) else '' else $input"/>
</xsl:function>

<xsl:function name="ckbk:substring-after-last">
  <xsl:param name="input" as="xs:string"/>
  <xsl:param name="substr" as="xs:string"/>
  <xsl:sequence select="if ($substr) then if (contains($input, $substr)) then tokenize($input, $substr)[last( )] else '' else $input"/>
</xsl:function>

In beiden Funktionen müssen wir testen, ob substring leer ist, weil tokenize kein leeres Suchmuster erlaubt. Leider funktionieren diese Implementierungen nicht ganz genauso wie ihre nativen Gegenstücke. Dies liegt daran, dass tokenize sein zweites Argument als einen regulären und nicht als einen literalen String behandelt. Sie können dieses Problem beheben, indem Sie in der Funktion die Sonderzeichen schützen, die in regulären Ausdrücken verwendet werden. Über ein drittes, Boolesches Argument können Sie dieses Verhalten ein- und ausschalten. Die ursprüngliche Version mit zwei Argumenten und die neue Version mit drei Argumenten können koexistieren, weil XSLT das Überladen von Funktionen erlaubt (eine Funktion wird durch ihren Namen sowie durch ihre Stelligkeit oder die Anzahl der Argumente definiert):

<xsl:function name="ckbk:substring-before-last">
  <xsl:param name="input" as="xs:string"/>
  <xsl:param name="substr" as="xs:string"/>
  <xsl:param name="mask-regex" as="xs:boolean"/>
  <xsl:variable name="matchstr" select="if ($mask-regex) then replace($substr,'([.+?*^$])','\$1') else $substr"/>
  <xsl:sequence select="ckbk:substring-before-last($input,$matchstr)"/>
</xsl:function>

<xsl:function name="ckbk:substring-after-last">
  <xsl:param name="input" as="xs:string"/>
  <xsl:param name="substr" as="xs:string"/>
  <xsl:param name="mask-regex" as="xs:boolean"/>
  <xsl:variable name="matchstr" select="if ($mask-regex) then replace($substr,'([.+?*^$])','\$1') else $substr"/>
  <xsl:sequence select="ckbk:substring-after-last($input,$matchstr)"/>
</xsl:function>

Diskussion

Beide XSLT-Stringsuchfunktionen (substring-before und substring-after) beginnen die Suche am Anfang des Strings. Manchmal müssen Sie einen String vom Ende an durchsuchen. Die einfachste Methode, dies in XSLT zu erledigen, besteht darin, die integrierten Suchfunktionen rekursiv anzuwenden, bis die letzte Instanz des Teilstrings gefunden wurde.

Achtung!
Bei meinem ersten Versuch mit diesen Templates gab es eine hässliche Falle, die Sie beachten sollten, wenn Sie mit rekursiven Templates arbeiten, die Strings durchsuchen. Merken Sie sich, dass contains($irgendwas, '') immer true zurückliefert! Aus diesem Grund stelle ich sicher, dass ich auch auf die Existenz eines $substr-Wertes in den rekursiven Aufrufen von substring-before-last und substring-after-last teste, der nicht leer ist. Ohne diese Tests gelangt der Code in eine unendliche Schleife von leeren Sucheingaben oder es kommt zu einem Stack-Überlauf bei Implementierungen, die nicht mit Endrekursion umgehen können.

Ein weiterer Algorithmus ist teile und herrsche. Die zugrunde liegende Idee besteht darin, den String in zwei Hälften zu teilen. Wenn sich der Suchstring in der zweiten Hälfte befindet, können Sie die erste Hälfte verwerfen, wodurch das Problem nur noch halb so groß wird. Dieser Vorgang wird rekursiv wiederholt. Spannend wird es, wenn der Suchstring nicht in der zweiten Hälfte liegt, weil Sie den Suchstring womöglich zwischen den beiden Hälften aufgeteilt haben. Hier ist eine Lösung für substring-before-last:

<xsl:template name="str:substring-before-last">
  <xsl:param name="input"/>
  <xsl:param name="substr"/>
  <xsl:variable name="mid" select="ceiling(string-length($input) div 2)"/>
  <xsl:variable name="temp1" select="substring($input,1, $mid)"/>
  <xsl:variable name="temp2" select="substring($input,$mid +1)"/>
  <xsl:choose>
    <xsl:when test="$temp2 and contains($temp2,$substr)">
      <!-- Suchstring ist in der zweiten Hälfte, die erste Hälfte wird also einfach angehängt, auf der zweiten Hälfte wird rekursiv fortgefahren -->
      <xsl:value-of select="$temp1"/>
      <xsl:call-template name="str:substring-before-last">
        <xsl:with-param name="input" select="$temp2"/>
        <xsl:with-param name="substr" select="$substr"/>
      </xsl:call-template>
    </xsl:when>
    <!--Suchstring befindet sich in den Grenzen, ein einfaches substring-before reicht daher-->
    <xsl:when test="contains(substring($input, $mid - string-length($substr) +1), $substr)">
      <xsl:value-of select="substring-before($input,$substr)"/>
    </xsl:when>
    <!--Suchstring befindet sich in der ersten Hälfte, die zweite wird daher einfach weggeworfen-->
    <xsl:when test="contains($temp1,$substr)">
      <xsl:call-template name="str:substring-before-last">
        <xsl:with-param name="input" select="$temp1"/>
        <xsl:with-param name="substr" select="$substr"/>
      </xsl:call-template>
    </xsl:when>
    <!-- Kein Auftreten des Suchstrings, wir sind fertig -->
    <xsl:otherwise/>
  </xsl:choose>  
</xsl:template>

Es stellt sich heraus, dass teile und herrsche nur wenige bis keine Vorteile bietet, wenn Sie nicht gerade große Texte durchsuchen müssen (ab etwa 4000 Zeichen). Sie könnten ein Wrapper-Template schreiben, das je nach der Länge des Textes den passenden Algorithmus wählt oder von teile und herrsche auf den einfacheren Algorithmus umschaltet, wenn der Teil klein genug geworden ist.

  

zum Seitenanfang

<< 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