Часть шаблона можно заключить в скобки (...). Это называется «скобочная группа».
У такого выделения есть два эффекта:
- Позволяет поместить часть совпадения в отдельный массив.
- Если установить квантификатор после скобок, то он будет применяться ко всему содержимому скобки, а не к одному символу.
Примеры
Разберём скобки на примерах.
Пример: gogogo
Без скобок шаблон go+ означает символ g и идущий после него символ o, который повторяется один или более раз. Например, goooo или gooooooooo.
Скобки группируют символы вместе. Так что (go)+ означает go, gogo, gogogo и т.п.
alert( 'Gogogo now!'.match(/(go)+/ig) ); // "Gogogo"
Пример: домен
Сделаем что-то более сложное – регулярное выражение, которое соответствует домену сайта.
Например:
mail.com
users.mail.com
smith.users.mail.com
Как видно, домен состоит из повторяющихся слов, причём после каждого, кроме последнего, стоит точка.
На языке регулярных выражений (\w+\.)+\w+:
let regexp = /(\w+\.)+\w+/g;
alert( "site.com my.site.com".match(regexp) ); // site.com,my.site.com
Поиск работает, но такому шаблону не соответствует домен с дефисом, например, my-site.com, так как дефис не входит в класс \w.
Можно исправить это, заменим \w на [\w-] везде, кроме как в конце: ([\w-]+\.)+\w+.
Пример: email
Предыдущий пример можно расширить, создав регулярное выражение для поиска email.
Формат email: имя@домен. В качестве имени может быть любое слово, разрешены дефисы и точки. На языке регулярных выражений это [-.\w]+.
Итоговый шаблон:
let regexp = /[-.\w]+@([\w-]+\.)+[\w-]+/g;
alert("my@mail.com @ his@site.com.uk".match(regexp)); // my@mail.com, his@site.com.uk
Это регулярное выражение не идеальное, но, как правило, работает и помогает исправлять опечатки. Окончательную проверку правильности email, в любом случае, можно осуществить, лишь послав на него письмо.
Содержимое скобок в match
Скобочные группы нумеруются слева направо. Поисковый движок запоминает содержимое, которое соответствует каждой скобочной группе, и позволяет получить его в результате.
Метод str.match(regexp), если у регулярного выражения regexp нет флага g, ищет первое совпадение и возвращает его в виде массива:
- На позиции
0будет всё совпадение целиком. - На позиции
1– содержимое первой скобочной группы. - На позиции
2– содержимое второй скобочной группы. - …и так далее…
Например, мы хотим найти HTML теги <.*?> и обработать их. Было бы удобно иметь содержимое тега (то, что внутри уголков) в отдельной переменной.
Давайте заключим внутреннее содержимое в круглые скобки: <(.*?)>.
Теперь получим как тег целиком <h1>, так и его содержимое h1 в виде массива:
let str = '<h1>Hello, world!</h1>';
let tag = str.match(/<(.*?)>/);
alert( tag[0] ); // <h1>
alert( tag[1] ); // h1
Вложенные группы
Скобки могут быть и вложенными.
Например, при поиске тега в <span class="my"> нас может интересовать:
- Содержимое тега целиком:
span class="my". - Название тега:
span. - Атрибуты тега:
class="my".
Заключим их в скобки в шаблоне: <(([a-z]+)\s*([^>]*))>.
Вот их номера (слева направо, по открывающей скобке):
В действии:
let str = '<span class="my">';
let regexp = /<(([a-z]+)\s*([^>]*))>/;
let result = str.match(regexp);
alert(result[0]); // <span class="my">
alert(result[1]); // span class="my"
alert(result[2]); // span
alert(result[3]); // class="my"
По нулевому индексу в result всегда идёт полное совпадение.
Затем следуют группы, нумеруемые слева направо, по открывающим скобкам. Группа, открывающая скобка которой идёт первой, получает первый индекс в результате – result[1]. Там находится всё содержимое тега.
Затем в result[2] идёт группа, образованная второй открывающей скобкой ([a-z]+) – имя тега, далее в result[3] будет остальное содержимое тега: ([^>]*).
Соответствие для каждой группы в строке:
Необязательные группы
Даже если скобочная группа необязательна (например, стоит квантификатор (...)?), соответствующий элемент массива result существует и равен undefined.
Например, рассмотрим регулярное выражение a(z)?(c)?. Оно ищет букву "a", за которой идёт необязательная буква "z", за которой, в свою очередь, идёт необязательная буква "c".
Если применить его к строке из одной буквы a, то результат будет такой:
let match = 'a'.match(/a(z)?(c)?/);
alert( match.length ); // 3
alert( match[0] ); // a (всё совпадение)
alert( match[1] ); // undefined
alert( match[2] ); // undefined
Массив имеет длину 3, но все скобочные группы пустые.
А теперь более сложная ситуация для строки ac:
let match = 'ac'.match(/a(z)?(c)?/)
alert( match.length ); // 3
alert( match[0] ); // ac (всё совпадение)
alert( match[1] ); // undefined, потому что для (z)? ничего нет
alert( match[2] ); // c
Длина массива всегда равна 3. Для группы (z)? ничего нет, поэтому результат: ["ac", undefined, "c"].
Поиск всех совпадений с группами: matchAll
matchAll является новым, может потребоваться полифилМетод не поддерживается в старых браузерах.
Может потребоваться полифил, например https://github.com/ljharb/String.prototype.matchAll.
При поиске всех совпадений (флаг g) метод match не возвращает скобочные группы.
Например, попробуем найти все теги в строке:
let str = '<h1> <h2>';
let tags = str.match(/<(.*?)>/g);
alert( tags ); // <h1>,<h2>
Результат – массив совпадений, но без деталей о каждом. Но на практике скобочные группы тоже часто нужны.
Для того, чтобы их получать, мы можем использовать метод str.matchAll(regexp).
Он был добавлен в язык JavaScript гораздо позже чем str.match, как его «новая и улучшенная» версия.
Он, как и str.match(regexp), ищет совпадения, но у него есть три отличия:
- Он возвращает не массив, а перебираемый объект.
- При поиске с флагом
g, он возвращает каждое совпадение в виде массива со скобочными группами. - Если совпадений нет, он возвращает не
null, а просто пустой перебираемый объект.
Например:
let results = '<h1> <h2>'.matchAll(/<(.*?)>/gi);
// results - не массив, а перебираемый объект
alert(results); // [object RegExp String Iterator]
alert(results[0]); // undefined (*)
results = Array.from(results); // превращаем в массив
alert(results[0]); // <h1>,h1 (первый тег)
alert(results[1]); // <h2>,h2 (второй тег)
Как видите, первое отличие – очень важное, это демонстрирует строка (*). Мы не можем получить совпадение как results[0], так как этот объект не является псевдомассивом. Его можно превратить в настоящий массив при помощи Array.from. Более подробно о псевдомассивах и перебираемых объектов мы говорили в главе Перебираемые объекты.
В явном преобразовании через Array.from нет необходимости, если мы перебираем результаты в цикле, вот так:
let results = '<h1> <h2>'.matchAll(/<(.*?)>/gi);
for(let result of results) {
alert(result);
// первый вывод: <h1>,h1
// второй: <h2>,h2
}
…Или используем деструктуризацию:
let [tag1, tag2] = '<h1> <h2>'.matchAll(/<(.*?)>/gi);
Каждое совпадение, возвращаемое matchAll, имеет тот же вид, что и при match без флага g: это массив с дополнительными свойствами index (позиция совпадения) и input (исходный текст):
let results = '<h1> <h2>'.matchAll(/<(.*?)>/gi);
let [tag1, tag2] = results;
alert( tag1[0] ); // <h1>
alert( tag1[1] ); // h1
alert( tag1.index ); // 0
alert( tag1.input ); // <h1> <h2>
matchAll – перебираемый объект, а не обычный массив?Зачем так сделано? Причина проста – для оптимизации.
При вызове matchAll движок JavaScript возвращает перебираемый объект, в котором ещё нет результатов. Поиск осуществляется по мере того, как мы запрашиваем результаты, например, в цикле.
Таким образом, будет найдено ровно столько результатов, сколько нам нужно.
Например, всего в тексте может быть 100 совпадений, а в цикле после 5-го результата мы поняли, что нам их достаточно и сделали break. Тогда движок не будет тратить время на поиск остальных 95.
Именованные группы
Запоминать группы по номерам не очень удобно. Для простых шаблонов это допустимо, но в сложных регулярных выражениях считать скобки затруднительно. Гораздо лучше – давать скобкам имена.
Это делается добавлением ?<name> непосредственно после открытия скобки.
Например, поищем дату в формате «год-месяц-день»:
let dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;
let str = "2019-04-30";
let groups = str.match(dateRegexp).groups;
alert(groups.year); // 2019
alert(groups.month); // 04
alert(groups.day); // 30
Как вы можете видеть, группы располагаются в свойстве groups результата match.
Чтобы найти не только первую дату, используем флаг g.
Также нам понадобится matchAll, чтобы получить скобочные группы:
let dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/g;
let str = "2019-10-30 2020-01-01";
let results = str.matchAll(dateRegexp);
for(let result of results) {
let {year, month, day} = result.groups;
alert(`${day}.${month}.${year}`);
// первый вывод: 30.10.2019
// второй: 01.01.2020
}
Скобочные группы при замене
Метод str.replace(regexp, replacement), осуществляющий замену совпадений с regexp в строке str, позволяет использовать в строке замены содержимое скобок. Это делается при помощи обозначений вида $n, где n – номер скобочной группы.
Например:
let str = "John Bull";
let regexp = /(\w+) (\w+)/;
alert( str.replace(regexp, '$2, $1') ); // Bull, John
Для именованных скобок ссылка будет выглядеть как $<имя>.
Например, заменим даты в формате «год-месяц-день» на «день.месяц.год»:
let regexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/g;
let str = "2019-10-30, 2020-01-01";
alert( str.replace(regexp, '$<day>.$<month>.$<year>') );
// 30.10.2019, 01.01.2020
Исключение из запоминания через ?:
Бывает так, что скобки нужны, чтобы квантификатор правильно применился, но мы не хотим, чтобы их содержимое было выделено в результате.
Скобочную группу можно исключить из запоминаемых и нумеруемых, добавив в её начало ?:.
Например, если мы хотим найти (go)+, но не хотим иметь в массиве-результате отдельным элементом содержимое скобок (go), то можем написать (?:go)+.
В примере ниже мы получим только имя John как отдельный элемент совпадения:
let str = "Gogogo John!";
// ?: исключает go из запоминания
let regexp = /(?:go)+ (\w+)/i;
let result = str.match(regexp);
alert( result[0] ); // Gogogo John (полное совпадение)
alert( result[1] ); // John
alert( result.length ); // 2 (больше в массиве элементов нет)
Как видно, содержимое скобок (?:go) не стало отдельным элементом массива result.
Итого
Круглые скобки группируют вместе часть регулярного выражения, так что квантификатор применяется к ним в целом.
Скобочные группы нумеруются слева направо. Также им можно дать имя с помощью (?<name>...).
Часть совпадения, соответствующую скобочной группе, мы можем получить в результатах поиска.
- Метод
str.matchвозвращает скобочные группы только без флагаg. - Метод
str.matchAllвозвращает скобочные группы всегда.
Если скобка не имеет имени, то содержимое группы будет по своему номеру в массиве-результате, если имеет, то также в свойстве groups.
Содержимое скобочной группы можно также использовать при замене str.replace(regexp, replacement): по номеру $n или по имени $<имя>.
Можно исключить скобочную группу из запоминания, добавив в её начало ?:. Это используется, если необходимо применить квантификатор ко всей группе, но не запоминать их содержимое в отдельном элементе массива-результата. Также мы не можем ссылаться на такие скобки в строке замены.
Комментарии
<code>, для нескольких строк кода — тег<pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)