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

         

Альтернативные шаблоны


Символ "|", напоминающий операцию "или", играет в регулярных выражениях роль, схожую с этой логической связкой. Это конструкция выбора (альтернативы). Дойдя до нее, система поиска соответствия начинает с текущей позиции целевой строки сопоставлять ей все шаблоны из конструкции выбора (альтернативные шаблоны) в порядке их написания (т.е. слева направо). Используется первый совпавший шаблон, после чего оставшиеся шаблоны пропускаются и управление передается за конструкцию выбора. В качестве шаблонов могут выступать регулярные выражения любой сложности, а не только такие, как в нашем примере. Слова шаблон и регулярное выражение для нас являются синонимами.

Оператор "|" имеет очень низкий приоритет, поэтому, если перед или после конструкции выбора имеется еще что-то, то ее надо заключить в скобки.

Во втором примере символ "^" означает начало строки, а символ "$" - ее конец. Точный смысл этих символов мы обсудим позже. Эти символы также называют мнимыми символами, т.к. они совпадают не с текстом, а с позицией в целевой строке. Также их еще называют условиями, якорными метасимволами или просто якорями.

Если бы во втором примере регулярное выражение было записано без скобок:

^stop|quit|exit|abort$

то оно бы означало следующее: либо stop в начале строки, либо слова quit или exit в любом месте строки, либо abort, стоящее в конце строки. А это было бы не то, что нам нужно.



Классы символов


