В некоторых случаях нам нужно найти соответствия шаблону, но только те, за которыми или перед которыми следует другой шаблон.
Для этого в регулярных выражениях есть специальный синтаксис: опережающая (lookahead) и ретроспективная (lookbehind) проверка.
В качестве первого примера найдём стоимость из строки subject:1 индейка стоит 30€. То есть, найдём число, после которого есть знак валюты subject:€.
Синтаксис опережающей проверки: pattern:X(?=Y).
Он означает: найди pattern:X при условии, что за ним следует pattern:Y. Вместо pattern:X и pattern:Y здесь может быть любой шаблон.
Для целого числа, за которым идёт знак subject:€, шаблон регулярного выражения будет pattern:\d+(?=€):
let str = "1 индейка стоит 30€";
alert( str.match(/\d+(?=€)/) ); // 30, число 1 проигнорировано, так как за ним НЕ следует €Обратим внимание, что проверка - это именно проверка, содержимое скобок pattern:(?=...) не включается в результат match:30.
При поиске pattern:X(?=Y) движок регулярных выражений, найдя pattern:X, проверяет, есть ли после него pattern:Y. Если это не так, то игнорирует совпадение и продолжает поиск дальше.
Возможны и более сложные проверки, например pattern:X(?=Y)(?=Z) означает:
- Найти
pattern:X. - Проверить, идёт ли
pattern:Yсразу послеpattern:X(если нет - не подходит). - Проверить, идёт ли
pattern:Zсразу послеpattern:X(если нет - не подходит). - Если обе проверки прошли - совпадение найдено.
То есть этот шаблон означает, что мы ищем pattern:X при условии, что за ним идут и pattern:Y, и pattern:Z.
Такое возможно только при условии, что шаблоны pattern:Y и pattern:Z не являются взаимно исключающими.
Например, pattern:\d+(?=\s)(?=.*30) ищет pattern:\d+ при условии, что за ним идёт пробел, и где-то далее есть 30:
let str = "1 индейка стоит 30€";
alert( str.match(/\d+(?=\s)(?=.*30)/) ); // 1В нашей строке это как раз число 1.
Допустим, нам нужно узнать из этой же строки количество индеек, то есть число pattern:\d+, за которым НЕ следует знак subject:€.
Для этой задачи мы можем применить негативную опережающую проверку.
Синтаксис: pattern:X(?!Y)
Он означает: найди такой pattern:X, за которым НЕ следует pattern:Y.
let str = "2 индейки стоят 60€";
alert( str.match(/\d+(?!€)/) ); // 2 (в этот раз проигнорирована цена)Обратите внимание: Lookbehind не поддерживается в браузерах построенных не на движке V8, таких как Safari и Internet Explorer.
Опережающие проверки позволяют задавать условия на то, что "идёт после".
Ретроспективная проверка выполняет такую же функцию, но с просмотром назад. Другими словами, она находит соответствие шаблону, только если перед ним есть что-то заранее определённое.
Синтаксис:
- Позитивная ретроспективная проверка:
pattern:(?<=Y)X, ищет совпадение сpattern:Xпри условии, что перед ним ЕСТЬpattern:Y. - Негативная ретроспективная проверка:
pattern:(?<!Y)X, ищет совпадение сpattern:Xпри условии, что перед ним НЕТpattern:Y.
Чтобы протестировать ретроспективную проверку, давайте поменяем валюту на доллары США. Знак доллара обычно ставится перед суммой денег, поэтому для того чтобы найти $30, мы используем pattern:(?<=\$)\d+ - число, перед которым идёт subject:$:
let str = "1 индейка стоит $30";
// знак доллара экранируем \$, так как это специальный символ
alert( str.match(/(?<=\$)\d+/) ); // 30, одинокое число игнорируетсяЕсли нам необходимо найти количество индеек -- число, перед которым не идёт subject:$, мы можем использовать негативную ретроспективную проверку pattern:(?<!\$)\d+:
let str = "2 индейки стоят $60";
alert( str.match(/(?<!\$)\d+/) ); // 2 (проигнорировалась цена)Как правило, то что находится внутри скобок, задающих опережающую и ретроспективную проверку, не включается в результат совпадения.
Например, в шаблоне pattern:\d+(?=€) знак pattern:€ не будет включён в результат. Это логично, ведь мы ищем число pattern:\d+, а pattern:(?=€) - это всего лишь проверка, что за ним идёт знак subject:€.
Но в некоторых ситуациях нам может быть интересно захватить и то, что в проверке. Для этого нужно обернуть это в дополнительные скобки.
В следующем примере знак валюты pattern:(€|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, $Опережающая и ретроспективная проверки удобны, когда мы хотим искать шаблон по дополнительному условию на контекст, в котором он находится.
Для простых регулярных выражений мы можем сделать похожую вещь "вручную". То есть, найти все совпадения, независимо от контекста, а затем в цикле отфильтровать подходящие.
Как мы помним, regexp.match (без флага g) и str.matchAll (всегда) возвращают совпадения со свойством index, которое содержит позицию совпадения в строке, так что мы можем посмотреть на контекст.
Но обычно регулярные выражения удобнее.
Виды проверок:
| Шаблон | Тип | Совпадение |
|---|---|---|
X(?=Y) |
Позитивная опережающая | pattern:X, если за ним следует pattern:Y |
X(?!Y) |
Негативная опережающая | pattern:X, если за ним НЕ следует pattern:Y |
(?<=Y)X |
Позитивная ретроспективная | pattern:X, если следует за pattern:Y |
(?<!Y)X |
Негативная ретроспективная | pattern:X, если НЕ следует за pattern:Y |