Регулярные выражения Perl и их применение

Дополнительные конструкции в регулярных выражениях

Показывать лекцию целиком

3.1. Комментарии в регулярных выражениях

Регулярные выражения могут быть очень громоздкими и сложными, и разработчики предусмотрели возможность создания в них комментариев, так же, как и в текстах программ. Для этого есть два варианта.

В первом, более простом варианте, вы можете использовать специальные скобки

(?# комментарий ).

Эта конструкция игнорируется. Комментарий не может содержать закрывающую круглую скобку, т.к. по ее наличию определяется, где заканчивается конструкция комментария. Сам комментарий может отсутствовать: закрывающая скобка может стоять сразу за решеткой. Конструкция комментария может даже стоять между подшаблоном и его квантификатором: оператор

print "bb" =~ /^b(?#aaaaa){2}$/;

напечатает единицу. Но это еще не значит, что конструкция комментария может стоять абсолютно в любом месте регулярного выражения. Обычно ее используют между подшаблонами, и этого бывает достаточно.

Вот еще одна тонкость: если это наше регулярное выражение будет ограничено решетками, то произойдет ошибка синтаксиса:

print "bb" =~ m#^b(?#aaaaa){2}$#;

Возникнет сообщение

Sequence (? incomplete in regex; …

Дело в том, что Perl определяет, где заканчивается регулярное выражение, по символу, который соответствует ограничителю, стоящему перед этим выражением. И если внутри регулярного выражения этот символ присутствует в явном виде (а, скажем, не как эскейп-последовательность \x23 ), то он должен быть замаскирован обратной косой чертой:

print "bb" =~ m#^b(?\#aaaaa){2}$#;

Здесь уже все в порядке.

Но это, конечно, запутывает регулярное выражение. Представьте, что в качестве ограничителей мы выбрали круглые скобки:

m( регулярное выражение )

и хотим в этом выражении применять скобочные конструкции. Тогда нам придется маскировать обратным слэшем все круглые скобки! А как в шаблоне записать литерал, состоящий из такой скобки? Похоже, что в явном виде вообще никак. Поэтому, если внутри шаблона встречается косая черта /, то в качестве ограничителей лучше выбирать восклицательный знак, решетку, двоеточие, запятую, знак процента или аналогичный символ, который отсутствует в шаблоне. Если вы, конечно, не участвуете в конкурсе на самую непонятную программу на Perl.

Чаще используют запись регулярного выражения в свободном стиле, для чего существует модификатор x. В зоне действия этого модификатора все, что начинается с символа решетки, и до конца строки в файле программы считается комментарием и игнорируется. Например:

print "bb" =~ 
/^          # Это начало строки
  b         # Далее идет символ b
   {2}      # За ним следует квантификатор
      $     # И в конце - конец строки
/x;

И этот пример работает. Как видим, мы включили не только комментарии, но и пробелы для красивых отступов вне комментариев! Да, в пределах действия квантификатора x все пробельные символы (а не только пробелы) игнорируются. Ведь комментарии надо чем-то отделять для наглядности от кода. Но теперь в регулярное выражение нельзя вставить эти самые пробельные символы в явном виде, ведь они не будут работать. Придется воспользоваться эскейп-последовательностями

\x20 для пробела
\t для табуляции и т.д.

Или же в общем виде: \s.

3.2. Группировка элементов шаблона

Мы уже применяли захватывающие и просто группирующие скобки. Рассмотрим еще некоторые тонкости.

Как работают группирующие скобки с квантификаторами? Например, что захватят в переменную $1 скобки в регулярном выражении 'abc' =~ /(\w)*/? Это выражение похоже на (\w*), которое в переменную $1 захватит все три символа, но алгоритм его работы иной. Скобки в шаблоне (\w)* захватят последний символ - c. Для тех, кто впервые с этим сталкивается, это кажется непонятным. Это работает так: сначала \w совпадет с буквой a, затем квантификатор * будет заставлять повторяться всю внутрискобочную конструкцию снова и снова, насколько это возможно, оставляя при этом сохраненные состояния. При возвратах, когда происходит выход за открывающую захватывающую скобку, соответствующая ей нумерованная переменная становится неопределенной (перестает существовать). Если же при возврате происходит вход внутрь захватывающих скобок, то при следующем выходе за закрывающую скобку произойдет коррекция значения этой нумерованной переменной. Механизм работы с нумерованными переменными такой: при встрече открывающей скобки при движении вправо в структуре данных для соответствующей нумерованной переменной запоминается адрес позиции в результирующем тексте, где она начинается, а при выходе за закрывающую захватывающую скобку запоминается адрес позиции, где значение этой переменной заканчивается. Таким образом, нумерованные переменные не копируют фрагменты найденного текста, а просто ссылаются на них.

В нашем случае выражение \w повторится трижды, захватывая каждый раз следующую букву, и в конце концов захваченной окажется буква c. Мы получаем интересный тактический прием захвата последнего фрагмента текста, который соответствует заданному подшаблону. Я думаю, вы уже можете догадаться, как в общем случае можно захватить такой фрагмент текста, который стоит на n-ном месте с начала или конца ряда таких фрагментов. Например, чтобы захватить предпоследний символ, надо записать

/(\w)*\w/

А как бы стал работать этот шаблон, если бы квантификатор был минимальным? В этом случае

'abc' =~ /(\w)*?/;

скобки бы совпали с пустым фрагментом в начале строки, а переменная $1 получила бы неопределенное значение. Ее попросту не было бы ввиду нулевого значения квантификатора. Если бы этот квантификатор стоял внутри скобок:

'abc' =~ /(\w*?)/;

то переменная $1 существовала бы и имела бы пустое значение.

Теперь рассмотрим несохраняющие (группирующие) скобки:

(?: шаблон )

Шаблон в них может и отсутствовать (быть пустым).

Такие скобки, как мы знаем, не создают нумерованной переменной, а служат для группировки подшаблонов, чтобы поставить к этой группе квантификатор или отделить альтернативные шаблоны от остального регулярного выражения. Между знаком вопроса и двоеточием могут стоять квантификаторы, например:

(?im:^passport nr\. \d+)

Область действия этих квантификаторов ограничена шаблоном, стоящим в этих группирующих скобках. Это означает, что внутри этих скобок будет действовать режим поиска без учета регистра и метасимвол ^ будет совпадать не только в начале текста, но также в начале каждой логической строки, т.е. сразу после символа \n, если только он не стоит самым последним в тексте. Но, конечно, эти режимы могут быть отменены другими квантификаторами, которые могут находиться в шаблонах внутри этих скобок.

Аналогично можно отменить действие модификаторов, если поставить перед ними знак минус. Например:

(?-i:^passport nr\. \d+)

Поиск будет вестись с учетом регистра символов. Или

(?i-ms:^passport nr\. \d+)

Включается модификатор i и отключаются модификаторы m и s.

И в конце добавлю, что скобки, как и другие метасимволы в регулярном выражении, нельзя задать как эскейп-последовательность: \x3a вместо (. Но! Забегая вперед, скажу, что почти произвольные части регулярного выражения могут находиться внутри интерполированных переменных. Смотрите, как это работает:

my $a='(';
'abc' =~ /$a\w)*/;
print $1;

На печать выйдет символ c. Этот не описанный в руководствах способ работает потому, что интерполяция переменных в регулярном выражении происходит на ранней стадии обработки, что открывает большие возможности для комбинаций: ведь содержимое этих интерполируемых переменных можно менять от одного обращения к данному оператору поиска/замены - к другому. Например, в зависимости от предыдущих вычислений можно добавить пару захватывающих скобок или изменить квантификатор.

3.3. Опережающая проверка

Это сложное условие ( мнимый символ, якорь ). Эту проверку еще называют "заглядыванием вперед". Вводится она конструкцией

(?= шаблон )

Шаблон может быть как угодно сложным. Это условие истинно, если в текущей позиции находится текст, совпадающий с заданным шаблоном. Обратите внимание, что эти условные конструкции совпадают не с текстом, а с позицией в тексте подобно своим более простым собратьям \b, \A, $ и т.д.

Замечу, что атомарная группировка имитируется позитивной опережающей проверкой и обратной ссылкой:

(?> шаблон )  эквивалентно  (?=( шаблон ))\1

Происходит это потому, что после выполнения опережающей (как и ретроспективной) проверки уничтожаются все сохраненные состояния, возникшие внутри нее.

Имеется противоположный случай проверки - негативная опережающая проверка:

(?! шаблон )

Такое условие истинно, если в текущей позиции нет текста, совпадающего с заданным шаблоном.

Например, шаблон

\w+(?=\s+)

найдет слово, за которым стоит один или несколько пробельных символов, но сами пробельные символы в результат поиска не войдут. Дело в том, что позиционная проверка не поглощает текст: после применения этих условных конструкций текущая позиция в тексте вернется на прежнее место, где была до применения этого якоря. Интересно, что захватывающие скобки внутри этих проверок захватывают текст, о чем забывают упомянуть авторы книг по Perl.

Рассмотрим такой пример:

$_='abc';
print "1\n" if /a(?=(b))bc/;
print $1;

Будут напечатаны такие строки:

1
b

Как видим, после совпадения с символом a после него ищется символ b. Он находится, и поиск продолжается дальше. Если сразу бы после a не стоял символ b, то поиск потерпел бы неудачу и началась бы следующая итерация, т.к. сохраненных состояний, к которым можно вернуться, нет. Внутри опережающей проверки этот символ захватывается в переменную $1, после чего проверка заканчивается успешно, и текущая позиция в строке возвращается к символу b. Затем выполняется соответствие подшаблона bc символам bc, на этом оператор поиска успешно завершается.

Подобные сложные проверки бывают необходимы в сложных случаях, когда надо принять решение в зависимости от того, какой фрагмент текста находится в текущей позиции (или вообще впереди нее). Эти условия обычно используются в условных конструкциях, о которых речь впереди.

Обратите внимание на интересную особенность сложных условий: в результате работы оператора

"a" =~ /(?=(a))/

переменная $1 получит значение a, а в результате работы похожего регулярного выражения

"a" =~ /((?=a))/

переменная $1 получит пустое значение. Это можно объяснить тем, что в первом случае внутри сложного условия происходит захват буквы a, а затем после выхода за это сложное условие (на конец регулярного выражения ) текущая позиция в шаблоне возвращается в точку перед буквой a, но сама буква уже сидит в переменной $1. Во втором случае после выполнения фрагмента шаблона ((?=a текущая позиция в шаблоне передвигается за букву a, но после закрытия скобки в сложном условии ((?=a) происходит возврат текущей позиции в шаблоне перед буквой а, и после закрытия захватывающей скобки эта текущая позиция остается перед буквой a, как и в момент открытия захватывающей пары скобок. В результата эта пара скобок захватывает пустой фрагмент текста.

3.4. Ретроспективная проверка

Имеется аналогичная возможность также "заглянуть назад":

(?<= шаблон )

Это условие истинно, если перед текущей позицией имеется текст, совпадающий с шаблоном. Но не надо думать, что проверка здесь идет справа налево. Например:

$_='abcd';
print "1\n" if /ab(?<=(ab))/;
print $1;

В результате получим вывод

1
ab

Как видим, захват текста в этих якорях также происходит.

Негативная ретроспективная проверка задается выражением

(?<! шаблон )

и требует, чтобы непосредственно перед текущей позицией не было текста, соответствующего данному шаблону.

Но в ретроспективных проверках шаблон уже не может быть каким угодно, а должен соответствовать только определенному числу символов. Механизм поиска должен по этому шаблону определить длину текста, которому он может соответствовать, и искать левее текущей позиции на число символов, с которыми этот шаблон может совпасть.

Иначе пришлось бы искать с начала всего текста, а это большие непроизводительные затраты времени. Поэтому шаблон не может содержать квантификаторы переменной длины, а все альтернативы в альтернативных конструкциях должны совпадать с текстом одной и той же длины. Вот примеры ошибок:

(?<=\w+)
(?<=\w{1,2})
(?<=\w{3}|abcd)

В первых двух случаях имеем переменную длину квантификатора, а в последнем - разную длину альтернатив.

В качестве примера использования этих якорей рассмотрим задачу разделения разрядов числа запятыми. Большие числа удобнее воспринимать, когда они поделены запятыми по три разряда: 12,345,678. Хотя тройки разрядов отсчитываются справа, а механизм поиска просматривает текст слева, эту техническую сложность нетрудно обойти. Сформулируем условие вставки запятой так: запятая вставляется в позицию, слева от которой имеется цифра, а справа - произвольное ненулевое число групп из трех цифр и далее не стоит цифра. Вот программа, которая моделирует этот пример:

$_='number 1: 12345678 number 2: 98765432154321';
s/(?<=\d)(?=(?:\d{3})+(?!\d))/,/g;
print $_;

На печать выведется

number 1: 12,345,678 number 2: 98,765,432,154,321

Вторая проверка здесь сложная: она включает в себя еще одну проверку того, что дальше не стоит цифра. Модификатор g обеспечивает расстановку запятых по всему тексту в нужных местах.

Если бы мы вынесли за скобку проверку (?!\d) и написали бы так:

s/(?<=\d)(?=(?:\d{3})+)(?!\d)/,/g;

то это было бы неправильно. Смысл получился бы таким: позиция, перед которой стоит цифра, за которой идет группа из троек цифр и за которой нет цифры. Этот набор условий никогда не выполняется.

Благодаря тому, что позиционные проверки не поглощают текст, для данной позиции текста можно записать сколько угодно таких проверок. Все они должны выполняться, иначе произойдет локальная неудача поиска. Например:

/(?=\d)(?![09])/;

означает, что в этой позиции должна стоять цифра и она не должна быть равна нулю и девяти. Это равносильно условию

/(?=[1-8])/;

Ко всему сказанному добавлю, что к позиционным проверкам, как и вообще к условиям с нулевой длиной совпадения, нельзя ставить квантификаторы.

3.5. Встроенный код Perl

Встроенный код Perl является условием, которое всегда выполняется. Когда такой код попадается при движении в шаблоне слева направо, он выполняется. В этом коде можно проделать полезную работу, например, запомнить промежуточные значения нумерованных переменных, ведь позже поиск может завершиться неудачей. При прохождении встроенного кода в обратном направлении в случае возврата тоже могут происходить интересные и тонкие действия, которые мы рассмотрим в соответствующих лекциях. Но не вставляйте в этот код операторы перехода next, last, break, redo, goto и вызовы подпрограмм - это не предусмотрено правилами, хотя транслятор за этим не следит. Не следут также использовать во встроенном коде операторы, в которые входят регулярные выражения, - я не слышал, чтобы кто-то гарантировал правильность работы таких нестандартных приемов. Если вы хотите выйти из регулярного выражения при каком-то условии, то надо воспользоваться возможностями, которые даются регулярными выражениями. Иначе интерпретатор Perl может зависнуть, ведь перед выполнением оператора с регулярным выражением он выделяет память, инициализирует структуры данных. Эти действия корректно завершаются только при нормальном завершении оператора.

Также не пытайтесь изменять во встроенном коде результирующий текст - это не даст эффекта.

Иногда вы можете обнаружить, что код Perl должен был бы выполниться, но почему-то не выполняется. Здесь причина в том, что Perl имеет много оптимизаций при поиске по регулярному выражению. Если по каким-то причинам механизм поиска решит, что совпадения быть не может, он не будет пытаться делать этот поиск. Вот здесь поиск вообще не начнется:

'abc' =~ /(?{ print '123' })abcd/;

Система поиска определит, что для совпадения длина результирующего текста должна быть не меньше четырех символов, а их в тексте только три. Оператор закончится неудачно без применения поиска по регулярному выражению, и напечатано ничего не будет.

Мы уже знаем, как выглядит конструкция вставки кода Perl:

(?{ код Perl })

Подробнее о встроенном коде мы поговорим в дальнейшем, когда наберемся опыта.

3.6. Условная конструкция

В регулярных выражениях Perl имеется условная конструкция, которая позволяет сделать выбор подобно операторам if … then или if … then … else … Она имеет такой синтаксис:

(? условие шаблон-да )

или полный вариант

(? условие шаблон-да | шаблон-нет )

Работает эта конструкция так: вначале проверяется условие, и если оно истинно, то вся эта конструкция как бы заменяется на шаблон-да, а если условие ложно, то вместо конструкции подставляется шаблон-нет (если он есть). А если его нет, то на место этой конструкции ничего не подставляется, как будто этой конструкции не было.

После символов (? в реальной программе всегда будет стоять открывающая круглая скобка. Эта скобка не может отделяться пробельными символами от знака вопроса, даже если регулярное выражение записано в свободном формате (с модификатором x ).

Шаблон-да и шаблон-нет представляют из себя произвольные регулярные выражения, а условие может иметь следующие значения.

  • Число в круглых скобках. Тогда оно считается номером каких-то захватывающих скобок. Если захватывающие скобки с данным номером участвовали в совпадении, то условие считается истинным, если нет - ложным. Здесь опять повторю замечание, что участвовать в совпадении и иметь непустое значение - не одно и то же. В операторе
    '' =~ /(.*)/;

    скобки участвовали в совпадении и переменная $1 получила пустое значение. В операторе

    '' =~ /(.)*/;

    скобки не участвовали в совпадении, хотя поиск также завершился удачно. Но т.к. квантификатор имел значение 0, то переменная $1 не существует (имеет неопределенное значение).

    В следующем примере отыскивается ссылка href, которая может быть заключена в кавычки, апострофы или не быть ограничена ничем:

    my $text='<a target="_blank" href="http://www.intuit.ru/">Internet-обучение</a>';
    if ($text =~
     m!<a\s+[^>]*?href\s*=\s*
      (["'])?                  # совпадение для разделителя (', " или пусто). Запоминаем его
      ([^"'>\x20]+)      # ссылка (все кроме пробела, ' и ")
      (?(1)\1)                       # если был разделитель, то подставляем шаблон для него
      [^>]*>[^<]+</a>!ix) { print $2 }

    Обратите внимание, что при появлении квантификатора !) и внутри класса символов пробел задан как \x20.

  • Позиционная проверка, четыре варианта которой мы рассматривали. Если проверка возвращает истину, то подставляется шаблон да, иначе подставляется шаблон нет или "пусто" в случае короткого варианта условной конструкции.

    Для этого случая рассмотрим такой пример: пусть надо обрабатывать десятичные и 16-ные числа и пусть 16-ные числа предваряются символом $. Программа обработки может быть такой:

    $_=' 1234 aaa $12aF  $abc $z 1456';
    /(?(?=\$|\d)               	# начало числа?
       (                            # да, начинаем его захват
       (?(?=\$)                  	# 16-ное число?
         \$[\da-fA-F]+      	# да, подставляем шаблон 16-ного числа
         (?{ print 'hex ' })  	# печатаем префикс hex
         |
         \d+                        # нет, ставим шаблон 10-ного числа
         (?{ print 'dec ' })   	# печатаем префикс dec
       )                            # конец захвата числа
       )                            # конец вложенного условия
       (?{ print "$1\n" })   	# печатаем само число
      [^\$\d]*                    	# это не начало числа, пропускаем все до числа
     )/gx;

    На печать выдается следующее:

    dec 1234
    hex $12aF
    hex $abc
    dec 1456

    А теперь разберемся, как работает эта программа. В регулярном выражении имеется две условные конструкции, вторая из них вложена в первую (используется в качестве шаблона да ). Внешняя условная конструкция имеет такую логику: следующий символ - $ или десятичная цифра? Да - тогда применяем вложенную условную конструкцию, которая разделит десятичные и 16-ные числа. Нет - тогда подставим шаблон [^\$\d]*, который быстро пропустит все до следующего числа.

    Вложенная конструкция тоже имеет полную форму и работает так: если следующий символ - $, то она подставляет шаблон \$[\da-fA-F]+, который соответствует 16-ному числу, а иначе подставляет шаблон \d+ для десятичного числа.

    Кроме того, в первой альтернативе внешней условной конструкции производится захват числа в нумерованную переменную $1, а после шаблонов для каждого типа чисел выводится на печать, соответственно, слово hex или dec. После закрывающей захватывающей скобки выводится само число из $1.

    Может возникнуть вопрос: как будет обрабатываться фрагмент текста $z, который не является числом, но начинается с символа $? В этом случае сработает первая альтернатива из внешней условной конструкции, которая подставит шаблон \$[\da-fA-F]+. Он не совпадет с этим фрагментом, а это будет означать, что все регулярное выражение не совпало с данной позиции. Но благодаря модификатору g, поиск будет продолжаться в цикле со следующей позиции текста (символа z ), и все числа будут напечатаны.

    Подробнее о работе модификатора g мы будем говорить, когда будем описывать работу операторов m/…/ и s/…/…/.

  • Фрагмент кода Perl. Если в качестве условия выступает фрагмент кода Perl, то используется возвращаемое им значение, т.е. результат последнего вычисления. Если он отличен от пустой строки, строки, которая состоит только из числа 0 (0.0), неопределенности и числового нуля, то считается истиной, иначе ложью. Здесь еще скажу, что в Perl есть интересная специальная переменная $^R - результат последнего выполнения кода Perl в регулярном выражении. Здесь слово "последнего" понимается как результат кода, который выполнился последний раз по времени в ходе работы системы поиска соответствия. Т.е. $^R всегда хранит самый "свежий" результат выполнения встроенного кода Perl. Кроме того, при возврате назад за код Perl вычисленное этим кодом значение "забывается" и восстанавливается предыдущее вычисленное значение, т.е. эта переменная автоматически локализуется. В начале работы программы переменная $^R имеет неопределенное значение, а после выхода из оператора поиска или проверки имеет последнее присвоенное ей значение. Кроме того, код Perl, который стоит в части условия условной конструкции, не изменяет значения переменной $^R! Интересно, что Perl не запрещает присваивать этой переменной значение напрямую.
  • 3.7. Задание модификаторов внутри регулярного выражения

    Нами уже был рассмотрен вариант расположения модификаторов между знаком вопроса и двоеточием в группирующих скобках. Есть еще один вариант задания модификаторов:

    (? модификаторы )

    Например, (?is-mx) - включение режимов i и s и отключение режимов m и x. Область действия этой конструкции начинается сразу после нее и продолжается до следующей закрывающей круглой скобки. Имеется в виду, конечно, "настоящая" скобка, а не литерал \), который эквивалентен коду \x29. Если справа от этой конструкции нет закрывающей скобки, то ее область действия простирается до конца регулярного выражения. Эта закрывающая скобка может принадлежать захватывающей или незахватывающей паре скобок, закрывать условную конструкцию и т.д. Но, конечно, эту установку модификаторов может отменить другая установка тех же модификаторов. В конечном случае имеет действие та установка модификатора, которая находится "ближе" к данному фрагменту регулярного выражения.

    Атомарная группировка тоже относится к дополнительным конструкциям, но мы ее уже рассмотрели.

    Вернуться к учебному плану