XSLT使用递归替换多个字符串

时间:2011-03-06 21:47:14

标签: xslt recursion substring tail-recursion

我一直试图用递归执行多个(不同的)字符串替换,并且遇到了障碍。我成功地获得了第一个更换工作,但随后的更换从未开火。我知道这与递归以及with-param字符串如何传递回调用模板有关。我看到了我的错误和为什么下一个xsl:从不触发,但我似乎无法弄清楚如何从第一个xsl传递完整的修改后的字符串:何时到第二个xsl:when。非常感谢任何帮助。

<xsl:template name="replace">
    <xsl:param name="string" select="." />
    <xsl:choose>
        <xsl:when test="contains($string, '&#13;&#10;')">
            <xsl:value-of select="substring-before($string, '&#13;&#10;')" />
            <br/>
            <xsl:call-template name="replace">
                <xsl:with-param name="string" select="substring-after($string, '&#13;&#10;')"/>
            </xsl:call-template>
        </xsl:when>
        <xsl:when test="contains($string, 'TXT')">
            <xsl:value-of select="substring-before($string, '&#13;TXT')" />
            <xsl:call-template name="replace">
                <xsl:with-param name="string" select="substring-after($string, '&#13;')" />
            </xsl:call-template>
        </xsl:when>
        <xsl:otherwise>
            <xsl:value-of select="$string"/>
        </xsl:otherwise>

    </xsl:choose>
</xsl:template>

5 个答案:

答案 0 :(得分:4)

此转换已完全参数化,不需要使用默认命名空间的任何技巧

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:my="my:my">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <my:params xml:space="preserve">
  <pattern>
   <old>&#xA;</old>
   <new><br/></new>
  </pattern>
  <pattern>
   <old>quick</old>
   <new>slow</new>
  </pattern>
  <pattern>
   <old>fox</old>
   <new>elephant</new>
  </pattern>
  <pattern>
   <old>brown</old>
   <new>white</new>
  </pattern>
 </my:params>

 <xsl:variable name="vPats"
      select="document('')/*/my:params/*"/>

 <xsl:template match="text()" name="multiReplace">
  <xsl:param name="pText" select="."/>
  <xsl:param name="pPatterns" select="$vPats"/>

  <xsl:if test=
   "string-length($pText) >0">

    <xsl:variable name="vPat" select=
     "$vPats[starts-with($pText, old)][1]"/>
    <xsl:choose>
     <xsl:when test="not($vPat)">
       <xsl:copy-of select="substring($pText,1,1)"/>
     </xsl:when>
     <xsl:otherwise>
       <xsl:copy-of select="$vPat/new/node()"/>
     </xsl:otherwise>
    </xsl:choose>

    <xsl:call-template name="multiReplace">
      <xsl:with-param name="pText" select=
       "substring($pText, 1 + not($vPat) + string-length($vPat/old/node()))"/>
    </xsl:call-template>
  </xsl:if>
 </xsl:template>
</xsl:stylesheet>

应用于此XML文档时:

<t>The quick
brown fox</t>

产生了想要的正确结果:

The slow<br/>white elephant

<强>解释

从左到右和任何位置扫描文本,如果剩余的字符串以其中一个指定的模式开始,则起始子字符串将替换为为firat匹配模式指定的替换。

请注意:如果我们有搜索模式:

   "relation"   --> "mapping" 
   "corelation" --> "similarity"

按上述顺序和文字:

   "corelation"

然后此解决方案产生更正确的结果:

"similarity"

和@Alejandro目前接受的解决方案产生:

"comapping"

