Скобочные группы

Часть шаблона можно заключить в скобки (...). Это называется «скобочная группа».

У такого выделения есть два эффекта:

  1. Позволяет поместить часть совпадения в отдельный массив.
  2. Если установить квантификтор после скобок, то он будет применяться ко всему содержимому скобки, а не к одному символу.

Пример

В примере ниже шаблон (go)+ ищет как минимум одно совпадение с 'go':

alert( 'Gogogo now!'.match(/(go)+/i) ); // "Gogogo"

Без скобок, шаблон /go+/ означает символ g и идущий после него символ o, который повторяется один или более раз. Например, goooo или gooooooooo.

Скобки группируют символы в слово (go).

Сделаем что-то более сложное – регулярное выражение, которое соответствует адресу электронной почты.

Пример такой почты:

my@mail.com
john.smith@site.com.uk

Шаблон: [-.\w]+@([\w-]+\.)+[\w-]{2,20}.

  1. Первая часть [-.\w]+ (перед @) может включать любые числовые или буквенные символы, точку и тире, чтобы соответствовать john.smith.
  2. Затем идёт @ и домен. Это может быть поддомен (например, host.site.com.uk), поэтому мы сопоставляем его как слово, за которым следует точка ([\w-]+\.) (повторяется). Затем в конце должно быть слово: com или uk (но не очень длинное: 2-20 символов).

Это выражение не идеальное, но достаточно хорошее для исправления ошибок и опечаток.

Например, мы можем найти все электронные адреса в строке:

let reg = /[-.\w]+@([\w-]+\.)+[\w-]{2,20}/g;

alert("my@mail.com @ his@site.com.uk".match(reg)); // my@mail.com, his@site.com.uk

В этом примере скобки были использованы для создания повторяющейся группы (...)+. Но есть и другие применения. Посмотрим на них.

Содержимое скобок

Скобочные группы нумеруются слева направо. Поисковой движок запоминает содержимое, которое «поймала» каждая группа, и позволяет ссылаться на него в шаблоне регулярного выражения или строке для замены.

Например, мы хотим найти HTML теги <.*?> и обработать их.

Давайте заключим внутреннее содержимое в круглые скобки: <(.*?)>.

Мы получим как тег целиком, так и его содержимое в виде массива:

let str = '<h1>Hello, world!</h1>';
let reg = /<(.*?)>/;

alert( str.match(reg) ); // Array: ["<h1>", "h1"]

Вызов String#match возвращает группы, лишь если регулярное выражение ищет только первое совпадение, то есть не имеет флага /.../g.

Если необходимы все совпадения с их группировкой, то мы можем использовать .matchAll или regexp.exec, как описано в Методы RegExp и String:

let str = '<h1>Hello, world!</h1>';

// два совпадения: теги открытия <h1> и закрытия </h1>
let reg = /<(.*?)>/g;

let matches = Array.from( str.matchAll(reg) );

alert(matches[0]); //  Array: ["<h1>", "h1"]
alert(matches[1]); //  Array: ["</h1>", "/h1"]

Здесь мы имеем два совпадения для <(.*?)>. Каждое из них является массивом с полным совпадением и группами.

Вложенные группы

Скобки могут быть и вложенными. В этом случае нумерация также идёт слева направо.

Например, при поиске тега в <span class="my"> нас может интересовать:

  1. Содержимое тега целиком: span class="my".
  2. Название тега: span.
  3. Атрибуты тега: class="my".

Давайте добавим скобки для них:

let str = '<span class="my">';

let reg = /<(([a-z]+)\s*([^>]*))>/;

let result = str.match(reg);
alert(result); // <span class="my">, span class="my", span, class="my"

Вот так выглядят скобочные группы:

По нулевому индексу в result всегда идёт полное совпадение.

Затем следуют группы, нумеруемые слева направо. Группа, которая идёт первой, получает первый индекс в результате – result[1]. Там находится всё содержимое тега.

Затем в result[2] идёт группа, образованная второй открывающей скобкой ( до следующей закрывающей скобки ) – имя тега, далее в 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, но все скобочные группы пустые.

