Часть шаблона можно заключить в скобки (...)
. Это называется «скобочная группа».
У такого выделения есть два эффекта:
- Позволяет поместить часть совпадения в отдельный массив.
- Если установить квантификатор после скобок, то он будет применяться ко всему содержимому скобки, а не к одному символу.
Примеры
Разберём скобки на примерах.
Пример: 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…)