Пошук уроків, статей та іншого контенту
Освоїте позитивні й негативні випереджальні та ретроспективні перевірки без включення умовного тексту до збігу.
Lookaround — це перевірка умови навколо поточної позиції в тексті. Вона перевіряє символи, але не включає їх до збігу.
Завдяки lookaround можна описати умову:
знайти слово, якщо перед ним стоїть певний текст;
знайти слово, якщо після нього є певний текст;
виключити збіги з небажаним оточенням;
перевірити кілька незалежних умов для одного фрагмента.
Звичайна група:
/price: (\d+)/
включає price: у повний збіг, а lookaround дозволяє отримати лише число.
/(?<=price: )\d+/У цьому випадку до збігу потрапить тільки число.
Lookaround є нульової ширини: після перевірки позиція пошуку не переміщується через перевірений текст.
У JavaScript доступні чотири основні конструкції:
| Конструкція | Назва | Призначення | |---|---|---| | (?=pattern) | позитивний lookahead | після поточної позиції має бути pattern | | (?!pattern) | негативний lookahead | після поточної позиції не повинно бути pattern | | (?<=pattern) | позитивний lookbehind | перед поточною позицією має бути pattern | | (?<!pattern) | негативний lookbehind | перед поточною позицією не повинно бути pattern |
Усі вони перевіряють контекст, але сам контекст не стає частиною збігу.
Синтаксис:
(?=pattern)Позитивний lookahead означає: «після поточної позиції має знаходитися pattern».
Потрібно знайти числа, після яких написано грн:
const text = "Кава коштує 85 грн, а десерт — 120 доларів.";
const prices = text.match(/\d+(?= грн)/g);
console.log(prices);
// ["85"]Розглянемо регулярний вираз:
\d+(?= грн)\d+ знаходить число.
(?= грн) перевіряє, що після числа є пробіл і грн.
грн не входить до результату.
Без lookahead:
const result = text.match(/\d+ грн/g);
console.log(result);
// ["85 грн"]З lookahead результат містить лише потрібне число:
["85"]Lookahead можна використовувати кілька разів поспіль. Це зручно для перевірки пароля.
function isStrongPassword(password) {
return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{12,}$/.test(password);
}
console.log(isStrongPassword("SecurePass123!"));
// true
console.log(isStrongPassword("securepass123!"));
// false
console.log(isStrongPassword("SecurePassword"));
// falseРегулярний вираз перевіряє:
(?=.*[a-z]) — є мала латинська літера;
(?=.*[A-Z]) — є велика латинська літера;
(?=.*\d) — є цифра;
(?=.*[^\w\s]) — є спеціальний символ;
.{12,} — довжина не менше 12 символів.
Кожен lookahead починає перевірку з початку рядка, оскільки перед ними стоїть ^.
Можна знайти слово лише тоді, коли воно є безпосередньо перед комою:
const text = "JavaScript, TypeScript; Python, Rust";
const words = text.match(/\b[A-Za-z]+(?=,)/g);
console.log(words);
// ["JavaScript", "Python"]Тут кома використовується як умова, але не включається до результату.
Синтаксис:
(?!pattern)Негативний lookahead означає: «після поточної позиції не повинно знаходитися pattern».
Потрібно знайти всі ідентифікатори, крім тих, після яких стоїть ():
const text = "userName getValue() calculate parse() result";
const values = text.match(/\b[A-Za-z_]\w*\b(?!\()/g);
console.log(values);
// ["userName", "result"]Пояснення:
\b[A-Za-z_]\w*\b знаходить ідентифікатор;
(?!\() перевіряє, що після нього немає відкривної дужки;
тому назви функцій getValue і parse не потрапляють до результату.
Потрібно знайти файли, які не мають розширення .test.js:
const files = "app.js app.test.js config.js utils.test.js";
const result = files.match(/\b[\w-]+\.js\b(?!\s|$)/g);
console.log(result);
// ["app.js", "config.js"]У цьому прикладі вираз не є найнадійнішим способом розбору списку файлів, але демонструє принцип: умова після збігу може виключати небажане оточення.
Для чіткішого прикладу можна перевірити окремі назви:
const filePattern = /^(?!.*\.test\.js$)[\w-]+\.js$/;
console.log(filePattern.test("app.js"));
// true
console.log(filePattern.test("app.test.js"));
// falseТут:
^ встановлює початок рядка;
(?!.*\.test\.js$) забороняє рядки, що завершуються на .test.js;
[\w-]+\.js вимагає звичайне розширення .js;
$ встановлює кінець рядка.
Синтаксис:
(?<=pattern)Позитивний lookbehind означає: «перед поточною позицією має знаходитися pattern».
const text = "Ціни: $25, €30, ₴450.";
const dollarValues = text.match(/(?<=\$)\d+/g);
console.log(dollarValues);
// ["25"]Регулярний вираз:
(?<=\$)\d+(?<=\$) перевіряє символ $ перед числом;
\d+ споживає саме число;
символ $ не входить до збігу.
Потрібно отримати число між символом $ і суфіксом USD:
const text = "Ціни: $25 USD, €30 EUR, $100 USD.";
const values = text.match(/(?<=\$)\d+(?=\sUSD)/g);
console.log(values);
// ["25", "100"]Тут:
(?<=\$) перевіряє текст ліворуч;
\d+ є фактичним збігом;
(?=\sUSD) перевіряє текст праворуч.
Отриманий результат складається лише з чисел.
const text = "ID: user-42, ID: admin-7, guest-3";
const ids = text.match(/(?<=ID: )[\w-]+/g);
console.log(ids);
// ["user-42", "admin-7"]Тільки значення після ID: відповідають умові.
Синтаксис:
(?<!pattern)Негативний lookbehind означає: «перед поточною позицією не повинно знаходитися pattern».
$const text = "$25, 30, €40, 50";
const numbers = text.match(/(?<![$€])\b\d+\b/g);
console.log(numbers);
// ["30", "50"]Перед числами 25 і 40 стоять символи $ та €, тому вони виключаються.
const text = "ready already not-ready steady";
const words = text.match(/(?<!-)\b[a-z]+\b/gi);
console.log(words);
// ["ready", "already", "not", "steady"]У цьому прикладі not все одно може бути знайдено, тому що пошук слова починається після дефіса, а сам збіг not не має перед собою дефіса в позиції, яка перевіряється так, як очікується в конкретній структурі рядка.
Для виключення всього слова разом із дефісом краще змінити сам шаблон:
const words = text.match(/(?<!\S-)\b[a-z]+\b/gi);Або, якщо потрібно пропускати конструкції типу not-ready, виконати фільтрацію повних токенів:
const tokens = text.match(/\b[a-z]+(?:-[a-z]+)?\b/gi) ?? [];
const words = tokens.filter((token) => !token.includes("-"));
console.log(words);
// ["ready", "already", "steady"]Це важливий практичний принцип: lookaround не завжди є найкращим інструментом для складного синтаксичного розбору. Іноді зрозуміліше спочатку знайти повний фрагмент, а потім обробити його кодом.
Наступний приклад демонструє всі чотири види lookaround:
const text = `
Купівля: $25 USD
Поповнення: €100 EUR
Код: DEV-42
Код: TEST-99
Ціна: 750 грн
`;
// Позитивний lookahead:
// знаходить число, після якого є " грн".
const hryvniaPrices = text.match(/\d+(?=\sгрн)/g) ?? [];
// Негативний lookahead:
// знаходить ідентифікатори, після яких немає дужки,
// тобто це не виклик функції.
const identifiers = text.match(/\b[A-Za-z_]\w*\b(?!\()/g) ?? [];
// Позитивний lookbehind і позитивний lookahead:
// знаходить число між "$" та " USD".
const dollarValues = text.match(/(?<=\$)\d+(?=\sUSD)/g) ?? [];
// Негативний lookbehind:
// знаходить числа, перед якими немає "$" або "€".
const nonCurrencyNumbers = text.match(/(?<![$€])\b\d+\b/g) ?? [];
console.log({ hryvniaPrices });
console.log({ identifiers });
console.log({ dollarValues });
console.log({ nonCurrencyNumbers });Очікувані ключові результати:
hryvniaPrices: ["750"]
dollarValues: ["25"]Результат identifiers міститиме фрагменти на кшталт USD, EUR, DEV, TEST, оскільки регулярний вираз шукає всі слова, які не завершуються відкривною дужкою. Для реального парсингу різних типів даних краще використовувати окремі, точніші шаблони.
Lookaround можна поєднувати з групами захоплення, але важливо розуміти різницю:
lookaround перевіряє контекст і не споживає його;
група захоплення споживає частину збігу, якщо вона розташована поза lookaround;
група всередині lookaround може захоплювати текст для подальшого доступу до груп, але повний збіг залишиться нульової ширини щодо цієї частини.
Для звичайного вилучення значення часто зручніше залишати потрібну частину поза lookaround:
const text = "price: 150";
const match = text.match(/(?<=price:\s)\d+/);
console.log(match[0]);
// "150"Порівняйте з варіантом через групу:
const match = text.match(/price:\s(\d+)/);
console.log(match[0]);
// "price: 150"
console.log(match[1]);
// "150"Обидва варіанти правильні, але мають різний API-результат:
lookbehind робить потрібний текст повним збігом;
група захоплення залишає потрібний текст у match[1].
Lookahead часто використовують на початку шаблону для перевірки всього рядка:
function isValidUsername(username) {
return /^(?=.{3,20}$)(?!.*\s)[a-zA-Z0-9_]+$/.test(username);
}
console.log(isValidUsername("developer_42"));
// true
console.log(isValidUsername("dev user"));
// false
console.log(isValidUsername("ab"));
// falseПеревірки:
(?=.{3,20}$) — довжина від 3 до 20 символів;
(?!.*\s) — у рядку немає пробілів;
[a-zA-Z0-9_]+ — дозволені лише латинські літери, цифри та _.
Оскільки перевірки не споживають текст, після них основний шаблон усе одно починає збіг із початку рядка.
mLookaround часто використовують разом із ^ і $. Їхня поведінка залежить від прапорця m.
Без m:
^ відповідає початку всього рядка;
$ відповідає кінцю всього рядка.
З m:
^ може відповідати початку кожного рядка;
$ може відповідати кінцю кожного рядка.
const text = "admin\nuser\nroot";
const result = text.match(/^(?!root$)\w+$/gm);
console.log(result);
// ["admin", "user"]Тут для кожного рядка перевіряється, що він не є точним рядком root.
Lookahead підтримується JavaScript давно, а lookbehind потребує сучасного рушія JavaScript.
У сучасних браузерах і середовищах Node.js конструкції на кшталт (?<=...) та (?<!...) зазвичай доступні. Проте під час розробки бібліотеки або вебзастосунку для старих браузерів потрібно перевірити цільове середовище.
Якщо lookbehind недоступний, часто можна переписати вираз із групою захоплення:
const text = "price: 150";
const withLookbehind = text.match(/(?<=price:\s)\d+/);
console.log(withLookbehind?.[0]);
// "150"
const withoutLookbehind = text.match(/price:\s(\d+)/);
console.log(withoutLookbehind?.[1]);
// "150"Для заміни або глобального вилучення потрібно враховувати, що група захоплення змінює структуру результату. У складніших випадках краще застосувати matchAll() або окрему обробку рядка.
Завдання: знайти домени після @, але не включати @ до результату.
Варіант із lookbehind:
const text = "user@example.com admin@test.org";
const domains = text.match(/(?<=@)[a-z0-9.-]+\.[a-z]{2,}/gi);
console.log(domains);
// ["example.com", "test.org"]Варіант із групою:
const matches = [...text.matchAll(/@([a-z0-9.-]+\.[a-z]{2,})/gi)];
const domains = matches.map((match) => match[1]);
console.log(domains);
// ["example.com", "test.org"]Перший варіант компактніший. Другий може бути суміснішим із середовищами, де lookbehind не підтримується.
Непотрібно:
const text = "price: 150";
console.log(text.match(/price:\s\d+/g));
// ["price: 150"]Якщо потрібне лише число, використайте lookbehind:
console.log(text.match(/(?<=price:\s)\d+/g));
// ["150"](?=...) перевіряє текст праворуч;
(?<=...) перевіряє текст ліворуч.
const text = "ID: 42";
console.log(text.match(/\d+(?=)/));
// ["42"]
console.log(text.match(/(?<=ID: )\d+/));
// ["42"]У першому випадку перевірка відбувається після числа, у другому — перед числом.
Символ $ має спеціальне значення в регулярних виразах: він позначає кінець рядка. Для пошуку буквального $ його потрібно екранувати:
/(?<=\$)\d+/Так само часто потрібно екранувати:
. + * ? ( ) [ ] { } ^ $ |Розглянемо:
const text = "start one end, start two end";
const result = text.match(/start(?=.*end)/g);
console.log(result);
// ["start", "start"].* може пройти далеко вправо до останнього можливого end. Якщо потрібно обмежити пошук межами одного фрагмента, використовуйте точніший шаблон або клас символів:
const result = text.match(/start(?=[^,]*end)/g);
console.log(result);
// ["start", "start"]Тут [^,]* не дозволяє перевірці перетнути кому.
Регулярний вираз із великою кількістю lookaround може стати складним для читання та супроводу. Якщо потрібно розібрати вкладені конструкції, HTML, програмний код або складний формат даних, звичайного регулярного виразу може бути недостатньо.
Lookaround найкраще підходить для локальних умов:
контекст навколо числа;
префікс або суфікс;
виключення короткого шаблону;
кілька незалежних умов валідації.
Для максимальної сумісності використовуйте в lookbehind прості та передбачувані шаблони:
/(?<=ID: )\d+/Складні варіанти з альтернативами або змінною довжиною можуть по-різному підтримуватися рушіями та бути важчими для оптимізації. Якщо умова складна, група захоплення часто є зрозумілою альтернативою.
.* і «будь-яким символом»У JavaScript крапка . за замовчуванням не відповідає символам переведення рядка. Для багаторядкового тексту це може змінити поведінку lookahead:
const text = "start\nend";
console.log(/start(?=.*end)/.test(text));
// falseІз прапорцем s крапка також відповідає переведенню рядка:
console.log(/start(?=.*end)/s.test(text));
// trueІноді замість .* краще явно описати дозволений діапазон символів.
Під час створення регулярного виразу виконайте такі кроки:
Визначте, що саме має потрапити до збігу.
Окремо випишіть умови ліворуч від збігу.
Окремо випишіть умови праворуч від збігу.
Умови ліворуч оформіть через lookbehind.
Умови праворуч оформіть через lookahead.
Для заборонених умов використайте негативні варіанти.
Перевірте шаблон на позитивних і негативних прикладах.
Переконайтеся, що прапорці g, i, m, s, u відповідають завданню.
Наприклад, для числа, яке:
стоїть після amount: ;
не має символу $ безпосередньо перед собою;
завершується суфіксом EUR;
можна почати з такого виразу:
/(?<!\$)(?<=amount:\s)\d+(?=\sEUR)/Проте умови можуть суперечити одна одній, якщо перед числом одночасно має бути amount: і $. Тому lookaround потрібно не лише синтаксично комбінувати, а й перевіряти на реальних даних.
Lookaround перевіряє контекст, не включаючи його до збігу.
(?=...) — позитивна перевірка після поточної позиції.
(?!...) — негативна перевірка після поточної позиції.
(?<=...) — позитивна перевірка перед поточною позицією.
(?<!...) — негативна перевірка перед поточною позицією.
Lookahead зручно використовувати для суфіксів і складених умов валідації.
Lookbehind зручно використовувати для префіксів, валютних символів і міток.
Кілька lookahead можна комбінувати для незалежних правил.
Lookaround допомагає отримувати лише потрібну частину тексту.
Складні регулярні вирази з великою кількістю перевірок потрібно тестувати на граничних випадках.
Якщо lookbehind несумісний із цільовим середовищем, його часто можна замінити групою захоплення.