16 липня 2023 р.

Перевірка уперед та назад

Іноді, нам потрібно знайти тільки такі співпадіння з шаблоном, за якими слідує, або яким передує інший шаблон.

Для цього існують спеціальні синтаксичні конструкції, котрі називається “перевірка уперед” та “перевірка назад”.

Для початку, давайте знайдемо ціну у рядку 1 індичка коштує 30€. Маємо: число, за яким йде символ .

Перевірка уперед

Синтаксис виглядає наступнм чином: X(?=Y), це означає “шукай X, але вважай його співпадінням, тільки якщо за ним слідує Y”. Замість X та Y можуть бути будь-які інші шаблони.

Для цілого числа, за яким слідує , регулярний вираз виглядатиме наступним чином \d+(?=€):

let str = "1 індичка коштує 30€";

alert( str.match(/\d+(?=€)/) ); // 30, число 1 ігнорується, оскільки після нього не стоїть символ €

Зверніть увагу: перевірка уперед це свого роду тест, вміст в дужках (?=...) не входить до відображуваного регулярним виразом співпадіння 30.

Коли ми шукаємо X(?=Y), регулярний вираз знаходить X і далі перевіряє наявність Y одразу після нього. Якщо це не так, тоді потенційне співпадіння пропускається і регулярний вираз продовжує пошук.

Можливі і більш складні тести, наприклад X(?=Y)(?=Z) означає:

  1. Знайди X.
  2. Перевір, чи Y йде одразу після X (пропускай, якщо це не так).
  3. Перевір, чи Z також йде одразу після X (пропускай, якщо це не так).
  4. Якщо обидва тести пройдено, тоді X відповідає умовам пошуку, в інщому випадку – продовжуй пошук.

Інакше кажучи, такий шаблон означає, що ми шукаємо на X за яким одночасно слідують Y та Z.

Це можливо тільки за умови, якщо шаблон Y та Z не виключають один одного.

Наприклад, \d+(?=\s)(?=.*30) шукає на \d+ за яким йде пробільний символ (?=\s), а також 30 десь після нього (?=.*30):

let str = "1 індичка коштує 30€";

alert( str.match(/\d+(?=\s)(?=.*30)/) ); // 1

В нашому рядку цим параметрам повністю відповідає число 1.

Негативна перевірка уперед

Скажімо, ми хочем знайти кількість, а не ціну в тому самому рядку. Тобто, шукаємо число \d+, за якийм НЕ слідує .

В такому випадку, доречним буде використання негативної перевірки уперед.

Синтаксис виглядає наступним чином: X(?!Y), і означає “шукай X, але за умови, що після нього не йде Y”.

let str = "2 індички коштують 60€";

alert( str.match(/\d+\b(?!€)/g) ); // 2 (ціна не відповідає вимогам шаблону і не відображається в результаті)

Перевірка назад

Сумісність браузерів з перевіркою назад

Зверніть увагу: Перевірка назад не підтримується в браузерах з відміннимим від V8 двигунами, зокрема Safari, Internet Explorer.

Перевірка уперед дозволяє додати умову на кшталт “те, що слідує після”.

Перевірка назад подібна, але дивиться у зворотньому напрямку. Таким чином, вона видає результат, тільки якщо співпадає і шаблон і те, що йде до нього.

Синтаксис наступний:

  • Позитивна перевірка назад: (?<=Y)X, співпадає з X, тільки за умови, якщо перед ним є Y.
  • Негативна перевірка назад: (?<!Y)X, співпадає X, тільки за умови, якщо перед ним немає Y.

Наприклад, змінимо ціну з євро на американські долари. Знак долару зазвичай стоїть перед числом, тому, для пошуку $30 ми використовуватимемо (?<=\$)\d+ – сума, перед якою є символ $:

let str = "1 індичка коштує $30";

// знак долара екрановано \$
alert( str.match(/(?<=\$)\d+/) ); // 30 (число 1 пропущено через відсутність знаку долару перед ним)

Також, якщо нам потрібна кількість – число, якому не передує $, в такому випадку ми можемо використати негативну перевірку назад (?<!\$)\d+:

let str = "2 індички коштують $60";

alert( str.match(/(?<!\$)\b\d+/g) ); // 2 (ціна не співпадає з умовами пошуку)

Дужкові групи

Зазвичай, в регулярних виразах вміст в дужках не є частиною співпадіння.

Наприклад, у шаблоні \d+(?=€), символ не відображається при виведенні співпадінь. Це нормально: ми шукаємо на число \d+, тоді як (?=€) це лише перевірка на те, чи дійсно за ним йде символ .

