Регулярные выражения: просмотр вперёд и назад
Большинство реализаций регулярных выражений поддерживают две полезные функции:
- просмотр вперед — опережающий поиск (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 * * * *