Регулярные выражения в javascript немного странные. Вроде – перловые, обычные, но с подводными камнями, на которые натыкаются даже опытные javascript-разработчики.
Эта статья ставит целью перечислить неожиданные фишки и особенности RegExp
в краткой и понятной форме.
Точка и перенос строки
Для поиска в многострочном режиме почти все модификации перловых регэкспов используют специальный multiline-флаг.
И javascript здесь не исключение.
Попробуем же сделать поиск и замену многострочного вхождения. Скажем, будем заменять [u] … [/u]
на тэг подчёркивания: <u>
:
function bbtagit(text) {
text = text.replace(/\[u\](.*?)\[\/u\]/gim, '<u>$1</u>')
return text
}
var line = "[u]мой\n текст[/u]"
alert(bbtagit(line))
Попробуйте запустить. Заменяет? Как бы не так!
Дело в том, что в javascript мультилайн режим (флаг m
) влияет только на символы ^ и $, которые начинают матчиться с началом и концом строки, а не всего текста.
Точка по-прежнему – любой символ, кроме новой строки. В javascript нет флага, который устанавливает мультилайн-режим для точки. Для того, чтобы заматчить совсем что угодно – используйте [\s\S]
.
Работающий вариант:
function bbtagit(text) {
text = text.replace(/\[u\]([\s\S]*)\[\/u\]/gim, '<u>$1</u>')
return text
}
var line = "[u]мой\n текст[/u]"
alert(bbtagit(line))
Жадность
Это не совсем особенность, скорее фича, но всё же достойная отдельного абзаца.
Все регулярные выражения в javascript – жадные. То есть, выражение старается отхватить как можно больший кусок строки.
Например, мы хотим заменить все открывающие тэги <a>
. На что и почему – не так важно.
var text = '1 <A href="#">...</A> 2'
text = text.replace(/<A(.*)>/, 'TEST')
alert(text)
При запуске вы увидите, что заменяется не открывающий тэг, а вся ссылка, выражение матчит её от начала и до конца.
Это происходит из-за того, что точка-звёздочка в «жадном» режиме пытается захватить как можно больше, в нашем случае – это как раз до последнего >
.
Последний символ >
точка-звёздочка не захватывает, т.к. иначе не будет совпадения.
Как вариант решения используют квадратные скобки: [^>]
:
var text = '1 <A href="#">...</A> 2'
text = text.replace(/<A([^>]*)>/, 'TEST')
alert(text)
Это работает. Но самым удобным вариантом является переключение точки-звёздочки в нежадный режим. Это осуществляется простым добавлением знака «?
» после звёздочки.
В нежадном режиме точка-звёздочка пустит поиск дальше сразу, как только нашла совпадение:
var text = '1 <A href="#">...</A> 2'
text = text.replace(/<A(.*?)>/, 'TEST')
alert(text)
В некоторых языках программирования можно переключить жадность на уровне всего регулярного выражения, флагом.
В javascript это сделать нельзя… Вот такая особенность. А вопросительный знак после звёздочки рулит – честное слово.
Backreferences в паттерне и при замене
Иногда нужно в самом паттерне поиска обратиться к предыдущей его части.
Например, при поиске BB-тагов, то есть строк вида [u]…[/u]
, [b]…[/b]
и [s]…[/s]
. Или при поиске атрибутов, которые могут быть в одинарных кавычках или двойных.
Обращение к предыдущей части паттерна в javascript осуществляется как \1, \2 и т.п., бэкслеш + номер скобочной группы:
var text = ' [b]a [u]b[/u] c [/b] ';
var reg = /\[([bus])\](.*?)\[\/\1\] /;
text = text.replace(reg, '<$1>$2</$1>'); // <b>a [u]b[/u] c </b>
alert(text);
Обращение к скобочной группе в строке замены идёт уже через доллар: $1
. Не знаю, почему, наверное так удобнее…
P.S. Понятно, что при таком способе поиска bb-тагов придётся пропустить текст через замену несколько раз – пока результат не перестанет отличаться от оригинала.
Найти все / Заменить все
Эти две задачи решаются в javascript принципиально по-разному.
Начнём с «простого».
Заменить все
Для замены всех вхождений используется метод String#replace. Он интересен тем, что допускает первый аргумент – регэксп или строку.
Если первый аргумент – строка, то будет осуществлён поиск подстроки, без преобразования в регулярное выражение.
Попробуйте:
alert("2 ++ 1".replace("+", "*"))
Как видите, заменился только один плюс, а не оба.
Чтобы заменить все вхождения, String#replace обязательно нужно использовать с регулярным выражением.
В режиме регулярного выражения плюс придётся экранировать, но зато replace
заменит все вхождения (при указании флага g
):
alert("2 ++ 1".replace(/\+/g, "*"))
Вот такая особенность работы со строкой.
Заменить функцией
Очень полезной особенностью replace
является возможность работать с функцией вместо строки замены. Такая функция получает первым аргументом – все совпадения, а последующими аргументами – скобочные группы.
Следующий пример произведёт операции вычитания:
var str = "count 36 - 26, 18 - 9"
str = str.replace(/(\d+) - (\d+)/g, function(a,b,c) { return b-c })
alert(str)
Найти всё
В javascript нет одного универсального метода для поиска всех совпадений. Для поиска без запоминания скобочных групп – можно использовать String#match:
var str = "count 36-26, 18-9";
var re = /(\d+)-(\d+)/g;
var result = str.match(re);
for (var i = 0; i < result.length; i++) {
alert(result[i]);
}
Как видите, оно исправно ищет все совпадения (флаг „g“
у регулярного выражения обязателен), но при этом не запоминает скобочные группы. Эдакий «облегчённый вариант».
Найти всё с учётом скобочных групп
В сколько-нибудь сложных задачах важны не только совпадения, но и скобочные группы. Чтобы их найти, предлагается использовать многократный вызов RegExp#exec.
Для этого регулярное выражение должно использовать флаг „g“
. Тогда результат поиска, запомненный в свойстве lastIndex
объекта RegExp
используется как точка отсчёта для следующего поиска:
var str = "count 36-26, 18-9"
var re = /(\d+)-(\d+)/g
var res
while ((res = re.exec(str)) != null) {
alert("Найдено " + res[0] + ": (" + res[1] + ") и (" + res[2] + ")")
alert("Дальше ищу с позиции " + re.lastIndex)
}
Проверка while( (res = re.exec(str)) != null)
нужна т.к. значение res = 0
является хорошим и означает, что вхождение найдено в самом начале строки (поиск успешен). Поэтому необходимо сравнивать именно с null
.