Замена текста¶
Задача¶
Требуется заменить все вхождения заданной подстроки другой строкой.
Решение¶
XSLT 1.0¶
Следующий рекурсивный шаблон заменяет все вхождения искомой строки на строку замены.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
Если вы хотите заменять только слова целиком, то следует проверять, что непосредственно до и после искомой строки находятся символы, принадлежащие классу разделителей слов. Мы будем считать, что разделителями являются символы, хранящиеся в переменной $punc
, а также все символы пропуска.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
|
Обратите внимание на то, как переменная $punc
строится с помощью функции concat()
, чтобы в нее вошли символы одиночной и двойной кавычек. Никак по-другому это сделать невозможно, поскольку ни XPath, ни XSLT, в отличие от языка C, не позволяют экранировать специальные символы с помощью обратной косой черты (\
). В XPath 2.0 кавычку можно ввести в текст программы, записав ее два раза подряд.
XSLT 2.0¶
Функциональность шаблона search-and-replace
в версии 2.0 встроена в функцию replace()
. Функциональность шаблона search-and-replace-whole-words-only
можно имитировать с помощью регулярных выражений для сопоставления со словами:
1 2 3 4 5 6 7 8 9 10 |
|
Во многих реализациях регулярных выражений для сопоставления с границей слова предусмотрен метасимвол \b
, но в XPath 2.0 он не поддерживается. Здесь мы строим регулярное выражение, окружая строку $search-string
конструкциями (^|\W)
и (\W|$)
, где \W
означает «не \w
» или «не символ, входящий в состав слова».
Метасимволы ^
и $
учитывают случай, когда слово находится в начале или в конце строки. Мы должны также вернуть сопоставленный символ назад в текст, воспользовавшись ссылками на запомненные группы $1
и $2
.
Функция replace()
позволяет больше, чем в решении для XPath 1.0, так как она пользуется регулярными выражениями и может запоминать отдельные сопоставленные части и подставлять их в строку замены с помощью псевдопеременных $1
, $2
и т. д.
Обсуждение¶
Поиск и замена – типичная задача обработки текста. Представленное выше решение – это самая прямолинейная реализация, написанная на чистом XSLT. У читателя может возникнуть мысль, что производительность такого решения недостаточна. Ведь для каждого выхождения искомой строки вызываются функции contains()
, substring-before()
и substring-after()
. Вполне вероятно, что каждая из этих функций повторно просматривает всю входную строку в поисках искомой. И, стало быть, при таком подходе выполняется на два поиска больше, чем необходимо. Немного поразмыслив, вы можете найти решения, показанные в примерах 2.4 и 2.5, которые на первый взгляд представляются более эффективными.
Пример 2.4. Использование временной строки в неудачной попытке улучшить производительность поиска и замены
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
|
Пример 2.5. Использование временного целого в неудачной попытке улучшить производительность поиска и замены
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
|
Идея обоих вариантов одна и та же: если запомнить, где функция substring-before()
нашла соответствие, то можно воспользоваться этой информацией для того, чтобы не вызывать функции contains()
и substring-after()
. Но мы вынуждены обращаться к функции starts-with()
, чтобы выделить случай, когда substring-before()
возвращает пустую строку; такое может случиться, если искомая строка отсутствует или исходная строка начинается с искомой.
Впрочем, starts-with()
, вероятно, работает быстрее, чем contains()
, поскольку ей не нужно просматривать больше символов, чем содержится в искомой строке. Второй вариант отличается от первого предположением, что сохранение целочисленного смещения может оказаться эффективнее сохранения подстроки целиком.
Увы, ни одна из этих оптимизаций не дает никакого выигрыша при использовании процессора XSLT Xalan. Более того, при некоторых входных данных реализации Saxon и XT показывают на порядок большее время работы! Столкнувшись с этим парадоксальным результатом, я сначала предположил, что использование переменной $temp
в рекурсивном вызове как-то препятствует оптимизации хвостовой рекурсии в Saxon (см. рецепт 2.6). Однако, экспериментируя с длинными входными строками, в которых искомая строка встречается много раз, я не сумел вызвать переполнение стека. Тогда я заподозрил, что по какой-то причине функция substring()
в XSLT работает медленнее, чем substring-before()
и substring-after()
.
Майкл Кэй, автор реализации Saxon, указал, что substring()
действительно работает медленно из-за сложных правил, которые приходится поддерживать, в том числе округления аргументов с плавающей точкой, обработки особых случаев, когда начальная или конечная точки оказываются за границами строки, и вопросов, связанных с суррогатными парами Unicode. Напротив, функции substring-before()
и substring-after()
гораздо лучше транслируются на язык Java.
Отсюда следует извлечь урок: оптимизация – дело непростое, особенно в XSLT, когда имеются существенные различия между реализациями, а в новых версиях авторы стараются применить дополнительные оптимизации. Если вы не готовы часто профилировать программу, то лучше ограничиться простыми решениями.
К числу достоинств простых решений можно отнести и то, что, скорее всего, они будут вести себя одинаково в разных реализациях XSLT.