Text ersetzen

(Auszug aus "XSLT Kochbuch" von Sal Mangano)

Problem

Sie wollen alle Vorkommen eines Teilstrings innerhalb eines Zielstrings durch einen anderen String ersetzen.

Lösung

XSLT 1.0

Das folgende rekursive Template ersetzt alle Vorkommen eines Suchstrings durch einen Ersatzstring:

<xsl:template name="search-and-replace">
  <xsl:param name="input"/>
  <xsl:param name="search-string"/>
  <xsl:param name="replace-string"/>
  <xsl:choose>
    <!-- Feststellen, ob die Eingabe den Suchstring enthält -->
    <xsl:when test="$search-string and contains($input,$search-string)">
      <!-- Ist dies der Fall, dann Verkettung des Teilstrings vor dem Suchstring mit dem Ersatzstring und mit dem Ergebnis der rekursiven Anwendung dieses Templates mit dem verbleibenden Teilstring. -->
      <xsl:value-of select="substring-before($input,$search-string)"/>
      <xsl:value-of select="$replace-string"/>
      <xsl:call-template name="search-and-replace">
        <xsl:with-param name="input" select="substring-after($input,$search-string)"/>
        <xsl:with-param name="search-string" select="$search-string"/>
        <xsl:with-param name="replace-string" select="$replace-string"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <!-- Keine weiteren Vorkommen des Suchstrings, daher einfach Rückgabe des aktuellen Eingabestrings -->
      <xsl:value-of select="$input"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

Falls Sie nur ganze Wörter ersetzen wollen, müssen Sie sicherstellen, dass die Zeichen unmittelbar vor und nach dem Suchstring aus der Klasse von Zeichen stammen, die als Worttrennzeichen betrachtet werden. Wir erklären die Zeichen in der Variablen $punc sowie Whitespace zu Worttrennzeichen:

<xsl:template name="search-and-replace-whole-words-only">
  <xsl:param name="input"/>
  <xsl:param name="search-string"/>
  <xsl:param name="replace-string"/>
  <xsl:variable name="punc" select="concat('.,;:( )[  ]!?$@&amp;&quot;',&quot;'&quot;)"/>
  <xsl:choose>
    <!-- Feststellen, ob die Eingabe den Suchstring enthält -->
    <xsl:when test="contains($input,$search-string)">
      <!-- Ist dies der Fall, dann testen, ob das vorherige und das folgende Zeichen Worttrennzeichen sind. -->
      <xsl:variable name="before" select="substring-before($input,$search-string)"/>
      <xsl:variable name="before-char" select="substring(concat(' ',$before),string-length($before) +1, 1)"/>
      <xsl:variable name="after" select="substring-after($input,$search-string)"/>
      <xsl:variable name="after-char" select="substring($after,1,1)"/>
      <xsl:value-of select="$before"/>
      <xsl:choose>
        <xsl:when test="(not(normalize-space($before-char)) or contains($punc,$before-char)) and (not(normalize-space($after-char)) or contains($punc,$after-char))">
          <xsl:value-of select="$replace-string"/>
        </xsl:when>
        <xsl:otherwise>
          <xsl:value-of select="$search-string"/>
        </xsl:otherwise>
      </xsl:choose>
      <xsl:call-template name="search-and-replace-whole-words-only">
        <xsl:with-param name="input" select="$after"/>
        <xsl:with-param name="search-string" select="$search-string"/>
        <xsl:with-param name="replace-string" select="$replace-string"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <!-- Es gibt keine weiteren Vorkommen des Suchstrings, daher einfach Rückgabe des aktuellen Eingabestrings -->
      <xsl:value-of select="$input"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

Anmerkung:
Beachten Sie, wie wir $punc mit Hilfe von concat( ) so konstruieren, dass es sowohl einfache als auch doppelte Anführungszeichen enthält. Es wäre nicht möglich, dies auf eine andere Weise zu erreichen, da XPath und XSLT es im Gegensatz zu C nicht erlauben, Sonderzeichen mit einem Backslash (\) zu schützen. XPath 2.0 erlaubt es, Anführungszeichen zu schützen, indem sie verdoppelt werden.

XSLT 2.0