Але в деяких ситуаціях ми можемо також потребувати виведення вмісту цього шаблону або його частини. Це можливо. Просто огорніть потрібну частину в додаткові круглі дужки.

В нижченаведеному прикладі знак валюти (€|kr) теж відображено у результаті, разом із сумою:

let str = "1 індичка коштує 30€";
let regexp = /\d+(?=(€|kr))/; // додаткові круглі дужки навколо €|kr

alert( str.match(regexp) ); // 30, €

І так само для перегляду назад:

let str = "1 індичка коштує $30";
let regexp = /(?<=(\$|£))\d+/;

alert( str.match(regexp) ); // 30, $

Підсумки

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

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

Пам’ятайте, str.match (без прапорцю g) і str.matchAll (завжди) повертає співпадіння у вигляді масиву з властивістю index, тож ми точно знаємо де саме в тексті вони знаходяться і можемо перевірити контекст.

Але загалом перевірка уперед і назад більш підходящі.

Типи переглядів:

Шаблон Тип Співпадіння
X(?=Y) Позитивна перевірка уперед X якщо за ним йде Y
X(?!Y) Негативна перевірка уперед X якщо за ним не йде Y
(?<=Y)X Позитивна перевірка назад X якщо він йде після Y
(?<!Y)X Негативна перевірка назад X якщо тільки він не йде після Y

Завдання

Є рядок з цілих чисел.

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

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

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

let str = "0 12 -5 123 -18";

alert( str.match(regexp) ); // 0, 12, 123

Регулярний вираз для цілого числа \d+.

Ми можемо виключити від’ємні числа попередньо написавши регулярний вираз для негативної зворотньої перевірки: (?<!-)\d+.

Хоча, випробувавши його, ми побачимо одне зайве співпадіння:

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

let str = "0 12 -5 123 -18";

console.log( str.match(regexp) ); // 0, 12, 123, 8

Як ви бачите, шаблон знаходить 8, у -18. Щоб виключити і його, нам необхідно переконатись, що регулярний вираз починає пошук не з середини іншого числа, яке не підходить.

Ми можемо це реалізувати вказавши додатковий вираз для негативної зворотньої перевірки: (?<!-)(?<!\d)\d+. Зараз (?<!\d) перевіряє, щоб пошук не починався одразу після іншого числа, як нам і було потрібно.

Ми можемо об’єднати їх в один таким чином:

let regexp = /(?<![-\d])\d+/g;

let str = "0 12 -5 123 -18";

alert( str.match(regexp) ); // 0, 12, 123

У нас є рядок з HTML-документом.

Напишіть регулярний вираз який вставляє <h1>Привіт</h1> одразу після тегу <body>. Тег може мати атрибути.

Приклад:

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

let str = `
<html>
  <body style="height: 200px">
  ...
  </body>
</html>
`;

str = str.replace(regexp, `<h1>Привіт</h1>`);

Після цього значення str має бути:

<html>
  <body style="height: 200px"><h1>Привіт</h1>
  ...
  </body>
</html>

Щоб вставити після тегу <body> , нам потрібно спершу його знайти. Ми можемо використати для цього регулярний вираз <body.*?>.

В цьому завданні нам не потрібно змінювати тег <body>. Нам потрібно тільки додати текст після нього.

Ось таким чином ми можемо це зробити:

let str = '...<body style="...">...';
str = str.replace(/<body.*?>/, '$&<h1>Привіт</h1>');

alert(str); // ...<body style="..."><h1>Привіт</h1>...

в заміненому рядку $& означає співпадіння саме по собі, тобто, частина вихідного тексту яка відповідає шаблону <body.*?>. Її замінено на неї ж плюс <h1>Привіт</h1>.

Альнернативою було би використання зворотньої перевірки:

let str = '...<body style="...">...';
str = str.replace(/(?<=<body.*?>)/, `<h1>Привіт</h1>`);

alert(str); // ...<body style="..."><h1>Привіт</h1>...

Як бачите, в цьому регулярному виразі є тільки зворотня перевірка.

Це працює таким чином:

  • На кожній позиції в тексті.
  • Перевірте, чи йому передує <body.*?>.
  • Якщо це так, то у нас є співпадіння.

The tag <body.*?> won’t be returned. The result of this regexp is literally an empty string, but it matches only at positions preceded by <body.*?>.

So it replaces the “empty line”, preceded by <body.*?>, with <h1>Hello</h1>. That’s the insertion after <body>.

P.S. Regexp flags, such as s and i can also be useful: /<body.*?>/si. The s flag makes the dot . match a newline character, and i flag makes <body> also match <BODY> case-insensitively.

Навчальна карта