16 липня 2023 р.

Групи захоплення

Частину виразу можна обгорнути в круглі дужки (...). Це називається “група захоплення”.

Такий прийом має два наслідки:

  1. Він дозволяє отримати частину збігу в якості окремого елементу в масиві результатів.
  2. Якщо ми поставимо квантифікатор після дужок, він застосується до всього їх вмісту.

Приклади

Розглянемо як працюють круглі дужки на прикладах.

Приклад: 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

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

Формат електронної пошти: name@domain. Name може бути будь-яким словом, дефіси та крапки допускаються. В регулярних виразах це [-.\w]+.

Вираз є наступним:

let regexp = /[-.\w]+@([\w-]+\.)+[\w-]+/g;

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

Це неідеальний регулярний вираз, але здебільшого робочий та здатний виправити випадкові помилки. Єдина справді надійна перевірка електронної пошти – відправка листа.

Вміст дужок всередині збігу

Дужки нумеруються зліва направо. Пошукова система запам’ятовує вміст збігу для кожної з них та дозволяє отримати його всередині результату.

У випадку, якщо regexp не містить прапорця g, метод str.match(regexp) шукає перший збіг та повертає його як масив:

  1. Індекс 0: повний збіг.
  2. Індекс 1: вміст перших дужок.
  3. Індекс 2: вміст других дужок.
  4. …і так далі…

Для прикладу, ми б хотіли знайти HTML теги <.*?> та обробити їх. Було б зручно мати вміст тегу (все, що всередині кутових дужок) в окремій змінній.

Огорнемо внутрішній вміст у круглі дужки, ось так: <(.*?)>.

Тепер ми отримаємо як тег в цілому <h1>, так і його вміст h1 в отриманому масиві:

let str = '<h1>Вітаю, світе!</h1>';

let tag = str.match(/<(.*?)>/);

alert( tag[0] ); // <h1>
alert( tag[1] ); // h1

Вкладені групи

Дужки можуть бути вкладеними. В нашому випадку, нумерація також проводиться зліва направо.

Наприклад, під час пошуку тегу в <span class="my">, нас може цікавити:

  1. Вміст тегу загалом: span class="my".
  2. Ім’я тегу: span.
  3. Атрибути тегу: 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 є новим методом, може знадобитись поліфіл

Метод 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 набагато пізніше за match в якості “нової та покращеної версії”.

Як і match, метод шукає збіги, але з урахуванням 3-ьох відмінностей:

  1. Він повертає не масив, а ітерований об’єкт.
  2. За наявності прапорцю g, він повертає кожен збіг у вигляді масиву з групами.
  3. Якщо збігів немає, він повертає замість 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, використовуючи Array.from. Детальніше про псевдомасиви та ітеровані об’єкти в статті Ітеративні об’єкти.

Нема потреби в Array.from, якщо ми циклічно проходимось по результатам:

let results = '<h1> <h2>'.matchAll(/<(.*?)>/gi);