修改:通过较小的更新我们得到了另一项改进:如果在给定位置可以进行多次替换,我们会执行最长的替换。

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:ext="http://exslt.org/common"
 xmlns:my="my:my">
    <xsl:output omit-xml-declaration="yes"/>
    <xsl:strip-space elements="*"/>

    <my:params xml:space="preserve">
        <pattern>
            <old>&#xA;</old>
            <new><br/></new>
        </pattern>
        <pattern>
            <old>quick</old>
            <new>slow</new>
        </pattern>
        <pattern>
            <old>fox</old>
            <new>elephant</new>
        </pattern>
        <pattern>
            <old>brown</old>
            <new>white</new>
        </pattern>
    </my:params>

    <xsl:variable name="vrtfPats">
     <xsl:for-each select="document('')/*/my:params/*">
      <xsl:sort select="string-length(old)"
           data-type="number" order="descending"/>
       <xsl:copy-of select="."/>
     </xsl:for-each>
    </xsl:variable>

    <xsl:variable name="vPats" select=
     "ext:node-set($vrtfPats)/*"/>

    <xsl:template match="text()" name="multiReplace">
        <xsl:param name="pText" select="."/>
        <xsl:param name="pPatterns" select="$vPats"/>
        <xsl:if test=    "string-length($pText) >0">      
            <xsl:variable name="vPat" select=
            "$vPats[starts-with($pText, old)][1]"/>

            <xsl:choose>
                <xsl:when test="not($vPat)">
                    <xsl:copy-of select="substring($pText,1,1)"/>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:copy-of select="$vPat/new/node()"/>
                </xsl:otherwise>
            </xsl:choose>

            <xsl:call-template name="multiReplace">
                <xsl:with-param name="pText" select=
                "substring($pText,
                          1 + not($vPat) + string-length($vPat/old/node())
                          )"/>
            </xsl:call-template>
        </xsl:if>
    </xsl:template>
</xsl:stylesheet>

因此,如果我们有两个代表,如“核心” - &gt; “内核”和“核心化” - &gt; “相似性”,第二个用于包含单词“corelation”的文本,无论代表如何排序。

答案 1 :(得分:2)

此样式表显示了一个详细的解决方案,仅供您学习模式:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:template match="node()|@*">
        <xsl:copy>
            <xsl:apply-templates select="node()|@*"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="text()" name="replace">
        <xsl:param name="pString" select="string()"/>
        <xsl:param name="pSearch" select="'THIS'"/>
        <xsl:param name="pReplace" select="'THAT'"/>
        <xsl:choose>
            <xsl:when test="contains($pString, '&#xA;')">
                <xsl:call-template name="replace">
                    <xsl:with-param
                         name="pString"
                         select="substring-before($pString, '&#xA;')"/>
                </xsl:call-template>
                <br/>
                <xsl:call-template name="replace">
                    <xsl:with-param
                         name="pString"
                         select="substring-after($pString, '&#xA;')"/>
                </xsl:call-template>
            </xsl:when>
            <xsl:when test="contains($pString, $pSearch)">
                <xsl:call-template name="replace">
                    <xsl:with-param
                         name="pString"
                         select="substring-before($pString, $pSearch)"/>
                </xsl:call-template>
                <xsl:value-of select="$pReplace"/>
                <xsl:call-template name="replace">
                    <xsl:with-param
                         name="pString"
                         select="substring-after($pString, $pSearch)"/>
                </xsl:call-template>
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="$pString"/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>
</xsl:stylesheet>

使用此输入:

<t>THIS is a test.
But THAT is not.
THIS is also a test.</t>

输出:

<t>THAT is a test.<br />But THAT is not.<br />THAT is also a test.</t>

编辑:完整参数化解决方案。

<stylesheet version="1.0" xmlns="http://www.w3.org/1999/XSL/Transform">
    <param name="pMap">
        <s t="&#xA;" xmlns=""><br/></s>
        <s t="THIS" xmlns="">THAT</s>
    </param>
    <template match="node()|@*">
        <copy>
            <apply-templates select="node()|@*"/>
        </copy>
    </template>
    <template match="text()" name="replace">
        <param name="pString" select="string()"/>
        <param name="pSearches"
                   select="document('')/*/*[@name='pMap']/s"/>
        <param name="vMatch" select="$pSearches[contains($pString,@t)][1]"/>
        <choose>
            <when test="$vMatch">
                <call-template name="replace">
                    <with-param
                         name="pString"
                         select="substring-before($pString, $vMatch/@t)"/>
                </call-template>
                <copy-of select="$vMatch/node()"/>
                <call-template name="replace">
                    <with-param
                         name="pString"
                         select="substring-after($pString, $vMatch/@t)"/>
                </call-template>
            </when>
            <otherwise>
                <value-of select="$pString"/>
            </otherwise>
        </choose>
    </template>
</stylesheet>

输出:

<t>THAT is a test.<br/>But THAT is not.<br/>THAT is also a test.</t>

注意:在XML 1.0中使用内联数据时出现问题:您无法像在XML 1.1中那样重置前缀命名空间声明。解决方案是使用不常见但有效的表示法:将XSLT名称空间声明为默认名称空间。

