XSLT замена нескольких строк с рекурсией - PullRequest
1 голос
/ 07 марта 2011

Я пытался выполнить множественную (другую) замену строки на рекурсию, и я наткнулся на контрольно-пропускной пункт. Я успешно получил первую замену на работу, но последующие замены никогда не срабатывают. Я знаю, что это связано с рекурсией и тем, как строка with-param передается обратно в шаблон вызова. Я вижу свою ошибку и почему следующий xsl: когда никогда не запускается, но я просто не могу понять, как именно передать полную измененную строку из первого xsl: when, во второй 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 ]

4 голосов
/ 07 марта 2011

Это преобразование полностью параметризовано и не требует никаких хитростей с пространствами имен по умолчанию :

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

Таким образом, если у нас есть два повторения, такие как «ядро» -> «ядро» и «корреляция» -> «сходство», второе будет использоваться для текста, содержащего слово «корреляция», независимо от того, как представители упорядочены.

2 голосов
/ 07 марта 2011

Эта таблица стилей показывает подробное решение только для того, чтобы вы могли изучить шаблон:

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

EDIT : полное параметризованное решение.

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

Примечание : существует проблема при использовании встроенных данных в XML1.0: вы не можете сбросить объявление пространства имен с префиксом, как в XML 1.1.Решение состоит в том, чтобы использовать не распространенную, но действительную нотацию: объявить пространство имен XSLT в качестве пространства имен по умолчанию.

0 голосов
/ 14 июля 2017

Хотя этот вопрос был задан (и получен ответ) несколько лет назад, ни этот ответ, ни (многие!) Другие варианты, которые я нашел при поиске в сети за последние пару дней, не смогли сделать то, что мне было нужно: заменить несколько строки в узлах, которые могут содержать несколько kb текста.

Версия Dimitre работает хорошо, когда узлы содержат очень мало текста, но когда я попытался использовать его, я почти сразу же испортился из-за переполнения страшного стека (рекурсивные вызовы, помните!) поиск шаблонов до начала текста. Это означает, что выполняется много (рекурсивных) вызовов, каждый из которых использует самые правые n-1 символов исходного текста. Для 1k текста это означает более 1000 рекурсивных вызовов!

После поиска альтернативных вариантов я наткнулся на пример Ибрагима Наджи (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
        (/4691314/xslt-zamena-neskolkih-strok-s-rekursiei).
        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>
0 голосов
/ 21 декабря 2016

Я изменил ответ Дмитрия, чтобы поместить его решение в шаблон и использовать расширение 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>
0 голосов
/ 07 марта 2011

Проблема может быть связана с различиями в кодировке новой строки, в результате чего процессор 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>
...