Скобочные группы
Часть шаблона можно заключить в скобки (. ) . Это называется «скобочная группа».
У такого выделения есть два эффекта:
- Позволяет поместить часть совпадения в отдельный массив.
- Если установить квантификатор после скобок, то он будет применяться ко всему содержимому скобки, а не к одному символу.
Примеры
Разберём скобки на примерах.
Пример: 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 в виде массива:
let str = 'Hello, world!
'; let tag = str.match(/<(.*?)>/); alert( tag[0] ); // alert( tag[1] ); // h1
Вложенные группы
Скобки могут быть и вложенными.
Например, при поиске тега в нас может интересовать:
Заключим их в скобки в шаблоне: <(([a-z]+)\s*([^>]*))> .
Вот их номера (слева направо, по открывающей скобке):
let str = ']*))>/; let result = str.match(regexp); alert(result[0]); //По нулевому индексу в 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
Метод не поддерживается в старых браузерах.
При поиске всех совпадений (флаг g ) метод match не возвращает скобочные группы.
Например, попробуем найти все теги в строке:
let str = ' '; let tags = str.match(/<(.*?)>/g); alert( tags ); // ,Результат – массив совпадений, но без деталей о каждом. Но на практике скобочные группы тоже часто нужны.
Для того, чтобы их получать, мы можем использовать метод str.matchAll(regexp) .
Он был добавлен в язык JavaScript гораздо позже чем str.match , как его «новая и улучшенная» версия.
Он, как и str.match(regexp) , ищет совпадения, но у него есть три отличия:
- Он возвращает не массив, а перебираемый объект.
- При поиске с флагом g , он возвращает каждое совпадение в виде массива со скобочными группами.
- Если совпадений нет, он возвращает не null , а просто пустой перебираемый объект.
let results = ' '.matchAll(/<(.*?)>/gi); // results - не массив, а перебираемый объект alert(results); // [object RegExp String Iterator] alert(results[0]); // undefined (*) results = Array.from(results); // превращаем в массив alert(results[0]); // ,h1 (первый тег) alert(results[1]); // ,h2 (второй тег)
Как видите, первое отличие – очень важное, это демонстрирует строка (*) . Мы не можем получить совпадение как results[0] , так как этот объект не является псевдомассивом. Его можно превратить в настоящий массив при помощи Array.from . Более подробно о псевдомассивах и перебираемых объектов мы говорили в главе Перебираемые объекты.
В явном преобразовании через Array.from нет необходимости, если мы перебираем результаты в цикле, вот так:
let results = ' '.matchAll(/<(.*?)>/gi); for(let result of results) < alert(result); // первый вывод: ,h1 // второй: ,h2 >
…Или используем деструктуризацию:
let [tag1, tag2] = ' '.matchAll(/<(.*?)>/gi);
Каждое совпадение, возвращаемое matchAll , имеет тот же вид, что и при match без флага g : это массив с дополнительными свойствами index (позиция совпадения) и input (исходный текст):
let results = ' '.matchAll(//gi); let [tag1, tag2] = results; alert( tag1[0] ); // alert( tag1[1] ); // h1 alert( tag1.index ); // 0 alert( tag1.input ); //
Зачем так сделано? Причина проста – для оптимизации.
При вызове matchAll движок JavaScript возвращает перебираемый объект, в котором ещё нет результатов. Поиск осуществляется по мере того, как мы запрашиваем результаты, например, в цикле.
Таким образом, будет найдено ровно столько результатов, сколько нам нужно.
Например, всего в тексте может быть 100 совпадений, а в цикле после 5-го результата мы поняли, что нам их достаточно и сделали break . Тогда движок не будет тратить время на поиск остальных 95.
Именованные группы
Запоминать группы по номерам не очень удобно. Для простых шаблонов это допустимо, но в сложных регулярных выражениях считать скобки затруднительно. Гораздо лучше – давать скобкам имена.
Это делается добавлением ? непосредственно после открытия скобки.
Например, поищем дату в формате «год-месяц-день»:
let dateRegexp = /(?7)-(?8)-(?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 = /(?8)-(?1)-(?1)/g; let str = "2019-10-30 2020-01-01"; let results = str.matchAll(dateRegexp); for(let result of results) < let = result.groups; alert(`$.$.$`); // первый вывод: 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 = /(?1)-(?7)-(?8)/g; let str = "2019-10-30, 2020-01-01"; alert( str.replace(regexp, '$.$.$') ); // 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 .
Итого
Круглые скобки группируют вместе часть регулярного выражения, так что квантификатор применяется к ним в целом.
Скобочные группы нумеруются слева направо. Также им можно дать имя с помощью (?. ) .
Часть совпадения, соответствующую скобочной группе, мы можем получить в результатах поиска.
- Метод str.match возвращает скобочные группы только без флага g .
- Метод str.matchAll возвращает скобочные группы всегда.
Если скобка не имеет имени, то содержимое группы будет по своему номеру в массиве-результате, если имеет, то также в свойстве groups .
Содержимое скобочной группы можно также использовать при замене str.replace(regexp, replacement) : по номеру $n или по имени $ .
Можно исключить скобочную группу из запоминания, добавив в её начало ?: . Это используется, если необходимо применить квантификатор ко всей группе, но не запоминать их содержимое в отдельном элементе массива-результата. Также мы не можем ссылаться на такие скобки в строке замены.
Задачи
Проверьте MAC-адрес
MAC-адрес сетевого интерфейса состоит из 6-ти двузначных шестнадцатеричных чисел, разделённых двоеточиями.
Напишите регулярное выражение, которое проверит, является ли строка MAC-адресом.
let regexp = /ваш regexp/; alert( regexp.test('01:32:54:67:89:AB') ); // true alert( regexp.test('0132546789AB') ); // false (нет двоеточий) alert( regexp.test('01:32:54:67:89') ); // false (5 чисел, должно быть 6) alert( regexp.test('01:32:54:67:89:ZZ') ) // false (ZZ в конце строки)
Двузначное шестнадцатеричное число – это [0-9a-f] (предполагается, что флаг i стоит).
Нам нужно число NN , после которого :NN повторяется ещё 5 раз.
Регулярное выражение: [0-9a-f](:[0-9a-f])
Теперь давайте покажем, что шаблон должен захватить весь текст (всю строку): от начала и до конца. Для этого обернём шаблон в ^. $ .
let regexp = /^[0-9a-f](:[0-9a-f])$/i; alert( regexp.test('01:32:54:67:89:AB') ); // true alert( regexp.test('0132546789AB') ); // false (нет двоеточий) alert( regexp.test('01:32:54:67:89') ); // false (5 чисел, должно быть 6) alert( regexp.test('01:32:54:67:89:ZZ') ) // false (ZZ в конце строки)