答案 2 :(得分:0)

问题可能源于换行编码的差异,导致XSLT处理器无法识别匹配字符串中的CRLF。我建议使用逗号代替换行进行测试。使用参数“abc,def,ghi”调用时,以下内容将为您提供预期结果:

<xsl:template name="replace">
  <xsl:param name="string" select="." />
  <xsl:choose>
    <xsl:when test="contains($string, ',')">
        <xsl:value-of select="substring-before($string, ',')" />
        <br/>
        <xsl:call-template name="replace">
            <xsl:with-param name="string" select="substring-after($string, ',')"/>
        </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
        <xsl:value-of select="$string"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

答案 3 :(得分:0)

我修改了Dimitrie的答案,将他的解决方案放在模板中并使用exsl扩展。请检查一下,可能对某人有用。

<?xml version='1.0' ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
    xmlns:exsl="http://exslt.org/common"
    xmlns:a="http://www.tralix.com/cfd/2" 
    extension-element-prefixes="exsl">
    <xsl:output indent="yes"/>
    <xsl:template match="/*">
        <xsl:variable name="replacesList">
            <replaces>
                <replace><old>01</old><new>01 - Efectivo</new></replace>
                <replace><old>02</old><new>02 - Cheque nominativo</new></replace>
                <replace><old>03</old><new>03 - Transferencia electrónica de fondos</new></replace>
                <replace><old>04</old><new>04 - Tarjeta de Crédito</new></replace>
                <replace><old>05</old><new>05 - Monedero Electrónico</new></replace>
                <replace><old>06</old><new>06 - Dinero electrónico</new></replace>
                <replace><old>08</old><new>08 - Vales de despensa</new></replace>
                <replace><old>28</old><new>28 - Tarjeta de Débito</new></replace>
                <replace><old>29</old><new>29 - Tarjeta de Servicio</new></replace>
                <replace><old>99</old><new>99 - Otros</new></replace>
            </replaces>
        </xsl:variable>     
        <descripcionMetodoDePago>
            <xsl:call-template name="replaces">
                <xsl:with-param name="text" select="text"/>
                <xsl:with-param name="replaces">
                    <xsl:copy-of select="exsl:node-set($replacesList/*/*)"/>
                </xsl:with-param>
            </xsl:call-template>
        </descripcionMetodoDePago>
    </xsl:template>
    <xsl:template name="replaces">
        <xsl:param name="text"/>
        <xsl:param name="replaces"/>
        <xsl:if test="$text!=''">
            <xsl:variable name="replace" select="$replaces/*[starts-with($text, old)][1]"/>
            <xsl:choose>
                <xsl:when test="not($replace)">
                    <xsl:copy-of select="substring($text,1,1)"/>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:copy-of select="$replace/new/node()"/>
                </xsl:otherwise>
            </xsl:choose>
            <xsl:call-template name="replaces">
                <xsl:with-param name="text" select=
                "substring($text, 1 + not($replace) + string-length($replace/old/node()))"/>
                <xsl:with-param name="replaces" select="$replaces"/>
            </xsl:call-template>
        </xsl:if>
    </xsl:template>
</xsl:stylesheet>

答案 4 :(得分:0)

虽然这个问题在几年前被提出(并得到了回答),但在过去几天搜索网络时我找到的这个答案和(很多!)其他变种都没有能够做我需要的:替换多个节点中的字符串,可能包含多个 kb 的文本。

当节点包含非常少的文本时,Dimitre的版本运行良好,但是当我尝试使用它时,我几乎立即违反了可怕的堆栈溢出(递归调用,请记住!)Dimitre解决方案的问题是它试图匹配搜索模式到文本的开头。这意味着进行了许多(递归)调用,每次调用使用原始文本的最右边的n-1个字符。对于1k文本,这意味着超过1000个递归调用!