А теперь более сложная ситуация для строки ack:

let match = 'ack'.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"].

Именованные группы

Запоминать группы по номерам не очень удобно. Для простых шаблонов это допустимо, но в более сложных случаях мы можем давать скобкам имена.

Это делается добавлением ?<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 совпадения.

Мы также можем использовать их в строке замены как $<name> (аналогично $1..9, но имя вместо цифры).

Например, давайте поменяем формат даты в день.месяц.год:

let dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;

let str = "2019-04-30";

let rearranged = str.replace(dateRegexp, '$<day>.$<month>.$<year>');

alert(rearranged); // 30.04.2019

Если используем функцию для замены, тогда именованный объект groups всегда является последним аргументом:

let dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;

let str = "2019-04-30";

let rearranged = str.replace(dateRegexp,
  (str, year, month, day, offset, input, groups) =>
   `${groups.day}.${groups.month}.${groups.year}`
);

alert(rearranged); // 30.04.2019

Обычно, когда мы планируем использовать именованные группы, то из всех аргументов функции нам нужны только str иgroups.

Так что мы можем написать код чуть короче:

let rearranged = str.replace(dateRegexp, (str, ...args) => {
  let {year, month, day} = args.pop();
  alert(str); // 2019-04-30
  alert(year); // 2019
  alert(month); // 04
  alert(day); // 30
});

Исключение из запоминания через ?:

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

Скобочную группу можно исключить из запоминаемых и нумеруемых, добавив в её начало ?:.

Например, если мы хотим найти (go)+, но не хотим запоминать содержимое (go) в отдельный элемент массива, то можем написать так: (?:go)+.

В примере ниже мы получаем только имя «John» как отдельный член массива results:

let str = "Gogo John!";
// исключает Gogo из запоминания
let reg = /(?:go)+ (\w+)/i;

let result = str.match(reg);

alert( result.length ); // 2
alert( result[1] ); // John

Итого

Круглые скобки группируют вместе часть регулярного выражения, так что квантификатор применяется к ним в целом.

Скобочные группы нумеруются слева направо и могут опционально именоваться с помощью (?<name>...).

На текст совпадения, соответствующий скобочной группе, можно ссылаться в строке замены через $1, $2 и т.д. или по имени $name, если она именована.

Часть совпадения, соответствующую скобочной группе, мы также получаем в результатах поиска, отдельным элементом массива (или в .groups, если группа именована).

Можно исключить скобочную группу из запоминания, добавив в её начало ?:(?:...). Это используется, если необходимо применить квантификатор ко всей группе, но исключить попадание их содержимого в результат.

Задачи

Напишите регулярное выражение, которое соответствует цветам в формате #abc или #abcdef. То есть: # и за ним 3 или 6 шестнадцатеричных цифр.

Пример использования:

let reg = /ваш регэксп/g;

let str = "color: #3f3; background-color: #AA00ef; and: #abcd";

alert( str.match(reg) ); // #3f3 #AA00ef

P.S. Это должно быть ровно 3 или 6 шестнадцатеричных цифр. При этом значения типа #abcd не должны совпадать в результат.

Регулярное выражение для поиска номера цвета из трёх символов #abc: /#[a-f0-9]{3}/i.

Мы можем задать ещё ровно 3 дополнительных шестнадцатеричных цифры. Нам не нужно больше или меньше – в цвете либо 3, либо 6 цифр.

Простейший способ добавить их – добавить в регулярное выражение: /#[a-f0-9]{3}([a-f0-9]{3})?/i

Мы можем сделать это более интересным способом: /#([a-f0-9]{3}){1,2}/i.

Регулярное выражение [a-f0-9]{3} заключено в скобки для корректного применения к нему квантификатора {1,2}.

В действии:

let reg = /#([a-f0-9]{3}){1,2}/gi;

let str = "color: #3f3; background-color: #AA00ef; and: #abcd";

alert( str.match(reg) ); // #3f3 #AA00ef #abc

