Мастер-классы по Javascript, AJAX/COMET, jQuery Узнать больше...
Javascript.RU

Особенности регулярных выражений в Javascript

Регулярные выражения в javascript немного странные. Вроде - перловые, обычные, но с подводными камнями, на которые натыкаются даже опытные javascript-разработчики.

Эта статья ставит целью перечислить неожиданные фишки и особенности RegExp в краткой и понятной форме.

Общую информацию о регулярных выражениях в javascript вы можете найти в статье Регулярные выражения.

Для поиска в многострочном режиме почти все модификации перловых регэкспов используют специальный 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) влияет только на символы ^ и $, которые начинают матчиться с началом и концом строки, а не всего текста.

Точка по-прежнему - любой символ, кроме новой строки. В javasript нет флага, который устанавливает мультилайн-режим для точки. Для того, чтобы заматчить совсем что угодно - используйте [\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>. На что и почему - не так важно.

text = '1 <A href="#">...</A> 2'
text = text.replace(/<A(.*)>/, 'TEST')
alert(text)

При запуске вы увидите, что заменяется не открывающий тэг, а вся ссылка, выражение матчит ее от начала и до конца.

Это происходит из-за того, что точка-звездочка в "жадном" режиме пытается захватить как можно больше, в нашем случае - это как раз до последнего >.

Последний символ > точка-звездочка не захватывает, т.к. иначе не будет совпадения.

Как вариант решения используют квадратные скобки: [^>]:

text = '1 <A href="#">...</A> 2'
text = text.replace(/<A([^>]*)>/, 'TEST')
alert(text)

Это работает. Но самым удобным вариантом является переключение точки-звездочки в нежадный режим. Это осуществляется простым добавлением знака "?" после звездочки.

В нежадном режиме точка-звездочка пустит поиск дальше сразу, как только нашла совпадение:

text = '1 <A href="#">...</A> 2'
text = text.replace(/<A(.*?)>/, 'TEST')
alert(text)

В некоторых языках программирования можно переключить жадность на уровне всего регулярного выражения, флагом.

В javascript это сделать нельзя.. Вот такая особенность. А вопросительный знак после звездочки рулит - честное слово.

Иногда нужно в самом паттерне поиска обратиться к предыдущей его части.

Например, при поиске BB-тагов, то есть строк вида [u]...[/u], [b]...[/b] и [s]...[/s]. Или при поиске атрибутов, которые могут быть в одинарных кавычках или двойных.

Обращение к предыдущей части паттерна в javascript осуществляется как \1, \2 и т.п., бэкслеш + номер скобочной группы:

text = ' [b]a [u]b[/u] c [/b] '

var reg = /\[([bus])\](.*?)\[\//*u*/\1/*/u*/\]/
text = text.replace(reg, '<$1>$2</$1>')
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
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.

Ну и напоследок - еще одна совсем оригинальная особенность регулярных выражений.

Вот - одна интересная функция.

Запустите ее один раз, запомните результат - и запустите еще раз.

function rere() {
    var re1 = /0/, re2 = new RegExp('0')
    alert([re1.foo, re2.foo])
    re1.foo = 1
    re2.foo = 1
}
rere()

В зависимости от браузера, результат первого запуска может отличаться от второго. На текущий момент, это так для Firefox, Opera. При этом в Internet Explorer все нормально.

С виду функция создает две локальные переменные и не зависит от каких-то внешних факторов.

Почему же разный результат?

Ответ кроется в стандарте ECMAScript, пункт 7.8.5:

Цитата...

A regular expression literal is an input element that is converted to a RegExp object (section 15.10)
when it is scanned. The object is created before evaluation of the containing program or function begins.
Evaluation of the literal produces a reference to that object; it does not create a new object.

То есть, простыми словами, литеральный регэксп не создается каждый раз при вызове var r = /regexp/.
Вместо этого браузер возвращает уже существующий объект, со всеми свойствами, оставшимися от предыдущего запуска.

В отличие от этого, new RegExp всегда создает новый объект, поэтому и ведет себя в примере по-другому.

Есть еще особенности?

Напишите в комментарии, и я добавлю их в статью.


Автор: Octane, дата: 18 января, 2010 - 20:17
#permalink

Наверное, стоит упомянуть о lastIndex, из-за которого иногда получаются вот такие, не совсем очевидные, ситуации:

var toLowerCamelCase = function () {
	var expr = /-([a-z])/g;
	return function(prop) {
		return expr.test(prop) ? prop.replace(expr, function () {
			return arguments[1].toUpperCase();
		}) : prop;
	};
}();

alert([
    toLowerCamelCase('background-image'),
    toLowerCamelCase('background-image'),
    toLowerCamelCase('background-image'),
    toLowerCamelCase('background-image')
].join("\n"));

Эффект можно наблюдать в IE и Chrome 4

backgroundImage
background-image
backgroundImage
background-image

Автор: Андрей Сидоров (не зарегистрирован), дата: 18 января, 2010 - 15:24
#permalink

регекспы яваскрипта позаимствовали синтаксис из перла, но реализовали только часть функционала. на сколько я знаю, своего они ничего не придумывали, и в таком случае скорее стоит говорить не об особенностях js-регекспов, а об их отличии от перловых.

а именно:

поддерживаются только три модификатора i,g,m
не поддерживаются модификаторы внутри регекспа (?i) (?-i)
не поддерживаются \A и \Z (начало и конец строки)
нет possessive quantifiers ?+ *+ ++
нет атомарной группировки (?>...)
нет lookbehind assertions (?<=...) (?...)
нет conditionals (?(condition)yes-pattern|no-pattern)
нет комментариев (?#comment)
нет рекурсивной конструкции (?R)

p.s. если не читали, рекомендую почитать книгу Фридла по регекспам - очень способствует пониманию регекспов.
http://www.amazon.com/Mastering-Regular-Expressions-Jeffrey-Friedl/dp/05...


Автор: Илья Кантор, дата: 18 января, 2010 - 19:35
#permalink

В этой статье описаны не отсутствующие фичи, по сравнению с перлом, а особенности.

То есть фичи и способы решения задач, которые ЕСТЬ, но работают ИНАЧЕ, чем мы привыкли видеть.


Автор: Rosya (не зарегистрирован), дата: 18 января, 2010 - 16:50
#permalink

Спасибо за рекомендации по поводу комментариев... Но дополнений и исправлений нет, есть только слова благодарности за такую информацию...


Автор: Regent, дата: 19 января, 2010 - 14:35
#permalink

.*? - действительно пробивная штука


Автор: anikey99, дата: 19 января, 2010 - 20:33
#permalink

вопрос почти в тему:
так как выражение replace в js работает только 1 раз -
как посчитать на странице количество bb тегов, в частности, ([url=ссылка]описание[/url] ), и если можно, как заменить такое выражение на желаемый мной текст?


Автор: B@rmaley.e><e, дата: 28 января, 2010 - 09:11
#permalink
var count = 0;
string.replace(/\[url=([^\]]+)\](.+?)\[\/url\]/ig, function($$, $1, $2){
  count++;
  return '<a href="' + $1 +'">' + $2 + '</a>';
});

Автор: Сашок (не зарегистрирован), дата: 25 января, 2010 - 14:03
#permalink

Да сложное дело java, а вы платную помощь не оказываете?


Автор: Илья Кантор, дата: 25 января, 2010 - 14:39
#permalink

Бесплатную оказываем. В форум.

P.S. При конкретной формулировке вопроса и адекватности спрашивающего.


Автор: anikey99, дата: 27 января, 2010 - 08:57
#permalink

помощь оказываем всякую, если надо что то конкретное написать обращайся


Автор: anikey99, дата: 27 января, 2010 - 08:57
#permalink

При конкретной формулировке вопроса и адекватности спрашивающего.

А что мой вопрос от 19 января, 2010 - 19:33 был не адекватен?


Автор: Илья Кантор, дата: 27 января, 2010 - 12:47
#permalink

Прочитайте статью еще раз, там появился раздел о поиске и замене. Надеюсь, он поможет в решении вашей задачи.


Автор: Гость (не зарегистрирован), дата: 19 февраля, 2010 - 22:09
#permalink

Подскажите, пожалуйста.
Делаю плагин autocomplete к jQuery. Не получается сделать проверку на вхождение и замену в строках с символами кириллицы.
Делаю так:

var re = new RegExp('\\b'+text,'gi');
if(re.search(str))
 str.replace(re,'<strong>$1</strong>');

где str — текст текущего элемент списка (в цикле обхожу все элементы),
text — это текст, содержащийся в поле ввода, т.е. подстрока.
С латиницей получается, с кириллицей — нет.


Автор: Гость (не зарегистрирован), дата: 19 февраля, 2010 - 22:14
#permalink

Ой, простите, перепутал немного.

if(str.search(re))

Автор: Гость (не зарегистрирован), дата: 20 февраля, 2010 - 09:22
#permalink

Нашёл примерчик регулярного выражения из готового плагина. Сработало! Объясните его, пожалуйста.

var re = new RegExp("(?![^&;]+;)(?!<[^<>]*)("+text+")(?![^<>]*>)(?![^&;]+;)",'gi');

Интересует само содержимое выражения.


Автор: DmitryRegEx (не зарегистрирован), дата: 2 марта, 2010 - 21:29
#permalink

Подскажите как лучше решить задачу, если есть большой текст и надо произвести замену только одного вхождения подстроки, но при этом не первого (как будет без указания global), а последнего.
То есть, например есть строка "1xy2xy3xy4xy" и надо убрать последнее вхождение "xy", то есть получить "1xy2xy3xy4"
Подскажите, как это реализовать средствами javascript ?


Автор: Яростный Меч, дата: 12 апреля, 2010 - 15:00
#permalink

Воспользуемся жадностью
var result = '1xy2xy3xy4xy'.replace(/([\s\S]*)xy/m, function(a,b){return b;})


Автор: Гость (не зарегистрирован), дата: 19 июня, 2010 - 19:25
#permalink

можно вот эти слова перевести в java выражения?
привет
пока
как дела
пошол ты
здарова
что делаеш

Буду очень блогодарен


Автор: Гость (не зарегистрирован), дата: 20 июня, 2010 - 10:00
#permalink

Для удаления тега, например span можно написать так:
x=x.replace(/<(\/?)span([^>]*)>/ig,'')
от тега останется только то, что в нём, например от:
Содержимое
останется только "Содержимое"
А как убрать всё целиком и тег и его содержимое?


Автор: Slawaq, дата: 26 августа, 2010 - 02:42
#permalink

то что искал 3 дня по бб кодам, только не давно начал работать с регами из-за это-го плохо понимал, статья супер, примеры идеальны


Отправить комментарий

Приветствуются комментарии:
  • Полезные.
  • Дополняющие прочитанное.
  • Вопросы по прочитанному. Именно по прочитанному, чтобы ответ на него помог другим разобраться в предмете статьи. Другие вопросы могут быть удалены.
    Для остальных вопросов и обсуждений есть форум.
P.S. Лучшее "спасибо" - не комментарий, как все здорово, а рекомендация или ссылка на статью.
Содержание этого поля является приватным и не предназначено к показу.
  • Адреса страниц и электронной почты автоматически преобразуются в ссылки.
  • Разрешены HTML-таги: <strike> <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <u> <i> <b> <pre> <img> <abbr> <blockquote> <h1> <h2> <h3> <h4> <h5> <p> <div> <span> <sub> <sup>
  • Строки и параграфы переносятся автоматически.
  • Текстовые смайлы будут заменены на графические.

Подробнее о форматировании

CAPTCHA
Антиспам
14 + 5 =
Введите результат. Например, для 1+3, введите 4.
 
Новости

Открылась регистрация на мастер-классы по профессиональному Javascript, AJAX/COMET, jQuery в городах:

  • Ярославль (24-25 сентября)
  • Новосибирск (3-4 октября)
  • Казань (9-10 октября)
  • Минск (16-17 октября)
  • Днепропетровск (23-24 октября)
  • Одесса (30-31 октября)
  • Самара (13-14 ноября)

Более подробно - на странице мастер-классов.

Если вас интересует другой город - посмотрите здесь, выбрав "Другие города".

Текущий раздел
Поиск по сайту
Содержание

Учебник javascript

Основные элементы языка

Сундучок с инструментами

Интерфейсы

Все об AJAX

Оптимизация

Разное

Дерево всех статей

Статьи и мероприятия

Будьте в курсе наших последних новостей!

Последние обсуждения на форуме
Forum
Последние комментарии