Опережающие и ретроспективные проверки

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

Для этого в регулярных выражениях есть специальный синтаксис: опережающая (lookahead) и ретроспективная (lookbehind) проверка.

В качестве первого примера найдём стоимость из строки 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 при условии, что за ним идёт и Y и Z.

Такое возможно только при условии, что шаблоны Y и Z не являются взаимно исключающими.

Например, \d+(?=\s)(?=.*30) ищет \d+ при условии, что за ним идёт пробел, и где-то впереди есть 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+(?!€)/) ); // 2 (в этот раз проигнорирована цена)

Ретроспективная проверка

Опережающие проверки позволяют задавать условия на то, что «идёт после».

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

Синтаксис:

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

Чтобы протестировать ретроспективную проверку, давайте поменяем валюту на доллары США. Знак доллара обычно ставится перед суммой денег, поэтому для того чтобы найти $30, мы используем (?<=\$)\d+ – число, перед которым идёт $:

let str = "1 индейка стоит $30";

// знак доллара экранируем \$, так как это специальный символ
alert( str.match(/(?<=\$)\d+/) ); // 30, одинокое число игнорируется

Если нам необходимо найти количество индеек – число, перед которым не идёт $, мы можем использовать негативную ретроспективную проверку (?<!\$)\d+:

let str = "2 индейки стоят $60";

alert( str.match(/(?<!\$)\d+/) ); // 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, $

Итого

Опережающая и ретроспективная проверки удобны, когда мы хотим искать шаблон по дополнительному условию на контекст, в котором он находится.

Для простых регулярных выражений мы можем сделать похожую вещь «вручную». То есть, найти все совпадения, независимо от контекста, а затем в цикле отфильтровать подходящие.

Как мы помним, regexp.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. То есть, берёт только цифру из числа -18, так как это формально подходит под регулярное выражение.

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

Мы можем сделать это добавлением ещё одной проверки: (?<!-)(?<!\d)\d+. Теперь (?<!\d) гарантирует, что поиск не начнётся после цифры.

Можем объединить проверки в одну:

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

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

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

Есть строка с HTML-документом.

Вставьте после тега <body> (у него могут быть атрибуты) строку <h1>Hello</h1>.

Например:

let regexp = /ваше регулярное выражение/;

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

str = str.replace(regexp, `<h1>Hello</h1>`);

После этого значение str:

<html>
  <body style="height: 200px"><h1>Hello</h1>
  ...
  </body>
</html>

Для того, чтобы вставить после тега <body>, нужно вначале его найти. Будем использовать регулярное выражение <body.*>.

Далее, нам нужно оставить сам тег <body> на месте и добавить текст после него.

Это можно сделать вот так:

let str = '...<body style="...">...';
str = str.replace(/<body.*>/, '$&<h1>Hello</h1>');

alert(str); // ...<body style="..."><h1>Hello</h1>...

В строке замены $& означает само совпадение, то есть мы заменяем <body.*> заменяется на самого себя плюс <h1>Hello</h1>.

Альтернативный вариант – использовать ретроспективную проверку:

let str = '...<body style="...">...';
str = str.replace(/(?<=<body.*>)/, `<h1>Hello</h1>`);

alert(str); // ...<body style="..."><h1>Hello</h1>...

Такое регулярное выражение на каждой позиции будет проверять, не идёт ли прямо перед ней <body.*>. Если да – совпадение найдено. Но сам тег <body.*> в совпадение не входит, он только участвует в проверке. А других символов после проверки в нём нет, так что текст совпадения будет пустым.

Происходит замена «пустой строки», перед которой идёт <body.*> на <h1>Hello</h1>. Что, как раз, и есть вставка этой строки после <body>.

P.S. Этому регулярному выражению не помешают флаги: /<body.*>/si, чтобы в «точку» входил перевод строки (тег может занимать несколько строк), а также чтобы теги в другом регистре типа <BODY> тоже находились.

Карта учебника

Комментарии

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