Die Funktionalität von search-and-replace ist in die 2.0-Funktion replace() integriert. Die Funktionalität von search-and-replace-whole-words-only kann ganz einfach mit einem regulären Ausdruck emuliert werden, der Wörter erfasst:

<xsl:function name="ckbk:search-and-replace-whole-words-only">
  <xsl:param name="input" as="xs:string"/>
  <xsl:param name="search-string" as="xs:string"/>
  <xsl:param name="replace-string" as="xs:string"/>
  <xsl:sequence select="replace($input, concat('(^|\W)',$search-string,'(\W|$)'), concat('$1',$replace-string,'$2'))"/>
</xsl:function>

Achtung!
Viele Regex-Engines verwenden \b, um Wortbegrenzungen zu erfassen, allerdings unterstützt XPath 2.0 dies nicht.

Hier erzeugen wir einen regulären Ausdruck, indem wir $search-string mit (^|\W) und (\W|$) umgeben, wobei \W »not \w« oder »kein Wortzeichen« bedeutet. Die Zeichen ^ und $ behandeln den Fall, dass ein Wort am Anfang oder am Ende des Strings auftaucht. Wir müssen außerdem das erfasste \W-Zeichen zurück in den Text setzen, indem wir Referenzen auf die Captured Groups $1 und $2 verwenden.

Die Funktion replace() ist leistungsfähiger als die genannten XSLT 1.0-Lösungen, da sie reguläre Ausdrücke verwendet und sich Teile des Filterergebnisses merken kann. Diese werden dann in der Ersetzung über die Variablen $1, $2 usw. verwendet. Wir untersuchen replace() im Rezept Ohne reguläre Ausdrücke auskommen näher.

Diskussion

Das Suchen und Ersetzen ist eine verbreitete Textverarbeitungsaufgabe. Die hier gezeigte Lösung ist die einfachste Implementierung von Suchen und Ersetzen, die rein in den Begriffen von XSLT geschrieben ist. Wenn man die Leistungsfähigkeit dieser Lösung betrachtet, könnte man glauben, sie wäre ineffizient. Für jedes Vorkommen des Suchstrings ruft der Code contains(), substring-before() und substring-after() auf. Jede Funktion durchsucht wahrscheinlich erneut den Eingabestring nach dem Suchstring. Es sieht so aus, als würde dieser Ansatz zwei Suchen mehr durchführen, als notwendig sind. Nach einigem Nachdenken könnten Sie eine der folgenden, in den beiden folgenden Beispielen gezeigten Lösungen vorschlagen, die scheinbar effizienter sind.

Beispiel: Verwendung eines temporären Strings in einem fehlgeschlagenen Versuch, das Suchen und Ersetzen zu verbessern.

<xsl:template name="search-and-replace">
  <xsl:param name="input"/>
  <xsl:param name="search-string"/>
  <xsl:param name="replace-string"/>
  <!-- Sucht den Teilstring vor dem Suchstring und speichert ihn in einer Variablen -->
  <xsl:variable name="temp" select="substring-before($input,$search-string)"/>
  <xsl:choose>
    <!-- Wenn $temp nicht leer ist oder die Eingabe mit dem Suchstring beginnt, dann wissen wir, dass wir eine Ersetzung vornehmen müssen. Damit wird die Notwendigkeit eliminiert, contains( ) zu verwenden. -->
    <xsl:when test="$temp or starts-with($input,$search-string)">
      <xsl:value-of select="concat($temp,$replace-string)"/>
      <xsl:call-template name="search-and-replace">
        <!-- Wir eliminieren die Notwendigkeit, substring-after aufzurufen, indem wir die Länge von temp und den Suchstring verwenden, um den verbleibenden String in dem rekursiven Aufruf zu extrahieren. -->
        <xsl:with-param name="input" select="substring($input,string-length($temp)+ string-length($search-string)+1)"/>
        <xsl:with-param name="search-string" select="$search-string"/>
        <xsl:with-param name="replace-string" select="$replace-string"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="$input"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

Beispiel: Verwendung eines temporären Integer-Wertes in einem fehlgeschlagenen Versuch, das Suchen und Ersetzen zu verbessern.

