Перейти к основному содержимому

Утверждения с обратным просмотром в регулярных выражениях

· 3 мин. чтения
Янг Гуо, инженер по регулярным выражениям

Регулярные выражения были введены в третьей редакции спецификации ECMA-262 и являются частью JavaScript с 1999 года. Функциональность и выразительность реализации регулярных выражений в JavaScript примерно совпадает с тем, что доступно в других языках программирования.

Одна из функций регулярных выражений в JavaScript, которая часто упускается из виду, но может быть весьма полезной, — это утверждения с прямым просмотром. Например, для нахождения последовательности цифр, за которой следует знак процента, мы можем использовать /\d+(?=%)/. Сам знак процента не является частью результата совпадения. Его отрицание, /\d+(?!%)/, будет соответствовать последовательности цифр, за которой не следует знак процента:

/\d+(?=%)/.exec('100% американских президентов были мужчинами'); // ['100']
/\d+(?!%)/.exec('это все 44 из них'); // ['44']

Противоположность утверждения с прямым просмотром — утверждения с обратным просмотром — отсутствовали в JavaScript, но доступны в других реализациях регулярных выражений, таких как платформа .NET. Вместо чтения вперед, механизм регулярных выражений читает назад для совпадения внутри утверждения. Последовательность цифр, следующая за знаком доллара, может быть найдена с помощью /(?<=\$)\d+/, где знак доллара не является частью результата совпадения. Его отрицание, /(?<!\$)\d+/, соответствует последовательности цифр, следующей за чем угодно, кроме знака доллара.

/(?<=\$)\d+/.exec('Бенджамин Франклин на купюре $100');       // ['100']
/(?<!\$)\d+/.exec('это стоит примерно €90'); // ['90']

В общем, существует два способа реализации утверждений с обратным просмотром. Например, в Perl от образца обратного просмотра требуется иметь фиксированную длину. Это означает, что квантователи, такие как * или +, запрещены. Таким образом, механизм регулярных выражений может просто отступить на фиксированную длину и сопоставить обратный просмотр точно так же, как он сопоставил бы прямой просмотр, с отступленной позиции.

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

Очевидно, что второй вариант мощнее первого. Именно поэтому команда V8 и представители TC39, поддерживающие эту функцию, решили, что JavaScript должен принять более выразительную версию, хотя её реализация немного сложнее.

Поскольку утверждения с обратным просмотром сопоставляются задом наперед, есть некоторые тонкости, которые в противном случае могут вызвать удивление. Например, захватывающая группа с квантователем захватывает последнее совпадение. Обычно это самое правое совпадение. Но внутри утверждения с обратным просмотром мы сопоставляем справа налево, поэтому захватывается самое левое совпадение:

/h(?=(\w)+)/.exec('hodor');  // ['h', 'r']
/(?<=(\w)+)r/.exec('hodor'); // ['r', 'h']

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

/(?<=(o)d\1)r/.exec('hodor'); // null
/(?<=\1d(o))r/.exec('hodor'); // ['r', 'o']

Утверждения с обратным просмотром находятся на очень ранней стадии процесса спецификации TC39. Однако, поскольку они являются очевидным расширением синтаксиса регулярных выражений, мы решили приоритетно их реализовать. Вы уже можете экспериментировать с утверждениями с обратным просмотром, используя V8 версии 4.9 или более поздней с ключом --harmony, или включив экспериментальные функции JavaScript (используйте about:flags) в Chrome начиная с версии 49.