Здесь есть небольшая проблема: шаблон находит #abc в #abcd. Чтобы предотвратить это, мы можем добавить \b в конец:

let reg = /#([a-f0-9]{3}){1,2}\b/gi;

let str = "color: #3f3; background-color: #AA00ef; and: #abcd";

alert( str.match(reg) ); // #3f3 #AA00ef

Напишите регулярное выражение, которое ищет любые десятичные числа, включая целочисленные, с плавающей точкой и отрицательные.

Пример использования:

let reg = /ваше выражение/g;

let str = "-1.5 0 2 -123.4.";

alert( str.match(reg) ); // -1.5, 0, 2, -123.4

Положительное число с необязательным присутствием десятичной части (из прошлой задачи): \d+(\.\d+)?.

Давайте добавим необязательный - в начало:

let reg = /-?\d+(\.\d+)?/g;

let str = "-1.5 0 2 -123.4.";

alert( str.match(reg) );   // -1.5, 0, 2, -123.4

Арифметическое выражение включает два числа и оператор между ними. Например:

  • 1 + 2
  • 1.2 * 3.4
  • -3 / -6
  • -2 - 2

Оператором может быть: "+", "-", "*" или "/".

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

Создайте функцию parse(expr), которая принимает выражение и возвращает массив из трёх элементов:

  1. Первое число.
  2. Оператор.
  3. Второе число.

Например:

let [a, op, b] = parse("1.2 * 3.4");

alert(a); // 1.2
alert(op); // *
alert(b); // 3.4

Регулярное выражение для числа: -?\d+(\.\d+)?. Мы создали его в предыдущих задачах.

Регулярное выражение для оператора [-+*/]. Мы вставили тире - в начало выражения, потому что в середине этот символ будет означать диапазон, а нам это не нужно.

Отметим, что косая черта должна быть экранирована внутри регулярного выражения JavaScript /.../.

Нам необходимо число, оператор и, затем, другие числа. И необязательные символы пробела между ними.

Полное выражение: -?\d+(\.\d+)?\s*[-+*/]\s*-?\d+(\.\d+)?.

Для получения результата в виде массива давайте вставим скобки вокруг данных, которые нам необходимы: чисел и операторов: (-?\d+(\.\d+)?)\s*([-+*/])\s*(-?\d+(\.\d+)?).

В действии:

let reg = /(-?\d+(\.\d+)?)\s*([-+*\/])\s*(-?\d+(\.\d+)?)/;

alert( "1.2 + 12".match(reg) );

Результат result включает в себя:

  • result[0] == "1.2 + 12" (полное совпадение)
  • result[1] == "1.2" (первая группа (-?\d+(\.\d+)?) – первое число, включая десятичную часть)
  • result[2] == ".2" (вторая группа (\.\d+)? – первая десятичная часть)
  • result[3] == "+" (третья группа ([-+*\/]) – оператор)
  • result[4] == "12" (чертвертая группа (-?\d+(\.\d+)?) – второе число)
  • result[5] == undefined (пятая группа (\.\d+)? – вторая десятичная часть отсутствует, поэтому значение undefined)

Нам необходимы только числа и оператор без полного совпадения или десятичной части.

Полное совпадение (первый элемент массива) может быть удалён при помощи сдвига массива result.shift().

От десятичных групп можно избавиться, если исключить захват скобочной группы, добавив ?: в начало: (?:\.\d+)?.

Итоговое решение:

function parse(expr) {
  let reg = /(-?\d+(?:\.\d+)?)\s*([-+*\/])\s*(-?\d+(?:\.\d+)?)/;

  let result = expr.match(reg);

  if (!result) return [];
  result.shift();

  return result;
}

alert( parse("-1.23 * 3.45") );  // -1.23, *, 3.45
Карта учебника

Комментарии

перед тем как писать…
  • Если вам кажется, что в статье что-то не так - вместо комментария напишите на GitHub.
  • Для одной строки кода используйте тег <code>, для нескольких строк кода — тег <pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)
  • Если что-то непонятно в статье — пишите, что именно и с какого места.