for(let result of results) {
  alert(result);
  // перший alert: <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 не проводить пошуку. Натомість, повертається ітерований об’єкт, спочатку без результатів. Пошук проводиться кожного разу ми проходимось об’єктом, наприклад, всередині циклу.

Тобто, буде знайдено потрібну кількість результатів, не більше.

Наведемо приклад: потенційно, в тексті існує 100 збігів, але всередині циклу for..of ми знайшли лише 5, тоді вирішили, що цього достатньо, та викликали break. Тоді система не витрачатиме час на пошук 95 інших збігів.

Іменовані групи

Пам’ятати групи за номерами важко. З простими виразами впоратись можна, але з підвищенням рівня складності рахувати дужки стає незручно. Існує кращий варіант: дати імена круглим дужкам.

Робиться це за допомогою ?<name> одразу після відкриваючої дужки.

На прикладі нижче, пошукаємо дату в форматі “year-month-day”:

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.

Аби зробити пошук по всіх датах, ми можемо додати прапор 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}`);
  // перший alert: 30.10.2019
  // другий: 01.01.2020
}

Групи захоплення для заміни

Метод str.replace(regexp, replacement) замінює всі збіги з regexp всередині str дозволяє користуватись вмістом дужок в рядку replacement. Це зроблено за допомогою $n, де n – номер групи.

Наприклад,

let str = "John Bull";
let regexp = /(\w+) (\w+)/;

alert( str.replace(regexp, '$2, $1') ); // Bull, John

Посилання на іменовані дужки можливе через $<name>.

Наприклад, реформатуємо дати з “year-month-day” на “day.month.year”:

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 (інших елементів у масиві немає)

Підсумки

Круглі дужки об’єднують в групу частину регулярного виразу, аби повноцінно застосувати до неї квантифікатор.

Групи дужок нумеруються зліва направо та за бажанням можуть бути іменовані за допомогою (?<name>...).

Вміст, що збігається з групою, може бути отриманий в результатах:

  • Метод str.match повертає групи захоплення лише без прапорцю g.
  • Метод str.matchAll завжди повертає групи захоплення.

Якщо в дужок нема імені, їх вміст доступний в масиві збігу за їх номером. Іменовані дужки також доступні через властивість groups.

Ми також можемо використовувати вміст дужок в рядку для заміни в str.replace: за номером $n або ім’ям $<name>.

Групу можна виключити з нумерації, додавши ?: на її початку. Цей прийом використовується за необхідності застосувати квантифікатор до всієї групи, але без появи окремого елементу в масиві результатів. Також, ми не можемо посилатись на такі дужки в рядку для заміни.

Завдання

MAC-адреса мережевого інтерфейсу складається з 6 двоцифрових шістнадцяткових чисел, розділених двокрапкою.

Наприклад: '01:32:54:67:89:AB'.

Напишіть регулярний вираз, який перевіряє, чи є рядок MAC-адресою.

Приклад використання:

let 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]{2}(припустивши, що задано прапорець i).

Нам потрібно число NN, а за ним :NN, повторене 5 разів (більше чисел);

Регулярний вираз: [0-9a-f]{2}(:[0-9a-f]{2}){5}

Тепер продемонструємо, що збіг має захоплювати весь текст: з самого початку до самого кінця. Робиться це через огортання виразу в ^...$.

В підсумку:

let regexp = /^[0-9a-f]{2}(:[0-9a-f]{2}){5}$/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 в кінці)

Напишіть регулярний вираз, що знаходить збіг по кольорам у форматі #abc або #abcdef. Формула є наступною: #, за яким знаходяться 3 або 6 шістнадцяткових цифр.

Приклад використання:

let regexp = /ваш регулярний вираз/g;

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

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

P.S. Має бути саме 3 або 6 шістнадцяткових цифр. Значення з 4 цифрами, такі як #abcd, не мають рахуватись за збіг.

Регулярний вираз для пошуку тризначного коду кольору #abc: /#[a-f0-9]{3}/i.

Ми можемо додати рівно 3 додаткові шістнадцяткові цифри, не більше й не менше. Колір містить 3 або 6 цифр.

Використаємо для цього квантифікатор {1,2}: отримаємо /#([a-f0-9]{3}){1,2}/i.

В цьому випадку, шаблон [a-f0-9]{3} оточений дужками для застосування квантифікатора {1,2}.

Код у дії:

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

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

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

Бачимо невелику проблему: вираз знайшов #abc в #abcd. Для запобігання цьому, додамо в кінці \b:

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

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

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

Напишіть регулярний вираз, який шукатиме всі десяткові числа, включно з числами цілими, з плаваючою комою та від’ємними.

Приклад використання:

let regexp = /ваш регулярний вираз/g;

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

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

Додатне число з необов’язковою десятковою частиною: \d+(\.\d+)?.

Додамо необов’язковий - на початку:

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

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

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

Арифметичний вираз складається з двох чисел та оператору між ними, наприклад:

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

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

Додаткові пробіли можуть бути на початку, в кінці чи всередині виразу.

Напишіть функцію parse(expr), яка приймає вираз та повертає масив з 3-ьох елементів:

  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+)?.

Він містить 3 частини, з \s* між ними:

  1. -?\d+(\.\d+)? – перше число,
  2. [-+*/] – оператор,
  3. -?\d+(\.\d+)? – друге число.

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

Код у дії:

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

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

Розглянемо результат:

  • 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().

Групи 2 та 5, що містять десяткові частини (.\d+), можна оминути, додавши ?: на початку: (?:\.\d+)?.

Кінцевий варіант:

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

  let result = expr.match(regexp);

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

  return result;
}

alert( parse("-1.23 * 3.45") );  // -1.23, *, 3.45

As an alternative to using the non-capturing ?:, we could name the groups, like this:

function parse(expr) {
  let regexp = /(?<a>-?\d+(?:\.\d+)?)\s*(?<operator>[-+*\/])\s*(?<b>-?\d+(?:\.\d+)?)/;

  let result = expr.match(regexp);

  return [result.groups.a, result.groups.operator, result.groups.b];
}

alert( parse("-1.23 * 3.45") );  // -1.23, *, 3.45;
Навчальна карта

Коментарі

прочитайте це, перш ніж коментувати…
  • Якщо у вас є пропозиції, щодо покращення підручника, будь ласка, створіть обговорення на GitHub або одразу створіть запит на злиття зі змінами.
  • Якщо ви не можете зрозуміти щось у статті, спробуйте покращити її, будь ласка.
  • Щоб вставити код, використовуйте тег <code>, для кількох рядків – обгорніть їх тегом <pre>, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)