Позиционная проверка (lookaround) (часть 2)

Используя позиционную проверку, мы никуда не перемещаемся

Я повторю это ещё раз, потому что этот момент часто остаётся без внимания. Пример с проверкой корректности пароля (из предыдущей части) демонстрирует, что используя позиционную проверку мы никуда не перемещаемся. Мы начинаем проверку с того места строки, на котором находимся в данный момент. После позиционной проверки мы остаёмся на том же самом месте, и если выполнить ещё одну проверку, она начнётся с того же самого места.

Поэтому опережающая позиционная проверка означает: "проверь, какой символ находится справа от тебя и, возможно, ещё несколько символов после него". Конечно, если в нашей проверке присутствует конструкция вроде .*, то она позволит "заглянуть" очень далеко, но как бы с использованием бинокля, то есть мы "смотрим" вперёд с того места, на котором стоим, никуда не перемещаясь.

Позиционную проверку необходимо привязывать

Когда регулярное выражение с позиционной проверкой не находит соответствия, движок делает то, что делает всегда: переходит ко второму символу в строке и пытается сопоставить его с регулярным выражением. Это означает, что может возникнуть ситуация, когда позиционная проверка, которая должна была отработать только один раз, отрабатывает множество раз. Возвращаясь к примеру с паролем, представим, что будет, если мы уберём привязку к началу строки. Пока проигнорируем символ + в конструкции {6,10}+ (в данном случае он означает нечто иное).

(?=\w{6,10}+$).*

Если мы применим эту проверку, которая ни к чему не привязана (изначальный вариант выглядел так: ^(?=\w{6,10}+$).*), к строке, состоящей из семи символов, то всё будет в порядке. Но что если мы применим её к строке, состоящей из сотни символов без пробелов, после которых идёт пробел? Регулярное выражение начнёт работу с первого символа; осуществится опережающая позиционная проверка (то есть мы "заглянем" вперёд). Совпадения не будет найдено, потому что строка очень длинная и регулярное выражение не обнаружит конца строки после десятого символа. Однако так как мы убрали привязку к началу строки, регулярное выражение передёт ко второму символу и снова попытается осуществить позиционную проверку начиная с него. Совпадения вновь не будет найдено. И, таким образом, регулярное выражение будет пытаться осуществлять такие проверки последовательно начиная с каждого последующего символа в строке! На 91-м символе (если представить, что наша строка состоит из 100 символов) будет найдено десять последовательных символов после которых, однако, идёт пробел, то есть соответствия снова не будет найдено (не забывайте, что запись \w означает буквы, цифры и нижние подчёркивания, пробел сюда не входит).

С точки зрения эффективности такая ситуация ужасна. Таким образом, мы понимаем, что регулярное выражение следует записать немного иначе, чтобы оно не пыталось бесцельно осуществлять позиционные проверки множество раз. И для этого необходимо осуществить привязку к началу строки: либо с помощью ^ (в случае Ruby рекомендуется \A, как мы выяснили в прошлой части), либо с помощью указания того текста, который должен идти ранее.

Заметка для любопытных

Если в записи ^(?=\w{6,10}+$).* убрать +, то ситуация может стать ещё хуже. Что значит этот символ в данном случае? В этом контексте запись \w{6,10}+ аналогична записи (?>\w{6,10}), которая определяет атомарную группу. Это означает, что после того, как регулярное выражение попыталось найти десять символов и не нашло их, опережающая проверка завершается и движок переходит к следующему символу в строке. Если убрать +, то перед тем, как движок перейдёт к следующему символу, регулярное выражение попытается найти девять символов подряд и конец строки за ними, затем восемь и так до тех пор, пока оно не сможет найти шесть символов подряд и конец строки. Только после этого интерпретатор поймёт, что позиционная проверка не нашла соответствия, и перейдёт к следующему символу в строке. Таким образом, если в нашем примере оставить привязку к концу строки, но убрать атомарную группу (удалив +), то для его отработки потребуется десять шагов. Если же убрать и привязку к началу строки, и атомарную группу, то потребуется около 4000 шагов!

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

Два основных способа использования позиционных проверок

Существует четыре типа позиционных проверок:

  • (?= - опережающая позитивная позиционная проверка (или просто опережающая проверка, о ней мы говорили в примере с валидацией пароля в прошлой части - в ней мы как бы "заглядываем" вперёд)
  • (?! - опережающая негативная
  • (?<= - ретроспективная позитивная (или просто ретроспективная, здесь мы, наоборот, "заглядываем" назад)
  • (?<! - ретроспективная негативная

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

Позиционная проверка перед соответствием

  • (?=\d{3} dollars).{3} (опережающая проверка). "Заглядывает" вперёд в поисках трёх цифр, за которыми следует слово "dollars". В строке "100 dollars" как совпадение будет помечено "100".
  • (?!=\d{3} pesos)\d{3} (опережающая негативная проверка). Проверяет, что после трёх цифр не идёт слово "pesos". В строке "100 dollars" как совпадение будет помечено "100".
  • (?<=USD)\d{3} (ретроспективная проверка). Проверяет, что перед совпадением находится текст "USD". Помечает как совпадение "100" в строке "USD100".
  • (?<!USD)\d{3} (ретроспективная негативная проверка). Проверяет, что перед совпадением не находится текст "USD". Помечает как совпадение "100" в строке "JPY100".

Позиционная проверка после соответствия

  • \d{3}(?= dollars) (опережающая проверка). Проверяет, что после трёх цифр идёт слово "dollars". В строке "100 dollars" как совпадение будет помечено "100".
  • \d{3}(?! dollars) (опережающая негативная проверка). Проверяет, что после трёх цифр не идёт слово "dollars". В строке "100 pesos" как совпадение будет помечено "100".
  • .{3}(?<=USD\d{3}) (ретроспективная проверка). Проверяет, что перед тремя цифрами находится строка "USD". Помечает как совпадение "100" в строке "USD100".
  • \d{3}(?<!USD\d{3}) (ретроспективная негативная проверка). Проверяет, что перед тремя цифрами не находится строка "USD". Помечает как совпадение "100" в строке "JPY100".

Что всё это значит?

Цель этих восьми примеров - не заставить вас запомнить все возможные способы использования позиционных проверок, а, скорее, показать то, как они работают в зависимости от расположения в регулярном выражении. Как видите, обычно существует два способа (как минимум!) получения одного и того же результата. Например, (?=\d{3} dollars).{3} и \d{3}(?= dollars) пометят "100" как соответствие в строке "100 dollars".

Follow me on Twitter to get updates about new articles >>

Comments

Please authorize via one of the following social networks to leave a comment: