Предотвращение зацикливания при поиске и замене
Версия 8 регулярных выражений системы программирования Perl, которую (версию) мы изучаем, дает очень мощные средства для поиска и замены образцов текста. Но за этой мощью кроются сложности ее применения. Сейчас мы рассмотрим один сложный аспект применения регулярных выражений Perl. Как вы уже знаете, совпадение может быть не только с фрагментом текста, но также и с позицией в тексте, а при замене, когда не было совпавшего текста, а была найдена только позиция совпадения, заменяющий текст подставляется в эту позицию. За этим кроется возможность зацикливания при поиске в цикле или с модификатором g. Рассмотрим такой пример:
while ('abcd' =~ /z?/) { … }
Ясно, что этот цикл будет выполняться вечно, т.к. без модификатора g не будет смещения текущей позиции поиска. Но здесь это нормально, так задумал программист.
Однако, бесконечный цикл можно организовать также и внутри регулярного выражения, применяя квантификатор к скобкам, которые захватили пустой фрагмент текста:
$_='abcd'; print "$`|$&$'" if /(z?)*/;
Мы печатаем все до совпадения, за ним вертикальную черту и далее все после совпадения. Будет напечатано
|abcd
Модификатор * заставляет совпадать z, взятое 0 раз, еще и еще с того же самого начала текста. Чтобы предотвратить подобное зацикливание внутри системы поиска совпадения, эта система прерывает такой цикл, обусловленный повтором совпадения с нулевой длиной.
Бесконечный цикл также можно задать во внешнем цикле выполнения поиска и замены, с помощью модификатора g:
$_='abcd'; s/.??/<$&>/g; print $_;
Будет напечатано:
<><a><><b><><c><><d><>
Это означает, что была найдена и заменена каждая буква, и кроме того, найденный фрагмент $& вставлен в каждую позицию между буквами, а также в начало и конец текста. А казалось бы, замена должна была бы выполняться бесконечно путем вставки $& в начало текста. Эта проблема решена разработчиками регулярных выражений Perl так: переменной, которая является операндом оператора поиска/замены, присваивается дополнительное состояние "предыдущее совпадение имело нулевую длину". Это состояние хранится в переменной лишь между циклами поиска, обусловленными модификатором g, и сбрасывается явным или неявным присвоением значения функции pos для этой переменной. Если в каком-то цикле под модификатором g получается совпадение нулевой длины и да нная переменная имеет установленное состояние "предыдущее совпадение было нулевой длины", то в конце такого цикла система поиска совпадения производит принудительный поиск с возвратами, пока не произойдет совпадения с непустым фрагментом текста.
В связи с этим материалом обратите внимание еще на такой парадоксальный пример:
print "$`|$&|$'\n" if 'abc' =~ /(a?)*/; print length $1;
Будет напечатано:
|a|bc 0
Текущий фрагмент совпадения $& равен a, а в $ 1 захвачен пустой фрагмент текста. Как будто бы это ошибка, но на самом деле ошибки в этом примере нет. Вначале квантификатор ? получает значение 1, квантификатор * тоже получает значение 1.
Происходит совпадение скобок с буквой a. Затем квантификатор * заставит подшаблон в скобках сделать повтор с позиции 1 (за буквой a), и в результате будет найдено совпадение нулевой длины для a с квантификатором ?, равным нулю. При этом переменная $1 обновится и получит пустое значение. В следующем повторе зафиксируется нулевая длина совпадения, и выполнение оператора поиска принудительно прервется. Если переписать этот пример в виде
print "$`|$&|$'\n" if 'abc' =~ /(?:(a?)(?{print 'pos='.pos($_).", text='$1'\n"}))*/; print length $1;
и посмотреть, что будет напечатано:
pos=1, text='a' pos=1, text='' |a|bc 0
то алгоритм работы системы поиска в этом примере станет ясен. Это немного напоминает уже рассмотренный пример
$_=':abc:'; print "$& $1" if /(\w)+/;
где вместо квантификатора * стоит квантификатор +. Этот пример напечатает
abc c