在挖掘替代方案之后,我遇到了Ibrahim Naji(http://thinknook.com/xslt-replace-multiple-strings-2010-09-07/)的一个例子,它使用更传统的substring-before / substring-after组合来执行替换。但是,该代码仅限于任意数量的搜索字符串的单个替换字符串。

因此,我决定实际让我的手弄脏(并同时学习XSLT!)结果是以下代码执行多个字符串替换(通过内部模板指定,但这很容易例如,用外部文件替换,并且(在我的测试中到目前为止)不会受到过多的递归调用。

应该注意,替换是非常基本的(与大多数其他现有实现一样)意味着例如不尝试仅匹配整个单词。我希望这些评论足以解释它的工作方式,特别是对于其他XSLT初学者(比如我自己)。

现在代码......

<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:exsl="http://exslt.org/common"
    xmlns:dps="dps:dps">

    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

    <!--
        The original version of this code was published by Ibrahim Naji (http://thinknook.com/xslt-replace-multiple-strings-2010-09-07/).
        It works but suffered the limitation of only being able to supply a single replacement text. An alternative implementation, which
        did allow find/replace pairs to be specified, was published by Dimitre Novatchev
        (https://stackoverflow.com/questions/5213644/xslt-multiple-string-replacement-with-recursion).
        However, that implementation suffers from stack overflow problems if the node contains more than a few hundred bytes of text (and
        in my case I needed to process nodes which could include several kb of data). Hence this version which combines the best features
        of both implementations.

        John Cullen, 14 July 2017.
     -->

    <!-- IdentityTransform, copy the input to the output -->
    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>

    <!-- Process all text nodes. -->
    <xsl:template match="text()">
        <xsl:call-template name="string-replace-all">
            <xsl:with-param name="text" select="."/>
        </xsl:call-template>
    </xsl:template>

    <!-- Table of replacement patterns -->
    <xsl:variable name="vPatterns">
        <dps:patterns>
            <pattern>
                <old>&lt;i&gt;</old>
                <new>&lt;em&gt;</new>
            </pattern>
            <pattern>
                <old>&lt;/i&gt;</old>
                <new>&lt;/em&gt;</new>
            </pattern>
            <pattern>
                <old>&lt;b&gt;</old>
                <new>&lt;strong&gt;</new>
            </pattern>
            <pattern>
                <old>&lt;/b&gt;</old>
                <new>&lt;/strong&gt;</new>
            </pattern>
        </dps:patterns>
    </xsl:variable>

    <!--
        Convert the internal table into a node-set. This could also be done via a call to document()
        for example select="document('')/*/myns:params/*" with a suitable namespace declaration, but
        in my case that was not possible because the code is being used in with a StreamSource.
     -->
    <xsl:variable name="vPats" select="exsl:node-set($vPatterns)/dps:patterns/*"/>

    <!-- This template matches all text() nodes, and calls itself recursively to performs the actual replacements. -->
    <xsl:template name="string-replace-all">
        <xsl:param name="text"/>
        <xsl:param name="pos" select="1"/>
        <xsl:variable name="replace" select="$vPats[$pos]/old"/>
        <xsl:variable name="by" select="$vPats[$pos]/new"/>
        <xsl:choose>

            <!-- Ignore empty strings -->
            <xsl:when test="string-length(translate(normalize-space($text), ' ', '')) = 0"> 
                <xsl:value-of select="$text"/>
            </xsl:when>

            <!-- Return the unchanged text if the replacement is larger than the input (so no match possible) -->
            <xsl:when test="string-length($replace) > string-length($text)">
                <xsl:value-of select="$text"/>
            </xsl:when>

            <!-- If the current text contains the next pattern -->
            <xsl:when test="contains($text, $replace)">
                <!-- Perform a recursive call, each time replacing the next occurrence of the current pattern -->
                <xsl:call-template name="string-replace-all">
                    <xsl:with-param name="text" select="concat(substring-before($text,$replace),$by,substring-after($text,$replace))"/>
                    <xsl:with-param name="pos" select="$pos"/>
                </xsl:call-template>
            </xsl:when>

            <!-- No (more) matches found -->
            <xsl:otherwise>
                <!-- Bump the counter to pick up the next pattern we want to search for -->
                <xsl:variable name="next" select="$pos+1"/>
                <xsl:choose>
                    <!-- If we haven't finished yet, perform a recursive call to process the next pattern in the list. -->
                    <xsl:when test="boolean($vPats[$next])">
                        <xsl:call-template name="string-replace-all">
                            <xsl:with-param name="text" select="$text"/>
                            <xsl:with-param name="pos" select="$next"/>
                        </xsl:call-template>
                    </xsl:when>

                    <!-- No more patterns, we're done. Return the fully processed text. -->
                    <xsl:otherwise>
                        <xsl:value-of select="$text"/>
                    </xsl:otherwise>
                </xsl:choose>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>
</xsl:stylesheet>