Регулярные выражения: просмотр вперёд и назад
Большинство реализаций регулярных выражений поддерживают две полезные функции:
- просмотр вперед — опережающий поиск (lookahead)
- просмотр назад — ретроспективный поиск (lookbehind)
Рассмотрим, зачем они нужны.
У нас есть следующее регулярное выражение, которое находит две подстроки:
/LudovicXVI/
LudovicXV, LudovicXVI, LudovicXVIII, LudovicLXVII, LudovicXXL
Предположим, что нам не нужно включать в результаты поиска часть подстроки с римскими цифрами XVI
. Для этого обернем цифру вот в такую конструкцию:
/Ludovic(?=XVI)/
LudovicXV, LudovicXVI, LudovicXVIII, LudovicLXVII, LudovicXXL
Как мы видим, условия сопоставления, заданные первоначальным выражением, не изменились. Сопоставились те же подстроки, что и в предыдущем примере.
Однако символы XVI в совпавших подстроках не были включены в окончательный результат поиска. Такое поведение называется позитивным просмотром вперед или позитивным опережающим поиском.
Логику позитивного просмотра вперед можно описать следующим образом:
регулярное выражение a(?=b)
находит совпадения таких a
, за которыми следует b
, при этом не делая b
частью сопоставления.
Просмотр вперед также может быть негативным. Тогда он будет искать совпадения в тех подстроках, где указанная в скобках часть подстроки отсутствует. В нашем случае, это по-прежнему XVI. Чтобы из позитивного просмотра сделать негативный, заменим символ =
на !
.
Теперь у нас сопоставились три другие подстроки:
/Ludovic(?!XVI)/
LudovicXV, LudovicXVI, LudovicXVIII, LudovicLXVII, LudovicXXL
Кроме просмотра вперед, существует просмотр назад или ретроспективный поиск. Он работает похожим образом, но ищет совпадения символов, расположенных после сгруппированной в скобках части регулярного выражения, которая не будет включена в сопоставление.
Иными словами, при позитивном просмотре назад регулярное выражение (?<=b)a
находит совпадения таких a
, перед которыми находится b
, не делая b
частью сопоставления.
Для позитивного просмотра назад используется дополнительный знак <
. В этом примере мы находим совпадения подстрок Two
, перед которыми следует One
:
/(?<=One )Two/
One Two, Three Two
Чтобы изменить позитивный просмотр назад на негативный, меняем =
на !
:
/(?<!One )Two/
Пример использования на Ruby
Стоит задача из строки "удалить" одиночные цифры (окружённые пробелами) - заменить их в строке, например, на символ '*'. Поначалу эта задача может показаться тривиальной, но не всё так просто, именно потому, что мы ищем одиночный символ числа, окружённый пробелами, при этом при нахождении совпадения пробелы тоже попадают в соответствие, и уже при поиске следующего совпаденияи не учитываются. Как раз в данном случае очень пригождается опережающий и ретроспективный поиск:
# в строке намеренно разделение как одним, так и несколькими # пробелами для демонстрации str = "1 2 3 4 5 12 34 56 78 90 12 3 4 5 6" str1 = str.gsub(/(^|\s+)\d(\s+|$)/, '*') str2 = str.gsub(/(^|\s+)\d(\s+|$)/, '\1*\2') str3 = str.gsub(/(^|(?=\s+))\d((?=\s+)|$)/, '*') str4 = str.gsub(/(^|(?<=\s))\d((?=\s+)|$)/, '*') puts "str: #{str}" #=> str: 1 2 3 4 5 12 34 56 78 90 12 3 4 5 6 puts "str1: #{str1}" #=> str1: *2*4*12 34 56 78 90 12*4*6 puts "str2: #{str2}" # str2: * 2 * 4 * 12 34 56 78 90 12 * 4 * 6 puts "str3: #{str3}" # str3: * 2 3 4 5 12 34 56 78 90 12 3 4 5 6 # и вот ожидаемый нами результат: puts "str4: #{str4}" # str4: * * * * * 12 34 56 78 90 12 * * * *