Cайт веб-разработчика, программиста Ruby on Rails ESV Corp. Екатеринбург, Москва, Санкт-Петербург, Новосибирск, Первоуральск

Регулярные выражения: просмотр вперёд и назад

Большинство реализаций регулярных выражений поддерживают две полезные функции:

  • просмотр вперед — опережающий поиск (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 * * * *