Как быть, если в данной позиции целевой строки могут стоять (ожидаются) разные символы? Например, параметры тега HTML могут заключаться в апострофы, а также двойные кавычки. Здесь конструкцию выбора применять неудобно. Для этого существуют классы символов. Класс - это последовательность символов, которая заключена в квадратные скобки. Например, класс ["'] совпадает с апострофом и с двойной кавычкой.

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

Классы имеют свои метасимволы, а некоторые метасимволы регулярных выражений внутри классов не действуют. Например, символ ] является метасимволом лишь внутри класса и поэтому должен быть в нем замаскирован: \]. А символ [ является метасимволом в регулярном выражении, но не внутри класса символов.

Внутри класса символов можно использовать диапазоны символов, например, класс [a-f0-9] - это то же, что [abcdef0123456789], но первая запись короче. Диапазон включает все промежуточные символы, чьи коды расположены между кодами крайних символов. Если вы захотите включить знак минус в класс символов, то его надо либо замаскировать обратным слэшем, либо поставить в самом начале или конце класса.

Символ "^" внутри класса уже не означает начала строки, мнимые символы внутри классов не имеют смысла и не используются. Символ "^", который стоит в самом начале класса, инвертирует этот класс, и такой инвертированный класс соответствет любому из символов, кроме перечисленных в этом классе.

Некоторые классы так часто используются, что для их обозначения придумали специальные эскейп-последовательности.

\d соответствует десятичной цифре: [0-9].\D соответствует одному символу, не являющемуся цифрой: [^0-9].\w соответствует символу, входящему в слово: [a-zA-Z0-9_]. Но вы должны помнить, что этот класс чувствителен к локальной установке и может включать символы букв национального алфавита.\W соотвтетсвует любому символу, не входящему в слово: [^a-zA-Z0-9_]. Он тоже чувствителен к локали.\s соответствует одному "пробельному" символу. Пробельными символами считаются: пробел с десятичным кодом 32;горизонтальная табуляция \t с кодом 9;перевод строки \n с кодом 10;возврат каретки \r с кодом 13;перевод формата \f с кодом 12.


Таким образом, \s - это класс [ \t\r\n\f].

\S соответствет непробельному символу: [^ \t\r\n\f].

Еще заметим, что если класс символов находится в зоне действия модификатора i, то в этот класс неявно включаются соответствующие буквы также другого регистра, чтобы он соответствовал символу без учета регистра букв. Например, класс [a-c] в зоне действия модификатора i соответствует символу из диапазонов a-cA-C, а класс [^a] в зоне действия модификатора i уже не соответствует символу A.

Для задания символа по его коду можно воспользоваться эскейп-последовательностью вида \xHH, где HH - две шестнадцатеричные цифры кода символа. Например, \x20 - это символ пробела, а \xFF - код буквы "я" в кодировке Windows (это число 255). Шестнадцатеричные цифры можно набирать в любом регистре, а буква x должна быть в нижнем регистре.

Имется возможность использовать для этого также восьмеричную систему, например, \040 - код пробела и \377 - код буквы "я". После обратной косой должно быть ровно три восьмеричных цифры. Но я не советую пользоваться восьмеричной системой, т.к. это вступает в конфликт с обозначением обратных ссылок, о которых речь еще впереди.


Квантификаторы, их "жадность" и ее ограничение


Ознакомимся с квантификаторами, также именуемыми числителями или повторителями. Если после подшаблона стоит квантификатор, то этот шаблон может повторяться столько раз, сколько указыват этот квантификатор. Например, шаблон a{0,4} соответствует нулю, одной, двум, трем и четырем буквам "a", идущим подряд.

Соответствие нулю каких-то символов означает соответствие пустой подстроке. Если максимальный параметр не ограничен, то его можно опустить: шаблон a{1,} соответствует последовательности из одной и более букв "а". Шаблон a{3} соответствует ровно трем подряд буквам a, это то же, что и шаблон aaa.

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

Некоторые числители используются так часто, что для них сделали краткие формы записи:

a* - это нуль или больше символов a: a{0,};a+ - это один или больше символов a: a{1,};a? - это нуль или один символ a: a{0,1}.

Квантификаторы имеют высокий приоритет, поэтому шаблон bil+ing соответствует символам bi, после которых идет одна или больше букв l и далее последовательность ing. Если вы хотите, чтобы квантификатор соответствовал последовательности символов или группе подшаблонов, то эту конструкцию надо взять в скобки:

(bil)+ing соответстсвует строкам biling bilbiling bilbilbiling и т.д.

По умолчанию, алгоритм работы квантификаторов таков, что они пытаются захватить как можно больше текста, который им соответствует. Например, в переменной $text имеем текст

<b>Текст выделен жирным шрифтом.</b> Простой текст <b>Это опять жирный</b>

Если применить к этому тексту регулярное выражение <b>.+</b>: $text =~ /<b>.+</b>/ то оно будет соответствовать всему этому тексту, а не фрагменту <b>Текст выделен жирным шрифтом.</b>

Аналогично, в шаблоне

\w*a

примененном к строке

abcabcaaaaa

подшаблон \w* будет соответствовать подстроке

abcabcaaaa

(без последней буквы a), а не подстроке

abca

или

abcabca

Имеется способ заставить квантификаторы захватывать как можно меньше текста, для этого после такого квантификатора надо поставить знак вопроса: "?". Например:

a+?

соответствует минимуму из одной и более букв "a". Если этот шаблон применить к строке

bbbaaaabbb



то такой минимальный квантификатор захватит первую попавшуюся букву а, после чего оператор поиска закончит работу, вернув число 1.

В шаблоне

a*?

минимальный квантификатор удовлетворится нулем символов a, а это значит, что совпадение будет найдено в любом месте, даже после истинного конца строки! Оператор

print "a" =~ /\za*/;

напечатает 1.

Вот другие примеры минимальных квантификаторов:

\w? (abc){3,5}?



Модификатор e в операторе замены


Оператор замены поддерживает модификатор e (Evaluation), которого нет в операторе поиска. В этом случае операнд для замены рассматривается как фрагмент кода Perl, который каждый раз во время замены выполняется аналогично функции eval, а полученный в скалярном контексте результат подставляется вместо найденного фрагмента текста. Это дает большую гибкость при замене текста. Более того, модификатор e может повторяться несколько раз, что влечет многократное применение функции eval к результату (столько раз, сколько раз повторен модификатор e). Заметим, что остальные модификаторы тоже могут повторяться, но это не влечет каких-либо последствий.

Рассмотрим такой пример на замену переменных их значениями.

my $a='a'; $_='This is $a'; s/(\$\w+)/$1/; print;

В результате будет напечатано

This is $a

В захватывающие скобки попала подстрока $a, операнд $1='$a' был интерполирован по правилам строк в кавычках и в результате интерполяции получился текст '$a', который и заменил найденный фрагмент текста '$a', т.е. сам себя.

Теперь к оператору замены добавим модификатор e:

my $a='a'; $_='This is $a'; s/(\$\w+)/$1/e; print;

В результате получается тот же вывод:

This is $a

Как это объяснить? Теперь операнд для замены $1='$a' был выполнен как код Perl, в результате получилась строка '$a', которая опять заменила саму себя.

Добавим еще один модификатор e:

my $a='a'; $_='This is $a'; s/(\$\w+)/$1/ee; print;

В результате получается текст

This is a

В этом случае после выполнения кода Perl $1 получается строка '$a', которая опять выполняется как код Perl, что и дает ее значение 'a'.

Теперь принцип ясен, мы можем продолжить эту аналогию и написать такую загадочную программу:

my $b='b'; my $a='$b'; $_='This is $a'; s/(\$\w+)/$1/eee; print;

В результате выводится текст

This is b

Но вряд ли кому-либо на практике придется применять модификатор e больше двух раз.



Модификаторы и якоря


А теперь рассмотрим другие модификаторы регулярных выражений. Модификатор m (Multiline) меняет смысл якорей ^ и $: в зоне его действия метасимвол ^ соответствует не только началу текста, но также началу каждой логической строки в этом тексте, т.е совпадает в начале текста и после каждого символа новой строки \n, который не стоит в самом конце текста. Метасимвол $ в зоне действия модификатора m совпадает не только в самом конце текста, но также и перед каждым символом \n. Но тогда необходимо иметь еще пару якорей, которые совпадают лишь в начале и конце текста. И такие якоря есть, их даже три.

Условие \A совпадает только в самом начале текста.Условие \Z совпадает только в самом конце текста и перед символом \n, который стоит в самом конце текста. (Например, на случай, если к этому тексту не применили функцию chomp).Условие \z совпадает исключительно в самом конце текста. Модификаторы на эти три якоря не действуют.

Еще имеется мнимый символ \b, который соответствует границе слова (word Boundary). Он соответствует позиции, с одной стороны которой находится буква, цифра или знак подчерка, а с другой стороны такого символа нет. Мнимый символ \B имеет противоположный смысл: он соответствует позиции внутри слова, т.е. с одной и другой стороны к нему примыкает буква, цифра или знак подчерка. Заметим, что эти якоря чувствительны к локальной установке (локали).

В регулярном выражении точка "." является метасимволом и соответствует одному любому символу за исключением символа новой строки \n. Но в зоне действия модификатора s точка соответствует одному любому символу. Такое различие бывает нужно, чтобы операция поиска не вышла за пределы логической строки.

В последней версии регулярных выражений появился метасимвол \C, который соответствует ровно одному байту независимо от того, является ли этот байт началом многобайтового символа или нет. Т.к. мы многобайтовые символы не рассматриваем, то можно в начале программы написать директиву

use bytes;

которая заставит Perl рассматривать символы как однобайтовые. Это, а также замена метасимвола точка на \C, даст некоторое ускорение работы регулярных выражений.



Обратные ссылки


В этом примере нас не интересовало, соответствует ли левый ограничитель у ссылки правому, но иногда подобные вещи нужно проверять. Например, кто-то по ошибке слева поставил апостроф, а справа - кавычку, или вообще забыл закрыть строку. Для подобных вещей в регулярных выражениях существуют обратные ссылки. Для каждой захватывающей пары скобок имеется метасимвол, который соответствует запомненному этой парой круглых скобок тексту. Для первой пары скобок это \1, для второй - \2 и т.д., для 99-й - \99. Обратные ссылки имеют смысл и значение только внутри регулярного выражения! За пределами оператора поиска и в части замены оператора s используйте нумерованные переменные.

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

Чтобы проверить, стоял ли ограничитель перед ссылкой, а если стоял, то соответствовал ли правый ограничитель левому, левый ограничитель надо захватить в скобки. Тогда ссылка окажется в нумерованной переменной $2, и оператор print надо исправить:

my $text='<a target="_blank" href="http://www.intuit.ru/">Internet-обучение</a>'; if ($text =~ m#<a\s+[^>]*?href\s*=\s*(["']?)([^"'> ]+)\1[^>]*>[^<]+</a>#i) { print $2 }

Теперь вместо второго подшаблона ["']? мы вставляем ссылку \1 на найденный текст. Если текст не найден (не было ограничителя), то \1 будет соответствовать пустому месту.

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

(["'])?

В чем была бы разница? В первом случае, когда знак вопроса стоит внутри захватывающих скобок, содержимое этих скобок обязательно (т.е. должно чему-то соответствовать в результирующей строке), а во втором случае, когда знак вопроса стоит за круглой скобкой, сами скобки являются необязательными, т.е. их содержимое может отсутствовать, и тогда \1 (и $1 тоже) получат неопределенное значение. Если ограничитель есть, то это не повлияет на работу оператора поиска. Но если ссылка не будет чем-то ограничена, поиск потерпит неудачу из-за того, что в шаблоне будет стоять обратная ссылка \1, которая ничему не будет соответствовать, потому что для нее нет соотнесенного фрагмента текста! Если же знак вопроса будет стоять внутри скобок:

(["']?)

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



Общее знакомство с регулярными выражениями


Если на компьютере у читателя еще не установлена система программирования Perl, то самое время это сделать. Дистрибутив Perl под Windows можно скачать с сайта www.activestate.com. Это все дается бесплатно. Поставка осуществляется в дистрибутиве MSI (MicroSoft Installer). Вы можете запустить его, найдя этот файл через "Мой компьютер" и дважды щелкнув на нем. Также можно использовать инсталлятор msiexec.exe, находящийся в подкаталоге system32 каталога Windows. Если запустить его без параметров, он в окне выдаст справку на русском языке.

Регулярные выражения обычно используются как операнды операторов поиска m/…/ и замены s/…/…/. Слово регулярные означает "составленные по правилам". То, что стоит вместо многоточия в операторе m и вместо первого многоточия в операторе s, - это и есть регулярное выражение. Буква m означает match (соответствие), а буква s означает search (поиск).

Предположим, в программе проверяется ввод пользователя, чтобы выяснить, хочет ли он завершить программу, введя слова stop, quit, exit или abort. Без регулярных выражений вам пришлось бы использовать ряд сравнений с этими образцами, предварительно преобразовав ввод к нижнему регистру. С оператором m эта проверка делается просто:

if ($input =~ m/stop|quit|exit|abort/i) { exit }

Вы можете также проверить, что пользователь кроме ключевого слова больше ничего не вводил. Для этого оператор m надо записать так:

if ($input =~ m/^(stop|quit|exit|abort)$/i) { exit }

Мы предполагаем, что ввод пользователя содержится в переменной $input. Символы "=~" надо рассматривать как единый символ, это оператор связывания переменной $input с данным оператором m. Этот оператор связывания возвратит число 1, если оператор поиска m найдет в $input текст, соотнесенный с шаблоном, иначе вернется пустая строка, которая в Perl трактуется как ложь. Поэтому оператор поиска удобно использовать в условных операторах и заголовках операторов цикла.

Символы "/" не принадлежат к регулярному выражению, а лишь ограничивают его подобно скобкам. Транслятор по ним определяет, где начинается и заканчивается регулярное выражение, которое может быть очень большим и сложным. Если символ-ограничитель используется в любом месте регулярного выражения, то он должен быть замаскирован обратным слэшем: \/. Это может порождать частокол из обратных и прямых слэшей внутри регулярного выражения, что часто встречается у новичков. Синаксис Perl позволяет выбирать в качестве ограничителей почти любые символы, кроме алфавитно-цифровых и пробельных: #, !, ,, :, % и т.д. Например, оператор

print ':' =~ m:abc\:def:;


напечатает единицу. Символы- ограничители лучше выбирать по возможности такими, которые не встречаются в регулярном выражении, чтобы не усложнять его вид. Также лучше не использовать символы "*, +, -, |, (, ), [, ], {, }", потому что в регулярном выражении они играют особую роль.

Символы-ограничители могут быть парными: это все виды скобок (), <>, [] и {}. Имеются в виду символы ASCII, т.к. существуют угловые скобки, не принадлежащие к 7-битным символам. В этом случае перед регулярным выражением ставится открывающая скобка, а после него - закрывающая.

Если в качестве ограничителей выступают слэши "/", то букву m можно не писать. Здесь еще следует добавить, что если ограничителями выступают вопросительные знаки, то букву m также можно не ставить, но эти ограничители включают довольно экзотический режим поиска, которые относится к "подозрительным" экспериментам и может не войти в будущие версии Perl.

Если в качестве целевого текста для оператора поиска или замены выступает переменная $_, то ее можно опустить вместе со связкой

=~: if (/stop|quit|exit|abort/i) { exit }

Буква i после завершителя регулярного выражения называется модификатором и включает режим поиска без учета регистра букв (case Insensitive). При этом шаблон /StOp/i будет соответствовать целевой строке 'stop', 'sTOp' и т.д.

Понятие буквы зависит от локальных установок среды выполнения программы. В русской Windows к буквам также будут относиться все русские буквы в кодировке Windows. Во французской Windows результаты будут другими. Существуют специальные директивы и операторы установки локали (use locale и setlocale), но аргумент setlocale может отличаться для разных операционных систем. Как установить локаль в Perl на сервере, надо уточнять у его администратора.


Оператор замены


Кратко рассмотрим оператор замены s. Он отличается от оператора поиска тем, что за регулярным выражением для поиска имеется выражение для замены найденного:

s/…/…/

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

Если регулярное выражение для поиска заключено в парные ограничители (скобки), то операнд замены заключается в собственные ограничители. Тогда между ограничителями регулярного выражения и замены могут стоять пробельные символы. Вот примеры:

s<…>'…' s(…) {…}

Если ограничителями операнда замены выступают апострофы, то выражение для замены рассматривается как строка, заключенная в апострофы, и интерполяция переменных при замене не происходит.

Оператор s может иметь модификатор g (Global). (Впрочем, оператор m также может иметь этот модификатор, но об этом речь пойдет дальше.) В этом случае поиск и замена продолжаются как бы в цикле с того места, где закончилась предыдущая замена текста, и до тех пор, пока поиск находит фрагменты, соответствующие регулярному выражению.

Как результат, оператор замены возвращает число сделанных замен или пустую строку, если замен не было. Если целевой строкой выступает переменная $_, то ее и связку =~ писать не обязательно. Пример:

$_='aabbaa'; print s/a/c/g."\n".$_;

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

4 ccbbcc

Вот пример на использование нумерованных переменных:

$_='aa bb aa'; s/(\w+)(?:\s+\w+){2}/$1 $1/; print;

Получаем вывод:

aa aa

Куда делась третья группа букв? Она была удалена, т.к. оператор s заменяет всю часть текста, что соответствует шаблону поиска. Если бы мы хотели оставить третью группу букв нетронутой, то ее не надо было бы включать в поиск:

$_='aa bb aa'; s/(\w+)\s+\w+/$1 $1/; print;

Получаем вывод:

aa aa aa

А если в поиск включается что-то, что должно остаться без изменений, то это надо взять в захватывающие скобки и в операнде замены на соответствующем месте поставить нужную нумерованную переменную.



Захватывающие и незахватывающие скобки


Поиск соответствия давал бы мало пользы, если бы нельзя было извлекать из текста интересующие нас фрагменты. Для извлечения фрагмента текста часть шаблона (или весь шаблон), который ему соответствует, надо заключить в круглые скобки. Всего в регулярном выражении может быть 99 захватывающих пар скобок. Такие скобки также называют сохраняющими. Если вы не хотите сохранять часть текста, а только группируете подшаблоны, то для этого существуют обычные скобки, которые не сохраняют текст; таких скобок в регулярном выражении может быть 200 пар. Чтобы сделать пару скобок обычной (несохраняющей), надо сразу после открывающей скобки поставить вопросительный знак и двоеточие. Сохраняющие и несохраняющие скобки могут иметь какой угодно уровень вложенности. Сохраняющие скобки нумеруются в порядке появления открывающей скобки от 1 до 99, чтобы за пределами оператора поиска иметь сохраненными нужные фрагменты текста. Текст, сопоставленный подшаблону в первой паре захватывающих скобок, окажется в специальной переменной $1, сопоставленный второй паре захватывающих скобок - в переменной $2 и т.д. до $99.

Разумеется, не обязательно иметь 99 пар скобок, - незадействованные специальные переменные будут иметь неопределенное значение. Обратите внимание, что нумерация начинается не с нуля и что перемнная $0 не имеет отношения к регулярным выражениям, а хранит имя файла выполняемого сценария.

Рассмотрим такой пример: пусть в переменной $text хранится текст для тега a

<a href="http://www.intuit.ru/">Internet-обучение</a>

Нам надо написать регулярное выражение, которое соответствует тегу a и извлекает из него ссылку. Можно написать так:

$text =~ m#<a href="([^"]+)">[^<]+</a>#; print $1;

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

http://www.intuit.ru/

Сначала в регулярном выражении идет литеральный текст

<a href="


который поглотит все, что будет стоять до этой скобки.

Это хорошо, но ведь параметры тега a ключевые, а не позиционные, и параметр href не обязан стоять первым. Тег может быть оформлен так:

<a target="_blank" href="http://www.intuit.ru/">Internet-обучение</a>

В этом случае наш оператор поиска не найдет соответствия. Надо пропускать символы, пока не встретится href. Это можно сделать с помощью конструкции

.*?

и не забыть поставить модификатор s, потому что тег может располагаться на нескольких строках (после target="_blank" может быть перевод строки), а метасимвол "точка" без этого модификатора не соответствует символу перевода строки (new line).

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

[^>]*?

Теперь модификатор s можно не ставить.

Еще я советую использовать директиву

use strict;

чтобы транслятор проверял, все ли переменные определены, и параметр w для выдачи предупреждающих сообщений транслятора.

Если вы запускаете скрипт на Web-сервере из браузера, то вставьте также директиву

use CGI::Carp qw(fatalsToBrowser);

чтобы Perl выводил ошибки в браузер, иначе вы будете долго гадать, почему скрипт не работает.

Вот законченная программа, которая "железобетонно" выводит ссылку из тега a:

#!/usr/bin/perl -w use strict;

my $text='<a target="_blank" href="http://www.intuit.ru/">Internet-обучение</a>'; if ($text =~ m#<a\s+[^>]*?href\s*=\s*["']?([^"'> ]+)["']?[^>]*>[^<]+</a>#i) { print $1 }

Если вы будете запускать Perl-программу на сервере Unix, то запоминайте текст в файл без символов возврата каретки \r. В редакторе Far это можно сделать по клавишам <Shift>+<F2>. Если вывод скрипта будет направлен веб-серверу и от него браузеру, то перед первым выводом (оператором print) должна идти команда

print "Content-Type: text/html\n\n";

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



который поглотит все, что будет стоять до этой скобки.

Это хорошо, но ведь параметры тега a ключевые, а не позиционные, и параметр href не обязан стоять первым. Тег может быть оформлен так:

<a target="_blank" href="http://www.intuit.ru/">Internet-обучение</a>

В этом случае наш оператор поиска не найдет соответствия. Надо пропускать символы, пока не встретится href. Это можно сделать с помощью конструкции

.*?

и не забыть поставить модификатор s, потому что тег может располагаться на нескольких строках (после target="_blank" может быть перевод строки), а метасимвол "точка" без этого модификатора не соответствует символу перевода строки (new line).

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

[^>]*?

Теперь модификатор s можно не ставить.

Еще я советую использовать директиву

use strict;

чтобы транслятор проверял, все ли переменные определены, и параметр w для выдачи предупреждающих сообщений транслятора.

Если вы запускаете скрипт на Web-сервере из браузера, то вставьте также директиву

use CGI::Carp qw(fatalsToBrowser);

чтобы Perl выводил ошибки в браузер, иначе вы будете долго гадать, почему скрипт не работает.

Вот законченная программа, которая "железобетонно" выводит ссылку из тега a:

#!/usr/bin/perl -w use strict;

my $text='<a target="_blank" href="http://www.intuit.ru/">Internet-обучение</a>'; if ($text =~ m#<a\s+[^>]*?href\s*=\s*["']?([^"'> ]+)["']?[^>]*>[^<]+</a>#i) { print $1 }

Если вы будете запускать Perl-программу на сервере Unix, то запоминайте текст в файл без символов возврата каретки \r. В редакторе Far это можно сделать по клавишам <Shift>+<F2>. Если вывод скрипта будет направлен веб-серверу и от него браузеру, то перед первым выводом (оператором print) должна идти команда

print "Content-Type: text/html\n\n";

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


Атомарная группировка


Иногда нужно, чтобы для какого-то квантификатора не создавались сохраненные состояния. Например, при извлечении ссылки вокруг знака равенства могли быть пробельные символы, и мы применяли подшаблон

\s*=\s*

Представим, что вокруг знака равенства стояли бы пробелы и впоследствии ссылка не была бы найдена. Тогда произошел бы возврат и квантификатор \s* отдал бы один пробел для продолжения поиска. Но мы-то знаем, что это не повлияет на нахождение ссылки, так зачем проделывать ненужную работу? Для уничтожения сохраненных состояний подшаблон \s* надо заключить в специальную скобочную конструкцию:

(?>\s*)

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

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

<a(?>\s+) (?>[^>]*) и т.д.

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

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

(?>\w*)c

и строку

abc

Как вы уже догадались, совпадения не будет найдено. Квантификатор * захватит все три символа, а после выхода за скобки сохраненные состояния будут уничтожены, поэтому этот квантификатор ни за что не отдаст захваченное. Будут перепробованы итерации со второго, третьего символа и с конца строки, но все будет безрезультатно. Без атомарной группировки совпадение было бы найдено при первой итерации с начала строки.

А что будет в случае минимального квантификатора? Возьмем шаблон

(?>\w*?)c


и строку

abc

Совпадение будет найдено, но только при третьей итерации. \w* захватит 0 символов, а литерал c совпадет с символом c. Если в "жадном" режиме квантификатор внутри атомарной группировки захватывает все и не отдает, то в минимальном режиме он берет минимум возможного и ничего больше.

Если бы мы вынесли квантификатор за пределы атомарной группировки:

(?>\w)*c (?>\w)*?c

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

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

23.34000012 23.345000023 34.4000025 456.00

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

23.34 23.345 34.40 456.00

Округление мы будем делать оператором s/…/…/. Поставим еще условие, что он не должен ничего менять, если число уже имеет правильный вид. Тогда получалась бы замена цифр самих на себя.

Вначале нас интересует десятичная точка: \., затем - две обязательных цифры: \d\d. За ними может идти цифра от 1 до 9, а может и не идти: [1-9]?. Все это мы возьмем в захватывающие скобки, чтобы этим заменить все от точки до конца числа. А после еще могут идти цифры: \d*, они будут удалены. Оператор получается такой:

s/(\.\d\d[1-9]?)\d*/$1/;

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

$_='23.345'; print s/(\.\d\d[1-9]?)\d*/$1/."\n"; print $_;

Он выведет

1 23.345

Напомню, что оператор s возвращает число успешно сделанных замен. Вот, как это происходит: подшаблон

(\.\d\d[1-9]?)

совпадает с

.345

Далее цифр нет, поэтому \d* совпадает с пустой подстрокой, и т.к. совпадение найдено, то замена срабатывает. А она должна срабатывать только, если после .345 есть цифра. Давайте для этого попробуем заменить \d* на \d+ и посмотрим, что получится. А получается результат

1 23.34

Третья цифра обрезается. Это происходит потому, что теперь \d+ совпасть не с чем. Но ведь у регулярного выражения в запасе есть сохраненное состояние в подшаблоне [1-9]?. Происходит возврат внутрь скобок к этому подшаблону, и для него пробуется состояние, когда квантификатор равен нулю. В этом случае скобки совпадают с .34, а \d+ совпадает с третьей цифрой 5, поэтому пятерка из результата удаляется. Нам нужно, чтобы при совпадении подшаблона [1-9]? с третьей цифрой это совпадение не отменялось. Но для этого надо удалить данное сохраненное состояние, т.е. заключить подшаблон [1-9]? в атомарные скобки:

s/(\.\d\d(?>[1-9]?))\d+/$1/;

Теперь пример работает правильно.



Третья цифра обрезается. Это происходит потому, что теперь \d+ совпасть не с чем. Но ведь у регулярного выражения в запасе есть сохраненное состояние в подшаблоне [1-9]?. Происходит возврат внутрь скобок к этому подшаблону, и для него пробуется состояние, когда квантификатор равен нулю. В этом случае скобки совпадают с .34, а \d+ совпадает с третьей цифрой 5, поэтому пятерка из результата удаляется. Нам нужно, чтобы при совпадении подшаблона [1-9]? с третьей цифрой это совпадение не отменялось. Но для этого надо удалить данное сохраненное состояние, т.е. заключить подшаблон [1-9]? в атомарные скобки:

s/(\.\d\d(?>[1-9]?))\d+/$1/;

Теперь пример работает правильно.

© 2003-2007 INTUIT.ru. Все права защищены.

Механизм работы регулярных выражений Поиск с возвратами


Рассмотрим еще один пример. Пусть мы имеем шаблон

abcd|abc|ab

и строку

ab

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

В нашем примере сначала будет попробован подшаблон abcd, эта проба закончится неудачей. Также неудачей закончится проба следующего подшаблона abc. И только последний подшаблон ab совпадет. Если бы после этой конструкции выбора шаблон продолжался и следующий за ней подшаблон не совпал, то в общем случае произошел бы возврат к этой конструкции выбора и в дело пошел бы следующий за ab подшаблон (если бы он существовал). В этом примере ab - последний подшаблон, поэтому в общем случае возврат продолжился бы еще левее, за конструкцию выбора и так далее до самого начала всего шаблона. И только в случае, когда при всевозможных значениях квантификаторов и номеров подшаблонов в конструкциях выбора совпадение не было бы найдено, произошло бы продвижение начальной позиции поиска в тексте на один символ.

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

(ab|abc)\w*



Текущая позиция поиска


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

a*

может совпасть со строкой

aaab

с первой, второй и третьей буквы, но при нахождении совпадения учитывается лишь более ранняя находка, поэтому этот шаблон будет соответствовать всем трем буквам "a" с начала строки. После того, как это совпадение будет найдено, механизм поиска совпадения закончит работу. (Но в случае модификатора g работа будет продолжена, что мы увидим при рассмотрении примеров работы этого модификатора.)

Если с текущей начальной позиции в тексте совпадение не будет обнаружено, то в дело вступает механизм смещения текущей позиции поиска: состояние шаблона и позиция поиска в шаблоне будут сброшены в начальное состояние, а текущая позиция в тексте будет продвинута на один символ. После этого начнется новая итерация поиска совпадения. (Но если шаблон привязан к началу текста якорем \A или ^, то в случае неудачи второй итерации не будет, ведь в этом случае шаблон должен совпасть только в начале текста и нигде больше.) Такие итерации будут повторяться, пока начальная позиция в тексте не выйдет за конец этого текста. В этом случае поиск закончится неудачей. Поиск заканчивается удачей, когда в процессе его работы текущая позиция в шабл оне выходит за пределы шаблона.

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



Возвраты и сохраненные состояния


Линейный алгоритм поиска не всегда обеспечивает нахождение совпадения. Рассмотрим такой пример: пусть мы имеем шаблон

\w+bb

и строку

aaabb

При сопоставлении этой строки шаблону модификатор + захватит все символы, и на долю подшаблона bb ничего не останется. Но после этой неудачи не произойдет сразу итерации поиска со следующего символа, т.к. в совпавшей части шаблона имеются квантификаторы, у которых может быть разное число повторов. В этом случае произойдет возврат к последнему подшаблону с таким квантификатором и к позиции в тексте, с которой он совпадал. Но для таких возвратов надо для каждого пройденного подшаблона с квантификатором запоминать значения этого квантификатора и позицию в тексте, с которой совпал этот подшаблон. В нашем случае вначале для подшаблона \w+ будет запомнено 5 состояний: что он захватил с начала текста a, aa, aaa, aaab и aaabb.

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

Т.к. для литерала b в шаблоне не находится соответствия, "жадность" квантификатора будет уменьшена на один символ, и мы будем иметь совпадение подшаблона \w+ с подстрокой aaab, далее подшаблон b совпадет с символом b, а на следующий подшаблон b ничего не останется. На самом деле, литералы сравниваются не по одному символу, а по целому литералу (если не указан модификатор i), а пример выше я привел для наглядности рассуждений.

Лишь при втором уменьшении "жадности" квантификатора + будет найдено совпадение для всего шаблона:

\w+ совпадет с aaa,

подшаблон bb совпадет с bb.

Если бы текст на этом не заканчивался, то все равно было бы зафиксировано совпадение и оператор поиска вернул бы истину. Ведь шаблон поиска не привязан к концу строки якорями $, \z или \Z. Если бы это имело место, то поиск мог бы закончиться успехом только тогда, когда шаблон и текст исчерпываются одновременно. Точнее говоря, после исчерпания шаблона, оканчивающегося на $ или \Z, в тексте может еще остаться единственный символ новой строки \n.

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

Разумеется, квантификаторы вида a{20} не могут порождать возвратов, это просто иная запись литерала.

Что будет в случае с минимальным квантификатором? В этом случае он начинает захват с наименьшего возможного значения квантификатора (от нуля и далее). А механизм поиска кроме запоминания текущей позиции совпадения для каждого такого подшаблона и его значения, конечно, "знает" о том, какой вид имеет этот квантификатор: минимальный или максимальный.

Если рассмотреть пример шаблона

\w{1,3}?b

и строку

aaaabb

то совпадение с первого символа строки вообще не будет найдено ни при каких значениях квантификатора. Оно будет найдено со второго символа строки и после того, как будут перепробованы значения квантификатора 1, 2 и 3. В результате получим совпадение подшаблона \w{1,3}? с подстрокой aaa, и затем подшаблон b совпадет с символом b. На этом поиск совпадения успешно закончится.


и строку

aaaabb

то совпадение с первого символа строки вообще не будет найдено ни при каких значениях квантификатора. Оно будет найдено со второго символа строки и после того, как будут перепробованы значения квантификатора 1, 2 и 3. В результате получим совпадение подшаблона \w{1,3}? с подстрокой aaa, и затем подшаблон b совпадет с символом b. На этом поиск совпадения успешно закончится.

Рассмотрим еще один пример. Пусть мы имеем шаблон

abcd|abc|ab

и строку

ab

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

В нашем примере сначала будет попробован подшаблон abcd, эта проба закончится неудачей. Также неудачей закончится проба следующего подшаблона abc. И только последний подшаблон ab совпадет. Если бы после этой конструкции выбора шаблон продолжался и следующий за ней подшаблон не совпал, то в общем случае произошел бы возврат к этой конструкции выбора и в дело пошел бы следующий за ab подшаблон (если бы он существовал). В этом примере ab - последний подшаблон, поэтому в общем случае возврат продолжился бы еще левее, за конструкцию выбора и так далее до самого начала всего шаблона. И только в случае, когда при всевозможных значениях квантификаторов и номеров подшаблонов в конструкциях выбора совпадение не было бы найдено, произошло бы продвижение начальной позиции поиска в тексте на один символ.

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

(ab|abc)\w*

и строка

abc

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

.*|abc



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

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

Такой алгоритм поиска называется недетерминированным конечным автоматом (НКА). Этот алгоритм управляется шаблоном, и программист может указать, какое именно совпадение ему нужно. Существуют другие алгоритмы поиска (детерминированный конечный автомат ДКА или смешанные алгоритмы), но в языке Perl и других языках и библиотеках (PHP, PCRE) применяется НКА, хотя он не самый быстрый среди алгоритмов поиска. Программисту, привыкшему к алгоритму НКА, другой язык программирования, который использует другой алгоритм поиска, может преподнести сюрприз. Я уже не говорю о том, что в других языках некоторые метасимволы могут работать иначе, чем в Perl.

Рассмотрим еще примеры:

$_='abcd'; /(\w+)(\w+)/; print "$1|$2";

Сначала первая пара скобок захватит всю строку, но во имя нахождения совпадения для всего шаблона пожертвует одним символом для второй скобки. В результате будет напечатано abc|d. Если во второй паре скобок вместо плюса стояла бы звездочка или вопросительный знак, то на их долю вообще не осталось бы символов - все их поглотил бы квантификатор из первой пары скобок, и переменная $2 получила бы пустое значение. Вот она, "жадность" в действии. Каждый "жадный" квантификатор оставляет остальной части шаблона минимум символов, лишь бы нашлось совпадение.

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

Возьмем шаблон

12?3



Чтобы избежать этой ошибки, нужно директивой

use re 'eval';

разрешить вставку кода Perl внутрь шаблонов.

Этот код у нас распечатывает текущую позицию поиска pos в строке $_ (позиция отсчитывается от нуля) и значение переменной $1.

Вначале мы видим строку

Starting from 0

Она говорит о старте очередной итерации поиска. Эта итерация в нашем примере единственная. Далее происходит сопоставление \w и символа a, а затем - печать текущей позиции поиска (2) и значения переменной $1. Но т.к. эта печать происходит еще до закрытия захватывающей скобки, когда переменная $1 еще не появилась, то возникает предупреждающее сообщение об использовании неопределенной переменной. Конечно, его можно было бы избежать, если написать

print "$1\n" if defined $1;

Но я не стал излишне загромождать код.

Затем скобки захватывают символ a, и переменная $1 приобретает значение $1='a', а квантификатор * заставляет вернуться за закрывающую скобку и повторить поиск, начиная от \w. Теперь \w соответствует символу b, а распечатывается текущая позиция 2 и текущее значение $1, которое равно a. Затем повторяется тот же самый цикл, который печатает очередные текущие позиции и захваченные символы. После того, как подшаблон \w совпадает с d, печатается последняя позиция этой строки 4 и текущее значение $1, которое равно c. Значение d не вышло на печать, т.к. печать происходит до закрытия захватывающей скобки. А если бы мы вставили печать после нее или следующим оператором за оператором поиска, то увидели бы и значение d.

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


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


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

Как работают группирующие скобки с квантификаторами? Например, что захватят в переменную $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. Этот не описанный в руководствах способ работает потому, что интерполяция переменных в регулярном выражении происходит на ранней стадии обработки, что открывает большие возможности для комбинаций: ведь содержимое этих интерполируемых переменных можно менять от одного обращения к данному оператору поиска/замены - к другому. Например, в зависимости от предыдущих вычислений можно добавить пару захватывающих скобок или изменить квантификатор.


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


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

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

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

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

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.



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


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

(?= шаблон )

Шаблон может быть как угодно сложным. Это условие истинно, если в текущей позиции находится текст, совпадающий с заданным шаблоном. Обратите внимание, что эти условные конструкции совпадают не с текстом, а с позицией в тексте подобно своим более простым собратьям \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, как и в момент открытия захватывающей пары скобок. В результата эта пара скобок захватывает пустой фрагмент текста.



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


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

(?<= шаблон )

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

$_='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])/;

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


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


В регулярных выражениях 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 }


Обратите внимаение, что при появлении квантификатора x изменились ограничители регулярного выражения (c # на !) и внутри класса символов пробел задан как \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 не запрещает присваивать этой переменной значение напрямую.


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


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

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

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

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

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

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

(?{ код Perl })

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



Задание модификаторов внутри регулярного выражения


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

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

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

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



Алгоритм работы операторов m// и s///


В языке Perl есть такое понятие - контекст выражения. Операторы могут возвращать значение, которое зависит от того, чему оно присваивается. Если оно присвавается скалярной переменной, то возвращаемое значение может быть одно, а если присвоение происходит массиву, то возвращаться может уже список значений. Таким является оператор m//. Например, если мы используем его в условии условного оператора:

if (m/…/) { … }

то это скалярный контекст, т.к. в условии ожидается одно значение, которое трактуется как истина или ложь. А если написать

my @m=m/…/;

то это списковый контекст, в котором оператор m// возвратит список значений. Оператор print является списковым, т.к. он ожидает список в качестве своего аргумента, поэтому, если написать

print m/…/;

то это также будет использование оператора поиска в списковом контексте.



Модификаторы операторов m// и s///


Всего в регулярных выражениях используется восемь модификаторов.

i - игнорирует различие между заглавными и строчными буквами. На этот модификатор влияет установка локали.s - метасимвол "точка" совпадает со всеми символами, включая символ новой строки \n.m - разрешает привязку метасимволам ^ и $ к промежуточным символам \n в тексте. В этом случае метасимвол ^ совпадает не только в начале текста, как якорь \A, но и после каждого символа \n, который не стоит в самом конце текста. А метасимвол $ начинает совпадать не только в самом конце текста и перед \n, стоящим в самом конце текста, как якорь \Z, но также после каждого символа \n, который не стоит в самом конце текста.x - игнорирует пробельные символы в регулярном выражении, разрешает использовать внутри шаблона комментарии.g - поиск и замена выполняются глобально (в неявном цикле). Подробности мы рассмотрим.c - работает только с модификатором g и только с оператором m//, который применяется в скалярном контексте. Запрещает сбрасывать текущую позицию поиска, когда не удалось найти очередного совпадения. Подробности будут далее.o - шаблон с этим модификатором однократной компиляции транслируется один раз во время компиляции всей программы. Здесь дело в том, что внутри регулярного выражения могут встречаться переменные, которые будут интерполироваться, т.е. их значения будут подставляться в шаблон в качестве литералов. (Не путайте эти переменные с теми, которые присутствуют в исполняемом коде Perl, такие переменные не интерполируются.) Если интерпретатор видит, что происходит интерполяция переменных, то он каждый раз перед использованием этого оператора поиска/замены будет транслировать регулярное выражение во внутреннее представление (байт код). Это может отнять много времени, если такое выражение используется в цикле. Но

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



Оператор m// в режиме однократного поиска и в скалярном контексте


В режиме однократного поиска (т.е. без модификатора g) и в скалярном контексте оператор m// возвращает логическое значение: целое число 1, если поиск оказался успешным, или пустую строку в случае неудачи. Если в шаблоне имелись захватывающие скобки, то при успешном поиске создаются нумерованные переменные $1, $2, …, которые содержат соответствующие фрагменты захваченного текста. Если поиск оказался неудачным, то эти переменные хранят последнее состояние от предыдущего оператора поиска или замены.

После успешного поиска также можно использовать специальные переменые:

$& - копия текста, совпавшего со всем регулярным выражением. Если вы заключите все регулярное выражение в круглые скобки: m/(…)/, то в случае успеха в переменной $1 получите то же самое значение.$` - копия текста, предшествующего началу совпадения (т.е. текст, который был слева от совпадения).$' - копия текста, следующего после совпадения (т.е. расположенного справа от совпавшего текста). После успешного совпадения текст "$`$&$'" представляет из себя копию исходного текста. Есть малозначительное исключение: если оператор m// был успешно применен к неопределенной переменной, то эти специальные переменные будут иметь пустые значения.$+ - копия переменной $1 или $2… с максимальным номером, которой было присвоено значение. Если в регулярном выражении нет сохраняющих скобок или они не задействованы в совпадении, то эта переменная получает неопределенное значение.$^N - копия переменной $1 или $2…, которая соответствует последней только что закрытой паре скобок на момент использования этой переменной. Если в регулярном выражении нет сохраняющих скобок или они не задействованы в совпадении, то эта переменная получает неопределенное значение.@ - массив начальных смещений в целевом тексте. $-[0] хранит смещение начала совпадения от начала текста для всего регулярного выражения. $-[1] хранит смещение начала совпадения от начала текста для фрагмента текста, захваченного в переменную $1, $-[2] - для переменной $2 и т.д.@+ - аналогичный массив конечных смещений в целевом тексте. Т.е. смещений символов, следующих за конечными символами совпадения во всем тексте, в переменной $1, $2 и т.д. Имеет место соотношение substr($text,$-[0],$+[0]-$-[0]) эквивалентно $&, substr($text,$-[1],$+[1]-$-[1]) эквивалентно $1 и т.д.


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

Начинающие часто путают специальные переменные $+ и $^N, между которыми имеется тонкое различие. Сначала рассмотрим пример использования переменной $+. Пусть в регулярном выражении мы имеем альтернативную конструкцию, альтернативы которой заключены в захватывающие скобки:

$text =~ /(…)|(…)|(…)/;

Мы знаем, что совпадение может быть не более чем с одной альтернативой, и хотим получить фрагмент текста, совпавшего с какой-либо альтернативой. Не будь переменной $+, нам пришлось бы перебирать переменные $1, $2,… и определять, какая из них имеет определенное значение.

Для понимания отличия переменной $+ от $^N рассмотрим такой пример:

print "\$+=$+, \$^N=$^N" if "abc" =~ /(a(bc))/;

На печати окажется строка

$+=bc, $^N=abc

Различие происходит от того, что скобки могут быть вложенными. Нумерованная переменная с наибольшим номером здесь $2, а последними закрываются скобки у переменной $1.

Использовать значения переменных $+ и $^N можно также в коде Perl внутри регулярного выражения. Эти переменные корректируются после того, как закроется очередная захватывающая скобка. Например:

print "\$+=$+, \$^N=$^N" if "abcd" =~ /(a(bc)(d)(?{ print "\$+=$+, \$^N=$^N\n" }))/;

Здесь выводятся значения переменных $+ и $^N после третьей скобки внутри регулярного выражения, а также после того, как оператор поиска отработал. В итоге получается результат:

$+=d, $^N=d $+=d, $^N=abcd

$+ в обоих случаях относится к нумерованной переменной с максимальным номером - $3, а переменная $^N внутри регулярного выражения копирует переменную $3, а вне регулярного выражения является копией переменной $1.


Оператор m// в скалярном контексте с модификатором g


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

Вот пример:

$_='1234567890'; /(\d)(\d)/g; print "$1,$2\n"; my @a=/(\d)(\d)/g; print join ',',@a;

На печать выйдут строки

1,2 3,4,5,6,7,8,9,0

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

В случае применения оператора поиска к одной и той же константе:

while ('abcd'=~/(\w)/g) { print $1 }

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

abcd



Оператор m// в списковом контексте без модификатора g


Рассмотрим работу оператора m// в списковом контексте в режиме однократного поиска (т.е без модификатора g). Возвращаемое значение зависит от того, есть ли в шаблоне захватывающие скобки. Если есть хотя бы одна пара захватывающих скобок, то в результате будет возвращен список из значений всех нумерованных переменных по числу использованных в регулярном выражении захватывающих скобок: ($1, $2, $3, …).

Сами эти нумерованные переменные также создаются. В этом варианте оператор m// обычно применяется, когда надо извлечь из строки заранее известное число групп переменная=значение. Например:

my $url='var1=value1&var2=value2'; my @pairs=$url =~ /(\w+)=(\w+)&(\w+)=(\w+)/; print join ',',@pairs;

На печать выйдет строка

var1,value1,var2,value2

Если в шаблоне нет захватывающих скобок, то в случае успешного поиска возвращается список из одного элемента - числа 1.

В случае неудачного поиска в обоих случаях возвращается пустой список.

Вот пример идиомы для применения оператора m// в списковом контексте:

my $date='2007/3/12'; if (my($year,$month,$day)=$date =~ m!^(\d+)/(\d+)/(\d+)$!) { print "Year=$1, month=$2, day=$3"; } else { print 'Not found!'; }

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

Year=2007, month=3, day=12

Мы присваиваем вовращаемое значение списку, поэтому применяется списковый контекст оператора поиска. Затем в операторе if этот список рассматривается в скалярном контексте, что дает число элементов этого списка. Полученный результат используется для проверки успешности выполнения оператора поиска.



Оператор m// в списковом контексте с модификатором g


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

Пример:

my $text='123 234 345 456'; my @m=$text =~ /(\d+)(\s+)/g; print @m;

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

123 234 345 456

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

my $text='123 234 345 456'; my @m=$text =~ /\d+/g; print join ',',@m;

Будет напечатано

123,234,345,456



Предварительная обработка регулярных выражений


Регулярные выражения в операторах m// и s/// обрабатываются особым образом. Вообще говоря, они "обрабатываются как строка в кавычках с учетом специфики регулярных выражений". Но если в операторе поиска/подстановки регулярное выражение ограничено апострофами, то такое регулярное выражение, как обычно пишут, "обрабатывается как строка в апострофах", т.е. интерполяция переменных в нем не производится. Здесь надо уточнить, что такие эскейп-последовательности, как \r, \n, \t, все равно работают в регулярном выражении, которое ограничено апострофами, хотя в строке, ограниченной апострофами, они не работают. В этом - специфика регулярных выражений.

В части замены оператора s/// действует то же правило, за тем исключением, когда там стоит код Perl (есть модификатор e). В строке, ограниченной апострофами, имеются только два метасимвола: апостроф и обратный слэш. Здесь замечу, что в строке замены, которая ограничена апострофами, эскейп-последовательности \r, \n, \t, … не работают. В случае, если регулярное выражение не ограничено апострофами, в нем символы $ и @ являются метасимволами. Но символ $ используется как якорь для конца строки, не получается ли здесь конфликта? Этот вопрос мы рассмотрим позднее.

Если скалярная переменная просто заменяется своим значением, то массив интерполируется всеми своими значениями или срезом своих элементов, который тоже может быть задан. Разделителем между элементами массива служит значение специальной переменной $", которая по умолчанию содержит пробел. Хеши в регулярное выражение не интерполируются, т.к. это не имеет смысла, поэтому символ % в шаблонах не является метасимволом.

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

$_='abc'; my $s='c'; print '1' if /^ab$s$/;

Будет напечатана единица. После интерполяции регулярное выражение станет таким:

/^abс$/

Другой пример:

$_='abc'; my $s='ab'; print /[$s]/g;

Напечатается ab. Это регулярное выражение эквивалентно такому:

/[ab]/g

Класс совпал два раза и захватил на первой итерации букву a, а на второй - букву b.

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

После интерполяции переменных происходит обработка конструкций изменения регистра символов /L, /l, /U, /u, а также конструкций вставки литерального текста \Q…\E.

Полученный результат передается механизму регулярных выражений для интерпретации.


Класс совпал два раза и захватил на первой итерации букву a, а на второй - букву b.

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

После интерполяции переменных происходит обработка конструкций изменения регистра символов /L, /l, /U, /u, а также конструкций вставки литерального текста \Q…\E.

Полученный результат передается механизму регулярных выражений для интерпретации.

© 2003-2007 INTUIT.ru. Все права защищены.

Работа оператор s/// с модификатором g и без него


Оператор замены s/// без модификатора g ищет в заданной переменной прототип регулярного выражения и в случае нахождения заменяет его на выражение для замены, после чего заканчивает свою работу. При наличии модификатора g замена производится столько раз, сколько раз был найден фрагмент текста, соответствующий регулярному выражению. В результате все найденные фрагменты будут заменены на заданное выражение. В результате возвращается число произведенных замен или пустая строка (или соответствующий список в случае спискового контекста). Например:

my $text='123 234 345 456'; $text =~ s/\d+/a/; print $text;

Получим строку

a 234 345 456

Еще пример:

my $text='123 234 345 456'; $text =~ s/\d+/reverse $&/ge; print $text;

Здесь получим такой результат:

321 432 543 654

В последнем операторе поиска и замены мы использовали режим замены с модификатором e (с выполнением кода Perl). В результате каждое найденное число заменилось на это же число, записанное в обратном порядке. Также мы могли бы написать:

$text =~ s/(\d+)/reverse $1/ge;

В операторах m// и s/// специальные переменные $1, $2, …, $&, $`, $' и т.д. для каждой итерации, обусловленной модификатором g, создаются заново (локализуются). В результате при замене используются нужные значение этих переменных, которые возникли при последней итерации поиска.



Как укоротить длинные URL и длинные слова?


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

Рекомендую посетить: http://www.lsafhwoeufqfsjvdkghalhasdghasglhasgl.com/fasfasfasdf/

На самом деле ссылка может оказаться еще длинее. В HTML-коде страницы форума эта ссылка должна появиться в виде URL:

Рекомендую посетить: <a href="http://www.lsafhwoeufqfsjvdkghalhasdghasglhasgl.com/fasfasfasdf/" target="_blank"> http://www.lsafhwoeufqfsjvdkghalhasdghasglhasgl.com/fasfasfasdf/</a>

Внутри тега a ссылку, естественно, обрезать не надо, тем более, что в видимом тексте ее не будет, а вот то, что стоит перед тегом </a>, будет видно, и если это "слово" будет слишком длинным, то это может испортить дизайн. Надо после 50-го символа такой ссылки вставить три точки и получить такой текст:

Рекомендую посетить: <a href="http://www.lsafhwoeufqfsjvdkghalhasdghasglhasgl.com/fasfasfasdf/" target="_blank"> http://www.lsafhwoeufqfsjvdkghalhasdghasglhasgl.co…</a>

Другие длинные слова (не ссылки) надо также урезать. Мало ли что может ввести пользователь… Я нашел такое решение:

s/(\A|\s)(?!href=")(\S{50})\S+/$1$2.../g;

Предполагаем, что текст находится в переменной $_. Регулярное выражение начинает поиск от начала текста и каждого пробельного символа. Если после этого не стоит фрагмент href=, то проверяется, есть ли после этого "слово" длиной 50 символов, после которого стоит непробельный символ. Если да, то происходит замена всего, что найдено на $1 - пробельный символ перед длинным "словом", 50 символов с начала этого "слова" на $1, эти же 50 символов с начала длинного "слова", а последующие непробельные символы заменяются на три точки. Если бы в начале шаблона вместо (\A|\s) стояло просто (\s), то длинное "слово", стоящее в самом начале текста, не было бы укорочено.

Вот еще один вариант с использованием кода Perl в части замены:

s/((\S{50})\S+)/index($1,'href=') > -1 ? $1 : "$2..."/ge;


Ищем 50 непробельных символов, сразу за которыми есть хотя бы один непробельный символ. Эти 50 символов берем в переменную $2, а все длинное "слово" берем в переменную $1. В части замены проверяем, есть ли фрагмент текста href= в перемнной $1.

Если он есть, то заменяем найденное на $1, т.е. на себя, а если нет, то заменяем найденное на текст $2, за которым идут три точки.

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

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

$_=' <pppp>' x 13000; $_.='<table>a'; if (/[^\s<>](?![^<>]*>)/g) { print "1\n".pos; } else { print "0\n"; }

Вначале мы создаем длинную строку (больше 90000 символов) из тегов, в конец которой вставляем букву a вне тегов. В регулярном выражении мы ищем символ, отличный от пробельного и символов < и >, который не находится внутри тега. "Не находится внутри тега" означает, что сразу после этого символа не должно быть текста, соответствующего шаблону [^<>]*>. В случае нахождения такого символа программа печатает единицу и смещение этого символа от начала текста.

Вот другое решение, которое в отличие от первого настроено быстро пропускать все символы, которые нас не интересуют, т.е. пробельные и теги <> вместе с их содержимым:

/\A(?>(?:(?>\s*)<(?>[^>]*)>)*)\S/g

Оно кажется сложным из-за наличия трех атомарных группировок. Вот этот же шаблон без них:

/\A(?:\s*<[^>]*>)*\S/g

Внутри скобок мы пропускаем последовательности пробельных символов \s* и конструкции <[^>]*>. Обратите внимание, что между этими подшаблонами излишне ставить знак альтернативы |. Вначале стоит привязка \A, чтобы поиск не начался внутри тега.

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


Поиск множественных совпадений


Для поиска множественных совпадений можно применить модификатор g и скалярный контекст. Например:

while ('12345' =~ /\d/g) { print "Найдена еще цифра: $&\n" }

Будет напечатано:

Найдена еще цифра: 1 Найдена еще цифра: 2 Найдена еще цифра: 3 Найдена еще цифра: 4 Найдена еще цифра: 5

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

Модификатор g и списковый контекст позволяет запомнить сразу все совпадения в тексте:

my @a='12345' =~ /\d/g; print join ',',@a;

Получаем

1,2,3,4,5



Поиск n-ного совпадения


С модификатором g происходит поиск всех прототипов шаблона, но что делать, если нужно только вполне определенное, скажем, третье, совпадение? Здесь можно применить оператор цикла while:

use locale;

my $s='Тел. 2-3344. Другой тел. 3-2233, а вот еще один тел. 4-1122'; my $count=0; while ($s =~ /тел\.\s+([\d-]+)/gi) { if (++$count == 3) { print "Третий номер: $1\n"; } }

На печать выйдет строка

Третий номер: 4-1122

В регулярном выражении мы захватываем в переменную $1 последовательность из цифр и знака минус, которая стоит после фрагмента "тел. ". Модификатор g обеспечивает поиск всех прототипов шаблона друг за другом, сохраняя в переменной $s смещение конца предыдущего совпадения. При очередном успехе оператор поиска возвращает единицу, что заставляет цикл while продолжать работать. После того, как будет найден последний номер телефона, оператор поиска вернет пустую строку, и оператор while закончит выполнение. Также можно было бы после печати третьего телефона поставить оператор last.

Модификатор i нужен, т.к. не все фрагменты "тел." набраны строчными буквами. Но тогда нужна русская локаль, чтобы модификатор i действовал также на русские буквы.

Иначе в этом примере совпадение начнется со второго телефонного номера и на печать ничего не выйдет.

С таким же успехом можно было использовать цикл for:

use locale;

my $s='Тел. 2-3344. Другой тел. 3-2233, а вот еще один тел. 4-1122'; for (my $count=0; $s =~ /тел\.\s+([\d-]+)/gi;) { if (++$count == 3) { print "Третий номер: $1\n"; last; } }

Наконец, можно использовать встроенный код Perl, но тогда надо форсировать списочный контекст выполнения оператора поиска, чтобы поиск выполнялся итеративно под действием модификатора g, иначе после нахождения первого телефона поиск закончится:

use locale;

my $s='Тел. 2-3344. Другой тел. 3-2233, а вот еще один тел. 4-1122'; my $count=0; my @a=$s =~ /тел\.\s+([\d-]+) (?{ if (++$count == 3) { print "Третий номер: $1\n"; } })/gix;

Это, конечно, не так красиво из-за того, что пришлось присваивать все найденные телефоны массиву.



Поиск нечувствительных к регистру совпадений


По умолчанию, модификатор i отключен и сравнение символов чувствительно к регистру. Если вам все равно, большими или маленькими буквами написано то, что вы ищете, то применяйте модификатор i: m/…/i. Бывает так, что что-то отыскивается с учетом регистра символов, а что-то - без такого учета. В этом случае можно использовать интервальное задание квантификаторов или задание их локально для данных группирующих скобок между вопросительным знаком и двоеточием. Рассмотрим такой пример:

/ab(cd(?-i)ef(?i:gh)ij)kl/i;

В этом регулярном выражении глобально действует модификатор i, т.е. сравнение производится без учета регистра символов. По такому правилу отыскиваются символы ab и cd. Затем внутри захватывающих скобок после себя конструкция (?-i) выключает модификатор i, и символы ef отыскиваются с учетом регистра. Внутри скобочной конструкции (?i:gh) опять включается режим сравнения без учета регистра символов, и символы gh отыскиваются без учета регистра. Символы ij стоят после этой конструкции, а в этом месте восстанавливается режим -i, который был задан выражением (?-i), и символы ij отыскиваются с учетом регистра. Символы kl отыскиваются опять без учета регистра, т.к. стоят за закрывающей скобкой, за которой кончается область действия выражения (?-i) и восстанавливается действие глобального модификатора i.

Также заметьте, что модификатор i зависит от локальной установки системы, ведь он действует только на сравнение букв.



Поиск отдельных слов


В регулярных выражениях словом называется последовательность символов \w. В множество \w входят все строчные и прописные латинские буквы, десятичные цифры и знак подчерка: [a zA Z0 9_]. К этому множеству относятся все символы, считающиеся буквами в той локальной установке, которая используется. Для ActiveState Perl под Windows для этого достаточно написать директиву

use locale;

и все русские буквы (включая буквы ё и Ё) в кодировке Windows-1251 также будут считаться буквами.

Если вы под буквами понимаете что-то другое (например, вы не считаете символ подчерка за букву), то определите соответствующий класс. Если вас интересуют все непробельные символы, то используйте эскейп-последовательность \S.

Эскейп-последовательность \w соответствует ровно одной "букве". Для слов применяйте шаблон \w+. Слово всегда ограничено с обеих сторон мнимыми символами \b - границами слов. Действие этих якорей также зависит от локальной установки операционной системы. Для гарантии того, что совпадение со словом начнется и закончится на его границе, можно применять шаблон \b\w+\b. Чтобы уничтожить ненужные сохраненные состояния, можно также использовать атомарную группировку: \b(?>\w+)\b.

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

(?<!\w)(?=\w)

Слева нет буквы, а справа она есть.

А вот якорь, который соответствует концу слова:

(?<=\w)(?!\w)

Слева есть буква, а справа буквы нет.

Я надеюсь, вы уже понимаете разницу между выражениями (?!\w) и (?=\W). В первом случае справа может и ничего не стоять (конец текста), а второй случай требует, чтобы справа стоял символ, который не является буквой.



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


Для привязки к началу текста можно использовать якорь \A или ^ без модификатора m.

Вот как можно проверить, что текст не начинается с точки:

if ($text =~ /^\./) { print "Текст не должен начинаться с точки!" }

Так можно вставить по одному пробелу в начало каждой строки текста:

$_=<<EOF; Первая строка. Вторая строка. Третья строка. EOF

s/^/ /gm;

Без модификатора m пробел будет вставлен лишь в начало первой строки.

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

$_=<<EOF; Первая строка.

Вторая строка. Третья строка. EOF

s/^\n//gm; print $_;

Получаем на выходе:

Первая строка. Вторая строка. Третья строка.

Может показаться, что оператор s/^$//gm тоже должен удалить пустые строки, но он не сделает замены. Удалиться должен реальный символ, а не совпадение с началом и концом строки, поэтому сперва он должен быть найден. А что-то вставить в пустую строку таким оператором s вполне возможно.

При преобразовании текста в HTML нужно заменять символы & на &amp; Это делается просто:

s/&/&amp;/g

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

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

$_=<<EOF; Первая строка 123. Вторая строка 456. Третья строка 789. EOF

/.*(\d+)/s; print $1;

На печать выходит лишь цифра девять: 9. Удивлены? Квантификатор вернул несколько символов, и текущая позиция в тексте оказалась на цифре 9. Этого достаточно для совпадения шаблона \d+. Чтобы получить правильный результат, надо потребовать, чтобы перед подшаблоном \d+ не было цифры:

$_=<<EOF; Первая строка 123. Вторая строка 456. Третья строка 789. EOF

/.*(?<!\d)(\d+)/s; print $1;

Печатает 789.

Чтобы привязать шаблон к концу текста, можно воспользоваться якорями \z (для привязки к истинному концу текста) или \Z ($ без модификатора m):

$_=<<EOF; Первая строка 123. Вторая строка 456. Третья строка 789 EOF

/(\d+)$/; print $1;

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

$_=<<EOF; Первая строка 123. Вторая строка 456. Третья строка 789. EOF

/(\d+)\D*$/; print $1;

Опять получаем 789, но поиск будет медленнее из-за увеличивающегося числа проверок.



Привязка к началу и концу строк и текста


Мнимые символы \A, \Z и \z не зависят от установок модификаторов и работают всегда одинаково. Не таковы якоря ^ и $, работа которых зависит от модификатора m. В зоне, где модификатор m отключен, якорь ^ соответствует якорю \A, а якорь $ - якорю \Z. Рассмотрим такой пример:

$_="a\r\n"; print '1' if m'a\r$\n';

Здесь я использовал в качестве ограничителей регулярного выражения апострофы, чтобы не было попытки интерполяции специальной переменной $\. Метасимвол $ стоит перед символом \n, который является последним в тексте, и на печать выйдет единица.

Если переставить символ $ перед символом \r:

$_="a\r\n"; print '1' if m'a$\r\n';

то поиск закончится неудачей, потому что здесь $ не стоит непосредственно перед символом \n.

Теперь вернемся к первому примеру и добавим в конце текста какой-нибудь символ:

$_="a\r\nb"; print '1' if m'a\r$\n';

В этом случае поиск вновь завершится неудачно, потому что символ \n здесь уже не последний в тексте. Но если поставить модификатор m, то поиск закончится удачей:

$_="a\r\nb"; print '1' if m'a\r$\n'm;

Теперь такой пример:

$_="a\n"; print '1' if /'a\n^/m;

Единица напечатана не будет, потому что якорь ^ не совпадает после символа \n, если он стоит последним в тексте, и модификатор m здесь не поможет. Но если после \n поставить еще символ:

$_="a\n\n"; print '1' if m'a\n^'m;

то совпадение будет найдено. Теперь в последнем примере уберем модификатор m:

$_="a\n\n"; print '1' if /a\n^/;

Совпадения шаблона тексту уже не будет, т.к. без модификатора m якорь ^ совпадает только в начале текста.



Замена n-го совпадения


Теперь рассмотрим замену n-го найденного фрагмента текста.

use locale;

my $s='Тел. 2-3344. Другой тел. 3-2233, а вот еще один тел. 4-1122'; my $count=0; $s =~ s/(тел\.\s+)([\d-]+)/ ++$count == 3 ? "${1}9-9999" : "$1$2" /egi; print $s;

Напечатается строка

Тел. 2-3344. Другой тел. 3-2233, а вот еще один тел. 9-9999

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

Мы хотели третий номер телефона заменить на 9-9999, но после каждого найденного номера выполняется замена, поэтому то, что не нужно менять, должно быть заменено "на себя". Для этого мы взяли все перед номером телефона в переменную $1, а все остальное - в переменную $2. Если номер телефона не равен 3, то мы найденный фрагмент текста (который соответствует всему регулярному выражению!) меняем на строку $1$2, а в третьем случае в замене вместо номера телефона подставляем 9-9999. Для разделения имени переменной от цифр используем фигурные скобки. Не забываем также поставить модификатор e.

Как быть, когда нужно заменить n-й от конца найденный фрагмент текста, если заранее неизвестно, сколько таких фрагментов будет найдено? Рассмотрим такую идею.

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

#!/usr/bin/perl -w use strict; use locale;

my $s='Тел. 2-3344. Другой тел. 3-2233, а вот еще один тел. 4-1122'; my $tel='тел\.\s+[\d-]+'; my $n=1; $s =~ s/(тел\.\s+)[\d-]+ # ищем слово "тел." + номер (?=(?:.*?$tel){$n} # за которым $n раз идет "тел." + номер (?!.*?$tel) # после чего не встречается "тел." +номер )/${1}9-9999/isx; print $s;

Поясню, как она должна работать по первоначальному замыслу.

Т.к. литерал тел\.\s+[\d-]+ в регулярном выражении будет встречаться несколько раз, то оформим его в виде переменной $tel, которую будем подставлять. Сформулируем задачу в терминах регулярных выражений: нам надо найти фрагмент, соответствующий шаблону тел\.\s+[\d-]+, за которым (фрагментом) в тексте встречается ровно $n таких же фрагментов. Номер телефона в таком фрагменте мы меняем на 9-9999.

Чтобы в этом фрагменте всё, кроме номера телефона, сохранилось, мы берем это остальное в скобки и получаем подшаблон (тел\.\s+)[\d-]+, с которого начинается регулярное выражение. За фрагментом текста, соответствующим этому шаблону, должен идти фрагмент (?:.*?$tel){$n}. ."*" впереди него означает, что эти $n фрагментов не обязательно должны идти сразу за первым фрагментом и друг за другом, между ними могут быть посторонние включения, которые поглощаются конструкцией ".*?". После того, как эти $n фрагментов поглощены, не должно стоять такого же фрагмента текста с любой позиции после этих $n фрагментов, это проверяет условие (?!.*?$tel). Оба этих подшаблона (?:.*?$tel){$n} и (?!.*?$tel) включены в позиционную проверку (?=…). Т.е. за найденным номером телефона должен быть фрагмент текста, который стоит внутри всей этой проверки (?=…).

Что ж, как будто бы все верно, начинаем проверять работу этой программы. Задаем $n=0 и видим результат:

Тел. 2-3344. Другой тел. 3-2233, а вот еще один тел. 9-9999


Верно! Заменился номер телефона, который стоит нулевым справа. Далее даем $n значение 1 и смотрим результат… Вот неожиданность: заменился первый телефон

Тел. 9-9999. Другой тел. 3-2233, а вот еще один тел. 4-1122

а должен был бы тот, что посередине! При $n=2 - та же картина… Видимо, где-то закралась ошибка. Можете вы найти (и исправить) ее самостоятельно? Тогда читайте дальше.

Разберем работу этого регулярного выражения при $n=1. После того, как нашелся первый номер телефона по подшаблону (тел\.\s+)[\d-]+, началось заглядывание вперед, и подшаблон (?:.*?$tel){$n} поглотил второй телефонный номер. За ним началась проверка (?!.*?$tel), которая закончилась неудачей, т.к. после второго телефонного номера есть еще номер. "Не беда! - говорит механизм поиска соответствия. - Буду увеличивать значение минимального квантификатора .*? в подшаблоне (?:.*?$tel){$n}, авось поможет." И начинает его увеличивать и пробовать эти значения. Когда этот квантификатор захватит все до вертикальной черты в следу ющей строке:

Тел. 2-3344. Другой тел. 3-2233, а вот еще один т|ел. 4-1122

весь подшаблон захватит фрагмент

, а вот еще один тел. 4-1122

т.е. все до конца текста, и после этого проверка (?!.*?$tel) пройдет успешно. Налицо совпадение всего регулярного выражения, поэтому первый номер телефона будет заменен на 9-9999. Ошибка ясна: .*? начал поглощать символы, которые относились к номеру телефона, а он не должен был этого делать. Надо заменить его на правильный подшаблон - он должен поглощать символы до фрагмента тел., за которым идет номер.

Когда речь шла о пропуске символов до закрывающей угловой скобки при поиске ссылки, то все было просто: [^>]*, но здесь уже не один символ, поэтому конструкция [^$tel]*, вообще говоря, не годится, у нас не просто множество символов, а часть текста. Вот практический прием пропуска символов до данного фрагмента текста:

(?:(?!$tel).)*$tel

Мы каждый раз в цикле проверяем, находится ли в текущей позиции фрагмент текста $tel, и если нет, то берем следующий символ точкой. А после этого цикла должен идти текст $tel. Такой цикл хотя и медлителен, но он гарантирует, что мы не проскочим мимо искомого фрагмента текста.

Вся программа теперь выглядит так:

#!/usr/bin/perl -w use strict; use locale;



my $s='Тел. 2-3344. Другой тел. 3-2233, а вот еще один тел. 4-1122'; my $tel='тел\.\s+[\d-]+'; my $n=2; $s =~ s/(тел\.\s+)[\d-]+ # ищем слово "тел." + номер (?=(?:(?:(?!$tel).)*$tel){$n} # за которым $n раз идет "тел." + номер (?!.*?$tel) # после чего не встречается "тел." +номер )/${1}9-9999/isx; print $s;

Она верно заменяет нужный телефонный номер, а если задано слишком большое значение для $n, которого нет, замена не производится. Обратите внимание, что в подшаблоне (?!.*?$tel) мы не стали заменять конструкцию .*? на новую, т.к. здесь она работает правильно: если впереди есть текст, соответствующий шаблону .*?$tel, то он будет найден и негативная опережающая проверка (?!.*?$tel) вернет ложь.

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

Атомарная группировка в этом шаблоне пришлась бы кстати. Например, подшаблон [\d-]+ можно заключить в атомарные скобки, т.к. нет смысла возвращать цифры номера телефона, это не приведет к совпадению.

Другой способ заменить n-е от конца совпадение - повторять в цикле while поиск номера телефона, взяв его в захватывающие скобки, и запоминать в массивы значения $-[1] и $+[1]. Затем отсчитать от конца массивов $n, получить смещение начала и конца нужного телефонного номера и воспользоваться функцией substr, которой можно присваивать значение.


Якорь \G, его смысл и использование


Мнимый символ \G означает позицию конца предыдущего совпадения. Это аналог функции pos, но только внутри регулярного выражения. Этот метасимвол впервые появился в Perl для проведения глобального поиска и замены. Он совпадает с позицией, в которой закончилась предыдущая итерация поиска с модификатором g. Свое значение этот якорь хранит также и после окончания работы оператора поиска/замены, как и функция pos. При первой итерации поиска/замены или после сброса текущей позиции поиска в начало текста \G совпадает в начале текста как и метасимвол \A. При принудительной переустановке текущей позиции поиска при обращении к функции pos это якорь соответствует установленной позиции. Рассмотрим примеры, которые показывают, как работает якорь \G и чем он может быть полезен.

Мы уже рассматривали примеры поиска в скалярном контексте с модификатором g. При обращению к другому или тому же оператору с модификатором g и той же переменной с целевым текстом отыскивается следующее совпадение. Но оно ищется без привязки к концу предыдущего совпадния и может быть найдено с любой позиции после конца предыдущего совпадения. Иногда бывает нужно, чтобы следующий поиск был привязан к концу последнего совпадения и чтобы поиск заканчивался неудачей, если поиск следующего образца начинается не с этой начальной позиции. Это аналогично привязке к началу текста \A.

Например, мы ищем слова, разделенные вертикальной чертой:

$_='|ab|cd |ef'; while (/\|(\w+)/g) { print "$1\n" }

Этот пример выведет

ab cd ef

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

$_='|ab|cd |ef'; while (/\G\|(\w+)/g) { print "$1\n" }

В результате получаем:

ab cd

Якорь \G надежно работает, если он стоит в самом начале регулярного выражения, которое не имеет высокоуровневой конструкции выбора альтернативы. Если такая конструкция имеется, то якорь \G надо выносить за скобки:

/\G(?: шаблон1 | шаблон2 | … )/

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

Теперь поясню, чем же отличается конец предыдущего совпадения от начала текущего совпадения. Неужели это не одно и то же? Если совпадение было с непустным фрагментом текста, то это то же самое. Но если совпадение было с "пустотой", то, как мы уже знаем, механизм регулярных выражений принудительно передвигает текущую позицию поиска на один символ, чтобы избежать зацикливания. Рассмотрим такие примеры:

$_='abcd'; s/z*/!/g; print $_;


Восклицание будет вставлено в каждую позицию строки:

!a!b!c!d!

Теперь поставим в начало регулярного выражения якорь \G:

$_='abcd'; s/\Gz*/!/g; print $_;

Восклицание будет вставлено только в начало строки:

!abcd

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

Заметим еще, что если комбинируется поиск в скалярном и списковом контексте, то якорь \G хранит свое значение только после успешного поиска в скалярном контексте с модификатором g. После поиска в списковом контексте с модификатором g якорь \G сбрасывается в начало текста.

Рассмотрим такие примеры. Сначала ищем в скалярном, а затем в списковом контексте:

$_='abcd'; /\w/g; my @a=/\w/g; print @a;

Будет напечатано:

bcd

Символ a был пройден при первом поиске.

Теперь поменяем операторы:

$_='abcd'; my @a=/\w/g; print "@a\n"; if (/(\w)/g) { print "Found $1" }

Напечатается

a b c d Found a

Это можно объяснить тем, что поиск в списковом контексте продожается до своей неудачи, а она сбрасывает позицию поиска и якорь \G в начало текста. То же самое сделает и поиск в скалярном контексте, если он будет выполняться в цикле до исчерпания совпадений:

$_='abcd'; while (/(\w)/g) { print "$1 " } my @a=/\w/g; print "\n@a";

Будет напечатано:

a b c d a b c d

Запомните, что оператор поиска/замены, который хочет искать от конца предыдущего совпадения \G, обязательно должен иметь модификатор g!


Лексический анализ текста с помощью якоря \G и модификатора gc


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

my $text='123 234 345 456'; while ($text =~ /\d+/g) { print "Найдено число $&\npos=".pos($text)."\n" }

На печати увидим:

Найдено число 123 pos=3 Найдено число 234 pos=7 Найдено число 345 pos=11 Найдено число 456 pos=15

Отпечатаются позиции за каждым найденным числом.

Предположим, что мы делаем разбор текста на имена, которые состоят из латинских букв, чисел и символов перевода строк. Мы должны последовательно извлекать из текста эти объекты, называть и выводить их. Если встречается что-то, что не соотвтетствует синтаксису ни одного объекта, то это будет называться unknown (неизвестно). Регулярное выражение будет начинаться с \G и представлять собой высокоуровневую конструкцию выбора:

$_=<<EOF; 123 abc -234 def \$\@ xyz EOF while (/\G (?:(?>[\000-\011\013-\040]+)| # все от \0 до пробела кроме \n (\b(?>[a-zA-Z]+)\b)(?{ print "Name: $^N\n" })| # имя ((?>[+-]?)(?>\d+)\b)(?{ print "Number: $^N\n" })| # число (\n)(?{ print "Newline:\n" })| # \n ((?>\S+))(?{ print "Unknown: $^N\n" }) # все остальное )/gx) {}

Чтобы отделить первую альтернативу выбора от \G, необходимо всю конструкцию выбора взять в скобки. На печать выйдет

Number: 123 Name: abc Newline: Number: -234 Name: def Newline: Unknown: $@ Name: xyz Newline:

В этом примере обратите внимание на то, что ветка \S+ для unknown поставлена последней, иначе разбор получился бы некорректным.

Не обязательно все шаблоны хранить в одной большой конструкции выбора, можно делать разбор текста по нескольким регулярным выражениям, но тогда надо использовать модификатор gc. Вот упрощенный пример программы проверки кода HTML с проверкой правильности закрытия тега A:

#!/usr/bin/perl -w use strict; use bytes; use locale;

$_=<<EOF; <html> <body> &lt;слово1 слово2 1234&gt; <a href="http://www.intuit.ru">Это ссылка</a> </body> </html> EOF


my $close_a=0; while (!/\G\z/gc) { if (/\G<(\w+)[^>]*>\s*/gc) { print "Тег $^N открылся\n"; if ($^N =~ /^a$/i) { print "Ошибка: вложенный тег A\n" if $close_a++; } } elsif (m!\G</(\w+)[^>]*>\s*!gc) { print "Тег $^N закрылся\n"; if ($^N =~ /^a$/i) { print "Ошибка: нет парного тега A\n" if !$close_a--; } } elsif (/\G(\w+)\s*/gc) { print "Найдено слово/число $^N\n"; } elsif (/\G(&#?\w+;)\s*/gc) { print "Найдена подстановка $^N\n"; } # Пропускаем все кроме тегов, слов и подстановок elsif (/\G[^<>&\w]+/gc) { } else # Нашли ошибку, сделаем сообщение об этом { my $offset=pos($_); my ($error)=$_ =~ /\G.{1,10}/gs; die "Непонятные символы\n$error\nв позиции $offset\n"; } } print "Имеется незакрытый тег A\n" if $close_a;

Эта программа напечатает следующее:

Тег html открылся Тег body открылся Найдена подстановка &lt; Найдено слово/число слово1 Найдено слово/число слово2 Найдено слово/число 1234 Найдена подстановка &gt; Тег a открылся Найдено слово/число Это Найдено слово/число ссылка Тег a закрылся Тег body закрылся Тег html закрылся


Предотвращение зацикливания при поиске и замене


Версия 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


Предварительная настройка начальной позиции поиска


Полезным свойством функции pos является то, что ей можно присваивать значение!

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

pos($s)=1000;

Напомню, что счет символов идет с нуля. Но оператор поиска для использования этого значения должен иметь модификатор g (или gc). Вот пример:

$_='abcd'; pos($_)=1; /\w/g; print "$&\n"; pos($_)=1; print /\w/g;

На печати получим:

b bcd

Второй раз оператор поиска был использован в списковом контексте, потому что оператор print ожидает список значений.

Якорь \G учитывает значение, присвоенное функции pos, что видно на следующих примерах:

$_='abcd'; pos($_)=1; /\G\w/g; print "$&\n"; pos($_)=1; print /\G\w/g;

Результат такой же:

b bcd

А теперь рассмотрим более сложный пример, когда без якоря \G трудно организовать поиск. Предположим, данные представляют собой сплошную последовательность черырехзначных чисел без разделителей, и нам нужно отобрать из нее все числа, которые начинаются на 99.

Прямая попытка

my @a=/99\d\d/g

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

$_='12991234'; my @a=/99\d\d/g; print @a;

будет "найдено" число 9912.

Попробуем вариант с минимальным квантификатором, который пропускает все четверки чисел до числа 99\d\d, а сохраняющие скобки захватывают в массив @a нужные числа:

$_='12991234'; my @a=/(?:\d{4})*?(99\d\d)/g; print @a;

Опять "найдено" число 9912… В этом примере при отсутствии совпадения (когда в тексте не останется нужных чисел) также применяется механизм смещения на один символ, и поиск продолжается не с границы чисел. Аналогично ведет себя негативная опережающая проверка с максимальным квантификатором:

$_='12991234'; my @a=/(?:(?!99)\d{4})*(99\d\d)/g; print @a;

Опять 9912… Первая пара скобок всегда заканчивается на границе числа, но при несовпадении второй пары будет смещение текущей позиции поиска, что все и портит.

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

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

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

$_='12991234'; my @a=/\G(?:(?!99)\d{4})*(99\d\d)/g; print @a;

В этом примере поиск закончится неудачей и возвратится пустой список. В примере

$_='129912349934'; my @a=/\G(?:(?!99)\d{4})*(99\d\d)/g; print @a;

напечатается число 9934.



Запрет сброса позиции \G модификатором c


Мы можем запретить сброс позиции \G в начало текста, если совпадение не было найдено.

Для этого имеется модификатор c. Он не применяется без модификатора g, поэтому gc является идиомой, как бы одним модификатором gc, хотя модификаторы могут записываться в любом порядке и даже повторяться, что не влечет каких-лидо последствий (за исключением повтора модификатора e в операторе подстановки s///).

Вот как выглядит последний пример с использованием модификатора c:

$_='abcd'; while (/(\w)/gc) { print "$1 " } my @a=/\w/g; print "\n".scalar @a;

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

a b c d 0

Мы видим, что второй поиск завершился неудачей, и размер массива @a равен нулю.

Аналогично работает и предпоследний пример:

$_='abcd'; my @a=/\w/gc; print "@a\n"; if (/\w/g) { print "Found $1" } else { print 'Not found' }

Будет напечатано:

a b c d Not found

Если бы мы распечатали значение функции pos($_) после исчерпывающего поиска с модификатором gc, то pos указывала бы в конец строки (в нашем случае это 4). После исчерпывающего поиска с одним модификатором g, когда \G сбрасывается в начало текста, значение pos($_) становится неопределенным, как и для любой переменной, для которой не выполнялся поиск с модификатором g, или после присвоения этой переменной значения.



Интерполяция кода Perl


В строку можно интерполировать результат выполнения кода Perl. Вот как работает функция join в предыдущем примере с интерполяцией массива значений:

my @a=([1,2],[3,4]); $_="abc${\(join ',',(@{$a[0]},@{$a[1]}))}def"; print $_;

Результат получается тем же:

abc1,2,3,4def

Конструкция \(join ',',(@{$a[0]},@{$a[1]}) обрабатывается как ссылка на анонимный список, элементы которого выдает функция join. Конструкция {…} выполняется как блок команд, она возвращает ссылку на этот анонимный список, которая оператором @ разыменовывается и получается сам список. Обратите внимание, что если бы мы внутри этой конструкции использовали ограничители строки ", то их надо было бы маскировать обратными слэшами:

my @a=([1,2],[3,4]); $_="abc${\(join \",\",(@{$a[0]},@{$a[1]}))}def"; print $_;

Этот пример можно переписать с использованием конструкции @{[ список ]}:

my @a=([1,2],[3,4]); $_="abc@{[join ',',(@{$a[0]},@{$a[1]})]}def"; print $_;

Результат тот же:

abc1,2,3,4def

Конструкция [ список ] является генератором анонимного массива, ссылка на который разыменовывается префиксом @.

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

$_="abc${\(&subr('Bill'))}def"; print $_;

sub subr($) { return "Hello $_[0]!" }

На печати получим:

abcHello Bill!def

Аналогично, в строку можно интерполировать другие операторы Perl, например:

my $a=3; $_="abc${\($a == 3 ? '$a == 3' : '$a != 3')}def"; print $_;

Получаем:

abc$a == 3def

Здесь обратите внимание на то, что хотя код интерполируется в строку, ограниченную двойными кавычками, маскировать $ внутри апострофов не надо, т.к. они обрабатываются внутри кода Perl. В ином случае внутри строки, ограниченной двойными кавычками, надо было бы маскировать $ и @, даже если бы они были внутри своей строки, заключенной в апострофы.



Интерполяция массива


Массивы интерполируются всеми своими значениями:

my @a=(1,2,3,4); $_="abc@{a}def"; print $_;

На печати окажется

abc1 2 3 4def

Не проходит аналогичная интерполяция многомерного массива:

my @a=([1,2],[3,4]); $_="abc@{a}def"; print $_;

Напечатается

abcARRAY(0x224ea4) ARRAY(0x224f88)def

На самом деле в Perl нет многомерных массивов как в C или Pascal. @a является массивом из двух ссылок (размер массива @a равен двум). При его интерполяции выводятся эти две ссылки на два подмассива. Для интерполяции элементов массива надо подставлять массивы конечных элементов (не ссылок):

my @a=([1,2],[3,4]); $_="abc@{$a[0]}@{$a[1]}def"; print $_;

Напечатается

abc1 23 4def

Конструкция @{$a[0]} означает следующее: $a[0] является ссылкой на массив, а оператор @ ее разыменовывает, получая в результате сам этот массив.

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

$"=','; my @a=([1,2],[3,4]); $_="abc@{$a[0]},@{$a[1]}def"; print $_;

На печать выйдет

abc1,2,3,4def



Интерполяция переменных и кода в регулярне выражение


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

Авторы книг по Perl не упоминают, как происходит интерполяция переменной с индексами, ведь символ [ является метасимволом внутри шаблонов.

my @a=('abc','def'); $_='def'; print $& if /^$a[1]$/;

Напечатается def.

Как видим, конструкция […] после имени переменной интерпретируется как индекс. Если же интерполируется скалярная переменная, после которой идет класс, то транслятор выдает ошибку:

#!/usr/bin/perl -w use strict;

my $a=2; $_='21'; print $& if /^$a[1]$/;

Global symbol "@a" requires explicit package name at a.pl line 6. Execution of a.pl aborted due to compilation errors.

Здесь не поможет заключение имени переменной в фигуный скобки: ${a}, ошибка останется той же. Если отделить имя переменной от квадратной скобки пробелом, то программа работает верно:

#!/usr/bin/perl -w use strict;

my $a=2; $_='21'; print $& if /^$a [1]$/x;

На выходе имеем 21.

А если мы не планируем использовать модификатор x? Тогда может помочь такое искусственное решение:

my $a=2; $_='21'; print $& if /^$a(?#)[1]$/;

Опять получаем 21.

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

my $a=2; $_='21'; print $& if /^$a(?=)[1]$/;

И снова выводится 21.

Опережающая проверка (?=) всегда возвращает истину, т.к. пустой фрагмент в тексте можно встретить всюду.

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

my $a=2; $_='21'; print $& if /^${a}a{0}[1]$/;

Опять 21.

Мы разделили имя и класс подшаблоном a{0}, который равносилен пустому фрагменту.

Теперь рассмотрим интерполяцию переменной с двумя индексами:

my @a=([1,2],[3,4]); $_='2'; print $& if /^$a[0][1]$/;

На печати получаем 2. Отсюда вывод: конструкции […] интепретируются как индексы переменной, если они идут непосредственно после ее имени. Отделение классов от индексов происходит аналогично уже рассмотренным случаям с одним индексом.

Массивы в регулярные выражения тоже интерполируются как в строку, ограниченную двойными кавычками:

$"=','; my @a=(1,2,3); $_='1,2,3'; print $& if /^@a$/;

На печать выйдет

1,2,3,4

После интерполяции массива @a с учетом разделителя $"=',' регулярное выражение стало эквивалентно такому: /^1,2,3,4$/.


Опять 21.

Мы разделили имя и класс подшаблоном a{0}, который равносилен пустому фрагменту.

Теперь рассмотрим интерполяцию переменной с двумя индексами:

my @a=([1,2],[3,4]); $_='2'; print $& if /^$a[0][1]$/;

На печати получаем 2. Отсюда вывод: конструкции […] интепретируются как индексы переменной, если они идут непосредственно после ее имени. Отделение классов от индексов происходит аналогично уже рассмотренным случаям с одним индексом.

Массивы в регулярные выражения тоже интерполируются как в строку, ограниченную двойными кавычками:

$"=','; my @a=(1,2,3); $_='1,2,3'; print $& if /^@a$/;

На печать выйдет

1,2,3,4

После интерполяции массива @a с учетом разделителя $"=',' регулярное выражение стало эквивалентно такому: /^1,2,3,4$/.

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

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

Сравните:

my $a='abc'; $_='abc'; for my $count (0..1) { print "$&\n" if /^$a$/; $a='123'; $_='123'; }

Напечатается

abc 123

и

my $a='abc'; $_='abc'; for my $count (0..1) { print "$&\n" if /^$a$/o; $a='123'; $_='123'; }

Напечатается только

abc

Во втором случае в обоих повторах цикла совпадение отыскивается по тому же самому регулярному выражению /^abc$/, хотя переменная $a поменяла значение на '123'. В результате при втором повторе цикла совпадение с шаблоном не обнаруживается.

Модификатор o, как и модификаторы g и с, могут быть применены только ко всему регулярному выражению и не могут встречаться внутри него.

Как будут интерполироваться переменные, которые устанавливаются внутри регулярного выражения, в частности, нумерованные переменные $1, $2, …, $99? Так же, как и ваши переменные: своим значением, которое они имели до работы этого регулярного выражения. Поэтому не надейтесь, что в ходе выполнения регулярного выражения вы будете получать самые "свежие" их значения. Другое дело - встроенный код Perl и динамические регулярные выражения (которые мы рассмотрим в дальнейшем): здесь нет интерполяции переменных, а используются их текущие значения.

Внутри классов тоже происходит интерполяция переменных. В данном примере мы интерполируем два значения переменных с двумя индексами:

my @a=([1,2],[3,4]); $_='23'; print $& if /^[$a[0][1]$a[1][0]]{2}$/;


Интерполяция переменных и кода в строку


Сначала рассмотрим примеры интерполяции значений в строки, которые ограничены двойными кавычками. Аналогично интерполяция происходит в строки, ограниченные обратными кавычками: ``.