Часть шаблона можно заключить в скобки pattern:(...). Это называется "скобочная группа".
У такого выделения есть два эффекта:
- Позволяет поместить часть совпадения в отдельный массив.
- Если установить квантификатор после скобок, то он будет применяться ко всему содержимому группы, а не к одному символу.
Разберём скобки на примерах.
Без скобок шаблон pattern:go+ означает символ subject:g и идущий после него символ subject:o, который повторяется один или более раз. Например, match:goooo или match:gooooooooo.
Скобки группируют символы вместе. Так что pattern:(go)+ означает match:go, match:gogo, match:gogogo и т.п.
alert( 'Gogogo now!'.match(/(go)+/ig) ); // "Gogogo"Сделаем что-то более сложное -- регулярное выражение, которое соответствует домену сайта.
Например:
mail.com
users.mail.com
smith.users.mail.com
Как видно, домен состоит из повторяющихся слов, причём после каждого, кроме последнего, стоит точка.
На языке регулярных выражений pattern:(\w+\.)+\w+:
let regexp = /(\w+\.)+\w+/g;
alert( "site.com my.site.com".match(regexp) ); // site.com,my.site.comПоиск работает, но такому шаблону не соответствует домен с дефисом, например, my-site.com, так как дефис не входит в класс pattern:\w.
Можно исправить это, заменим pattern:\w на pattern:[\w-] везде, кроме как в конце: pattern:([\w-]+\.)+\w+.
Предыдущий пример можно расширить, создав регулярное выражение для поиска email.
Формат email: имя@домен. В качестве имени может быть любое слово, разрешены дефисы и точки. На языке регулярных выражений это pattern:[-.\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, в любом случае, можно осуществить, лишь послав на него письмо.
Скобочные группы нумеруются слева направо. Поисковый движок запоминает содержимое, которое соответствует каждой скобочной группе, и позволяет получить его в результате.
Метод str.match(regexp), если у регулярного выражения regexp нет флага g, ищет первое совпадение и возвращает его в виде массива:
- На позиции
0будет всё совпадение целиком. - На позиции
1- содержимое первой скобочной группы. - На позиции
2- содержимое второй скобочной группы. - ...и так далее...
Например, мы хотим найти HTML теги pattern:<.*?> и обработать их. Было бы удобно иметь содержимое тега (то, что внутри уголков) в отдельной переменной.
Давайте заключим внутреннее содержимое в круглые скобки: pattern:<(.*?)>.
Теперь получим как тег целиком match:<h1>, так и его содержимое match:h1 в виде массива:
let str = '<h1>Hello, world!</h1>';
let tag = str.match(/<(.*?)>/);
alert( tag[0] ); // <h1>
alert( tag[1] ); // h1Скобки могут быть и вложенными.
Например, при поиске тега в subject:<span class="my"> нас может интересовать:
- Содержимое тега целиком:
match:span class="my". - Название тега:
match:span. - Атрибуты тега:
match:class="my".
Заключим их в скобки в шаблоне: pattern:<(([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] идёт группа, образованная второй открывающей скобкой pattern:([a-z]+) -- имя тега, далее в result[3] будет остальное содержимое тега: pattern:([^>]*).
Соответствие для каждой группы в строке:
Даже если скобочная группа необязательна (например, стоит квантификатор pattern:(...)?), соответствующий элемент массива result существует и равен undefined.
Например, рассмотрим регулярное выражение pattern:a(z)?(c)?. Оно ищет букву "a", за которой идёт необязательная буква "z", за которой, в свою очередь, идёт необязательная буква "c".
Если применить его к строке из одной буквы subject: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, но все скобочные группы пустые.
А теперь более сложная ситуация для строки subject: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. Для группы pattern:(z)? ничего нет, поэтому результат: ["ac", undefined, "c"].
```warn header="matchAll является новым, может потребоваться полифил"
Метод не поддерживается в старых браузерах.
Может потребоваться полифил, например https://github.com/ljharb/String.prototype.matchAll.
При поиске всех совпадений (флаг `pattern:g`) метод `match` не возвращает скобочные группы.
Например, попробуем найти все теги в строке:
```js run
let str = '<h1> <h2>';
let tags = str.match(/<(.*?)>/g);
alert( tags ); // <h1>,<h2>
Результат - массив совпадений, но без деталей о каждом. Но на практике скобочные группы тоже часто нужны.
Для того, чтобы их получать, мы можем использовать метод str.matchAll(regexp).
Он был добавлен в язык JavaScript гораздо позже чем str.match, как его "новая и улучшенная" версия.
Он, как и str.match(regexp), ищет совпадения, но у него есть три отличия:
- Он возвращает не массив, а перебираемый объект.
- При поиске с флагом
pattern: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. Более подробно о псевдомассивах и перебираемых объектах мы говорили в главе info:iterable.
В явном преобразовании через 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 без флага pattern: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>```smart header="Почему результат matchAll - перебираемый объект, а не обычный массив?"
Зачем так сделано? Причина проста - для оптимизации.
При вызове matchAll движок JavaScript возвращает перебираемый объект, в котором ещё нет результатов. Поиск осуществляется по мере того, как мы запрашиваем результаты, например, в цикле.
Таким образом, будет найдено ровно столько результатов, сколько нам нужно.
Например, всего в тексте может быть 100 совпадений, а в цикле после 5-го результата мы поняли, что нам их достаточно и сделали break. Тогда движок не будет тратить время на поиск остальных 95.
## Именованные группы
Запоминать группы по номерам не очень удобно. Для простых шаблонов это допустимо, но в сложных регулярных выражениях считать скобки затруднительно. Гораздо лучше - давать группам имена.
Это делается добавлением `pattern:?<name>` непосредственно после открытия скобки.
Например, поищем дату в формате "год-месяц-день":
```js run
*!*
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.
Чтобы найти не только первую дату, используем флаг pattern: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, позволяет использовать в строке замены содержимое скобок. Это делается при помощи обозначений вида pattern:$n, где pattern:n - номер скобочной группы.
Например:
let str = "John Bull";
let regexp = /(\w+) (\w+)/;
alert( str.replace(regexp, '$2, $1') ); // Bull, JohnДля именованных групп ссылка будет выглядеть как pattern:$<имя>.
Например, заменим даты в формате "год-месяц-день" на "день.месяц.год":
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Бывает так, что скобки нужны, чтобы квантификатор правильно применился, но мы не хотим, чтобы их содержимое было выделено в результате.
Скобочную группу можно исключить из запоминаемых и нумеруемых, добавив в её начало pattern:?:.
Например, если мы хотим найти pattern:(go)+, но не хотим иметь в массиве-результате отдельным элементом содержимое скобок (go), то можем написать pattern:(?:go)+.
В примере ниже мы получим только имя match: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 (больше в массиве элементов нет)Как видно, содержимое скобок pattern:(?:go) не стало отдельным элементом массива result.
Круглые скобки группируют вместе часть регулярного выражения, так что квантификатор применяется к ней в целом.
Скобочные группы нумеруются слева направо. Также им можно дать имя с помощью pattern:(?<name>...).
Часть совпадения, соответствующую скобочной группе, мы можем получить в результатах поиска.
- Метод
str.matchвозвращает скобочные группы только без флагаpattern:g. - Метод
str.matchAllвозвращает скобочные группы всегда.
Если скобочная группа не имеет имени, то её содержимое будет по своему номеру в массиве-результате, а если имеет, то также в свойстве groups.
Содержимое скобочной группы можно также использовать при замене str.replace(regexp, replacement): по номеру $n или по имени $<имя>.
Можно исключить скобочную группу из запоминания, добавив в её начало pattern:?:. Это используется, если необходимо применить квантификатор ко всей группе, но не запоминать её содержимое в отдельном элементе массива-результата. Также мы не можем ссылаться на такую группу в строке замены.