<xsl:template name="search-and-replace">
  <xsl:param name="input"/>
  <xsl:param name="search-string"/>
  <xsl:param name="replace-string"/>
  <!-- Ermittelt die Länge des Teilstrings vor dem Suchstring und speichert sie in einer Variablen -->
  <xsl:variable name="temp" select="string-length(substring-before($input,$search-string))"/>
  <xsl:choose>
    <!-- Wenn $temp nicht 0 ist oder die Eingabe mit dem Suchstring beginnt, dann wissen wir, dass wir eine Ersetzung vornehmen müssen. Dies eleminiert die Notwendigkeit, contains( ) zu verwenden. -->
    <xsl:when test="$temp or starts-with($input,$search-string)">
      <xsl:value-of select="substring($input,1,$temp)"/>
      <xsl:value-of select="$replace-string"/>
      <!-- Wir eliminieren die Notwendigkeit, substring-after aufzurufen, indem wir temp und die Länge des Suchstrings verwenden, um den verbleibenden String in dem rekursiven Aufruf zu extrahieren. -->
      <xsl:call-template name="search-and-replace">
        <xsl:with-param name="input" select="substring($input,$temp + string-length($search-string)+1)"/>
        <xsl:with-param name="search-string" select="$search-string"/>
        <xsl:with-param name="replace-string" select="$replace-string"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="$input"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

Folgender Grundgedanke verbirgt sich hinter beiden Versuchen: Wenn Sie sich die Stelle merken, an der substring-before( ) einen Treffer landet, können Sie diese Information einsetzen, um die Notwendigkeit zu eliminieren, contains( ) und substring-after( ) aufzurufen. Sie sind gezwungen, einen Aufruf zu starts-with( ) einzuführen, um den Fall zu behandeln, wenn substring-before( ) den leeren String zurückliefert. Dies kann geschehen, wenn der Suchstring fehlt oder wenn der Eingabestring mit dem Suchstring beginnt. Allerdings ist starts-with( ) wahrscheinlich schneller als contains( ), da es nicht über die Länge des Suchstrings hinaus suchen muss. Der zweite Versuch unterscheidet sich vom ersten durch die Überlegung, dass das Speichern eines Integer-Wertes möglicherweise effizienter ist als das Speichern des gesamten Teilstrings.

Leider zeitigen diese angeblichen Optimierungen keine Verbesserung, wenn die Xalan-XSLT-Implementierung verwendet wird, und liefern tatsächlich zeitliche Ergebnisse, die bei einigen Eingaben um eine Größenordnung langsamer sind als bei der Verwendung von Saxon oder XT! Meine erste Hypothese bezüglich dieses unerwarteten Ergebnisses lautete, dass die Verwendung der Variablen $temp in dem rekursiven Aufruf der Optimierung auf Endrekursion bei Saxon in die Quere kam (siehe Rezept Einen String umkehren). Als ich jedoch Versuche mit großen Eingaben vornahm, die viele Treffer hatten, konnte ich auch keinen Stack-Überlauf verursachen. Mein nächster Verdacht war, dass XSLT-substring( ) aus irgendeinem Grund tatsächlich langsamer ist als die substring-before( )- und substring-after( )-Aufrufe. Michael Kay, der Autor von Saxon, gab an, dass die Saxon-Implementierung von substring( ) aufgrund der komplizierten Regeln langsam ist, die XSLT-substring implementieren muss, z.B. wegen der Gleitkommarundung von Argumenten, der Behandlung von Sonderfällen, bei denen die Start- oder Endpunkte außerhalb der Grenzen des Strings liegen, und wegen der Probleme mit Unicode-Surrogate-Pairs. Im Gegensatz dazu können substring-before( ) und substring-after( ) direkter in Java übersetzt werden.

Die eigentliche Lehre, die wir hier ziehen können, besteht darin, dass Optimierung ein kompliziertes Geschäft ist, vor allem in XSLT, wo es eine große Ungleichheit zwischen den Implementierungen gibt und neue Versionen stetig neue Optimierungen anwenden. Es ist am besten, bei einfachen Lösungen zu bleiben, wenn Sie sich nicht gerade profilieren wollen. Ein weiterer Vorteil offensichtlicher Lösungen ist es, dass sie sich wahrscheinlich auf unterschiedlichen XSLT-Implementierungen konsistent verhalten.

  

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