Примеры применения динамических регулярных выражений
Для начала приведу простой и немного искусственный пример: пусть нам надо проверить правильность строки
Далее стоит 13 нулей: 0000000000000
Причем, число нулей может быть произвольным от 1 и более, например,
Далее стоит 2 нуля: 00
Нам надо составить регулярное выражение, которое проверяет, что число соответствует количеству нарисованных нуликов. Этот шаблон может выглядеть так:
(\d+) \D+(??{"0{$1}"})$
Работает он таким образом: вначале в переменную $1 захватывается число (13, 2 и т.д.).
Затем идет пропуск всех нецифровых символов. Внутри динамического регулярного выражения конструируется подшаблон, состоящий из символа нуля и числителя, который равен содержимому переменной $1. Для фразы
Далее стоит 13 нулей: 0000000000000
Весь шаблон после подстановки результата выполнения кода Perl будет иметь вид
(\d+) \D+0{13}$
В случае строки
Далее стоит 2 нуля: 00
Это регулярное выражение примет вид
(\d+) \D+0{2}$
Даже для строки
Далее стоит 0 нулей:
будет найдено совпадение.
Для начала приведу простой и немного искусственный пример: пусть нам надо проверить правильность строки
Далее стоит 13 нулей: 0000000000000
Причем, число нулей может быть произвольным от 1 и более, например,
Далее стоит 2 нуля: 00
Нам надо составить регулярное выражение, которое проверяет, что число соответствует количеству нарисованных нуликов. Этот шаблон может выглядеть так:
(\d+) \D+(??{"0{$1}"})$
Работает он таким образом: вначале в переменную $1 захватывается число (13, 2 и т.д.).
Затем идет пропуск всех нецифровых символов. Внутри динамического регулярного выражения конструируется подшаблон, состоящий из символа нуля и числителя, который равен содержимому переменной $1. Для фразы
Далее стоит 13 нулей: 0000000000000
Весь шаблон после подстановки результата выполнения кода Perl будет иметь вид
(\d+) \D+0{13}$
В случае строки
Далее стоит 2 нуля: 00
Это регулярное выражение примет вид
(\d+) \D+0{2}$
Даже для строки
Далее стоит 0 нулей:
будет найдено совпадение.
В документации по Perl приводится пример применения динамических регулярных выражений для того, чтобы выяснить, является ли заданная в тексте последовательность чисел последовательностью Фибоначчи. А мы в качестве более сложного примера поставим немного другую задачу: найти в $_ первую наидлиннейшую последовательность 10-ных цифр, которые стоят подряд и возрастают на 1. Например, в строке
$_='0123 1234345678910 ';
искомая наидлиннейшая последовательность такая: 3456789.
Вот сама программа:
#!/usr/bin/perl -w use strict;
my ($len,$d,$res)=(0); $_='0123 1234345678910 '; no warnings;
/((\d) (?{$d=$+}) (?> (??{ ++$d < 10 ? "$d" : "(?!)" })* ) ) (?{ # Эти символы нельзя разделять if (length $1 > $len) { $len=length $1; $res=$1; } }) # Эту скобку нельзя отделять от предыдущей скобки "}" (?!) /x; print $res if defined $res;
Вначале переменной $len присваивается 0. В комментариях указана особенность конструкций (?{ и }), в которых фигурную скобку нельзя отделять от соседнего символа даже и при свободном форматировании.
Теперь разберемся, как работает это регулярное выражение. Вначале открывается захватывающая скобка. Позже выяснится, что она захватывает очередную длиннейшую последовательность цифр, которые возрастают на единицу. Вторая пара захватывающих скобок захватывает одну цифру. Во второй строке стоит встроенный код Perl, который запоминает эту цифру в переменной $d. Далее идет динамическое регулярное выражение, к которому записан квантификатор *. Оно вставлено в атомарные скобки.
Код в этом динамическом регулярном выражении при каждой итерации, обусловленной квантификатором *, увеличивает на единицу переменную $d, и если ее значение меньше десяти, то он возвращает значение этой переменной, а если $d после инкремента принимает значение 10, которое уже не подходит для цифры, то этот код Perl возвращает уже знакомый нам подшаблон (?!), который заставляет квантификатор * сделать шаг назад и остановиться на своем предыдущем состоянии, при котором было совпадение с наибольшей цифрой. После этого совпадения для атомарных скобок управление передается за них и выполняется код Perl, который проверяет, больше ли длина переменной $1 длины переменной $len. Если больше, значит, мы нашли более длинную последовательность цифр, поэтому мы запоминаем ее в переменной $res и обновляем содержимое переменной $len, которая хранит размер найденной длиннейшей последовательности цифр. После этого кода опять стоит подшаблон (?!), который заставляет все регулярное выражение сделать повтор со следующего символа целевого текста.
Директива no warnings нужна, чтобы отменить предупреждение
matches null string many times in regex; …
которое появится, когда динамическое регулярное выражение вернет подшаблон (?!).
Perl предупреждает, что подшаблон, который не поглощает текст, имеет квантификатор. Но в нашем примере это не приводит к зацикливанию, т.к. этот подшаблон возвращает несовпадение, и происходит выход за пределы квантифицируемого подшаблона.
Здесь опять замечу, что это регулярное это выражение возвращает несовпадение и используется только ради побочного эффекта (установки переменной $res).
Насколько эффективно работает это регулярное выражение? Вставьте встроенный код
Perl после атомарных скобок, который будет печатать содержимое переменной $1:
/((\d) (?{$d=$+}) (?> (??{ ++$d < 10 ? "$d" : "(?!)" })* ) ) (?{print "$1\n"}) (?{ # Эти символы нельзя разделять if (length $1 > $len) { $len=length $1; $res=$1; } }) # Эту скобку нельзя отделять от предыдущей скобки "}" (?!) /x; print $res if defined $res;
В результате на печати получим:
0123 123 23 3 1234 234 34 4 3456789 456789 56789 6789 789 89 9 1 0 3456789
Видим, что вначале регулярное выражение находит длиннейшую последовательность цифр, а потом перестартовывает с каждой следующей цифры этой последовательности благодаря второму подшаблону (?!). Хотелось бы, чтобы вместо этого начальная позиция при итерациях устанавливалась сразу за найденной последовательностью цифр, дабы не делать лишней работы. Но мне кажется, это невозможно, т.к. присвоение внутри регулярного выражения функции pos (pos($_)=…) не дает эффекта.
Вместо этого могу показать технический прием, как можно завершить все регулярное выражение при выполнении какого-то условия. Например, в нашем случае нужно завершить поиск, если найдена последовательность цифр длиной четыре цифры. В начало всего регулярного выражения надо вставить условную конструкцию
(?(?{$len == 4})\G(?!))
При достижении этого условия встроенный код Perl вернет истину, и на место всей условной конструкции будет подставлен подшаблон
\G(?!)
Он говорит, что с конца предыдущего совпадения (или с начала текста при первой итерации поиска) нет совпадения. На этом все регулярное выражение закончит работу, т.к. итерация его со следующей позиции невозможна из-за привязки к концу предыдущего совпадения. В том месте, где вы установили, что надо поскорее выйти из регулярного выражения, подставьте подшаблон (?!), чтобы произошла итерация всего регулярного выражения. Но помните, что она не произойдет до тех пор, пока слева от текущей позици в шаблоне имеются квантификаторы или конструкции выбора альтернативы с сохраненными состояниями.
Что будет, если убрать атомарную группировку?
/((\d) (?{$d=$+})
(??{ ++$d < 10 ? "$d" : "(?!)" })*
) (?{print "$1\n"}) (?{ # Эти символы нельзя разделять if (length $1 > $len) { $len=length $1; $res=$1; } }) # Эту скобку нельзя отделять от предыдущей скобки "}" (?!) /x; print $res if defined $res;
Тогда на печать выйдет гораздо больше строк:
0123 012 01 0 123 12 1 23 2 3 1234 123 12 1 234 23 2 34 3 4 3456789 345678 34567 3456 345 34 3 456789 45678 4567 456 45 4 56789 5678 567 56 5 6789 678 67 6 789 78 7 89 8 9 1 0 3456789
Из этой распечатки видно, что после выполнения второго подшаблона (?!) не происходит итерация всего регулярного выражения, как в первом случае, ведь квантификатор * имеет сохраненные состояния. Он начинает отдавать по одной захваченной цифре, пока не отдаст их все. Только после этого происходит рестарт всего регулярного выражения со следующего символа целевого текста.