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

Perl часто упоминается на страницах книги, и не без оснований. Это популярный язык, обладающий исключительно богатыми возможностями в области регулярных выражений, бесплатный и доступный, понятный для начинающих и существующий на множестве платформ, в том числе Amiga, DOS, MacOS, NT, OS/2, Windows, VMS и практически всех разновидностях Unix.

Некоторые программные конструкции Perl напоминают C и другие традиционные языки программирования, но на этом все сходство и кончается. Подход к решению задач на Perl — Путь Perl — сильно отличается от традиционных языков. В Perl-программах часто используются традиционные концепции структурного и объектно-ориентированного программирования, но обработка данных очень сильно зависит от регулярных выражений. В сущности, можно без преувеличения заявить, что регулярные выражения играют ключевую роль практически в любой Perl-программе. Это утверждение справедливо как для гигантской системы из 100 000 строк, так и для простых однострочных программ типа:

% perl -pi -e 's{([-+]?\d+(\.\d*)?)F\b}{sprintf "%.0fC",($1-32) * 5/9)eg' *.txt

Эта программа просматривает файлы *.txt и преобразует температуру из шкалы Фаренгейта в шкалу Цельсия (вспомните первый пример из главы 2).

В этой главе

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

Однако познания в области Perl — не главное. Вероятно, еще важнее ваше стремление узнать больше. Эта глава ни в коем случае не является «легким чтивом». Поскольку я не собираюсь учить вас Perl с самого начала, у меня появляется возможность, которое нет у авторов общих учебников Perl — мне не придется опускать важные детали, чтобы добиться связного повествования, плавно разворачивающегося на протяжении всей главы. Если что и остается постоянным, так это стремление к пониманию общей картины. Некоторые проблемы довольно сложны и загромождены обилием деталей. Не огорчайтесь, если вам не удастся усвоить все сразу. Я рекомендую один раз прочитать главу, чтобы получить общее представление о теме, а затем возвращаться к ней в будущем по мере надобности.

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

l      В разделе «Путь Perl» приведены общие сведения того, как Perl и регулярные выражения взаимодействуют друг с другом и с программистом. Я приведу короткий пример решения задачи в соответствии с Путем Perl и представлю некоторые важные концепции, которые будут снова и снова встречаться в этой главе — а также на протяжении всей вашей работы на Perl.

l      В разделе «Perl’измы из области регулярных выражений» (с. <$R[P#,R7-9]>) рассматриваются некоторые аспекты Perl, имеющие особенно важное значение для регулярных выражений. Здесь подробно анализируются такие концепции, как динамическая видимость, контекст выражения и интерполяция, при этом особое внимание уделяется их связи с регулярными выражениями.

l      В разделе «Диалект регулярных выражений Perl» (с. <$R[P#,R7-10]>) описаны практические стороны работы с регулярными выражениями, в том числе все интересные новшества Perl версии 5. Впрочем, все эти возможности хороши лишь в сочетании со средствами для их применения. В разделах «Оператор поиска» (с. <$R[P#,R7-11]>), «Оператор подстановки» (с. <$R[P#,R7-12]>) и «Оператор разбиения» (с. <$R[P#,R7-13]>) подробно описаны все операторы регулярных выражений, которые непосвященным часто кажутся каким-то волшебством.

l      Раздел «Проблемы эффективности в Perl» (с. <$R[P#,R7-6]>) затрагивает тему, близкую сердцу любого программиста. В Perl используется традиционный механизм НКА, поэтому вы можете смело начинать эксперименты со всеми приемами, описанными в главе 5. Конечно, существует немало факторов, специфических для Perl, сильно влияющих на то, как и насколько быстро применяются регулярные выражения в Perl; эти факторы будут рассмотрены в этом разделе.

l      Наконец, раздел «Все вместе» (с. <$R[P#,R7-14]>) завершает главу несколькими примерами, объединяющими все сказанное в общую картину. Кроме того, мы вернемся к примеру из раздела «Путь Perl» и пересмотрим его в свете того, о чем говорилось в главе. В последнем примере (поиск адресов электронной почты Интернета — с. <$R[P#,R7-15]>) будут задействованы практически все приемы, которые нам знакомы. Итоговое регулярное выражение состоит почти из 5000 символов, однако вы разберетесь в нем и при необходимости сможете отредактировать.

Путь Perl

В табл. 7.1 приведена краткая сводка исключительно богатого диалекта регулярных выражений Perl. Если вы только начинаете работать на Perl, но имеете опыт использования других программ с поддержкой регулярных выражений, многие конструкции покажутся незнакомыми. Незнакомыми, да — но заманчивыми! Вероятно, диалект регулярных выражений Perl обладает наибольшими возможностями из всех современных программ. Чтобы ваше понимание регулярных выражений Perl не было поверхностным, вы должны знать, что в Perl используется механизм НКА. Это означает, что все сказанное о НКА в предыдущих главах, в равной степени относится и к Perl.

Но не одними метасимволами жив программист. Регулярные выражения бесполезны, если вы не располагаете средствами для их применения. Впрочем, Perl и здесь вас не подведет. В этой области Perl, как обычно, следует своему девизу: «У каждой задачи есть несколько решений».

Таблица 7.1. Язык регулярных выражений в Perl

. (точка)

любой байт, кроме символа новой строки (с. <$R[P#,R7-16]>) или абсолютно любой байт с модификатором /s[1] с. <$R[P#,R7-17]>)

|

конструкция выбора

максимальные квантификаторы

* + ? {n}  {мин,}  {мин, макс}

См. с. <$R[P#,R7-18]>

минимальные квантификаторы[2]

*?  +?  ??  {n}?  {мин,}?  {мин, макс}?

См. с. <$R[P#,R7-19]>

(…)

обычные круглые скобки (группировка и сохранение)

(?:…)

только группировка (с. <$R[P#,R7-20]>)

(?=…)

позитивная опережающая проверка (с. <$R[P#,R7-1]>)

(?!…)

негативная опережающая проверка (с. <$R[P#,R7-8]>)

(?#…)

комментарий[3] (с. <$R[P#,R7-21]>)

#…

(с модификатором /x, с. <$R[P#,R7-22]>) комментарий[4]

до конца строки или выражения

встроенные модификаторы [5] (с. <$R[P#,R7-23]>)

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

модификаторы i, x, m и s

якорные метасимволы

\b[6] \B

граница слова/не граница слова (с. <$R[P#,R7-24]>)

^ $

начало/конец строки (или начало и конец логической строки) (с. <$R[P#,R7-25]>)

\A \Z

начало/конец строки[7] (с. <$R[P#,R7-26]>)

\G

конец предыдущего совпадения[8] (с. <$R[P#,R7-27]>)

\1, \2 и т. д.

текст, совпавший с заданной парой круглых скобок (с. <$M[R7-28]>)

[…]  [^…]

символьные классы (нормальный и инвертированный). См. с. <$R[P#,R7-29]>.

(Следующие метасимволы также действительны внутри символьных классов)

Сокращенные обозначения символов

\b[9]  \t  \n  \r  \f  \a  \e  \число  \xчисло \ссимвол

См. с. <$R[P#,R7-30]>

Сокращенные обозначения классов

\w  \W  \s  \S  \d  \D

См. с. <$R[P#,R7-30]>

 

\l  \u  \L  \U  \Q[10]  \E

Оперативное изменение регистра символов (с. <$R[P#,R7-31]>)

Регулярные выражения как компонент языка

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

Возможно, вы никогда не рассматривали …=~ m/…/ как оператор. Но подобно тому, как оператор сложения + получает два операнда и возвращает сумму, оператор поиска также получает два операнда (регулярное выражение и целевую строку) и возвращает значение. Как упоминалось в разделе «Функции, интегрированные средства и объекты» главы 5 (с. <$R[P#,R5-20]>), главное отличие между функцией и оператором заключается в том, что оператор может обрабатывать свои операнды особым образом, обычно недоступным для функций[11]. Конечно, операторы регулярных выражений в Perl выглядят таинственно. Но вспомните, что я говорил в главе 1: в фокусах нет ничего волшебного, если вы понимаете, что при этом происходит. Эта глава поможет вам разобраться в происходящем.

Между понятиями регулярное выражение и регулярное выражение-операнд существуют определенные отличия. В сценарии задаются «сырые» регулярные выражения-операнды. Perl подвергает их небольшой «предварительной обработке» и передает результат механизму поиска регулярных выражений. Выполняемые на стадии предварительной обработки операции аналогичны (но не идентичны!) тем, которые выполняется для строк в кавычках. Для поверхностного понимания эти различия несущественны — вот почему я буду объяснять и подчеркивать их при каждой возможности!

Пусть краткость секции «Операторы регулярных выражений» в табл. 7.2 вас не смущает. Благодаря разнообразным параметрам и особым случаям применения каждый из трех операторов решает много задач.

Самая сильная сторона Perl

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

Таблица 7.2. Обзор синтаксиса Perl, относящегося к работе с регулярными выражениями

Операторы для работы с регулярными выражениями

m/выражение/модификаторы[12]

См. с. <$R[P#,R7-11]>

s/выражение/замена/модификаторы[13]

См. с. <$R[P#,R7-12]>

split(…)

См. с. <$R[P#,R7-13]>

Вспомогательные переменные

$_

Целевой текст по умолчанию

$*

Устаревший многострочный режим (с. <$R[P#,R7-32]>)

модификатор (с. <$R[P#,R7-33]>)

Влияние

/x[14]   /o

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

/s[15]   /m[16]   /i

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

/g   /e

Прочее

Переменные с информацией о совпадении (с. <$R[P#,R7-34]>)

$1, $2 и т. д.

Сохраненный текст.

$+

Старшее из присвоенных значений $1, $2…

$`   $&   &'

Предшествующий текст, текст совпадения и текст после совпадения

(использовать не рекомендуется — см. «Проблемы эффективности в Perl», с. <$R[P#,R7-6]>)

Связанные функции

pos

См. с. <$R[P#,R7-36]>

study

См. с. <$R[P#,R7-5]>

quotemeta

 

lc, lcfirst, uc, ucfirst

См. с. <$R[P#,R7-35]>

Самая слабая сторона Perl

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

В весеннем выпуске «The Perl Journal» за 1996 г.[17] Ларри Уолл писал:

«Одна из идей, которые я все время подчеркиваю при проектировании Perl — то, что РАЗНЫЕ вещи должны ПО-РАЗНОМУ выглядеть».

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

if (m/…/g) {

Не пытайтесь, я все равно не поверю. Через это проходят все. А если вы не считаете себя знатоком и не понимаете, в чем проблема — не огорчайтесь: для того и написана эта книга.

В той же самой статье Ларри написал:

«Стремясь сделать программирование предсказуемым, теоретики сделали его скучным».

И это тоже правда, но забавно другое: всего за неделю до чтения статьи Ларри я сам написал, что «в программировании настоящим произведением искусства нередко считается скучный, последовательный, предсказуемый интерфейс»! В моем представлении «произведения искусства» создают инженеры, а не художники, но кто я такой, чтобы судить об этом? В любом случае, я настоятельно рекомендую прочитать увлекательную, наводящую на размышления статью Ларри. Вы найдете в ней исключительно глубокие комментарии о Perl, языках, и… искусстве.

Курица, яйцо и Путь Perl

В регулярных выражениях Perl сплетено столько взаимосвязанных концепций, что при любых попытках навести хоть какой-то порядок возникает классическая проблема «курицы и яйца». Поскольку во введении к главе 2 был представлен упрощенный подход к регулярным выражениям Perl, я хочу привести полноценный пример, чтобы вы слегка размялись перед решительным наступлением на операторы и метасимволы. Надеюсь, этот пример окажется полезным, поскольку в нем обсуждаются многие концепции, рассматриваемые в этой главе. Этот пример показывает, как следовать «Пути Perl» при решении задач и избежать некоторых ловушек, с которыми вы можете столкнуться в будущем.

Ознакомительный пример: анализ текста, разделенного запятыми

Предположим<$M[R7-54]>, переменная $text содержит некоторые данные, разделенные запятыми. Данные в этом формате могут быть прочитаны из файлов, созданных в dBASE, Excel или другой программе. Файл состоит из строк вида:

"earth",1,,"moon",9.374

В этой строке определяются пять полей. Нам хотелось бы оформить эту информацию в виде массива (допустим, @field), чтобы элемент $field[0] содержал строку earth, элемент $field[1] — число 1, элемент $field[2] имел неопределенное значение, и т. д. Для этого придется не только разделить данные на поля, но и удалить кавычки из строковых полей. Первое, что приходит в голову — воспользоваться разбиением строки при помощи split:

@fields = split(/,/, $text);

Команда находит в $text все места, где совпадает [,], и заполняет @fields фрагментами строки, которые находятся между найденными совпадениями (вместо самих совпадений!).

К сожалению, хотя оператор split приносит немалую пользу, в данном случае он не подходит. Дело в том, что при разбиении по запятым остаются кавычки, которые мы также хотели удалить. Проблему можно решить выражением ["?,"?], но остаются и другие проблемы. Например, строковые поля в кавычках наверняка могут содержать внутренние кавычки, которые не должны интерпретироваться как ограничители полей, однако вы не сможете приказать split не обращать на них внимания.

Инструментарий Perl предлагает много возможных решений; приведу то, что получилось у меня<$M[R7-125]>:

@fields = (); # Инициализировать пустой список @fields

while ($text =~ m/"([^"\\]*(\\.[^"\\]*)*)",?|([^,]+),?|,/g) {

    push(@fields, defined($1) ? $1 : $3); # Добавить совпавшее поле

}

push(@fields, undef) if $text =~ m/,$/; # Учесть последнее пустое поле

# Теперь можно работать с данными через @fields

Вероятно, даже опытный программист Perl не сразу разберется в этом фрагменте, поэтому давайте разберем его шаг за шагом.

Контекст оператора регулярного выражения

Регулярное выражение выглядит внушительно:

["([^"\\]*(\\.[^"\\]*)*)",?|([^,]+),?|,]

Чтобы в полной мере понять смысл регулярного выражения, необходимо разобраться в том, как оно используется. В данном случае оно применяется оператором поиска, с применением модификатора /g, в условии цикла while. Эта тема подробно рассматривается в основном тексте главы, но главное заключается в том, что оператор поиска по-разному ведет себя в зависимости от того, где и как он используется. В данном случае тело цикла while выполняется каждый раз, когда регулярное выражение совпадает в переменной $text. В теле цикла доступны значения переменных $&, $1, $2 и т. д., устанавливаемые для каждого совпадения.

Подробное описание регулярного выражения

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

l      [" [^"\\]*(\\.[^"\\]*)*)",?]: наш старый знакомый из главы 5 — выражение для поиска строк в кавычках с присоединенным суффиксом [,?]. Помеченные скобки не влияют на смысл регулярного выражения — конечно, они используются лишь для сохранения текста в переменной $1. Эта альтернатива предназначена для обработки полей, заключенных в кавычки.

l      [([^,]+),?]: непустая последовательность символов, не являющихся запятыми, за которой может следовать запятая. Как и в первой альтернативе, круглые скобки используются только для сохранения совпавшего текста — на этот раз всех символов до запятой (или до конца текста). Эта альтернатива обрабатывает простые поля данных, не заключенные в кавычки.

l      [,]: не о чем говорить — просто запятая.

Понять смысл отдельных компонентов выражения нетрудно — возможно, кроме смысла [,?], к которому мы еще вернемся. Однако по отдельности эти выражения мало о чем говорят — мы должны посмотреть, как они объединяются и как они работают в сочетании с другими командами программы.

Как применяется выражение

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

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

Обратите внимание на важное обстоятельство — с какой бы альтернативой не совпало первое поле, выражение всегда распространяется до запятой, разделяющей поля. При этом текущая позиция модификатора /g будет находиться в начале следующего поля. Таким образом, при циклическом выполнении комбинации while и m/…/g и многократном применении регулярного выражения поиск заведомо будет начинаться с начала поля. Подобная<$M[R7-65]> «синхронизация» играет важную роль во многих ситуациях, где используется модификатор /g. Именно из-за нее первые две альтернативы завершаются подвыражением [,?] (вопросительный знак необходим из-за того, что последнее поле в строке не завершается запятой). Мы еще встретимся с проявлением этой «синхронизации».

Итак, мы можем идентифицировать каждое поле. Но как заполнить данными очередной элемент @fields после каждого совпадения? Если поле заключено в кавычки, то первая альтернатива совпадает, а текст в кавычках присваивается переменной $1. Но если поле не заключено в кавычки, поиск первой альтернативы завершается неудачей, а вторая альтернатива совпадает. В результате переменная $1 имеет неопределенное значение, а текст поля присваивается переменной $3. Наконец, для пустого поля совпадает третья альтернатива, а переменным $1 и $3 присваиваются неопределенные значения. Все сказанное реализуется следующей командой:

push(@fields, defined($1) ? $1 :$3);

Подчеркнутая часть означает: «Использовать переменную $1, если она имеет определенное значение, или переменную $3 в противном случае». Если обе переменные имеют неопределенные значения, то переменная $3 заведомо не определена, а именно это нам и нужно от пустого поля. Таким образом, в любом случае в @fields заносится именно то значение, которое нам нужно, а сочетание цикла while, модификатора /g и принципа синхронизации позволяет обработать все поля.

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

Регулярные выражения и Путь Perl

Хотя мы немного отвлеклись на анализ использованного регулярного выражения, полагаю, этот пример поможет вам освоиться с применением регулярных выражений на Пути Perl:

l      Судя по объему переписки в электронных конференциях Perl и в моей личной почте, анализ данных, разделенных запятыми, является весьма распространенной задачей. В языках типа C и Pascal эта задача обычно решается «грубой силой», то есть перебором символов, однако в Perl к ней лучше подойти с другой стороны.

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

l      <$M[R7-55]>Задача демонстрирует некоторые потенциальные опасности, встречающиеся в ситуациях, когда структура регулярного выражения тесно связана с логикой работы программы. Например, нам хотелось бы рассматривать три альтернативы как практически независимые конструкции, однако приходится учитывать наличие в первой альтернативе двух пар круглых скобок и обращаться к скобкам второй альтернативы через переменную $3. Добавление или удаление скобок в первой альтернативе приведет к тому, что нам придется изменять все ссылки на $3, которые могут находиться в другом месте программы на некотором расстоянии от регулярного выражения.

l      В рассмотренном примере мы «изобретаем велосипед». В стандартный библиотечный модуль Perl (версии 5) Text::ParseWords входит функция quotewords[18], поэтому на самом деле задача решается совсем просто:

use Text::ParseWords;

@fields = quotewords(',', 0, $text);

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

Пришествие Perl

Ларри Уолл выпустил первую версию Perl в декабре 1987 года, и с тех пор язык постоянно обновляется. В версии 1 использовался механизм регулярных выражений, построенный на основе на rn — программе просмотра электронных новостей, написанной самим Ларри. В свою очередь, rn создавался на основе механизма регулярных выражений от Emacs Джеймса Гослинга (первая версия Emacs для Unix). Возможности этого механизма оставляли желать лучшего, и в версии 2 он был заменен улучшенной версией распространенного пакета для работы с регулярными выражениями, написанного Генри Спенсером. С появлением нового, мощного диалекта средства для работы с регулярными выражениями вскоре стали неотъемлемой частью языка Perl.

Версия 5 (сокращенно — Perl5) была официально выпущена в октябре 1994 года. Язык подвергся значительной переработке, в нем появились или были модифицированы многие средства для работы с регулярными выражениями. Одна из проблем, с которыми столкнулся Ларри Уолл при создании новых возможностей — проблема совместимости. Язык регулярных выражений Perl почти не оставлял возможностей для расширения, поэтому ему пришлось ввести ряд новых обозначений. Результаты не всегда радуют глаз; многие новые конструкции кажутся неэстетичными и новичкам, и экспертам. Но несмотря на уродство, они оказываются чрезвычайно мощными.

Perl4 и Perl5

Из-за многочисленных изменений в версии 5.000 многие пользователи продолжали работать с надежной версией 4.036 (сокращенно — Perl4). Хотя Perl5 выдержал испытание временем, на момент написания этой книги Perl4 продолжает использоваться. У всех авторов, пишущих о Perl (в данном случае — у меня) возникают проблемы. Можно ограничиться программами, которые будут работать в обоих версиях, но в этом случае вы лишаетесь многих замечательных возможностей Perl5. Например, в современной версии Perl основную часть приведенного примера можно записать в следующем виде:

push(@fields, $+) while $text =~ m{ # Стандартная строка в кавычках (

    "([^"\\]*(\\.[^"\\]*)*)",?       # ИЛИ символы до следующей запятой

|([^,]+),?                                 # ИЛИ  просто запятая.

| ,

}gx;

Эти комментарии являются частью регулярного выражения… гораздо нагляднее, не правда ли? Как будет показано ниже, другие возможности Perl5 позволяют усовершенствовать и это решение — мы вернемся к этому примеру в разделе «Все вместе» (с. <$R[P#,R7-14]>), когда объем пройденного материала позволит вам разобраться в этих возможностях.

Мне хотелось бы сконцентрироваться на Perl5, однако игнорировать Perl4 значило бы игнорировать тяжелую реальность. Я буду упоминать о важнейших отличиях Perl4 в области регулярных выражений при помощи пометок вида<$M[R7-38]> [1 с.<$R[P#,R7-37]>]. Эта пометка относится к первому примечанию к Perl4, приведенному на с. <$R[P#,R7-37]>. Perl4 существует давно, и я не вижу необходимости пересказывать страницы руководства Perl4. Обычно в примечаниях приводится краткая, конкретная информация для тех несчастных, которым приходится сопровождать программный код для обеих версий. Если ваше знакомство с Perl только начинается, вам определенно не стоит беспокоиться о старых версиях вроде Perl4.

Perl5 и…Perl5

Ситуация осложняется тем, что в горячую пору после выхода ранних версий Perl5 дискуссии в конференции USENET comp.lang.perl.misc вызвали ряд серьезных изменений в языке и в его регулярных выражениях. Например, однажды мне пришлось отвечать на сообщение с очень длинным регулярным выражением. Чтобы выражение лучше смотрелось, я разбил его на строки и добавил пробелы для пущей наглядности. Ларри Уолл увидел сообщение, подумал, что подобное оформление регулярных выражений может пригодиться, и включил в Perl5 модификатор /x, при наличии которого большинство пропусков в регулярном выражении игнорируется.

Примерно в то же время Ларри добавил конструкцию [(?#…)], позволяющую использовать встроенные комментарии в регулярном выражении. Но через несколько месяцев, после обсуждения в конференции, было внесено очередное изменение — при наличии модификатора /x символ # стал обозначать начало комментария. Это новшество появилось в версии 5.002[19]. Встречаются и другие модификации и исправления — ранние версии могут оказаться несовместимыми с приведенными примерами. Я рекомендую использовать версию 5.002 и выше.

На момент выпуска второго издания этой книги[20] версия 5.004 вышла на стадию бета-тестирования, а выход окончательной версии был намечен на весну 1997 года. Среди предполагаемых изменений в области регулярных выражений — усовершенствованная поддержка локальных контекстов, модификация поддержки pos, а также различные улучшения и оптимизации (например, табл. 7.10 должна стать почти пустой).

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

<$M[R7-9]>Существуют множество концепций, которые являются частью «Perl вообще», но представляют интерес для нашего изучения регулярных выражений. В нескольких ближайших разделах рассматриваются следующие темы:

l      Контекст. Многие функции и операторы Perl учитывают контекст, в котором они используются. Например, Perl ожидает, что в условии цикла while задается скалярная величина — главный оператор поиска из предыдущего примера повел бы себя иначе в ситуации, когда Perl ожидает список.

l      Динамическая видимость. Во многих языках программирования существуют концепции локальных и глобальных переменных. В Perl картина дополняется так называемой динамической видимостью. Динамическая область видимости временно «защищает» глобальную переменную; для этого сохраняется копия переменной, которая позднее автоматически восстанавливается. Эта любопытная концепция важна для нас, поскольку она влияет на $1 и имеет другие побочные эффекты в области регулярных выражений.

l      Обработка строк. Возможно, программиста с опытом работы на традиционных языках типа C или Pascal удивит то, что строковые «константы» в Perl в действительности представляют собой динамические операторы, наделенные широкими возможностями. Из строки даже можно вызвать функцию! Регулярные выражения Perl обычно обрабатываются очень похожим образом; при этом возникают некоторые интересные эффекты, которые будут рассмотрены ниже.

Контекст выражения

Понятие контекста играет важную роль в языке Perl, и в частности, при использовании оператора поиска. Каждое выражение может относиться к одному из двух контекстов. В списковом контексте[21] ожидается список величин, а в скалярном контексте ожидается одна величина.

Рассмотрим две команды присваивания:

$s = выражение_1;

@a = выражение_2;

Поскольку $s является простой скалярной переменной (то есть содержит одну величину, а не список), выражение_1, каким бы оно ни было, принадлежит к скалярному контексту. Аналогично, поскольку переменная @a представляет собой массив и содержит список значений, выражение_2 принадлежит к списковому контексту. Хотя выражения могут быть одинаковыми, в зависимости от контекста они могут возвращать абсолютно разные значения и иметь разные побочные эффекты.

Иногда тип выражения не соответствует типу значения, которое будет получено в результате его вычисления. В таких случаях Perl выбирает одно из двух решений: 1) выражение обрабатывается в соответствии с контекстом и возвращает ожидаемый тип; 2) результат преобразуется к нужному типу.

Обработка выражения в соответствии с контекстом

Простейшим примером является оператор файлового ввода/вывода (например, <MYDATA>). В списковом контексте он возвращает список всех (оставшихся) строк файла. В скалярном контексте возвращается следующая строка.

Многие конструкции Perl обрабатываются в соответствии с контекстом, и операторы регулярных выражений не являются исключением. Например, оператор m/…/ в одних ситуациях возвращает простую логическую величину «истина/ложь», а в других — список совпадений. Подробности будут приведены ниже.

Преобразование типа

Если обработка выражения в списковом контексте дает скалярный результат, автоматически создается список, состоящий из одного элемента. Таким образом, команда @a=42 эквивалентна @a=(42). С другой стороны, общих правил преобразования списка в скаляр не существует. Например, для литерального списка:

$ver = (this, &is, 0xA, 'list');

переменной $var присваивается последний элемент, 'list'. В команде вида $var=@array переменной $var присваивается длина массива.

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

Глобальные и закрытые переменные

В Perl существует два типа переменных: глобальные и закрытые[22] (private). Закрытые переменные, появившиеся только в Perl5, объявляются директивой my(…). Глобальные переменные вообще не объявляются, а просто существуют, начиная с момента использования. Глобальные переменные доступны в любой точке программы, а закрытые переменные с точки зрения лексики остаются доступными до конца внешнего блока. Другими словами, с закрытыми переменными может работать только код Perl, расположенный между соответствующим объявлением my и концом программного блока, внутри которого расположено объявление my.

Значения переменных с динамической видимостью

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

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

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

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

Имя функции local было выбрано на редкость неудачно. Эта функция создает новую динамическую область видимости. Я должен сразу заявить, что вызов local не создает новой переменной. Если у вас имеется глобальная переменная, local выполняет три операции:

1.     Сохранение внутренней копии значения переменной.

2.     Копирование нового значения в переменную (undef или значения, указанного при вызове local).

3.     Восстановление исходного значения переменной при выходе за пределы блока, в котором находится вызов local.

Таким образом, «локальность» в данном случае относится лишь к времени, в течение которого будут существовать изменения, внесенные в переменную. Глобальная переменная, значение которой было скопировано, по-прежнему видна из любой точки программы — если после создания новой динамической области видимости вызвать функцию, внесенные изменения будут видны в этой функции. В этом отношении переменная ничем не отличается от обычных глобальных переменных. Отличие заключается в том, что после завершения внешнего блока автоматически восстанавливается предыдущее значение.

Автоматическое сохранение и восстановления значения переменной — вот, в сущности, и все, что происходит при вызове local. Хотя при использовании local часто возникают недоразумения, на самом деле эта функция эквивалентна фрагменту, приведенному в правом столбце табл. 7.3.

Таблица 7.3. Смысл функции local

Обычный код Perl

Эквивалентный фрагмент

{

    local($SomeVar);  # Сохранить копию

    $SomeVar = 'My Value';

    …

}  # Автоматическое

   # восстановление $SomeVar

{

    my $TempCopy = $SomeVar;

    $SomeVar = undef;

    $SomeVar = 'My Value';

    …

     $SomeVar = $TempCopy;

}

Для удобства конструкции local($SomeVar) можно присвоить новое значение; это в точности эквивалентно присваиванию значения $SomeVar вместо присваивания undef в табл. 7.3. Кроме того, вы можете опустить круглые скобки, чтобы форсировать скалярный контекст.

Ссылки на $SomeVar в границах блока, а также в функциях, вызванных из блока, или инициированных из него обработчиках сигналов — короче, любые ссылки от момента вызова local до момента выхода из блока — будут относиться к величине 'MyspcValue'. Если код внутри блока (или где-то еще) модифицирует $SomeVar, это изменение отразится везде (в том числе и в программном коде блока), но при выходе из блока оно будет утрачено с восстановлением исходной копии.

Предположим, вам приходится вызывать функцию из небрежно написанной библиотеки. Функция генерирует множество предупреждений Use of uninitialized warnings. Вы, как и все порядочные программисты Perl, используете ключ -w, но автор библиотеки этого, видимо, не сделал. Предупреждения вызывают у вас нарастающее раздражение, но что делать, если изменить библиотеку невозможно — полностью отказаться от использования -w? Можно воспользоваться программным флагом выдачи предупреждений $^W<$M[R7-63]> (имя переменной ^W может состоять из двух символов, «крышка» и W, или из одного символа Control+W):

{

    local $^W = 0; # Отключить выдачу предупреждений

    &unruly_function(…);

}

# При выходе из блока восстанавливается исходное значение $^W

Вызов local сохраняет внутреннюю копию предыдущего значения глобальной переменной $^W, каким бы оно ни было. Затем той же переменной $^W присваивается новое нулевое значение. При выполнении unruly_function Perl проверяет переменную $^W, находит присвоенный ей ноль и не выдает предупреждений. При возвращении из функции переменная по-прежнему равна нулю.

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

Для полноты картины давайте посмотрим, что произойдет, если вместо local используется my[23]. Директива my создает новую переменную, которая первоначально имеет неопределенное значение. Эта переменная видна только в том лексическом блоке, в котором она была объявлена (то есть в программном коде между my и концом внешнего блока). Однако появление новой переменной никак не сказывается на других переменных, в том числе и на существующих глобальных переменных с тем же именем. Вновь созданная переменная останется невидимой для программы, в том числе и в unruly_function. В приведенном фрагменте новой переменной $^W немедленно присваивается ноль, но эта переменная нигде не используется, поэтому все усилия оказываются напрасными (во время выполнения unruly_function и принятия решения о выдаче предупреждений Perl обращается к глобальной переменной $^W, которая никак не связана с переменной, созданной нами).

Аналогия с пленкой

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

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

Расширенный пример создания динамической области видимости

Более реальный и отвечающий «Пути Perl» пример приведен в следующем листинге. Главная функция, ProcessFile, получает имя файла, открывает файл и построчно обрабатывает хранящиеся в нем команды. В этом простом примере существует всего три типа команд, которые обрабатываются соответственно в точках (6), (7) и (8). В данном случае нас интересуют глобальные переменные $filename, $command, $. и %HaveRead, а также глобальный файловый манипулятор FILE. При вызове ProcessFile для всех переменных, кроме %HaveRead, создаются динамические области видимости вызовом local в точке (3).

# Обработка команды "this"

sub DoThis    # (1)

{

    print "$filename line $.: processing $command";

    …

}

 

# Обработка команды "that"

sub DoThat    # (2)

{

    print "$filename line $.: processing $command";

    …

}

 

# Открыть файл по имени и обработать команды

sub ProcessFile

{

    local($filename) = @_;        # (3)

    local(*FILE, $command, $.);

 

    open(FILE, $filename) || die qq/can't open "$filename": $!\n/;

 

    $HaveRead($filename) = 1;     # (4)

 

    while ($command = <FILE>)

    {

           if ($command =~ m/^#include "(.*)"$/) { # (5)

              if (defined $HaveRead{$1}) {

                  warn qq/$filename $.: ignoring repeat include of "$1"\n/;

              } else {

                  ProcessFile($1); # (6)

              }

        } elsif ($command =~ m/^do-this/) {

           DoThis;          # (7)

        } elsif ($command =~ m/^do-that/) {

           DoThat;          # (8)

        } else {

           warn "$filename $.: unknown command: $command";

        }

    }

    close(FILE);

}   # (9)

При обнаружении команды do-this (7) вызывается функция DoThis, которая обрабатывает эту команду. Эта функция (1) ссылается на глобальные переменные $filename, $. и $command. Значения переменных, видимые в DoThis, были присвоены в функции ProcessFile — впрочем, функция DoThis об этом не знает, да это для нее и неважно.

Обработка команды #include начинается с выделения имени файла из строки (5). Убедившись, что файл не обрабатывался ранее, мы производим рекурсивный вызов ProcessFile (6). При новом вызове глобальные переменные $filename, $command и $., а также файловый манипулятор FILE, снова «накрываются пленкой», на которой вскоре появляются изменения, отражающие состояние и команды второго файла. В процессе обработки команд нового файла в ProcessFile и двух других функциях $filename и остальные переменные видны в программе, как и прежде.

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

Преимущества динамической видимости наглядно проявляются в тот момент, когда обработка второго файла завершается, и происходит выход из соответствующего вызова ProcessFile. При выходе из блока (9) «пленки», наложенные в точке 3, снимаются, восстанавливая значения $filename и других переменных для исходного файла. В частности, файловый манипулятор FILE теперь снова ссылается на первый файл, а не на второй.

Наконец, рассмотрим переменную %HaveRead, предназначенную для отслеживания обработанных файлов (4 и 5). Для нее динамическая область видимости намеренно не создается, поскольку эта переменная должна быть глобальной на все время выполнения сценария. В противном случае включенные файлы будут забываться при выходе из ProcessFile.

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

Но какое отношение все эти разговоры о динамической видимости имеют к регулярным выражениям? Самое прямое. В результате успешного совпадения некоторым переменным автоматически присваиваются значения — своего рода побочный эффект. К числу этих переменных, подробно описанных в следующем разделе, принадлежат, например, $& (текст совпадения) и $1 (текст, совпавший с первым подвыражением в круглых скобках). Для этих переменных динамическая область действия создается автоматически при входе в каждый блок.

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

В качестве примера рассмотрим следующий фрагмент:

if (m/(…)/)

{

    &do_some_other_stuff();

    print "the matched text was $1.\n";

}

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

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

if ($result =~ m/ERROR=(.*)/) {

    warn "Hey, tell $Config{perladmin} about $1!\n";

}

(В стандартном библиотечном модуле Config определяется ассоциативный массив %Config, элемент которого $Config{perladmin} содержит адрес электронной почты локального Perl-мастера). Если бы значение переменной $1 не сохранялось, этот код преподнес бы вам сюрприз. Дело в том, что %Config в действительности является связанной переменной; это означает, что при любой ссылке на эту переменную происходит автоматический вызов функции. В данном случае функция, осуществляющая выборку нужного значения для $Config{…}, использует регулярное выражение. Поскольку эта операция поиска выполняется между вашей операцией поиска и выводом $1, при отсутствии динамической области видимости она испортила бы значение $1, которое вы собирались использовать. К счастью, любые изменения, внесенные в функции $Config{…}, надежно изолируются благодаря динамической видимости.

Динамическая и лексическая видимость

При продуманном использовании динамическая видимость приносит немалую пользу, но легкомысленное применение local может превратить сопровождение кода в сущий кошмар. Как упоминалось выше, объявление my(…) создает закрытую переменную с лексической видимостью. Лексическая видимость закрытой переменной является противоположностью глобальной видимости глобальных переменных, однако она не имеет отношения к динамической видимости (если не считать того, что к переменным my нельзя применять local). Помните: local — это действие, а my — это действие и объявление.

Специальные переменные, изменяемые при поиске

<$R[P#,R7-34]>При успешном выполнении поиска или замены автоматически присваиваются значения некоторых глобальных, доступных только для чтения переменных, для которых автоматически создается новая динамическая область видимости [1 с. <$R[P#,R7-37]>][24]. Значения этих переменных никогда не изменяются в том случае, если совпадение не найдено, и всегда изменяются в случае, если совпадение находится. В некоторых случаях переменным может быть присвоена пустая строка (то есть строка, не содержащая ни одного символа) или неопределенное<$M[R7-131]> значение (похожее на пустую строку, но принципиально отличающееся от нее).

l      $& Копия текста, успешно совпавшего с регулярным выражением. Использовать эту переменную (вместе с другими переменными $` и $') не рекомендуется (см. раздел «Нежелательная переменная $& и ее друзья» на с. <$R[P#,R7-3]>). В случае успешного совпадения переменной $& никогда не присваивается неопределенное значение.

l      $` Копия целевого текста, предшествующего началу совпадения (то есть расположенного слева от него). В сочетании с модификатором /g иногда бывает нужно, чтобы в переменной $` хранился текст от начальной позиции поиска, а не от начала строки. К сожалению, переменная работает не так<$M[R7-40]> [2 с. <$R[P#,R7-39]>]. Если вы хотите воспроизвести такое поведение, попробуйте поставить перед регулярным выражением [\G([\x00-\xff]*?)] и затем сослаться на $1. В случае успешного совпадения переменной $` никогда не присваивается неопределенное значение.

l      $' Копия целевого текста, следующего после совпадения (то есть расположенного справа от него). После успешного совпадения строка "$`$&$'" всегда представляет собой копию исходного целевого текста[25]. В случае успешного совпадения переменной $' никогда не присваивается неопределенное значение.

l      $1, $2, $3 и т. д. Текст, совпавший с первой, второй, третьей и т. д. парой сохраняющих круглых скобок (обратите внимание: переменная $0 в список не входит — в ней хранится копия имени сценария, и эта переменная не имеет отношения к регулярным выражениям). Если переменная относится к паре скобок, не существующей в регулярном выражении или не задействованной в совпадении, ей гарантированно присваивается неопределенное значение.

Эти переменные используются после совпадения, в том числе и в строке замены оператора s/…/…/, однако в самом регулярном выражении они не используются (для этого существуют [\1] и другие метасимволы из того же семейства). См. раздел «Можно ли использовать $1 в регулярном выражении?».

Присваивание значений этим переменным наглядно демонстрирует различия между [(\w+)] и [(\w)+]. Оба регулярных выражения совпадают с одним и тем же текстом, но текст, сохраненный в круглых скобках, будет разным. Допустим, выражения применяются к строке tubby. Для первого выражения переменной $1 будет присвоена строка tubby, а для второго — символ y: квантификатор + находится вне круглых скобок, поэтому при каждой итерации текст сохраняется заново.

Кроме того, необходимо понимать отличия между [(x)?] и [(x?)]. В первом случае круглые скобки и заключенный в них текст являются необязательным элементом, поэтому переменная $1 либо равна x, либо имеет неопределенное значение. Однако для выражения [(x?)] в скобки заключено обязательное совпадение — необязательным является его содержимое. Если все регулярное выражение совпадает, то и содержимое с чем-то совпадет, хотя это «что-то» может быть «ничем» — [x?] это разрешает. Таким образом, для [(x?)] допустимыми значениями $1 являются x и пустая строка.

При обработке некоторых нестандартных ситуаций, в которых задействованы круглые скобки и итерации с применением квантификаторов, в Perl4 и Perl5 существуют некоторые различия. Для большинства читателей эта тема несущественна, но я хочу по крайней мере упомянуть о ней. Различия проявляются в том, какое значение будет присвоено переменной $2 в том случае, если выражение типа [(main(OPT)?)+] при последней успешной итерации квантификатора + совпадает только с [main], но не с [OPT]. В Perl5, поскольку подвыражение [(OPT)] не совпало при успешном последнем совпадении внешнего подвыражения, $2 присваивается неопределенное значение (что, на мой взгляд, вполне логично). Однако Perl4 в такой ситуации оставляет переменную $2 в том состоянии, которое было присвоено ей при последнем успешном совпадении [(OPT)]. Таким образом, в Perl4 переменная $2 содержит OPT в том случае, если это подвыражение совпало хотя бы один раз за все время общего поиска.

l      $+ Копия значения $1, $2 (c максимальным номером), явно присвоенного при совпадении. Если в выражении нет сохраняющих круглых скобок (или они не задействованы в совпадении), переменной присваивается неопределенное значение<$M[R7-42]> [3 с.<$R[P#,R7-41]>]. При использовании неопределенной переменной $+ Perl не выдает предупреждений.

При многократном применении регулярного выражения с модификатором /g каждая итерация заново присваивает значения этим переменным. В частности, это объясняет, почему вы можете использовать $1 в строке замены s/…/…/g, и при каждой итерации этой переменной будет соответствовать новый фрагмент текста (в отличие от операнда-выражения, операнд-замена при каждой итерации вычисляется заново; см. с. <$R[P#,R7-43]>).

Можно ли использовать $1 в регулярном выражении?

В документации Perl неоднократно указывается, что [\1] не может использоваться в качестве обратной ссылки вне регулярного выражения (вместо этого следует использовать переменную $1). Однако<$M[R7-52]> \1 — нечто большее, чем простое удобное сокращение. Переменная $1 ссылается на строку статического текста, совпавшего в результате уже завершенной операции поиска. С другой стороны, [\1] — это метасимвол регулярного выражения, который ссылается на текст, идентичный совпавшему с первым подвыражением в круглых скобках на тот момент, когда управляемый регулярным выражением механизм НКА достигает [\1]. Текст, совпадающий с [\1], может измениться в процессе поиска совпадения, с происходящими в НКА смещениями начальной позиции и возвратами.

С этим вопросом связан другой: можно ли использовать $1 в регулярном выражении, передаваемом в качестве операнда? Ответ: «Можно, но не так, как вы думаете». Переменная $1 в операнде-выражении обрабатывается точно так же, как и любая другая переменная: ее значение интерполируется (см. следующий раздел) перед началом операции поиска или замены. Таким образом, с позиций регулярного выражения значение $1 никак не связано с текущим совпадением, а наследуется от предыдущего совпадения.

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

Предварительная обработка и интерполяция переменных

Строки как операторы

<$M[R7-130]>Большинство программистов рассматривает строки как константы. Впрочем, для строк вида:

$month = "January";

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

$message = "Report for $month:";

значение, присваиваемое $message, зависит от значения переменной $month. Теоретически оно может изменяться каждый раз, когда в программе выполняется это присваивание. Заключенная в кавычки строка "Report for $month" в точности эквивалентна следующей конструкции:

'Report for ' . $month . ':'

(В общем синтаксисе Perl точка является оператором конкатенации строк; в строках, заключенных в кавычки, конкатенация выполняется косвенно).

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

"the month is $MonthName[&GetMonthNum]!"

эквивалентна выражению

'the month is ' . $MonthName[&GetMonthNum] . '!'

Функция GetMonthNum вызывается каждый раз, когда в программе вычисляется значение этой строки<$M[R7-45]> [4 с.<$R[P#,R7-44]>]. Как видите, из строк, заключенных в кавычки, можно вызывать функции — потому что кавычки являются операторами! Обычные строковые константы в Perl заключаются в апострофы. Примеры использования строк, заключенных в апострофы, встречаются в приведенных выше командах-эквивалентах.

Одна из уникальных особенностей Perl заключается в том, что для ограничения строк, заключенных в кавычки, не обязательно использовать символы ". Запись qq/…/ обеспечивает те же функциональные возможности, что и "…", поэтому конструкция qq/Report for month:/ интерпретируется по тем же правилам, что и строки в кавычках. Кроме того, нестандартные ограничители можно выбрать и для qq (вместо /). В следующем примере строка в кавычках определяется при помощи конструкции qq{…}:

warn qq{"$ARGV" line $.: $ErrorMessage\n};

Для строк в апострофах вместо qq/…/ используется обозначение q/…/. Впрочем, возможность выбора нестандартных ограничителей не является уникальной особенностью Perl — ed и его потомки поддерживают ее уже свыше 25 лет.

Регулярные выражения как строки, строки как регулярные выражения

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

$field = "From";

if ($header =~ m/^$field:/) {

}

Подчеркнутая часть интерпретируется как имя переменной и заменяется ее значением, в результате будет фактически использовано регулярное выражение [^From:]. Здесь [^$field:] — операнд, а [^From:] — регулярное выражение, полученное после предварительной обработки операнда. На первый взгляд кажется, что регулярные выражения обрабатываются точно так же, как и строки в кавычках, но в действительности существуют некоторые отличия.

Чтобы продемонстрировать логическую последовательность выполнения операций в математическом выражении ($F - 32) * 5/9, его можно сформулировать в виде:

Деление( Умножение( Вычитание($F,32), 5), 9)

Весьма поучительно взглянуть на аналогичную формулировку выражения $headerline =~ m/^$field:/:

Поиск( $headerline, Предварительная_обработка(^field:))

Таким образом, ^$field может рассматриваться как операнд только косвенно, поскольку эта строка должна пройти предварительную обработку. Рассмотрим более сложный пример<$M[R7-49]>:

$single = qq{'([^'\\\\]*(?:\\\\.[^'\\\\]*)*)'}; # строка в апострофах

$double = qq{"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"}; # строка в кавычках

$string = "(?:$single|$double)"; # один из типов строки

while (<CONFIG>) {

    if (m/^name=$string$/o) {

        $config{name} = $+;

    } else {

Построение переменной $string и использование ее в регулярном выражении гораздо нагляднее непосредственной записи всего регулярного выражения:

if (m/^name=(?:'[^'\\]*(?:\\.[^'\\]*)*)'|"[^"\\]*(?:\\.[^'\\]*)*")$/o) {

При построении регулярных выражений в строковом виде необходимо помнить о о некоторых важных обстоятельствах, в том числе о дополнительном символе \, модификаторе /o и использовании не-сохраняющих круглых скобок (?:…). Нетривиальный пример рассматривается в разделе «Поиск адресов электронной почты» (с. <$R[P#,R7-15]>). А пока давайте сосредоточимся на том, как регулярное выражение-операнд добирается до механизма регулярных выражений. Чтобы лучше понять, как это происходит, мы посмотрим, как Perl обрабатывает следующий фрагмент программы:

$header =~ m/^\Q$dir\E # Базовый каталог

           \/        # Разделитель

           (.*)         # Сохранить остальные символы (имя файла)

           /xgm;

Допустим, переменная $dir содержит строку ~/.bin.

Пара метасимволов \Q…\E, окружающая ссылку на переменную $dir — одна из возможностей обработки строк в кавычках, особенно удобная для регулярных выражений-операндов. Большинство символов, находящихся между ними, экранируется префиксом \. Когда результат используется как регулярное выражение, он совпадает с литеральным текстом, заключенным в конструкции \Q…\E, даже если некоторые символы этого литерального текста в другой ситуации интерпретировались бы как метасимволы регулярного выражения (в нашем примере ~/.bin экранируются первые три символа, хотя экранирование необходимо только для точки).

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

На рис. 7.1 показано, как данные передаются от необработанного сценария к регулярному выражению-операнду, затем к настоящему регулярному выражению, и как они затем используются при поиске. Некоторые из этих фаз являются необязательными. Например, лексический анализ (просмотр сценария и принятие решений о том, что является командой, строкой, регулярным выражением и т. д.) выполняется всего один раз при первоначальной загрузке сценария (или при использовании eval со строковым операндом — при каждом повторном вычислении eval). Это первая фаза на рис. 7.1. Другие фазы могут выполняться в другое время и даже могут повторяться. Рассмотрим происходящее подробнее.

Фаза — идентификация операнда

Во время первой фазы Perl просто пытается определить лексические границы регулярного выражения-операнда. Perl находит m, оператор поиска, и по нему узнает, что в сценарии следует искать регулярное выражение-операнд. В точке 1 Perl опознает символ / в качестве ограничителя операнда, начинает искать парный ограничитель и находит его в точке 4. В этой фазе аналогичные операции выполняются со строками и другими конструкциями, и никаких особых операций, связанных со спецификой регулярных выражений, здесь не выполняется. Единственное преобразование, выполняемое в этой фазе — удаление обратной косой черты из экранированного закрывающего ограничителя<$M[R7-47]> [5 с.<$R[P#,R7-46]>].

Фаза — предварительная обработка

<$M[R7-70]>Во время второй фазы выделенный операнд обрабатывается почти по тем же правилам, что и строка в кавычках. Происходит интерполяция переменных, обработка \Q…\E и других аналогичных конструкций (полный список приведен в табл. 7.8 на с. <$R[P#,R7-48]>). В нашем примере значение $dir интерполируется под влиянием \Q, поэтому в операнд реально вставляется строка \~\/\.bin.

При некотором внешнем сходстве в этой фазе проявляются и различия между обработкой строк в кавычках и регулярных выражений-операндов. В фазе B Perl понимает, что он работает с регулярным выражением-операндом, и поэтому выполняет некоторые специфические операции. Например, в строках, заключенных в кавычки, \b и \3 всегда обозначают соответственно забой и восьмеричный код символа. Однако в регулярном выражении они также могут обозначать соответственно метасимвол границы слова и обратную ссылку в зависимости от их местонахождения в регулярном выражении — по этой причине фаза B оставляет эти последовательности без изменений, чтобы позднее механизм регулярных выражений обработал их так, как считает нужным. Другое различие состоит в том, что считается или не считается ссылкой на переменную. Например, последовательность вида $| в строке всегда рассматривается как ссылка на переменную, однако в операнде она будет передана механизму регулярных выражений для интерпретации метасимволов [$] и [|]. Аналогично<$M[R7-128]>, $var[2-7] в строке всегда интерпретируется как ссылка на элемент -5 массива @var (что означает пятый элемент, считая от конца), но в регулярном выражении-операнде эта последовательность интерпретируется как ссылка на $var, за которой следует символьный класс. При желании можно форсировать интерпретацию ссылки на массив при помощи записи ${…}:

${var[2-7]}

Из-за выполнения интерполяции результат этой фазы может зависеть от значений переменных (изменяющихся в процессе выполнения программы). В этом случае фаза B выполняется лишь при достижении соответствующего фрагмента программы во время ее выполнения. Дополнительная информация по этому важному вопросу приведена в разделе «Проблемы эффективности в Perl» (с. <$R[P#,R7-6]>)<$M[R7-117]>.

 

Рис. 7.1. Обработка регулярного выражения в Perl от текста программы до механизма регулярных выражений

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

Фаза C — обработка /x

Фаза C относится только к регулярным выражениям-операндам, применяемым с модификатором<$M[R7-22]> /x. В этой фазе из выражения удаляются все пропуски (кроме расположенных в символьных классах) и комментарии. Поскольку это происходит после интерполяции переменных в фазе B, пропуски и комментарии, внесенные в регулярное выражение из переменных, также удаляются. Конечно, это удобно, однако необходимо учитывать одно возможное затруднение<$M[R7-126]>. Комментарии # продолжаются до следующего символа новой строки или до конца операнда — но не «до конца интерполированной строки»! Предположим, мы включили комментарий в переменную $single на с. <$R[P#,R7-49]>:

$single = qq{'(…выражение…)' # for singlequoted strings};

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

[^name=(?:'(…)'spc#spcforspcsinglequotedspcstrings|"(…)")$]

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

Предположим, мы воспользуемся следующим выражением<$M[R7-127]>:

$single = qq{'(…выражение…)' # for singlequoted strings\n};

На этот раз все нормально, поскольку \n интерпретируется по правилам строк в кавычках, и в регулярное выражение включается нужный символ новой строки. Но если вместо qq(…) использовать запись q(…), в регулярное выражение будет включен необработанный метасимвол \n, который совпадает с символом новой строки, но не является символом новой строки. Таким образом, он не будет считаться признаком завершения комментария и будет удален вместе с другими символами[26].

Фаза — компиляция регулярного выражения

Результат фазы C представляет собой полноценное регулярное выражение, используемое механизмом для поиска совпадений. Механизм не применяет выражение напрямую, а компилирует его во внутреннее представление. Я называю эту стадию «фазой D». Если на фазе B не производилась интерполяция переменных, одну и ту же откомпилированную форму можно было бы применять при каждом выполнении оператора поиска в процессе работы программы — это обеспечило бы немалую экономию времени. С другой стороны, наличие интерполяции означает, что при многократном выполнении этого оператора в программе могут использоваться разные регулярные выражения, поэтому фазы BD приходится каждый раз выполнять заново. Важным последствиям этого факта посвящен раздел «Компиляция регулярных выражений, модификатор /o и эффективность» (с. <$R[P#,R7-50]>). Также за дополнительной информацией по этой теме обращайтесь к разделу «Кэширование при компиляции» главы 5 (с. <$R[P#,R5-6]>).

Диалект регулярных выражений Perl

<$M[R7-10]>Разобравшись с некоторыми важными, но все же второстепенными вопросами, мы переходим собственно к диалекту регулярных выражений Perl. В Perl используется традиционный механизм НКА. Базовый набор метасимволов внешне напоминает egrep, однако на этом все сходство с egrep заканчивается. Важнейшие различия связаны с типом механизма и множеством дополнительных метасимволов, обеспечивающих как удобство записи, так и расширенный набор возможностей. В электронной документации Perl приведена лишь краткая сводка диалекта регулярных выражений Perl, поэтому я постараюсь предоставить более подробные сведения.

Квантификаторы — максимальные и минимальные

В Perl реализованы обычные максимальные квантификаторы, однако в Perl5 появились их минимальные аналоги, о которых говорилось в главе 4. Максимальные<$M[R7-18]> квантификаторы иногда называются «жадными» (greedy), а минимальные<$M[R7-19]> — «ленивыми» (lazy). Квантификаторы Perl перечислены в табл. 7.4.

Таблица 7.4. Квантификаторы Perl (максимальные и минимальные)

Количество повторений

Традиционный максимальный квантификатор

Минимальный квантификатор[27]

Произвольное (0, 1 и более)

*

*?

Одно и более

+

+?

Необязательный элемент (0 или 1)

?

??

В заданном интервале (не меньше мин и не больше макс)

{мин,макс}

{мин,макс}?

Нижняя граница (не меньше мин)

{мин, }

{мин, }?

Точное заданное число[28]

{число}

{число}?

Минимальные версии являются примером расширений диалекта регулярных выражений, появившихся в Perl5. Традиционно конструкция вида *? не имела смысла в регулярном выражении (более того, в Perl4 она считается синтаксической ошибкой), поэтому Ларри счел возможным закрепить за ней новый смысл. Также предлагалось для минимальных квантификаторов выбрать обозначения вида **, ++ и т. д. Наверное, в этом что-то есть, однако проблемы с выбором записи для [{мин,макс}] заставили Ларри выбрать вариант с присоединением вопросительного знака. Конструкция [**] и ее аналоги остаются для будущих расширений.

Эффективность минимальных квантификаторов

Многие аспекты влияния минимальных квантификаторов на эффективность рассматривались в главе 5, в разделах «Подробный анализ влияния круглых скобок и возвратов на быстродействие» (см. с. <$R[P#,R5-21]>) и «Простое повторение» (см. с. <$R[P#,R5-2]>). Другие последствия напрямую обусловлены природой механизма НКА Perl, управляемого регулярными выражениями.

Выбирать между максимальными и минимальными квантификаторами приходится не так уж часто, поскольку они имеют столь разный смысл. Но если такая ситуация все же возникает, выбор будет зависеть от ситуации. Проанализировав количество возвратов, которые потребуются в том и в другом случае для получения желаемого результата, можно принять обоснованное решение. Например, в статье из августовского номера «The Perl Journal» за 1996 год я исследую несколько вариантов решения простой задачи, в том числе и решения с использованием максимальных и минимальных квантификаторов.

Минимальные конструкции и инвертированные символьные классы

Я часто вижу, что программисты используют конструкции с минимальными квантификаторами как удобную замену для инвертированных символьных классов — например, [<(.+?)>] вместо [<([^>]+)>]. Иногда подобная замена работает, хотя и менее эффективно — цикл, связанный с квантификатором * или +, должен постоянно останавливаться и проверять, совпадает ли оставшаяся часть регулярного выражения. В частности, в приведенном примере она сопряжена с временным выходом из круглых скобок, что, как было показано в главе 5, также связано с определенными затратами (с. <$R[P#,R5-4]>). Даже несмотря на то, что минимальную конструкцию легче ввести и, вероятно, легче прочитать, не совершите ошибки: они могут совпадать с разным текстом.

Прежде всего, при отсутствии модификатора /s (с. <$R[P#,R7-51]>) точка в подвыражении [.+?] не совпадает с символом новой строки, а в инвертированном классе [[^>]+] — совпадает.

Более серьезная проблема, которую нетрудно упустить из виду, проявляется при простой разметке текста, в которой конструкция <…> используется для смыслового выделения:

Fred was very, <very> angry. <Angry!> I tell you.

Предположим, вы хотите особым образом обработать помеченный текст, который завершается восклицательным знаком. Одно из выражений, совпадающее с помеченным текстом, выглядит так: [<([^>]*!)>]. Однако выражение [<(.*?!)>], даже в сочетании с модификатором /s, дает совершенно иной результат. Первое выражение совпадает с …Angry!…, а второе — с …very> angry. <Angry!….

Необходимо запомнить, что инвертированный класс в [[^>]*>] никогда не совпадает с >, тогда как минимальная конструкция в [.*?>] — совпадает, если это необходимо для достижения общего совпадения. Если ничто после минимальной конструкции не заставляет механизм регулярных выражений выполнить возврат, эта проблема не возникает. Тем не менее, как демонстрирует этот пример с восклицательным знаком, необходимость совпадения позволит минимальной конструкции преодолеть точку, за которую она не должна заходить (а инвертированный класс зайти не сможет).

Несомненно, минимальные конструкции являются самым замечательным расширением диалекта регулярных выражений Perl5, однако ими следует пользоваться с осторожностью. Минимальная конструкция [.*?] почти никогда не является сколько-нибудь приемлемой заменой для [[^…]*] — она может подойти для конкретной ситуации, но принципиальные различия в смысле этих конструкций могут стать причиной ошибок.

Группировка

Как неоднократно упоминалось выше, круглые скобки традиционно выполняли две функции: группировка и сохранение совпавшего текста в переменных $1, $2 и т. д. (внутри регулярного выражения для ссылок на совпавший текст используются метасимволы<$M[R7-28]> \1, \2 и т. п.) Как объясняется на с. <$R[P#,R7-52]>, чисто внешними различиями дело не ограничивается. Кроме символьных классов, в которых обратные ссылки не имеют смысла, обозначения с \1 по \9 всегда относятся к обратным ссылкам. Дополнительные обозначения (\10, \11, …) могут использоваться в том случае, если того требует количество сохраняющих круглых скобок в выражении (с. <$R[P#,R7-53]>).

Одной из уникальных особенностей Perl является существование двух разновидностей круглых скобок: традиционные круглые скобки (…) для группировки и сохранения, и появившаяся в Perl5 конструкция<$M[R7-20]> (?:…), ограничивающаяся группировкой. В (?:…) «открывающая круглая скобка» в действительности представляет собой трехсимвольную последовательность (?:, тогда как «закрывающая скобка» выглядит стандартно.

Как и последовательности, использованные для обозначения минимальных квантификаторов, последовательность (? ранее считалась синтаксической ошибкой. Начиная с версии 5, она используется некоторыми расширениями языка регулярных выражений, из которых (?:…) является лишь одним частным случаем. Вскоре мы встретимся и с другими примерами.

Сохраняющие и не-сохраняющие круглые скобки

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

l      Повышение эффективности поиска за счет отказа от затрат (возможно, немалых) на сохранение текста, который вы не собираетесь использовать.

l      Возможность проведения улучшенных внутренних оптимизаций. Подвыражения, которые не нужно изолировать для целей сохранения, могут быть преобразованы механизмом к более эффективному виду. Например, выражения [(?:return-to|reply-to):spc] и [re(?:turn-to:spc|ply-to:spc)] логически эквивалентны, но второе выражение быстрее обнаруживает как совпадения, так и неудачи (если вы мне не верите, воспроизведите применение этих выражений к тексту forward-spcandspcreply-tospcfields…). В настоящее время механизм регулярных выражений Perl этого не делает, но возможно, в будущем ситуация изменится.

l      Удобство построения регулярных выражений в строковом виде.

Возможно, с точки зрения пользователя максимальную пользу приносит последний пункт. Вспомните пример с анализом данных, разделенных запятыми: поскольку в первой альтернативе использовались две пары круглых скобок, вторая альтернатива сохранялась в переменной $3 (с. <$R[P#,R7-54]> и <$R[P#,R7-55]>). При каждом изменении количества круглых скобок, используемых в первой альтернативе, приходится изменять все последующие ссылки. Сопровождение программы превращается в настоящий кошмар, однако не-сохраняющие круглые скобки могут сильно упростить вашу задачу[29].

По тем же причинам количество сохраняющих круглых скобок играет важную роль при использовании m/…/ в списковом контексте или с оператором split. Конкретные примеры будут приведены ниже в соответствующих разделах (с. <$R[P#,R7-56]>, <$R[P#,R7-57]>).

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

<$M[R7-1]>В Perl5 также появились конструкции опережения (?=…) и (?!…). Конструкция позитивной опережающей проверки [(?=подвыражение)], по аналогии с обычными не-сохраняющими круглыми скобками, истинна в том случае, если подвыражение совпадает. Однако подвыражение в этой конструкции не «поглощает» символов целевой строки — конструкция опережающей проверки, как и метасимволы границы слова, совпадает с позицией строки. Таким образом, опережающая проверка не изменяет содержимого переменной $& или любых внешних круглых скобок. Это позволяет механизму регулярных выражений «заглянуть вперед» без каких-либо последствий.

Конструкция негативной<$M[R7-8]> опережающей проверки [(?!подвыражение)] истинна в том случае, если совпадение для подвыражения в строке не существует. На первый взгляд негативное опережение кажется логическим аналогом инвертированного символьного класса, однако между ними существуют два принципиальных различия:

l      Для успешного совпадения инвертированный символьный класс должен с чем-то совпадать, что приводит к поглощению текста. Негативная опережающая проверка успешна в том случае, если подвыражение не совпадает ни с чем. Это обстоятельство продемонстрировано во втором и третьем примерах, приведенных ниже.

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

Рассмотрим несколько примеров опережающей проверки:

[Bill(?=spcThe Cat|spcClinton)]

Совпадает с Bill, но лишь в том случае, если дальше следует spcThespcCat или spcClinton.

[\d+(?!\.)]

Совпадает с числом, если за ним не следует точка.

[\d+(?=[^.])]

Совпадает с числом, если за ним следует не точка, а что-то другое. Убедитесь в том, что вы понимаете, чем это выражение отличается от предыдущего — представьте, что число находится в самом конце строки. Впрочем, я лучше задам очередной вопрос. <$M[R7-61]>Какое из двух последних выражений совпадет в строке OHspc44272 и где именно? refПодумайте над этим вопросом, затем переверните страницу и проверьте свой ответ.

[^(?![A-Z]*$)[a-zA-Z]*$]

Совпадает, если объект состоит только из одних букв, но не только в верхнем регистре.

[^(?=.*?this)(?=.*?that)]

Весьма изобретательный (хотя и не самый разумный) способ проверки того, существуют ли в целевом тексте совпадения для [this] и для [that]. Более логичное и в целом совместимое решение — двойное регулярное выражение /this/ && /that/[30].

Другие характерные примеры приведены в разделе «Синхронизация совпадений» (с. <$R[P#,R7-58]>). Для вашего развлечения приведу особенно хитрый пример, взятый из раздела «Разделение групп разрядов запятыми» (с. <$R[P#,R7-59]>)<$M[R7-62]>:

s<

    (\d{1,3}            # От одной до трех цифр перед запятой

    (?=              # После чего следуют, но не включаются в совпадение...

        (?:\d\d\d)+ #    Некоторое количество триплетов...

        (?!\d)       #      ...за которыми не следует очередная цифра

    )                   #      (иначе говоря, число завершается)

><$1,>gx

Круглые скобки опережающей проверки не сохраняют текста и не считаются парами круглых скобок при нумерации переменных. Тем не менее, они могут содержать круглые скобки для сохранения «предположительно совпадающего» текста. Хотя я не рекомендую злоупотреблять этой возможностью, в некоторых ситуациях она полезна. Например, [(.*?)(?=<(strong|em)\s*>)] совпадает со всеми символами до тегов HTML <strong> или <em>, но не включая их. Поглощенный текст присваивается переменной $1 (и, конечно, переменной $&), а сам тег <strong> или <em>, благодаря которому стало возможным совпадение, присваивается переменной $2. Если вас не интересует, какой именно тег привел к успешному прекращению поиска, конструкцию […(strong|em)…] лучше записать в виде […(?:strong|em)…], чтобы избежать лишнего сохранения текста. В примере на с. <$R[P#,R7-60]> подвыражение [(?=(.*))] применяется для имитации $& (из-за больших затрат использовать $& не рекомендуется — см. с. <$R[P#,R7-3]>).

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

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

Позитивная и негативная опережающая проверка

bref Ответ на вопрос со с. <$R[P#,R7-61]>

Оба выражения, [\d+(?!\.)] и [\d+(?=[^.])], совпадают в строке OHspc44272. Первое совпадает с OHspc44272, а второе — с OHspc44272.

Помните: максимальный квантификатор всегда отступает, если это необходимо для получения общего совпадения. Поскольку [\d+(?=[^.])] требует, чтобы после совпавшего числа следовал символ, отличный от точки, квантификатор уступает часть числа, чтобы при необходимости она могла интерпретироваться как «не-точка».

Трудно сказать, для чего могли бы понадобиться такие выражения, но их, вероятно, следовало бы записать в виде [\d+(?![\d.])] и [\d+(?=[^.\d])].

Например, выражение [(?!000)\d\d\d] означает «совпадение с тремя цифрами, если это не 000». Тем не менее, вы должны хорошо понимать, что оно не означает «совпадение с тремя цифрами, если им не предшествует 000». Это уже было бы ретроспективной проверкой, которая не поддерживается в Perl и вообще ни в одном из известных мне диалектов регулярных выражений. Впрочем, любой начальный якорь (тип, строка или слово) можно рассматривать как ограниченную разновидность ретроспективной проверки.

Опережающая проверка часто используется в конце выражения, чтобы предотвратить совпадение в том случае, если то, что за ним следует, удовлетворяет (или наоборот, не удовлетворяет) определенным условиям. Хотя использование этой конструкции в начале выражения может быть признаком ошибочной реализации ретроспективной проверки, в некоторых ситуациях это помогает сделать выражение более общим. Кроме примера с 000, следует упомянуть<$M[R7-2]> об использовании [(?!0+\.0+\.0+\.0+\b)] для подавления IP-адресов, состоящих из одних нулей (см. главу 4, с. <$R[P#,R4-33]>). Как было показано в разделе «Возврат с глобальной точки зрения» главы 5 (с. <$R[P#,R5-22]>), опережающая проверка в начале выражения может оказаться эффективным средством для ускорения поиска.

Будьте внимательны с негативной опережающей проверкой в начале выражения. Выражение [\w+] совпадает с первым словом в строке, однако поставить в начало [(?!cat)] недостаточно для того, чтобы выражение означало «первое слово, не начинающееся с cat». Выражение [(?!cat)\w+] не может совпасть в начале cattle, но ничто не помешает ему совпасть с cattle. Для получения желаемого эффекта необходимо принять дополнительные меры — например, [\b(?!cat)\w+].

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

Конструкция (?#…) воспринимается как комментарий<$M[R7-21]> и игнорируется. Ее содержимое не может быть абсолютно произвольным, поскольку все копии ограничителя операнда должны экранироваться[31].

Комментарии (?#… появились в версии 5.000, однако модификатор /x в версии 5.002 позволяет включать в регулярные выражения простые комментарии [#], продолжающиеся до следующего символа новой строки (или до конца регулярного выражения) по аналогии с комментариями в обычном коде (пример приведен на с. <$R[P#,R7-62]>). Модификатор /x также обеспечивает игнорирование большинства пропусков, поэтому выражение

$text =~ m/"([^"\\]*(\\.[^"\\]*)*)",?|([^,]+),?|,/g

из этого примера можно записать в более наглядном виде:

$text =~ m/                # Поле может относиться к одному из трех типов:

                           # 1) СТРОКА В КАВЫЧКАХ

"([^"\\]*(\\.[^"\\]*)*)" # - найти строку в кавычках и сохранить в $1

,?                     # - удалить конечную запятую

        |            # - ИЛИ -

                           # 2) ОБЫЧНОЕ ПОЛЕ

([^,]+)                    # - Сохранить текст до следующей запятой в $3

,?                     # - (удалить запятую, если она имеется)

        |            # - ИЛИ -

                           # 3) ПУСТОЕ ПОЛЕ

,                          # Просто запятая

/gx;

Регулярное выражение нисколько не изменилось. Как и в случае с комментариями (?#…), приходится помнить о закрывающем ограничителе регулярного выражения, поэтому содержимое комментария не является абсолютно произвольным. Как и многие другие метасимволы регулярного выражения, [#] и метасимволы пропусков, активизируемые модификатором /x, недоступны в символьных классах, поэтому классы не могут содержать комментариев или игнорируемых пропусков. Пропуски и #, как и другие метасимволы регулярных выражений, могут экранироваться с выражениях с модификатором /x:

m{

^                # Начало строки

(?:       # Далее следует одна из альтернатив:

        From      # From

        |Subject     # Subject

        |Date     # Date

)         #

:                # Затем следует двоеточие...

\ *       # ...и любое количество пробелов (обратите внимание на \)

(.*)             # Сохранить остаток строки (без символа новой строки) в $1

}x;

Другие конструкции (?…)

В специальной конструкции<$M[R7-23]> [(?модификаторы)] также используется запись (?, упоминавшаяся в предыдущем разделе, но для других целей. Традиционно для поиска без учета регистра использовался модификатор /i. Того же эффекта можно добиться и другим способом — включив в произвольную позицию регулярного выражения (обычно в начало) конструкцию [(?i)]. При помощи этой синтаксической конструкции можно задавать модификаторы /i (поиск без учета регистра), /m (многострочный режим), /s (однострочный режим) и /x (произвольное форматирование). Допускается объединение модификаторов; скажем, конструкция конструкция [(?si)] равносильна одновременному использованию модификаторов /i и /s.

Якорные метасимволы

Якорные метасимволы оказывают неоценимую помощь при построении надежных выражений. В Perl существует несколько разновидностей этих метасимволов.

Логические строки и простой текст

В Perl поддерживаются традиционные метасимволы<$M[R7-25]> ^ и $[32], однако они интерпретируются несколько сложнее, чем обычное начало или конец строки. При типичном простом использовании регулярного выражения в цикле while (<>) (при условии, что разделитель входных записей $/ сохраняет значение по умолчанию) вы знаете, что проверяемый текст содержит ровно одну логическую строку, поэтому различия между «началом логической строки» и «началом фрагмента» несущественны.

Но если текст содержит внутренние символы новой строки, было бы логично интерпретировать его как совокупность нескольких логических строк. Существует немало способов создания текста с внутренними символами новой строки (например, "this\nway"). Но если регулярное выражение применяется к тексту, независимо от его происхождения, где должно совпадать подвыражение [^…] — в начале каждой логической строки или только в начале всего многострочного фрагмента?

Perl позволяет выбрать любой из этих вариантов. Более того, в нем предусмотрено четыре разных режима, описанных в табл. 7.5.

Таблица 7.5. Режимы обработки логических строк

Режим

Интерпретация целевого текста метасимволами ^ и $

Точка

Стандартный

Единый фрагмент, внутренние новые строки не учитываются

Не совпадает с символом новой строки

Однострочный режим

Единый фрагмент, внутренние новые строки не учитываются

Совпадает со всеми символами

Многострочный режим

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

(То же, что и в стандартном режиме)

Чистый многострочный режим

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

Совпадает со всеми символами

При отсутствии внутренних символов новой строки в целевом тексте все режимы идентичны[33].

Возможно, вы заметили, что в табл. 7.5 не слова не сказано о том…

l      откуда в целевом тексте появились внутренние логические строки (это несущественно);

l      когда можно применять выражение к такой строке (в любой момент);

l      может ли [\n], соответствующий восьмеричный код или даже [[^x]] совпадать с символом новой строки (это всегда возможно).

При внимательном анализе табл. 7.5 выясняется, что различия между этими режимами сводятся к тому, как три метасимвола (^, $ и .) интерпретируют символ новой строки.

Стандартное поведение символов ^, $ и .

По умолчанию символ ^ в Perl совпадает только в начале фрагмента. Символ $ может совпадать в конце фрагмента или непосредственно перед символом новой строки, завершающим фрагмент. Последнее уточнение выглядит несколько странно, но оно вполне логично, если учесть, в каких условиях обычно работает Perl-программа: входные данные читаются построчно, а завершающий символ новой строки обычно считается частью данных. Поскольку в стандартном режиме точка<$M[R7-16]> не совпадает с символом новой строки, это правило позволяет [.*$] распространиться на весь текст до последнего символа новой строки, не включая его. Термин «стандартный режим» придумал я сам. Пользуйтесь на здоровье.

/m и «многострочный режим»

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

Итак, я повторяю снова:

l      Модификатор /m влияет только на интерпретацию символов новой строки метасимволами [^] и [$].

Модификатор /m действует только при поиске совпадений регулярного выражения, конкретнее — только по отношению к метасимволам ^ и $. Он не имеет абсолютно никакого отношения к чему-либо еще. Вероятно, многострочный режим было бы правильнее называть «режимом-в-котором-строковые-якори-учитывают-внутренние-символы-новой-строки». Возможно, из всех простых возможностей Perl именно /m и многострочный режим вызывают больше всего недоразумений, поэтому я хочу еще раз четко оговорить, что модификатор /m не имеет ничего общего с тем…

l      можно ли работать с данными, содержащими внутренние символы новых строк. Вы всегда можете работать с любыми данными по своему желанию. [\n] всегда совпадает с символом новой строки независимо от того, действует ли при этом многострочный режим или нет.

l      совпадает ли точка (или любой другой метасимвол) с символом новой строки. Модификатор /s влияет на точку<$M[R7-17]>, но модификатор /m — нет. Модификатор /m влияет лишь на то, могут ли якорные метасимволы совпадать в позиции символа новой строки. С концептуальных позиций нельзя сказать, что это никак не связано с тем, совпадает ли точка с самим символом новой строки, поэтому я часто использую удобный термин «многострочный режим» для описания стандартного поведения точки, однако эта связь является второстепенной.

l      каким образом данные, многострочные или нет, попадают в целевой текст. Многострочный режим часто используется в сочетании с другими полезными возможностями Perl, но эта связь также второстепенна. Главную роль здесь играет переменная $/, определяющая разделитель входных записей. Если присвоить ей пустую строку, <> и другие аналогичные конструкции переходят в режим чтения абзацев, при котором возвращаются все логические строки, объединенные в один фрагмент, до следующей пустой логической строки включительно. Если присвоить $/ значение undef, Perl переходит в режим поглощения файла (file slurp), при котором все содержимое файла (вернее, его оставшейся части) возвращается в виде одного фрагмента[34].

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

Модификатор /m появился в Perl5 — в Perl4 использовалась специальная переменная $*, которая в настоящее время считается устаревшей<$M[R7-32]>. Эта переменная включала многострочный режим для всех поисков совпадения. Если переменная $* истинна, ^ и $ ведут себя так же, как при наличии модификатора /m. Это не так удобно, как включение многострочного режима для отдельной операции, поэтому в современных программах $* не используется. Однако современным программистам часто приходится сталкиваться с использованием этой переменной в старых или не подчиняющихся общим правилам библиотеках, поэтому в дополнение к /m поддерживается модификатор /s.

Однострочный режим

Модификатор<$M[R7-51]> /s отменяет особую интерпретацию символов новой строки метасимволами ^ и $, даже если переменная $* истинна. Кроме того, модификатор влияет на работу метасимвола «тоска»: в сочетании с /s точка совпадает с любым символом. Уж если вы воспользовались модификатором /s и указали, что логические строки вас не интересуют, точка также не должна иметь особой интерпретации для символа новой строки.

Чистый многострочный режим

При совместном использовании модификаторов /m и /s возникает то, что я называю «чистым» (clean) многострочным режимом. Он аналогичен обычному многострочному режиму, за исключением того, что точка в нем совпадает с любым символом (вследствие влияния /s). Как мне кажется, исключение особой интерпретации точки обеспечивает более последовательное, более «чистое» поведение, отсюда и название.

Метасимволы начала и конца фрагмента

В Perl5 также появились метасимволы<$M[R7-26]> [\A] и [\Z], совпадающие в начале и конце фрагмента. Эти метасимволы никогда не учитывают присутствия внутренних символов новой строки. Они ведут себя точно так же, как стандартные и однострочные версии ^ и $, однако они могут использоваться даже в сочетании с модификатором /m и при истинном значении переменной $*.

Особая роль символа новой строки

Существует ситуация, при которой символ новой строки всегда обрабатывается особым образом. Независимо от действующего режима всегда допускается совпадение конструкций [$] и [\Z] перед символом новой строки, завершающим текст. В Perl4 регулярное выражение не могло потребовать «абсолютного» окончания фрагмента. В Perl 5 при необходимости можно воспользоваться выражением […(?!\n)$]. С другой стороны, если вы хотите особо выделить завершающий символ новой строки, просто воспользуйтесь выражением […\n$] в любой версии Perl.

Подавление предупреждений при использовании $*

<$M[R7-118]>В сценариях Perl обычно не рекомендуется использовать $*, однако иногда приходится писать программный код, рассчитанный и на поддержку Perl4. Если выдача предупреждений включена (как это обычно и должно быть), то при обнаружении $* Perl5 выдает предупреждение. Эти предупреждения сильно раздражают, но вместо того, чтобы отключать их для всего сценария, я рекомендую использовать фрагменты вида:

{ local($^W) = 0; eval'$* = 1' }

В этом варианте предупреждения отключаются на время модификации $*, а затем включаются снова — эта методика подробно рассматривалась выше, при описании динамической видимости переменных (с. <$R[P#,R7-63]>).

/m и (?m), /s и /m

Как упоминалось на с. <$R[P#,R7-23]>, некоторые модификаторы можно включать при помощи конструкции [(?мод)] непосредственно в регулярном выражении — например, вместо /m можно воспользоваться выражением [(?m)]. В Perl не существует хитроумных правил, которые бы описывали потенциальные конфликты модификатора /m с [(?m)] или регламентировали позиции регулярного выражения, в которых может находиться [(?m)]. Простое включение /m или [(?m)] (в любом месте регулярного выражения) активизирует многострочный режим для всего совпадения.

Хотя у программистов иногда возникает искушение менять режим на ходу, используя конструкции вида [(?m)…(?s)…(?m)…], строчный режим является универсальной характеристикой всего совпадения независимо от того, каким способом он активизирован.

При объединении /s с /m модификатор /m обладает более высоким приоритетом по отношению к метасимволам ^ и $. Тем не менее, наличие или отсутствие /m никак не влияет на то, совпадает ли точка с символом новой строки или нет — стандартное поведение точки изменяется только явным включением /s. Таким образом, активизация обоих режимов создает то, что я назвал «чистым многострочным режимом».

На первый взгляд кажется, что обилие режимов и комбинаций только усложняет ситуацию, но приведенная в табл. 7.6 сводка поможет вам разобраться в происходящем. Основной принцип можно сформулировать так: «/m определяет многострочный режим, а /— возможность совпадения точки с символом новой строки».

Все остальные конструкции работают так же, как прежде. [\n] всегда совпадает с символом новой строки. Символьный класс всегда может использоваться для совпадения или исключения символов новой строки. Инвертированные символьные классы вида [[^x]] всегда совпадают с символом новой строки (если, конечно, класс не содержит \n). Помните об этом, если решите заменить конструкцию вида [.*] более жестким (на первый взгляд) выражением [[^…]*].

Таблица 7.6. Сводка строчных режимов

См. расшифровку в файле pict!

Привязка к текущей позиции при глобальном поиске

В Perl5 появился новый якорный метасимвол<$M[R7-27]> [\G], который напоминает метасимвол [\A], адаптированный для использования с модификатором /g. При поиске первого совпадения /g или при отсутствии модификатора /g этот метасимвол ведет себя точно так же, как и [\A].

Пример использования \G

Рассмотрим довольно длинный пример, который может показаться несколько неестественным, но на самом деле хорошо демонстрирует некоторые важные обстоятельства. Предположим, данные представляют собой сплошную последовательность почтовых индексов США, каждый из которых состоит из 5 цифр. Вы хотите выбрать из потока все индексы, начинающиеся, допустим, с цифр 44. Ниже приведен пример данных с выделением нужных индексов жирным шрифтом:

03824531449411615213441829503544272752010217443235

Для начала рассмотрим возможность использования команды @zips = m/\d\d\d\d\d/g; для построения списка, каждый элемент которого соответствует одному индексу (предполагается, что данные находятся в стандартной переменной $_). Это регулярное выражение совпадает с одним индексом каждый раз, когда модификатор /g применяет его в очередной раз. Важное обстоятельство, смысл которого вскоре станет очевидным — применение регулярного выражения никогда не завершается неудачей до завершения обработки всего списка; возвраты и повторные попытки начисто отсутствуют (предполагается, что все данные имеют правильный формат; в реальном мире такое иногда случается, но довольно редко).

Итак, замена [\d\d\d\d\d] на [44\d\d\d] для нахождения индексов, начинающихся с 44, является очевидной глупостью — после того, как попытка совпадения завершится неудачей, механизм смещается на один символ, в результате чего теряется синхронизация [44] с началом очередного индекса. При использовании [44\d\d\d] первое совпадение будет ошибочно обнаружено в последовательности …5314494116….

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

Синхронизация совпадений

<$M[R7-58]>Я могу предложить несколько способов пропускания ненужных индексов в регулярном выражении. Чтобы добиться желаемого эффекта, достаточно начать регулярное выражение с любой из конструкций, перечисленных ниже.

[(?:[^4]\d\d\d\d|\d[^4]\d\d\d)*…]

Это решение можно назвать «методом грубой силы»: мы активно пропускаем все индексы, начинающиеся с чего-либо, кроме 44 (вероятно, вместо [^4] следовало бы использовать [[1235-9]], но как говорилось выше, я предполагаю, что мы работаем с правильно отформатированными данными). Кстати говоря, мы не можем использовать [(?:[^4][^4]\d\d\d)*], поскольку она не пропускает ненужные индексы типа 43210.

[(?:(?!44)\d\d\d\d\d)*…]

Аналогичное решение, которое также активно пропускает индексы, не начинающиеся с 44. Описание очень похоже на то, которое приведено выше, но на языке регулярных выражений оно выглядит совсем иначе. Сравните два описания и выражения. В нашем случае нужный индекс (начинающийся с 44) приводит конструкцию [(?!44)] к ложному результату, вследствие чего пропускание индексов прекращается.

[(?:\d\d\d\d\d)*?…]

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

Объединяя последнее выражение с [(44\d\d\d)], мы получаем:

@zips = m/(?:\d\d\d\d\d)*?(44\d\d\d)/g;

Это выражение извлекает из строки интересующие нас индексы 44xxx, активно пропуская промежуточные ненужные группы (в списковом контексте m/…/g возвращает список текста, совпадающего с подвыражениями в сохраняющих круглых скобках при каждом совпадении; см. с. <$R[P#,R7-64]>).

Полученное регулярное выражение может использоваться в сочетании с /g, поскольку мы знаем, что при каждом совпадении «текущая позиция» остается в начале следующего индекса, поэтому поиск следующего совпадения (обусловленный /g) начнется от начала очередного индекса, как и предполагает регулярное выражение. Возможно, вы помните, что аналогичная методика синхронизации использовалась в примере с анализом данных, разделенных запятыми (с. <$R[P#,R7-65]>).

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

Поддержание синхронизации при отсутствии совпадения

Действительно ли мы гарантируем, что регулярное выражение применяется только от начала очередного индекса? Нет! Мы пропускаем промежуточные ненужные индексы, но как только в тексте не останется ни одного нужного индекса, поиск совпадения завершится неудачей. Как всегда, при этом вступает в действие механизм смещения текущей позиции, и поиск продолжится с позиции внутри очередного индекса! Эта распространенная проблема уже встречалась раньше, в программе Tcl для удаления комментариев C из главы 5 (с. <$R[P#,R5-23]>).

Вернемся к нашим примерным данным:

03824 53144 94116 15213 44182 95035 44272lwr7lwr5lwr2lwr010217 443235

Совпадающие индексы выделены жирным шрифтом (третье совпадение является нежелательным). Активно пропущенные индексы подчеркнуты, а символы, пропущенные вследствие смещения текущей позиции поиска, помечены. После совпадения 44272 в строке не остается совпадающих индексов, поэтому следующая попытка поиска завершается неудачей. Но завершается ли при этом весь поиск? Конечно, нет. Механизм смещает текущую позицию и пытается применить регулярное выражение к следующему символу, нарушая тем самым синхронизацию с началом индексов. После четвертого смещения регулярное выражение пропускает 10217 и находит «индекс» 44323.

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

@zips = m/\G(?:\d\d\d\d\d)*?(44\d\d\d)/g;

\G совпадает в той позиции, где закончилось предыдущее совпадение /g (или в начале фрагмента при самой первой попытке). Поскольку регулярное выражение сконструировано так, чтобы совпадение заканчивалось на границе индекса, мы тем самым гарантировали, что все последующие попытки поиска, начинающиеся с \G, также начнутся на той же границе индекса. Если попытка завершается неудачей, поиск закончен, поскольку смещение текущей позиции невозможно — \G обеспечивает продолжение поиска с той позиции, где закончилась предыдущая попытка. Другими словами, в подобной ситуации \G фактически запрещает смещение текущей позиции.

Оказывается, механизм регулярных выражений оптимизируется таким образом, что в некоторых стандартных ситуациях он действительно запрещает смещение. Если совпадение должно начинаться с \G, смещение текущей позиции никогда не приведет к новому совпадению, поэтому поиск на этом завершается. Впрочем, оптимизатор легко обманывается, поэтому вы должны быть внимательны. Например, эта оптимизация не активизируется для конструкции [\Gthis|\Gthat], хотя она фактически эквивалентна оптимизируемой конструкции [\G(?:this|that)].

\G в перспективе

Метасимвол [\G] используется нечасто, но зато там, где это необходимо, он оказывает бесценную помощь. Каким бы поучительным ни был последний пример, в действительности эта задача решается и без \G. Для полноты картины я хочу упомянуть, что после успешного обнаружения индекса 44xxx мы можем воспользоваться любым из первых двух подвыражений, пропускающих нежелательные индексы, и обойти заодно и все продолжающие нежелательные индексы (третье подвыражение пропускает индексы только в том случае, если это необходимо для совпадения, поэтому здесь оно не подойдет):

@zips = m/(?:\d\d\d\d\d)*?(44\d\d\d)(?:(?!44)\d\d\d\d\d)*/g;

После того, как мы найдем совпадение для последнего индекса, добавленное подвыражение поглотит остаток строки (если он есть), и обработка m/…/g завершится.

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

@zips = grep {defined} m/(44\d\d\d)|\d\d\d\d\d)/g;

@zips = grep {m/^44/} m/\d\d\d\d\d/g;

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

Синхронизация поиска с применением /g

Даже если метасимвол \G не используется, необходимо учитывать некоторые особенности запоминания в Perl «конца предыдущего совпадения». В Perl4 он ассоциировался с конкретным оператором, но в Perl5 он ассоциируется с данными (целевым текстом). Эту позицию можно получить при помощи функции<$M[R7-36]> pos(…). Следовательно, одно регулярное выражение может продолжить поиск с позиции, в которой прекратилась обработка другого регулярного выражения. Фактически это позволяет проводить коллективный поиск совпадений с применением нескольких регулярных выражений. Рассмотрим следующий простой пример:

@nums = $data =~ m/\d+/g;

Эта команда возвращает в @nums список всех чисел в данных. Теперь усложним задание: если в строке встречается специальная комбинация <xx>, вы хотите ограничиться только теми числами, которые следуют после нее. Одно из простых решений выглядит так:

$data =~ m/<xx>/g; # Установить начальную позицию /g. Теперь pos($data)

                     # находится в позиции, следующей сразу же за <xx>.

@nums = $data =~ m/\d+/g;

Поиск <xx> выполняется в скалярном контексте, поэтому модификатор /g не ищет все совпадения (с. <$R[P#,R7-66]>). Вместо этого он устанавливает для $data значение pos, позицию «конца последнего совпадения», с которой начнется поиск следующего совпадения в тех же данных под управлением /g. Я называю этот прием «наводкой» (priming)». После этого m/\d+/g продолжает поиск с установленной позиции. Если совпадение для <xx> не находится, последующий поиск m/\d+/g начинается, как обычно, с начала строки.

Приведенный пример работает благодаря двум важным обстоятельствам. Во-первых, первый поиск выполняется в скалярном контексте. В списковом контексте выражение [<xx>] будет применяться многократно вплоть до последнего найденного экземпляра, и неудача поиска, находящегося под управлением /g, вернет pos в начало фрагмента. Во-вторых, при первом поиске должен использоваться модификатор /g. Без него позиция pos не изменяется.

И еще одно интересное замечание: благодаря возможности присваивания pos начальную позицию /g можно установить вручную:

pos($data) = $i if $i = index($data, "<xx>"), $i > 0;

@nums = $data =~ m/\d+/g;

Если функция index находит <xx> в тексте, она устанавливает начальную позицию следующего поиска в $data под управлением /g. Обратите внимание: в отличие от рассмотренного выше примера позиция устанавливается в начало последовательности <xx>, а не после нее, как раньше. Впрочем, в данном случае это несущественно.

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

Границы слов

В Perl нет отдельных якорных метасимволов для границ начала слова и конца слова, поддерживаемых во многих других программах (см. табл. 3.1, с. <$R[P#,R3-27]>, и табл. 6.1, с. <$R[P#,R6-1]>). Вместо этого[35] в Perl существуют метасимволы<$M[R7-24]> границы слова [\b] и не-границы слова [\B] (обратите внимание: в символьных классах и в строках, заключенных в кавычки, \b является сокращенной записью для символа «забой»). Границей слова считается любая позиция, разделяющая символы, один из которых совпадает с [\w], а другой — с [\W] (конец строки в этом определении интерпретируется как [\W]). В отличие от большинства программ, Perl включает в [\w] символ подчеркивания. При определении локального контекста в пользовательской среде в категорию [\w] могут включаться дополнительные символы (с. <$R[P#,R3-6]>, <$R[P#,R7-67]>).

Важное предупреждение

С метасимволом [\b] связан один специфический подвох, с которым я иногда сталкиваюсь[36]. Например, для поиска текста $item в поисковом Web-интерфейсе я воспользовался выражением m/\b\Q$item\E\b/. Искомый текст был заключен в конструкцию \b…\b, потому что я знал, что меня интересуют только экземпляры $item, оформленные в виде отдельного слова. Например, для поиска текста 3.75 регулярное выражение принимает вид [\b3\.75\b], и успешно находит искомый элемент в строке price is 3.75 plus tax.

Но если $item содержит текст $3.75, мы получаем регулярное выражение [\b\$3\.75\b]. В этом случае граница слова должна располагаться перед знаком доллара (граница слова может оказаться перед \W-символом вроде доллара лишь в одном случае — если слово здесь заканчивается). Мы используем префикс \b, предполагая, что он привязывает начало совпадения к позиции, в которой $item начинается как отдельное слово. Но поскольку начало $item не может быть началом отдельного слова (так как $ не совпадает с \w), \b предотвращает совпадение. Это регулярное выражение даже не совпадет с искомым текстом в строке …is $3.75 plus….

Если предшествующий символ совпадает с \w, то поиск совпадения даже не должен начинаться — но, как говорилось выше, ретроспективная проверка в Perl не поддерживается. Одно из возможных решений — добавлять \b лишь в том случае, если начальный или конечный символ $item совпадает с \w:

$regex = "\Q$item\E";             # "Надежная" часть искомого текста

$regex = '\b' . $regex if $regex =~ m/^\w/; # Если может начинать слово

$regex = $regex . '\b' if $regex =~ m/\w$/; # Если может заканчивать слово

В этом случае метасимвол [\b] не будет использоваться там, где он может вызвать проблемы, но проблема (если $item начинается или заканчивается \W-символом) остается. Например, если $item содержит текст -998, совпадение будет найдено в строке 800-988-9938. Если вас устраивает, что в совпадение могут быть включены лишние символы (что недопустимо, если подвыражение является частью другого, большего выражения, а также часто не подходит при использовании модификатора /g), можно воспользоваться простым, но эффективным выражением [(?:\W|^)\Q$item\E(?!\w)].

Отсутствие метасимволов начала и конца слова

На первый взгляд отсутствие в Perl отдельных метасимволов для начала и конца слова кажется существенным недостатком, но на самом деле все не так плохо, поскольку само использование [\b] в регулярном выражении почти всегда снимает все неоднозначности. Я еще никогда не сталкивался с ситуацией, когда был действительно необходим специализированный якорный метасимвол для начала или конца слова, но вам когда-нибудь не повезет — нужного эффекта можно добиться при помощи [\b(?=\w)] и [\b(?!=\w)]. Например, следующее выражение удаляет все символы между первым и вторым словом в строке:

s/\b(?!\w).*\b(?=\w)//

Для сравнения замечу, что в современных версиях sed с поддержкой специализированных метасимволов границ слов \< и \> аналогичная команда выглядела бы так:

s/\>.*\<//

Удобные сокращения и другие синтаксические элементы

Многие удобные сокращения<$M[R7-30]> Perl для стандартных конструкций уже встречались на страницах книги. Полный[37] список приведен в табл. 7.7.

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

Байтовые обозначения

Описание

\число

Символ, заданный в виде восьмеричного кода.

\xчисло

Символ, заданный в виде шестнадцатеричного кода.

\ссимвол

Управляющий символ

Сокращенные обозначения стандартных классов

Описание

\d

цифра [0-9]

\s

пропуск, обычно [spc\f\n\r\t]

\w

символ слова, обычно [a-zA-Z0-9_]

\D, \S, \W

отрицание \d, \s и \w

Платформенно-зависимые управляющие символы

Описание

\a

звуковой сигнал

\f

подача листа

\e

Escape

\n

новая строка

\r

возврат курсора

\t

табуляция

\b

забой (только внутри класса)

Вероятно, [\n] и другие сокращенные обозначения покажутся знакомыми — эти платформенно-зависимые (с. <$R[P#,R3-28]>) обозначения стандартных управляющих символов также поддерживаются и в строках, заключенных в кавычки. Вы должны четко понять, что сами по себе эти метасимволы регулярных выражений недоступны в строках, заключенных в кавычки — просто для строк определяются собственные метасимволы, аналогичные метасимволам регулярных выражений (с. <$R[P#,R2-8]>).

Сравните m/(?:\r\n)+$/ со следующим фрагментом:

$regex = "(\r\n)+";

m/$regex$/;

Выясняется, что они выдают абсолютно одинаковые результаты. Но не заблуждайтесь, эти решения не эквивалентны. Стоит вам по наивности использовать в этой ситуации что-нибудь вроде "\b[+\055/*]\d+\b", и вы будете сильно удивлены. Когда строке присваивается значение, оно интерпретируется как строка — то, что вы собираетесь использовать его в качестве регулярного выражения, несущественно для обработки строковых данных. Две конструкции \b в этом примере должны были обозначать границы слов, но в строке, заключенной в кавычки, они всего лишь являются сокращенными обозначениями символа «забой». Вместо [\b] регулярное выражение увидит простые символы «забой», которые не имеют особой интерпретации в регулярных выражениях и попросту совпадают с символами «забой» в тексте. Сюрприз!

С другой стороны, и регулярное выражение, и строка в кавычках преобразуют \055 в дефис (055 — ASCII-код дефиса), но если преобразование выполняется в регулярном выражении, то его результат не будет восприниматься как метасимвол. Впрочем, это не происходит и в строке, но итоговая конструкция [+-/*], в итоге получаемая регулярным выражением, содержит дефис, который интерпретируется как часть интервала в символьном классе. Еще сюрприз!

Я подчеркиваю все эти обстоятельства, потому что при построении строки, которая позднее будет использоваться в качестве регулярного выражения, необходимо знать, что является или не является метасимволом (а также кем интерпретируются эти метасимволы — строкой или регулярным выражением, когда и в каком порядке они обрабатываются). Конечно, на первых порах происходит немало недоразумений. Расширенный пример приведен в разделе «Поиск адресов электронной почты» (с. <$R[P#,R7-15]>).

Perl и локальные контексты POSIX

<$M[R7-67]>Как кратко упоминалось в разделе «Косвенная поддержка локальных контекстов» (с. <$R[P#,R3-29]>), поддержка локальных контекстов POSIX в Perl крайне ограничена. В частности, она не учитывает существования объединяющих последовательностей и других похожих возможностей, поэтому ASCII-интервалы в символьных классах не содержит символов локального контекста, не входящих в кодировку ASCII (сравнения строк, а также сортировка тоже выполняются без учета контекста).

Однако при компиляции с соответствующими библиотеками Perl использует проверочные функции (isalpha, isupper и т. д.), а также отображения между символами верхнего и нижнего регистра. Это влияет на работу модификатора /i и других конструкций, перечисленных в табл. 7.8 (с. <$R[P#,R7-48]>). Кроме того, \w, \W, \s, \S (но не \d!) и метасимволы границ слов также начинают учитывать локальный контекст. Впрочем, в диалекте регулярных выражений Perl не поддерживаются [:digit:] и другие групповые выражения POSIX, перечисленные на с. <$R[P#,R3-30]>.

Стандартные модули поддержки POSIX

В число стандартных модулей Perl входят модуль POSIX и модуль<$M[R7-69]> I18N::Collate, написанный Якко Хиетаниеми (Jarkko Hietaniemi). Кстати, I18N — распространенное сокращение для слова internalization. Хотя эти модули не относятся к поддержке регулярных выражений, возможно, они пригодятся вам в том случае, если локальный контекст играет важную роль в вашей программе. Модуль POSIX огромен, но документация по нему относительно невелика — за дополнительной информацией обращайтесь к соответствующим страницам руководства по библиотеке C.

Числовые коды

В Perl предусмотрена возможность определения символов по их числовым кодам. Восьмеричные коды из двух или трех цифр задаются в формате [\33] или [\177] , а шестнадцатеричные коды из одной или двух цифр — в виде [\xA] или [\xFF].

В строках Perl также допускаются восьмеричные коды из одной цифры, но в регулярных выражениях Perl это обычно невозможно, поскольку конструкция вида [\1] воспринимается как обратная ссылка. Более того, обратные ссылки из нескольких цифр допустимы в том случае, если строка содержит достаточное количество пар сохраняющих скобок. Таким образом<$M[R7-53]>, [\12] интерпретируется как обратная ссылка, если выражение содержит не менее 12 пар сохраняющих круглых скобок, или как восьмеричный код (10 в десятичной системе) в противном случае. Прочитав черновик этой главы, Уэйн Берк (Wayne Berke) внес предложение, которое я горячо поддерживаю: никогда не используйте восьмеричные коды из двух цифр (например, \12), а преобразуйте их к формату с тремя цифрами \012). Почему? Perl никогда не интерпретирует \012 как обратную ссылку, а \12 может случайно превратиться в обратную ссылку с увеличением числа сохраняющих круглых скобок.

Существует два особых случая. Во-первых, обратные ссылки в символьном классе бессмысленны, поэтому восьмеричные коды из одной цифры вполне допустимы в символьных классах (вот почему я употребил в предыдущем абзаце выражение обычно невозможно). Во-вторых [\0] является восьмеричным кодом всегда, поскольку эта конструкция не может использоваться в качестве обратной ссылки.

Байты и символы

Как объяснялось в разделе «Терминология регулярных выражений» главы 1 (с. <$R[P#,R1-12]>), точное соответствие между символами и представляющими их байтами зависит от кодировки. Обычно используется кодировка ASCII, однако пользователь данных может в любой момент изменить ее по своему усмотрению. Другая похожая проблема заключается в том, что значение \n не определяется на уровне Perl, а зависит от системы (с. <$R[P#,R3-28]>).

Символьные классы

Язык символьных классов<$M[R7-29]> Perl занимает особое место среди диалектов регулярных выражений, поскольку в нем поддерживается возможность экранирования префиксом \. Например, выражение [[\-\],]] определяет символьный класс, совпадающий с дефисом, правой квадратной скобкой или запятой (возможно, смысл [[\-\],]] дойдет до вас не сразу — внимательно проанализируйте выражение и убедитесь в том, что вы его понимаете). Многие другие диалекты регулярных выражений не поддерживают экранирование в классах, что весьма прискорбно, поскольку возможность экранирования метасимволов класса не только логична, но и полезна. Более того, очень удобно иметь возможность экранирования даже в том случае, если это не является абсолютно необходимым, поскольку это может сделать программу более понятной[38].

Я уже неоднократно упоминал о том, что метасимволы в символьных классах и вне их интерпретируются по-разному. Perl не является исключением из этого правила, хотя интерпретация многих метасимволов в обоих случаях совпадает. В частности, квантификаторы * и +, (), ., конструкция выбора, якорные метасимволы и т. д. в символьных классах бессмысленны. Мы уже видели, что \b, \3 и ^ в символьных классах имеют особый смысл, не связанный с их интерпретацией вне класса. Символы - и ] выполняют специальные функции в символьных классах, но являются обычными символами за их пределами.

Символьные классы и данные в других кодировках

Восьмеричные и шестнадцатеричные коды удобно использовать в символьных классах, особенно при определении интервалов. Например, для описания «отображаемого» ASCII-символа (то есть не являющегося пропуском или управляющим символом), традиционно используется класс [[!-~]] (восклицательный знак является первым, а тильда — последним отображаемым символом в ASCII). Чтобы класс был более наглядным, его можно записать в виде [[\x21-\x7e]]. Тот, кто уже сталкивался с этой задачей, поймет оба выражения, но при первом знакомстве класс [[!-~]] выглядит по меньшей мере загадочно. Выражение [[\x21-\x7e]] по крайней мере наводит на мысль, что в нем определяется некоторый интервал символов.

При отсутствии полноценной поддержки локальных контекстов POSIX восьмеричные и шестнадцатеричные коды приносят немалую пользу при работе с текстом в других кодировках (не ASCII). Например, при работе с популярной в Web кодировкой Latin-1 (ISO-8859-1) необходимо учитывать, что буква u может также выглядеть как щ, ъ, ы и ь (символы с кодами от \xf9 до \xfc). Таким образом, для совпадения с любой из разновидностей u используется класс [[u\xf9-\xfc]]. Версиям этих символов в верхнем регистре соответствуют коды с \xd9 до \xdc, поэтому для поиска совпадения без учета регистра будет использоваться выражение [[uU\xf9-\xfc\xd9-\xdc]] (модификатор /i относится только к ASCII-символу u, поэтому мы с таким же успехом можем включить U в класс и избавиться от затрат, связанных с применением /i; см. с. <$R[P#,R7-68]>).

На практике эта методика часто применяется при сортировке. Обычно сортировка @items выполняется командой sort @items, но в этом случае символы сортируются по своим кодам, и символ u (ASCII-код \x75) окажется слишком далеко от ы и других похожих символов. Если мы создадим копию каждого символа, свяжем с ней ключ сортировки и объединим пары в отдельном ассоциативном массиве, то этот массив можно будет использовать при вызове sort и таким образом обеспечить нужный результат сортировки.

Простейшая реализация выглядит так:

foreach $item (@Items) {

$key = lc $item;           # Скопировать символ с приведением

                               # к нижнему регистру в кодировке ASCII

;key =$ s.[\xd9-\xdc\xf9-\xfc].u.g; # Все разновидности u

                               # превращаются в обычный символ u.

# Выполнить аналогичные операции для других диакритических символов

    $pair{$item} = $key;          # Запомнить пару "символ-ключ"

}

 

# Отсортировать по содержимому ключа

@SortedItems = sort { $pair{$a} cmp $pair{$b} } @Items;

(lc — удобная функция Perl5, однако этот пример нетрудно переписать на Perl4). В действительности это лишь первый шаг, поскольку в каждом языке к сортировке предъявляются особые требования, однако этот шаг сделан в правильном направлении. Другим шагом будет использование модуля I18N::Collate, о котором упоминается на с. <$R[P#,R7-69]>.

Изменение регистра символов с применением \Q и аналогов

В любой документации, посвященной регулярным выражениям Perl (в том числе и в нижней части табл. 7.1), встречаются \L, \E, \u и другие комбинации, перечисленные в табл. 7.8. Как ни странно, они не являются метасимволами<$M[R7-31]> регулярных выражений. Механизм регулярных выражений знает, что [*] означает «произвольное количество экземпляров», а [[] является началом символьного класса, но о [\E] ему ничего не известно. Почему же я привожу здесь эти конструкции?<$M[R7-48]>

Таблица 7.8. Конструкции изменения регистра символов в строках и регулярных выражениях-операндах

Конструкция

Описание

Встроенная функция

\L, \U

Перевести символы в нижний (верхний) регистр до \E[39]

lc(…), uc(…)

\l, \u

Перевести в нижний (верхний) регистр следующий символ[40]

lcfirst(…), ucfirst(…)

\Q

добавлять экранирующий префикс перед всеми не-алфавитными символами до \E

quotemeta(…)

Специальные комбинации

\u\L

Перевести первый символ в верхний регистр; остальные символы переводятся в нижний регистр до комбинации \E или до конца текста

\l\U

Перевести первый символ в нижний регистр; остальные символы переводятся в верхний регистр до комбинации \E или до конца текста

На практике эти конструкции работают как обычные метасимволы регулярных выражений. При использовании в регулярных выражениях-операндах они обычно обрабатываются в фазе B процесса, изображенного на рис. 7.1, поэтому до механизма регулярных выражений они попросту не доходят (с. <$R[P#,R7-70]>). Но поскольку в некоторых ситуациях различия все же существуют, я называю конструкции из табл. 7.8 метасимволами второго класса.

Метасимволы второго класса

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

l      действуют только во время «высокоуровневого» сканирования операнда.

l      распространяются только на символы откомпилированного регулярного выражения.

Это объясняется тем, что метасимволы второго класса распознаются только во время фазы B (см. рис. 7.1), но не после выполнения интерполяции. Если не знать об этом, вероятно, вас удивит то, что следующий фрагмент не работает:

$ForceCase = $WantUpper ? '\U' : '\L';

if (m/$ForceCase$RestOfRegex/) {

Поскольку \U или \L в $ForceCase находится в интерполируемом тексте (который обрабатывается только в фазе C), особый смысл этой комбинации не опознается. Впрочем, механизм регулярных выражений опознает символ \, поэтому [\U] обрабатывается как общий случай неизвестного экранированного символа: префикс \ просто игнорируется. Если $RestOfRegex содержит текст Path, а переменная $WantUpper истинна, то оператор будет искать литеральный текст UPath, а не PATH, как предполагалось.

Из этого правила следует и другой вывод — команды вида [m/([a-z])…\U\1] не работают. Этот оператор ищет символ нижнего регистра, за которым следует этот же символ в верхнем регистре. Однако \U работает только с текстом самого регулярного выражения, а поскольку [\1] представляет текст, совпавший с некоторой частью регулярного выражения, этот текст остается неизвестным до непосредственного поиска совпадения (вероятно, поиск можно рассматривать как «фазу E» на рис. 7.1).

Оператор поиска

<$M[R7-11]>Поиск совпадений закладывает основу для всего применения регулярных выражений в Perl. К счастью, оператор поиска обладает достаточной гибкостью; впрочем, это несколько усложняет его изучение. При рассмотрении возможностей m/…/ я буду руководствоваться принципом «разделяй и властвуй». Мы последовательно рассмотрим следующие темы:

l      Определение самого оператора поиска (регулярное выражение-операнд).

l      Определение целевой строки, в которой осуществляется поиск.

l      Побочные эффекты поиска.

l      Значение, возвращаемое в результате поиска.

l      Внешние факторы, влияющие на поиск.

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

Ограничители регулярного выражения-операнда

В качестве ограничителя выражения-операнда обычно используется символ /, хотя вы можете выбрать любой другой символ по своему усмотрению (точнее, допускается использование любого символа, не являющегося алфавитно-цифровым или пропуском<$M[R7-72]> — [6 с.<$R[P#,R7-71]>]). Вероятно, это один из самых непривычных аспектов синтаксиса Perl, хотя при умелом использовании он делает программу более понятной.

Например, для применения выражения [^/(?:[^/]+/)+Perl$] со стандартными ограничителями потребуется команда [m/^\/(?:[^\/]+\/)+Perl$/]. Как говорится при описании фазы A на рис. 7.1, закрывающий ограничитель, встречающийся в регулярном выражении, должен экранироваться для того, чтобы замаскировать ограничивающую функцию этого символа (в приведенном примере экранирующие префиксы выделены жирным шрифтом). Символы, экранируемые по этой причине, передаются регулярному выражению так, словно никаких префиксов нет<$R[P#,R7-74]> [7 с.<$R[P#,R7-73]>]. Вместо того, чтобы перегружать выражение лишними символами \, проще воспользоваться другим ограничителем — в этом случае команда принимает вид m!^/(?:[^/]+/)+Perl$! или m,^/(?:[^/]+/)+Perl$,. В качестве ограничителей также часто используются символы m|…|, m#…# и m%…%.

Некоторые ограничители выполняют особые функции:

l      В четырех специальных конструкциях, m(…), m{…}, m[…] и m<…>, в качестве открывающего и закрывающего ограничителя используются разные символы, поэтому эти конструкции могут быть вложенными<$M[R7-76]> [8 с.<$R[P#,R7-75]>]. Поскольку круглые и квадратные скобки очень часто встречаются в регулярных выражениях, конструкции m(…) и m[…] применяются не так часто, но с двумя другими конструкциями дело обстоит иначе. В частности, при использовании модификатора /x становятся возможными фрагменты вида:

m{

    регулярное   # комментарии

    выражение # комментарии

}x;

При создании вложенных конструкций со специальными ограничителями экранирование парных символов не требуется. Например, команда m(^/((?:[^/]+/)+)Perl$) вполне допустима, хотя разобраться в ней нелегко.

l      Если в качестве ограничителя используется апостроф, регулярное выражение-операнд обрабатывается по правилам строк, заключенных в апострофы (в отличие от обычной обработки по правилам строк, заключенных в кавычки, подробно рассмотренной выше). Это означает, что фаза B на рис. 7.1 пропускается, интерполяция переменных не выполняется, а конструкции, перечисленные в табл. 7.8, не обрабатываются. Такая возможность бывает полезной в выражениях с большим количеством символов $ и @, экранирование которых приведет к чрезмерно громоздкому результату.

l      Если в качестве ограничителей используются вопросительные знаки<$M[R7-78]> [9 с.<$R[P#,R7-77]>], выполняется весьма специфическая разновидность поиска. Обычно оператор поиска возвращает признак успешного совпадения при каждом совпадении регулярного выражения-операнда в целевой строке, но если ограничителями регулярного выражения являются символы ?, то признак успеха возвращается только для первого совпадения. Все последующие найденные совпадения считаются неудачными, и это продолжается до вызова функции reset в текущем пакете<$M[R7-80]> [10 с.<$R[P#,R7-79]>].

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

$subject = $1 if m?^Subject: (.*)?;

После того, как строка темы будет найдена, нет смысла искать ее во всех последующих строках. Но если по какой-то причине в заголовке окажется несколько строк Subject:, конструкция m?…? совпадет только с первой из них, а m/…/ совпадет со всеми. После завершения обработки заголовка m?…? оставит в переменной $subject данные из первой, а m/…/ — из последней строки Subject:.

У оператора подстановки s/…/…/ также имеются специальные ограничители, которые будут рассмотрены ниже (с. <$R[P#,R7-81]>); перечисленные выше особые случаи относятся только к оператору поиска.

Впрочем, стоит упомянуть еще один особый случай: если в качестве ограничителя используется либо стандартный символ /, либо символ однократного совпадения ?, сама буква m становится необязательной. Для поиска совпадений часто используется конструкция /…/.

Наконец, в Perl поддерживается обобщенный шаблон целевой_текст =~ выражение (без ограничителей и без m). Выражение вычисляется как общее выражение Perl, интерпретируется как строка и передается механизму регулярных выражений. Это позволяет использовать команды вида $text =~ &GetRegex() вместо более длинных:

my $temp_regex = &GetRegex();

...$text =~ m/$temp_regex/...

Кроме того, команда $text =~ "…строка…" может пригодиться в том случае, если вы хотите выполнить полноценную обработку по правилам строк, заключенных в кавычки, вместо ее имитации — предварительной обработки, о которой говорилось выше. Впрочем, подобные финты лучше оставить для конкурсов на Самую Загадочную Программу на Perl.

Регулярное выражение по умолчанию

<$M[R7-86]>Если регулярное выражение не задано (например, m// или m/$regex/, где переменная $regex содержит пустую строку или имеет неопределенное значение), Perl заново использует последнее успешно использованное регулярное выражение во внешней динамической области видимости<$M[R7-83]> [11 с.<$R[P#,R7-82]>]. В этом случае любые модификаторы поиска (см. следующий раздел), даже /g и /i, полностью игнорируются. Модификаторы, использованные в выражении по умолчанию, остаются в силе.

Регулярное выражение, используемое по умолчанию, никогда не компилируется заново (даже если оригинал был построен с интерполяцией переменной без модификатора /o). Этим обстоятельством можно воспользоваться для создания эффективных тестов. Пример приведен в разделе «Модификатор /o и eval» (с. <$R[P#,R7-84]>).

Модификаторы поиска

Оператор поиска поддерживает ряд<$R[P#,R7-33]> модификаторов (параметров), влияющих на:

l      интерпретацию регулярного выражения-операнда (/o с. <$R[P#,R7-85]>; /x с. <$R[P#,R7-22]>);

l      интерпретацию целевого текста механизмом регулярных выражений (/i с. <$R[P#,R2-9]>; /m, /s с. <$R[P#,R7-51]>);

l      способ применения регулярного выражения механизмом (/g с. <$R[P#,R7-64]>).

Модификаторы можно группировать и располагать их после закрывающего ограничителя[41] в произвольном порядке. Например, команда m/<code>/i применяет регулярное выражение [<code>] с модификатором /i, обеспечивая поиск без учета регистра. Следует помнить, что символ / не является частью модификатора — команду можно записать в виде m|<code>|i, m{<code>}i и даже m<<code>>i. Как было сказано выше (с. <$R[P#,R7-23]>), модификаторы /x, /i, /m и /s также могут присутствовать и в самом регулярном выражении при помощи конструкции [(?…)]. Непосредственное включение этих модификаторов в регулярное выражение исключительно удобно в тех случаях, когда одни оператор в разное время использует разные выражения (обычно вследствие интерполяции переменных). Например, при использовании модификатора /i, каждое применение выражения данным оператором будет осуществляться без учета регистра символов. Выбирая модификаторы на уровне регулярного выражения, вы получаете более универсальный программный код.

Прекрасным примером является поисковый механизм на Web-странице, обеспечивающий полноценную поддержку регулярных выражений Perl. Большинство поисковых механизмов поддерживает очень простые спецификации поиска, не удовлетворяющие опытных пользователей, поэтому поддержка регулярных выражений многим придется по вкусу. В этом случае пользователь может включать в свои выражения [(?i)] и другие конструкции, и в сценариях CGI не придется предусматривать специальные параметры для активизации этих модификаторов.

Применение m/…/g с регулярным выражением, совпадающим с пустой строкой

Обычно поиск следующего совпадения регулярного выражения с модификатором /g начинается с той позиции, в которой завершилось предыдущее совпадение. Но что произойдет, если регулярное выражение может совпасть с пустой строкой<$M[R7-109]>? Рассмотрим простой пример — откровенно глупую команду m/^/g. Она совпадает в начале строки, но символов не поглощает, поэтому первое совпадение завершается в начале строки. Если следующая попытка начнется в этой же позиции, совпадение будет найдено в том же месте. И так далее до бесконечности.

Perl версии 5.000 в такой ситуации действительно зацикливается, но Perl4 и более поздние версии Perl5 работают иначе. Совпадение в них начинается в той позиции, где завершилось предыдущее совпадение — если только предыдущее совпадение не совпало с пустой строкой; в этом случае<$M[R7-87]> производится особое смещение текущей позиции, и поиск возобновляется со сдвигом на один символ. Таким образом, каждое совпадение после первого заведомо смещается в строке хотя бы на один символ, что предотвращает зацикливание.

Если речь идет не об операторе подстановки, Perl5 делает еще один шаг и исключает совпадения, заканчивающиеся в одной позиции с предыдущим совпадением — в этом случае выполняется автоматический сдвиг на один символ (если проверяемая позиция не находится в конце строки). Это отличие от Perl4 может оказаться существенным — в табл. 7.9 приведено несколько простых примеров (эта таблица ни в коем случае не является «легким чтивом» — возможно, вы не сразу разберетесь в ней). С оператором подстановки ситуация выглядит иначе, но эта тема будет рассмотрена в разделе «Оператор подстановки» (с. <$R[P#,R7-12]>)<$M[R7-106]>.

Таблица 7.9. Примеры использования m/…/g с регулярным выражением, которое может совпадать с текстом нулевой ширины

См. расшифровку в файле pict!

Определение целевого текста

К счастью, второй операнд (целевой текст) задается проще, чем операнд-выражение. Обычно текст, в котором осуществляется поиск, задается оператором =~ — например, $line =~ m/…/. Напоминаю, что =~ не является оператором присваивания или сравнения. Это всего лишь непривычный способ передачи операнда оператору поиска (обозначение было позаимствовано из awk).

Поскольку вся конструкция выражение =~ m/…/ является выражением, ее можно использовать везде, где допускается использование выражений. Рассмотрим несколько примеров:

$text =~ m/…/; # Просто выполнить - возможно, ради побочных эффектов

- - - - - - - - - - - -

if ($text =~ m/…/) {

## Выполнить некоторые действия в случае успеха

.

.

.

- - - - - - - - - - - -

$result = ( $text  =~ m/…/ ); # Присвоить $result результат поиска в $text

;result = ;text  =! m.…. ; # То же самое. =~ обладает более

                               # высоким приоритетом, чем =.

- - - - - - - - - - - -

$result = $text;           # Скопировать $text в $result...

$result           =~ m/…/ ; # ...и выполнить поиск в $result

( $result = $text ) =~ m/…/ ; # То же в одном выражении

Если целевым операндом является переменная $_, то конструкцию $_ =~ можно полностью исключить. Другими словами, $_ является операндом целевого текста по умолчанию.

Конструкция вида $line =~ m/выражение/ означает «применить регулярное выражение к тексту в $line; возвращаемое значение игнорируется, но побочные эффекты действуют». Если вы забудете о символе ~, то полученная строка $line = m/выражение/ будет означать «применить выражение к тексту в $_; затем присвоить возвращаемое логическое значение переменной $line». Другими словами, следующие две команды эквивалентны:

$line =       m/выражение/

$line = ($_ =~ m/выражение/)

Вы также можете использовать вместо =~ оператор !~, чтобы логически инвертировать возвращаемое значение (возвращаемые значения и побочные эффекты рассматриваются в следующих разделах). Команда $var !~ m/…/ фактически эквивалентна not ($var =~ m/…/). При этом действуют все стандартные побочные эффекты вроде присваивания переменной $1. Таким образом, это всего лишь удобное обозначение, предназначенное для ситуаций типа «Если совпадение отсутствует…». Хотя допускается использование !~ в списковом контексте, особого смысла в этом нет.

Другие побочные эффекты оператора поиска

Довольно часто вас интересует не столько значение, возвращаемое в результате поиска, сколько сопровождающие его побочные эффекты. Более того, при вызове оператора поиска возвращаемое значение нередко вообще игнорируется (по умолчанию используется скалярный контекст). Мы уже рассмотрели большинство побочных эффектов ($&, $1, $+ и т. д.; см. с. <$R[P#,R7-34]>), поэтому в этом разделе будут описаны остальные побочные эффекты, сопровождающие попытки поиска совпадений.

Два таких эффекта приводят к сохранению «невидимого статуса». Во-первых, если поиск осуществляется конструкцией m?…?, то успешное совпадение обрекает все будущие совпадения на неудачу, по крайней мере до ближайшего вызова reset (с. <$R[P#,R7-80]>). Конечно, при использовании m?…? именно этот побочный эффект оказывается наиболее важным. Во-вторых, заданное регулярное выражение используется по умолчанию вплоть до выхода из динамической области видимости или успешного совпадения другого регулярного выражения (с. <$R[P#,R7-86]>).

Для совпадений с модификатором /g данные pos для целевой строки обновляются в соответствии с конечной позицией текущего совпадения (а неудачная попытка всегда приводит к сбросу pos). Следующая попытка поиска под управлением /g начнется в той же позиции, если:

l      строка не была модифицирована (модификация строки приводит к сбросу pos);

l      pos не было присвоено значение (поиск начнется с заданной позиции);

l      предыдущее совпадение состоит хотя бы из одного символа.

Как упоминалось выше (с. <$R[P#,R7-87]>), для предотвращения зацикливания при успешном обнаружении совпадения нулевой ширины поиск следующего совпадения начинается со сдвигом в один символ. В процессе поиска после сдвига pos, как и должно быть, соответствует конечной позиции предыдущего совпадения, поскольку для предотвращения зацикливания сдвигается начальная позиция следующей попытки. При этом [\G] продолжает ссылаться на конец предыдущего совпадения (это единственная ситуация, при которой \G не означает «начало текущей попытки»).

Значение, возвращаемое оператором поиска

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

Скалярный контекст без модификатора /g

Самый обычный случай поиска — скалярный контекст без модификатора /g. Если совпадение найдено, оператор возвращает логическую величину:

if ($target =~ m/…/) {

    # Действия для найденного совпадения

    …

} else {

    # Действия для отсутствия совпадений

    …

}

В случае неудачи возвращается пустая строка (интерпретируемая как логическое ложное значение).

Списковый контекст без модификатора /g

Списковый контекст без /g — распространенный способ извлечения информации из строки. Возвращаемое значение представляет собой список, каждый элемент которого соответствует паре сохраняющих круглых скобок в регулярных выражениях. Простейшим примером является обработка даты в формате 69/8/31:

($year, $month, $day) = $date =~ m{^ (\d+) / (\d+) / (\d+) $}x;

После выполнения этой команды три совпавших числа будут присвоены трем переменным (а также переменным $1 и т. д.)<$M[R7-89]> [12 с.<$R[P#,R7-88]>]. Каждой паре сохраняющих круглых скобок в возвращаемом списке соответствует один элемент; в случае неудачи возвращается пустой список. Конечно, некоторые пары могут не входить в совпадение, как в команде m/(this)|(that)/. Элементы списка для таких пар существуют, но имеют неопределенное значение<$M[R7-91]> [13 с.<$R[P#,R7-90]>]. Если в выражении вообще отсутствуют сохраняющие<$M[R7-56]> круглые скобки, успешный поиск в списковом контексте без модификатора /g возвращает список (1).

Давайте усовершенствуем пример с анализом даты и воспользуемся оператором поиска в условии команды if(…). Из-за присваивания конструкции ($year,…) оператор поиска работает в списковом контексте и присваивает значения заданным переменным. Но поскольку в условии if(…) все выражение присваивания используется в скалярном контексте, результат преобразуется в количество элементов в списке. У нас появляется удобная возможность интерпретировать его как логическую ложь при отсутствии совпадении или логическую истину при их наличии.

if ( ($year, $month, $day) = $date =~ m{^ (\d+) / (\d+) / (\d+) $}x ) {

    # Выполнить действия для найденного совпадения;

    # переменные $year и т. д. имеют новые значения.

} else {

    # Выполнить действия для отсутствия совпадения;

    # переменным $year и т. д. присваивается неопределенное значение.

};

Списковый контекст с модификатором /g

<$M[R7-64]>Эта полезная конструкция, впервые появившаяся в версии 4.036, возвращает список всего текста, совпавшего с сохраняющими круглыми скобками (при отсутствии круглых скобок — текста, совпавшего со всем выражением) не только для одного совпадения, как в списковом контексте без модификатора /g, но для всех совпадений в строке. Для примера рассмотрим одну строку с полным текстом файла почтовых псевдонимов Unix в формате

alias   jeff         jfriedl@ora.com

alias   perlbug      perl5-porters@perl.org

alias   prez         president@whitehouse

Для извлечения псевдонимов и адресов из одной логической строки можно воспользоваться командой m/^alias\s+(\S+)\s+(.+)/. Скажем, для первой строки команда вернет список из двух элементов ('jeff', 'jfriedl@ora.com'). Следующим шагом будет обработка всех логических строк в одном фрагменте. Для нахождения всех совпадений одной командой можно воспользоваться модификатором /g (и /m, чтобы метасимвол ^ мог совпадать с началом логических строк). Команда возвратит список

('jeff', 'jfriedl@ora.com', 'perlbug',

 'perl5-porters@perl.org', 'prez', 'president@whitehouse' )

Если возвращаемые элементы образуют пары «ключ/значение», как в приведенном примере, результат можно присвоить ассоциативному массиву (хэшу). После выполнения команды

%alias = $text =~ m/^alias\s+(\S+)\s+(.+)/mg;

к полному адресу jeff можно обращаться через элемент хэша $alias{'jeff'}.

Скалярный контекст с модификатором /g

Скалярный<$M[R7-66]> контекст с модификатором /g представляет собой специальную конструкцию, заметно отличающуюся от трех других ситуаций. Как и обычный оператор m/…/, он находит только одно совпадение, но по аналогии со списковым оператором m/…/g запоминает позицию предыдущего совпадения. При каждом выполнении m/…/g в скалярном контексте находится «следующее» совпадение. Когда очередной поиск завершится неудачей, следующая проверка снова начинается с начала строки.

Такую конструкцию удобно использовать в условии цикла while. Рассмотрим следующий фрагмент:

while ($ConfigData =~ m/^(\w+)=(.*)/mg) {

    my($key, $value) = ($1, $2);

    …

}

В цикле будут найдены все совпадения, но между совпадениями (вернее, после каждого совпадения) будет выполняться тело цикла. После того, как очередная попытка поиска завершится неудачей, результат окажется ложным, и цикл while завершится. После неудачи происходит сброс текущей позиции /g (определяемой функцией pos). Наконец, старайтесь избегать модификации целевых данных в цикле, если вы полностью не уверены в том, что делаете — это приводит к сбросу данных pos<$M[R7-93]> [14 с.<$R[P#,R7-92]>].

Внешние факторы, влияющие на работу оператора поиска

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

l      контекст — оказывает значительное влияние на процесс поиска, а также на возвращаемое значение и побочные эффекты;

l      pos(…) — явное или косвенное (посредством модификатора /g) присваивание определяет позицию строки, в которой начнется следующий поиск под управлением модификатора /g. Также см. описание [\G] в разделе «Привязка к текущей позиции при глобальном поиске» (с. <$R[P#,R7-27]>);

l      $* — переменная, унаследованная из Perl4. Влияет на работу якорных метасимволов ^ и $;

l      регулярное выражение по умолчанию — используется вместо пустого регулярного выражения (с. <$R[P#,R7-86]>);

l      study — не влияет на результаты поиска или возвращаемые значения, но вызов study для целевой строки может ускорить (или замедлить) поиск. См. раздел «Функция study» (с. <$R[P#,R7-5]>);

l      m?…?/reset — влияет на «невидимый» статус совпадения/несовпадения операторов m?…? (с. <$R[P#,R7-80]>).

Учет контекста

Перед тем, как завершить описание оператора поиска, я хочу задать вам вопрос. При использовании регулярных выражений в управляющих конструкциях команд while, if и foreach необходимо действовать очень внимательно. Как вы думаете, что выведет следующий фрагмент<$M[R7-98]>?

while ("Larry Curly Moe" =~ m/\w+/g) {

    print "WHILE stooge is $&.\n";

}

print "\n";

 

if ("Larry Curly Moe" =~ m/\w+/g) {

    print "IF stooge is $&.\n";

}

print "\n";

 

foreach ("Larry Curly Moe" =~ m/\w+/g) {

    print "FOREACH stooge is $&.\n";

}

Задача не из простых. refПереверните страницу и проверьте свой ответ.

Оператор подстановки

<$M[R7-12]>Оператор подстановки Perl, s/выражение/замена/, расширяет концепцию поиска текста до поиска с заменой. Регулярное выражение-операнд задается по тем же правилам, как в операторе поиска, однако второй операнд предназначен для замены совпавшего текста новым. Многие проблемы, связанные с оператором подстановки, совпадают с аналогичными проблемами оператора поиска и рассматриваются в соответствующем разделе (см. с. <$R[P#,R7-11]>). Впрочем, заслуживают упоминания и некоторые новые аспекты:

l      Определение операнда-замены и дополнительные ограничители.

l      Модификатор /e.

l      Контекст и возвращаемое значение.

l      Использование модификатора /g с регулярными выражениями, которые могут совпадать с «ничем».

Операнд-замена

В обычной конструкции s/…/…/ операнд-замена<$M[R7-43]> указывается сразу же после регулярного выражения-операнда, поэтому в общей сложности используется три экземпляра ограничителя вместо двух в конструкции m/…/. Если регулярное выражение заключается в парные ограничители<$M[R7-81]> (например, <…>), операнд-замена также заключается в собственную пару ограничителей (и итоговый оператор содержит четыре ограничителя вместо трех). В таких случаях две пары ограничителей могут разделяться пропусками, а при наличии пропуска возможно наличие комментариев<$M[R7-97]> [15 с.<$R[P#,R7-96]>]. Парные ограничители обычно используются в сочетании с /x и /e:

$test =~ s{

большое регулярное выражение с обширными комментариями…

} {

 …фрагмент кода Perl, вычисление которого дает текст замены…

}ex

Perl обычно производит стандартную обработку операнда-замены по правилам строк, заключенных в кавычки, хотя возможно применение некоторых специальных ограничителей. Обработка выполняется после обнаружения совпадения (при наличии /g — после каждого совпадения), поэтому переменные $1 и т. д. могут использоваться для ссылок на текст, совпавший с соответствующими подвыражениями.

while, foreach и if

bref Ответ на вопрос со с. <$R[P#,R7-98]>

Результат зависит от версии Perl.

Perl4

Perl5

WHILE stooge is Larry.

WHILE stooge is Curly.

WHILE stooge is Moe.

WHILE stooge is Larry.

WHILE stooge is Curly.

WHILE stooge is Moe.

IF stooge is Larry.

IF stooge is Larry.

FOREACH stooge is .

FOREACH stooge is .

FOREACH stooge is .

FOREACH stooge is Moe

FOREACH stooge is Moe

FOREACH stooge is Moe

Обратите внимание: если бы в команде print в цикле foreach вместо $& использовалась переменная $_, то результат совпадал бы с результатом цикла while. Однако в этом случае не результат, возвращаемый командой m/…/g, ('Larry', 'Curly', 'Moe'), оставался бы неиспользованным. Вместо этого используется побочный эффект $&, почти всегда свидетельствующий об ошибке программирования, поскольку побочные контексты m/…/g в списковом контексте редко приносят пользу.

Для оператора подстановки определяются следующие специальные ограничители:

l      Как и в случае с оператором поиска, регулярное выражение-операнд может заключаться в апострофы<$M[R7-100]> [16 с.<$R[P#,R7-99]>], но по несколько иным правилам (если операнд-выражение заключен в апострофы, то же самое должно быть сделано с операндом-заменой, но не наоборот).

l      Специальный ограничитель оператора поиска ?…? не имеет специальной интерпретации для оператора замены.

l      Хотя в документации Perl5 утверждается обратное, обратные апострофы в качестве ограничителей не имеют специальной интерпретации, как это было в Perl4. В Perl4 операнд-замена сначала обрабатывался по правилам строк в кавычках, а затем выполнялся как системная команда. Полученные выходные данные использовались в качестве текста замены. В редких случаях, когда это действительно необходимо, это поведение легко имитируется в Perl5. Сравните две команды для определения имени команд и выборки ключей -version:

Perl4:            s`version of (\w+)`$1 --version 2>&1`g;

Perl4 и Perl5:       s/version of (\w+)/`$1 --version 2>&1`/e;

Подчеркнутая часть представляет собой операнд-замену. В первом варианте она выполняется как системная команда из-за присутствия специального ограничителя. Во втором варианте обратные апострофы не выполняют специальных функций до того момента, как вся строка замены будет вычислена в соответствии с модификатором /e. Модификатор /e, подробно описанный в следующем разделе, говорит о том, что операнд-замена представляет собой небольшой фрагмент кода Perl, который следует выполнить и использовать полученное значение как текст замены.

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

Модификатор /e

Модификатор /e может использоваться только в операторе подстановки. При использовании этого модификатора операнд-замена вычисляется по правилам eval {…} (включая проверку синтаксиса на стадии загрузки), и совпавший текст заменяется результатом вычисления. Операнд-замена не проходит дополнительную обработку перед eval (не считая определения лексических границ — см. фазу A на рис. 7.1) и даже не обрабатывается по правилам строк в кавычках<$M[R7-102]> [17 с.<$R[P#,R7-101]>]. Для каждого совпадения вычисление производится заново.

Например, в World Wide Web допускается кодирование специальных символов при помощи символа %, за которым следует шестнадцатеричный код из двух цифр. Кодирование всех символов, не являющихся алфавитно-цифровыми, выполняется командой<$M[R7-103]>

$url =~ s/([^a-zA-Z0-9])/sprintf('%%%02x', ord($1))/ge;

Команда декодирования выглядит так:

$url =~ s/%([0-9a-f][0-9a-f])/pack("C",hex($1))/ige;

Функция pack("C",число) преобразует числовой код в символ с заданным кодом, а функция sprintf('%%%02x', ord(символ)) решает противоположную задачу; за дополнительной информацией обращайтесь к документации Perl (а также см. сноску на с. <$R[P#,R3-31]>).

Уровни интерпретации

При использовании модификатора /e необходимо особенно четко понимать, кто, что и когда интерпретирует. Эта задача не такая уж сложна, но для того, чтобы разобраться в происходящем, потребуются некоторые усилия. Например, даже в простейших командах типа s/…/`echo $$`/e возникает вопрос: кто интерпретирует $$ — Perl или командный интерпретатор? Для Perl и для многих командных интерпретаторов переменная $$ содержит идентификатор процесса (Perl или командного интерпретатора). Таким образом, процесс интерпретации можно рассматривать на нескольких уровнях. Во-первых, операнд-замена в Perl5 не подвергается обработке перед вычислением, но в Perl4 выполняется обработка по правилам, напоминающим обработку строк в кавычках. При вычислении результата обратные апострофы обеспечивают обработку по правилам строк в кавычках (именно на этой стадии Perl интерполирует $$ — для предотвращения интерполяции можно воспользоваться экранированием). Наконец, результат передается командному интерпретатору, который выполняет команду echo (если экранировать $$, переменная будет передана интерпретатору в неэкранированном виде, что приведет к интерполяции $$ командным интерпретатором).

Ситуация осложняется еще одним обстоятельством. Если наряду с модификатором /e используется /g , сколько раз будет вычисляться `echo $$` — всего один раз (с использованием результата во всех заменах) или многократно, после каждого найденного совпадения? Если операнд подстановки содержит $1 и другие подобные конструкции, очевидно, вычисление должно производиться заново для каждого совпадения, чтобы переменная $1 правильно отражала состояние после поиска. В других ситуациях все не столь очевидно. Скажем, в примере с echo Perl версии 5.000 ограничивается одним вычислением, а другие версии (как предшествующие, так и последующие) для каждого совпадения производят вычисление заново.

/eieio

Вероятно, эта возможность пригодится только в конкурсе на Самую Загадочную Программу на Perl, и все же о ней стоит упомянуть. Если модификатор /e указывается несколько раз, операнд-замена также будет вычислен многократно (это единственный модификатор, для которого учитывается количество повторений). Эта «непреднамеренная», по выражению Ларри Уолла, возможность была «обнаружена» в начале 1991 года. Во время дискуссии в форуме comp.lang.perl Рэндал Шварц (Randal Schwartz) предложил одну из своих фирменных «подписей»[42]:

$Old_MacDonald = q#print #; $had_a_farm = (q-q:Just another Perl hacker,:-);

s/^/q[Sing it, boys and girls...],$Old_MacDonald.$had_a_farm/eieio;

Вызов eval, обусловленный первым модификатором /e, увидит фрагмент

q[Sing it, boys and girls...],$Old_MacDonald.$had_a_farm

В результате его выполнения получается строка q:Just another Perl hacker,:, которая при повторном вычислении выведет подпись Рэндала «Just another Perl hacker».

Впрочем, подобные конструкции иногда приносят практическую пользу. Допустим, вы хотите провести ручную интерполяцию переменных в строке (так, словно строка была прочитана из конфигурационного файла). Простейшее решение может выглядеть так: $data =~ s/(\$[a-zA-Z_]\w*)/$1/eeg; В строке option=$var регулярное выражение совпадает с подстрокой option=$var. При первом вычислении команда просто получит подстроку $1 из операнда-замены, которая будет расширена до $var. Из-за присутствия второго модификатора /e результат будет вычислен повторно, вследствие чего вместо $var будет подставлено текущее значение переменной. Оно заменит совпавшую подстроку $var, тем самым фактически будет выполнена интерполяция переменной.

Я нередко использую подобные приемы на своих Web-страницах — многие из них написаны на псевдокоде Perl/HTML, обрабатываемом средствами CGI при запросе со стороны удаленного клиента. Это позволяет мне производить некоторые вычисления в момент загрузки страницы — например, чтобы напомнить о том, сколько дней осталось до моего дня рождения[43].

Контекст и возвращаемое значение

Я уже говорил о том, что оператор поиска возвращает различные значения для разных сочетаний контекста с модификатором /g. С оператором подстановки дело обстоит проще — он всегда возвращает одинаковую информацию.

Возвращаемое значение либо равно количеству выполненных подстановок, либо (если ни одной подстановки не сделано) — пустую строку<$M[R7-105]> [18 с.<$R[P#,R7-104]>]. При логической интерпретации (например, в условии команды if) возвращаемое значение удобно интерпретируется как истина, если была выполнена хотя бы одна подстановка, и как ложь в противном случае.

Применение модификатора /g с регулярными выражениями, которые могут совпадать с пустой строкой

В предшествующем описании оператора поиска были подробно проанализированы те особые ситуации, когда регулярное выражение может совпасть с текстом нулевой ширины<$M[R7-110]>. Вдобавок разные версии Perl ведут себя по-разному, что вызывает еще больше путаницы. К счастью, оператор подстановки во всех версиях работает одинаково. Таблица 7.9 (с. <$R[P#,R7-106]>) в равной степени относится и к совпадениям s/…/…/g, но примеры с пометкой «Perl5» относятся только к оператору поиска. Примеры с пометкой «Perl4» относятся как к оператору поиска Perl4, так и к оператору подстановки во всех версиях Perl.

Оператор разбиения

<$M[R7-13]>Многогранный оператор split (в просторечии часто называемый функцией) обычно используется как некая противоположность конструкции m/…/…/g в списковом контексте (с. <$R[P#,R7-64]>). Последняя возвращает текст, совпавший с регулярным выражением, тогда как split с тем же регулярным выражением возвращает текст, разделяемый совпадениями. Так, применение команды $text =~ m/:/g к переменной $text, содержащей строку IO.SYS:225558:95-10-03:a-sh:optional, возвращает список из четырех элементов:

(':', ':', ':', ':')

Вряд ли этот список принесет какую-нибудь пользу. С другой стороны, команда split(/:/, $text) возвращает список из пяти элементов:

('IO.SYS', '225558', '95-10-03', '-a-sh', 'optional')

В обоих примерах [:] совпадает четыре раза. При использовании split эти четыре совпадения разделяют копию целевого текста на пять частей, возвращаемых в виде списка из пяти строк.

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

Простейшее разбиение

Оператор split выглядит как функция и получает до трех операндов:

split(совпадение, целевая_строка, ограничение)

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

Первый операнд (совпадение)

У первого операнда существует несколько особых случаев, но обычно он представляет собой простое регулярное выражение — например, /:/ или m/\s*<P>\s*/i. Традиционно вместо m/…/ используется /…/, хотя на самом деле это несущественно. Модификатор /g не нужен (он игнорируется), поскольку split и так обеспечивает многократное проведение поиска.

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

Второй операнд (целевая строка)

Оператор split только анализирует целевую строку и никогда не модифицирует ее. если целевая строка не задана, по умолчанию используется содержимое $_.

Третий операнд (ограничение)

Главная функция третьего операнда — ограничение количества фрагментов, на которые split разбивает строку. Например, для приведенного выше примера команда split(/:/, $text, 3) возвращает список:

('IO.SYS', '225558', '95-10-03:-a-sh:optional')

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

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

('IO.SYS', '225558', '95-10-03', '-a-sh:optional')

На практике происходит совсем иное.

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

($filename, $size, $date) = split(/:/, $text)

Вам нужно четыре фрагмента — имя файла, размер, дата и «все остальное». В принципе отдельный фрагмент со «всем остальным» вам не нужен, но без него эта информация содержалась бы во фрагменте $date. Итак, вы можете ограничиться четырьмя фрагментами, чтобы Perl не тратил времени на поиск дальнейших фрагментов. Но даже если ограничение не будет задано, Perl использует значение по умолчанию, обеспечивающее повышенное быстродействие без изменения результатов<$M[R7-108]> [19 с.<$R[P#,R7-107]>].

Нетривиальное разбиение

Поскольку split является оператором, а не функцией, он может интерпретировать свои операнды различными способами, не ограничиваясь стандартными правилами вызова функций. В частности, это объясняет, почему split может распознать свой первый операнд как оператор поиска, а не какое-то общее выражение, вычисляемое независимо перед вызовом «функции».

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

l      Первый операнд split отличается от обычного оператора поиска m/…/. Кроме того, у этого операнда существует несколько специальных значений.

l      Если первый операнд совпадает в начале или конце целевой строки, а также если совпадает дважды подряд, в одном из фрагментов обычно возвращается пустая строка. Обычно — но не всегда.

l      Что происходит в ситуации, когда регулярное выражение может совпадать с «ничем»?

l      Если регулярное выражение содержит сохраняющие круглые скобки, поведение split изменяется.

l      В скалярном контексте split (в настоящее время подобное использование не рекомендуется) вместо того, чтобы возвращать список, сохраняет его в @_.

Возвращение пустых фрагментов

Оператор split прежде всего предназначен для возвращения текста между совпадениями. Если оператор поиска совпадает два раза подряд, возвращается пустая строка между этими совпадениями. Предположим, конструкция m/:/ применяется к строке

:IO.SYS:225558:::95-10-03:a-sh:

В этом случае будет найдено семь совпадений (подчеркнутых). При использовании split целевая строка всегда[44] разбивается между совпадениями, в том числе и перед первым совпадением в самом начале строки, отделяющим '' (пустую строку, «ничто») от 'IO.SY…'. Аналогично, четвертое совпадение разделяет две пустые строки. В итоге семь совпадений разбивают целевую строку на восемь подстрок:

('', 'IO.SYS', '225558', '', '', '95-10-03', '-a-sh', '')

Тем не менее, при выполнении команды split(/:/, $text) будет получен другой результат. Удивлены?

Завершающие пустые элементы (обычно) не возвращаются

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

Еще одна функция третьего операнда

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

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

С другой стороны, если вы хотите удалить из списка все пустые элементы, поставьте grep{length} перед вызовом split. Функция grep оставит в списке только элементы, имеющие ненулевую длину.

Нетривиальное использование первого операнда split

Большинство сложностей, связанных с применением оператора split, связано с разносторонними применениями его первого операнда. Этот операнд может выступать в четырех видах:

l      Оператор поиска (вернее, его аналог).

l      Специальное скалярное значение 'spc' (одиночный пробел).

l      Любое общее скалярное выражение.

l      Значение по умолчанию в том случае, если операнд не передается.

Аналог оператора поиска

Самым распространенным случаем использования split является использование оператора поиска в качестве первого операнда. Тем не менее, между операндом split и настоящим оператором поиска существует ряд важных различий:

l      В аналоге оператора поиска не следует (а в некоторых версиях Perl — нельзя) указывать целевой текст. Никогда не используйте в split оператор =~, это приводит к непредсказуемым последствиям! split подберет для заданного операнда нужный целевой текст.

l      Мы уже обсуждали проблемы, связанные с регулярными выражениями, совпадающими с пустой строкой — например, m/x*/ (с. <$R[P#,R7-109]>) и s/x*/…/ (с. <$R[P#,R7-110]>). Возможно, из-за того, что split обеспечивает повторение вместо простого поиска, ситуация выглядит гораздо проще.

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

Например, m/\W*/ совпадает в строке lwrTlwrhlwrilwrs, "Tlwrhlwralwrt", Olwrtlwrhlwrelwrr! в указанных местах (подчеркнутых или обозначенных lwr при пустом совпадении). Из-за этого исключения пустое совпадение в начале строки игнорируется. Остается 13 совпадений, приводящих к построению списка из 14 элементов:

('T', 'h', 'i', 's', 'T', 'h', 'a', 't', 'O', 't', 'h', 'e', 'r', '')

Конечно, если третий операнд не указан, завершающий пустой элемент удаляется.

l       Пустое регулярное выражение, передаваемое при вызове split, означает не «использовать текущее регулярное выражение по умолчанию», а «разбивать строку после каждого символа». Например, для вставки комбинации «забой-подчеркивание» после каждого символа $text можно воспользоваться командой

$text = join "\b_", split(//, $text, -1);

Впрочем, по ряду причин (хотя бы по соображениям наглядности) лучше воспользоваться командой $text =~ s/(.)/$1\b_/g.

l      Использование регулярного выражения в операторе split не влияет на регулярное выражение по умолчанию для последующих операторов поиска и замены. Кроме того, split не устанавливает значения переменных $&, $', $1 и т. д. В отношении побочных эффектов split полностью изолируется от остальных частей программы.

l      Модификатор /g в split не имеет смысла (хотя и вреда не приносит).

l      Специальный ограничитель регулярных выражений ?…? в split не имеет особой интерпретации<$M[R7-112]> [20 с.<$R[P#,R7-111]>].

Специальный операнд: строка из одного пробела

Операнд, который представляет собой строку (не регулярное выражение!), состоящую ровно из одного пробела — особый случай. Он почти эквивалентен /\s+/, если не считать игнорирования начальных пропусков. Эта конструкция предназначалась для имитации стандартного разбиения по разделителям входных записей в awk, но она, несомненно, находит немало применений и в более общих случаях.

Например, вызов split('spc', "spcspcspcthisspcspcspcisspcaspcspcspcspcspctest") возвращает список из четырех элементов: ('this', 'is', 'a', 'test'). Для сравнения рассмотрим непосредственное использование m/\s+/. В этом случае начальные пропуски не игнорируются, и команда вернет список ('', 'this', 'is', 'a', 'test').

Наконец, оба случая заметно отличаются от применения m/spc/, при котором будут найдены совпадения и для внутренних пробелов:

('', '', '', 'this', '', '', 'is', 'a', '', '', '', '', 'test')

Любое общее скалярное выражение

Любое общее выражение Perl, использованное в качестве первого операнда split, вычисляется независимо, преобразуется к строковому виду и интерпретируется как регулярное выражение. Например, команда split(/\s+/, …) идентична split('\s+', …), за исключением того, что в первом случае регулярное выражение компилируется только один раз, а во втором — при каждом выполнении split<$M[R7-114]> [21 с.<$R[P#,R7-113]>].

Значение по умолчанию

В Perl5 в том случае, если первый операнд не указан (что в принципе отлично от // или ''), по умолчанию используется 'spc'. Таким образом, вызов split без операндов эквивалентен split('spc',$_, 0)<$M[R7-116]> [22 с.<$R[P#,R7-115]>].

Оператор split в скалярном контексте

В Perl4 поддерживался вариант split для скалярного контекста, возвращавший количество фрагментов вместо списка, а список фрагментов присваивался переменной @_ в качестве побочного эффекта. Хотя в настоящее время эта возможность поддерживается и в Perl5, пользоваться ей не рекомендуется, и в ближайшем будущем она, вероятно, исчезнет. Если предупреждения включены (как это обычно должно быть), при использовании split в скалярном контексте генерируется предупреждение.

Сохраняющие круглые скобки в первом операнде split

Использование сохраняющих<$M[R7-57]> круглых скобок в первом операнде split изменяет принцип работы split. В этом случае возвращаемый массив содержит дополнительные, независимые элементы, чередующиеся с элементами, совпавшие с подвыражениями в круглых скобках. Это означает, что текст, обычно полностью исключавшийся split при разбиении, теперь включается в возвращаемый список.

В Perl4 это приносило больше вреда, чем пользы, поскольку в регулярном выражении нельзя было (без особых усилий) использовать круглые скобки, предназначенные только для группировки. Если группировка была единственной целью, то загромождение возвращаемого списка дополнительными элементами определенно не обрадует. Теперь, когда можно выбрать стиль круглых скобок (сохраняющие или не-сохраняющие), это воистину замечательная возможность. Например, в процессе обработки HTML-кода команда split(/(<[^>]>)/) для текста

spcandspc<B>veryspc<FONTspccolor=red>very</FONT>spcmuch</B>spceffort

возвращает список

('...spcandspc', '<B>', 'veryspc', '<FONTspccolor=red>',

 'very', '</FONT>, 'spcmuch', '</B>, 'spceffort...' )

Возможно, работать с этим списком будет проще, чем со строкой. Этот пример работает и в Perl4, но если вы захотите, чтобы регулярное выражение распознавало, скажем, простые строки в кавычках ["[^"]*"] внутри тегов (что, вероятно, необходимо для нормальной работы с HTML), у вас возникнут проблемы, поскольку полное регулярное выражение будет выглядеть примерно так[45]:

[(<[^>"]*("[^"]*"[^>"]*)*>)]

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

Please <A HREF="test">press me</A>today

В этом случае возвращается следующий результат:

( 'Pleasespc',                       перед первым совпадением

'<AspcHREF="test">', '"test"',   из первого совпадения

'press me',                      между совпадениями

'</A>', '',                      из второго совпадения

'spctoday'                       после последнего совпадения

)

Лишние элементы загромождают список. Но если вместо дополнительных скобок использовать [(?:…)], работать со split становится удобнее, и команда возвращает следующий результат:

( 'Pleasespc',                    перед первым совпадением

'<AspcHREF="test">',          из первого совпадения

'press me',                   между совпадениями

'</A>',                       из второго совпадения

'spctoday'                    после последнего совпадения

)

Проблемы эффективности в Perl

<$M[R7-6]>Проблемы эффективности в Perl обычно решаются так же, как и во всех остальных программах с традиционным механизмом НКА — за счет использования приемов, описанных в главе 5: внутренних оптимизаций, раскрутки и т. д. Все эти приемы относятся и к Perl.

Конечно, существуют и приемы, специфические для Perl — например, применение не-сохраняющих круглых скобок во всех случаях, когда нет необходимости в сохранении текста. Существуют и другие, более важные проблемы, и даже применение сохраняющих круглых скобок вместо не-сохраняющих не сводится к микро-оптимизации, рассмотренной в главе 5 (с. <$R[P#,R5-24]>). В этом разделе мы рассмотрим эту тему (с. <$R[P#,R7-4]>), а также некоторые другие вопросы.

l      У каждой задачи есть несколько решений. Perl — набор инструментов, позволяющий решить одну и ту же задачу несколькими способами. Умение грамотно идентифицировать задачу происходит из осознания «Пути Perl», а умение выбрать правильный инструмент для ее решения является большим шагом на пути к построению более эффективных и понятных программ. Иногда эффективность и наглядность программы кажутся взаимоисключающими требованиями, но лучшее понимание вопроса поможет вам с правильным выбором средств.

l      Интерполяция. Интерполяция и компиляция регулярных выражений-операндов — благодатная почва для экономии времени. Модификатор /o, о котором я практически не упоминал, позволяет частично управлять затратным процессом повторной компиляции.

l      Затраты $& Значения трех переменных $`, $& и $' присваиваются в качестве побочного эффектам поиска. Эти переменные удобны, но любое использование их в сценарии отрицательно влияет на его эффективность. Более того, эти переменные даже необязательно использовать — весь сценарий страдает даже в том случае, если одна из этих переменных просто присутствует в нем.

l      Затраты /i Использование модификатора /i также влечет за собой затраты. Если вы работаете с очень длинной целевой строкой, постарайтесь переписать регулярное выражение так, чтобы избежать использования /i.

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

l      Хронометраж В конечном счете самая быстрая программа — та, которая первой заканчивает свою работу. Хронометраж всегда позволяет наиболее объективно оценить скорость работы программного кода, будь то маленькая функция, большой фрагмент или целая программа, работающая с реальными данными. В Perl существуют простые и удобные средства хронометража, хотя у этой задачи, как у большинства остальных, тоже существует несколько решений. Я покажу вам тот способ, которым пользуюсь сам — простой прием, при помощи которого я провел несколько сот тестов во время работы над книгой.

l      Study Функция study(…) существует в Perl с незапамятных времен. Как правило, многие что-то слышали о том, что study ускоряет обработку регулярных выражений, но лишь немногие понимают, что же именно она делает. Посмотрим, удастся ли нам в этом разобраться.

l      Ключ -dr При помощи флага отладки регулярных выражений Perl можно получить информацию о том, какие виды оптимизаций выполняются или не выполняются механизмом регулярных выражений. Вскоре вы узнаете, как это делается, и заставите Perl поделиться некоторыми секретами.

У каждой задачи есть несколько решений

Довольно часто к решению конкретной задачи можно подойти разными способами, поэтому нет ничего, что заменило бы хорошую осведомленность в вопросах балансировки эффективности и наглядности программ в Perl. Рассмотрим простой пример — дополнение IP-адресов (18.181.0.24) нулями, чтобы каждый из четырех компонентов состоял ровно из трех цифр (018.181.000.024). Одно простое и наглядное решение выглядит так:

$ip = sprintf "%03d.%03d.%03d.%03d", split(/\./, $ip);

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

1.    $ip =~ s/(\d+)/sprintf("%03d", $1)/eg;

2.    $ip =~ s/\b(\d{1,2}\b)/sprintf("%03d", $1)/eg;

3.    $ip = sprintf("%03d.%03d.%03d.%03d", $ip =~ m/(\d+)/g);

4.    $ip =~ s/\b(\d\d?\b)/'0' x (3-length($1)) . $1/eg;

5.    $ip = sprintf("%03d.%03d.%03d.%03d",

             $ip =~ m/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);

6.    $ip =~ s/\b(\d(\d?)\b)/$2 eq '' ? "00$1" : "0$1"/eg;

7.    $ip =~ s/\b(\d\b)/00$1/g;

     $ip =~ s/\b(\d\d\b)/0$1/g;

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

А как же эффективность? Я протестировал все эти решения на своем компьютере с Perl версии 5.003, и приведенный выше список упорядочен от наименее эффективного к наиболее эффективному решению. Исходное решение находится где-то между четвертым и пятым элементом списка. Лучшее решение занимает всего 80 процентов от времени его работы, а худшее — около 160 процентов[46]. Но если эффективность действительно важна, существуют и более быстрые решения:

substr($ip,  0, 0) = '0' if substr($ip,  1, 1) eq '.';

substr($ip,  0, 0) = '0' if substr($ip,  2, 1) eq '.';

substr($ip,  4, 0) = '0' if substr($ip,  5, 1) eq '.';

substr($ip,  4, 0) = '0' if substr($ip,  6, 1) eq '.';

substr($ip,  8, 0) = '0' if substr($ip,  9, 1) eq '.';

substr($ip,  8, 0) = '0' if substr($ip, 10, 1) eq '.';

substr($ip, 12, 0) = '0' while length($ip) < 15;

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

Компиляция регулярных выражений, модификатор /o и эффективность

<$M[R7-50]>В разделе «Предварительная обработка строк и интерполяция переменных» было показано, что обработка сценариев Perl делится на несколько фаз. Все действия, представленные на рис. 7.1 (с. <$R[P#,R7-117]>), могут быть сопряжены с немалыми затратами. Возможен следующий вариант оптимизации: Perl понимает, что при отсутствии интерполяции переменных бессмысленно заново выполнять все фазы при каждом применении регулярного выражения — если интерполяция не выполняется, регулярное выражение не изменяется между применениями. В таких случаях внутреннее представление сохраняется при первой компиляции регулярного выражения, а затем используется для всех последующих применений тем же самым оператором. При этом экономится значительный объем работы, связанной с повторными вычислениями.

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

$today = (qw(Sun Mon Tue Wed Thu Fri Sat>)[(localtime)[6]];

# $today содержит день недели ("Mon", "Tue" и т. д.)

$regex = "^$today";

while (<LOGFILE>) {

    if (m/$regex/) {

    …

Значение переменной $regex присваивается еще до начала цикла. Однако оператор поиска, в котором оно используется, находится в цикле, поэтому он применяется снова и снова, по одному разу для каждой строки <LOGFILE>. Мы можем взглянуть на сценарий и убедиться в том, что это значение в цикле не изменяется, но Perl этого не знает. Он знает лишь то, что регулярное выражение-операнд содержит интерполируемую переменную, поэтому этот операнд необходимо заново вычислять при каждом использовании.

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

На помощь приходит модификатор<$M[R7-85]> /o. Он приказывает Perl обработать и откомпилировать регулярное выражение-операнд при первом использовании, но затем слепо использовать ту же внутреннюю форму во всех последующих проверках в том же операторе. Модификатор /o «фиксирует» регулярное выражение при первом использовании оператора поиска. При последующем использовании будет применяться то же самое выражение — даже в случае изменения переменных, входящих в операнд. Perl попросту не утруждает себя проверками. Обычно модификатор /o используется для повышения эффективности, если вы не собираетесь изменять регулярное выражение, но вы должны хорошо понимать: даже если переменные изменятся, случайно или преднамеренно, при наличии модификатора /o Perl не станет компилировать или обрабатывать выражение заново.

Рассмотрим следующую ситуацию:

while(…)

{

    …

    $regex = &GetReply('Item to find');

 

    foreach $item (@items) {

        if ($item =~ m/$regex/o) { # Модификатор /o использован для

           …                # повышения эффективности

        }

    }

    …

}

При первой итерации внутреннего цикла foreach (и первой итерации внешнего цикла while) регулярное выражение обрабатывается и компилируется, а полученный результат используется при поиске. Из-за наличия модификатора /o откомпилированная форма будет использоваться во всех последующих попытках для данного оператора поиска. Позднее, во второй итерации внешнего цикла, пользователь вводит новое значение $regex, которое должно использоваться при новом поиске. Однако происходит нечто неожиданное — модификатор /o означает, что регулярное выражение оператора компилируется только один раз, и поскольку это уже произошло, старое регулярное выражение будет использовано заново — новое значение $regex попросту игнорируется.

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

Применение регулярного выражения по умолчанию

Если мы каким-то образом добьемся того, чтобы в результате успешного совпадения регулярное выражение было выбрано для использования по умолчанию, это выражение можно повторно использовать при помощи конструкции m// с пустым регулярным выражением (с. <$R[P#,R7-86>):

while (...)

{

    …

    $regex = &GetReply('Item to find');

 

    # Установить регулярное выражение по умолчанию

    # (требует успешного совпадения)

    if ($sample_text !~ m/$regex/) {

        die "internal error: sample text didn't match!";

    }

 

    foreach $item (@items) {

        if ($item =~ m//) { # Использовать регулярное

            …               # выражение по умолчанию

        }

    }

    …

}

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

Модификатор /o и eval

<$M[R7-84]>Ниже приведено решение, избавленное от всех перечисленных недостатков. Хотя решение несколько усложнилось, цель (повышение эффективности) часто оправдывает средства.

while (...)

{

    …

    $regex = &GetReply('Item to find');

    eval 'foreach $item (@items) {

           if ($item =~ m/$regex/o) {

                  …

           }

           }';

    # Если значение $@ определено, выполнение eval

    # завершилось с ошибкой.

    if ($@) {

           # Вывести сообщение об ошибке при выполнении eval

    }

    …

}

Обратите внимание: весь цикл foreach выполняется внутри строки в апострофах, которая является аргументом eval. При каждой обработке строки вызовом eval строка воспринимается как новый фрагмент кода Perl, поэтому Perl заново анализирует и выполняет ее. Выполнение происходит «на месте», поэтому eval имеет доступ ко всем переменным программы, словно они являются частью кода регулярного выражения. Собственно, вся суть подобного использования eval как раз и заключается в том, чтобы отложить анализ до момента, когда нам будет известно каждое из регулярных выражений.

Для нас в первую очередь интересно то, что этот фрагмент заново анализируется при каждом выполнении eval; при этом регулярные выражения-операнды также анализируются заново, начиная с фазы A рис. 7.1 (с. <$R[P#,R7-117]>). Вследствие этого регулярное выражение компилируется при первом его использовании в программе (при первой итерации в цикле foreach), но не компилируется заново из-за присутствия модификатора /o. После завершения eval этот экземпляр фрагмента навечно исчезает. При следующей итерации внешнего цикла while строка, переданная eval, будет той же самой, но поскольку eval интерпретирует ее заново, фрагмент кода будет считаться новым. Таким образом, регулярное выражение обрабатывается заново, поэтому оно компилируется (с новым значением $regex) в ближайшей ситуации, когда оно будет встречено при новом вызове eval.

Конечно, компиляция фрагмента кода при каждой итерации внешнего цикла также требует определенных затрат. Оправдывает ли экономия от /o эти затраты? При небольшом размере массива @items — наверное, нет. Если размер массива достаточно велик — вполне вероятно. Хронометраж (см. ниже) поможет вам принять правильное решение.

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

Вычисление строки и вычисление блока

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

eval {foreach $item (@items) {

           if ($item =~ m/$regex/o) {

           …

           }

        }};

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

К числу доводов в пользу eval также принадлежат эффекты перекомпиляции не-блочной формы и возможность перехвата ошибок. Ошибки времени выполнения могут перехватываться как в строковой, так и в блочной форме, но только строковая форма позволяет перехватывать ошибки стадии компиляции (пример встречался при описании $* на с. <$R[P#,R7-118]>). Я вообще не вижу особых причин для использования блочной версии eval {…}, кроме перехвата ошибок времени выполнения (например, проверки того, поддерживается ли та или иная возможность вашей версией Perl) и перехвата warn, die, exit и т. д.

Третья ситуация, в которой используется eval — выполнение кода, сгенерированного во время работы программы. В следующем примере продемонстрирован один распространенный фокус:

sub Build_MatchMany_Function

{

my @R = @_;            # Аргументами являются регулярные выражения

my $program = '';      # Переменная для построения программного фрагмента

foreach $regex (@R) {

         $program .= "return 1 if m/$regex/;"; # Проверка для каждого

                                                                            # регулярного выражения

}

my $sub = eval "sub { $program; return 0 }"; # Создать анонимную функцию

die $@ if $@;

$sub; # Вернуть функцию пользователю

}

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

# Создать функцию для проверки по нескольким регулярным выражениям

$CheckFunc = Build_MatchMany_Function(@regexes2check);

 

while(<>) {

    # Вызвать функцию для проверки текущего значения $_

    if (&$CheckFunc) {

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

    }

}

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

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

$regex = join('|', @regexes2check);  # Построить одно большое выражение

 

while (<>) {

    if (m/$regex/o) {

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

    }

}

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

while (<>) {

    foreach $regex (@regexes2check) {

           if (m/$regex/) {

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

                  last;

           }

    }

}

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

Если Build_MatchMany_Function передаются строки this, that и other, построенный и выполненный посредством eval фрагмент будет выглядеть так:

sub {

    return 1 if m/this/;

    return 1 if m/that/;

    return 1 if m/other/;

    return 0

}

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

Идея неплохая, но ее распространенные реализации (в том числе и только что приведенная) грешат некоторыми недостатками. Если переданная Build_MatchMany_Function строка содержит символы $ или @, которые могут быть особым образом интерпретированы в процессе интерполяции переменных, вас ждет большой сюрприз. Частичное решение проблемы заключается в использовании ограничителей-апострофов:

$program .= "return 1 if m'$regex';"; # Проверка для каждого

                                                          # регулярного выражения

Но существует и другая, более серьезная проблема. Что произойдет, если одно из регулярных выражений содержит апостроф (или другой ограничитель регулярного выражения)? Например, регулярное выражение [don't] добавит в построенный фрагмент команду

return 1 if m'don't';

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

sub Build_MatchMany_Function

{

my @R = @_;

my $expr = join '||', map { "m/\$R[$_]/o" } (0..$#R);

my $sub = eval "sub { $expr }";         # Создать анонимную функцию

die $@ if $@;

$sub; # Вернуть функцию пользователю

}

Анализ этого решения остается читателю для самостоятельной работы. Тем не менее, я задам вопрос<$M[R7-119]>: что произойдет, если при объявлении массива @R в этой функции вместо my было бы использовано ключевое слово local? refПереверните страницу и проверьте свой ответ.

Нежелательная переменная $& и ее друзья

<$M[R7-3]>Переменные $`, $& и &' ссылаются соответственно на текст, предшествующий совпадению, текст самого совпадения и текст, следующий за совпадением (с. <$R[P#,R7-34]>). Даже если целевая строка позднее изменится, эти переменные по-прежнему будут ссылаться на исходную версию текста. Целевая строка изменяется при подстановке, но переменная $& по-прежнему должна ссылаться на исходный текст (заменяемый в результате подстановки). Более того, даже если мы сами модифицируем целевую строку, переменные $1, $& и т. д. все равно должны ссылаться на исходный текст (по крайней мере до следующего успешного совпадения или выхода из блока). Как же образом Perl сохраняет исходную информацию, невзирая на все изменения?

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

Внутренние оптимизации

Не совсем. Если вы не собираетесь использовать эти внутренние переменные, необходимость в копировании отпадает, что может привести к немалой экономии памяти. К сожалению, Perl не знает, собираетесь вы использовать эти переменные или нет. С другой стороны, Perl иногда понимает, что создавать копию необязательно. Если вы научитесь инициировать подобные «озарения» в своих программах, они будут работать более эффективно. Отказ от создания копии не только экономит время, но и совершенно удивительным образом часто ускоряет работу оператора подстановки (эта тема рассматривается ниже в этом разделе).

local и my

bref Ответ на вопрос со с. <$R[P#,R7-119]>

Прежде чем отвечать на вопрос, следует сказать несколько слов о механизме связывания (binding). Когда Perl компилирует фрагмент программного кода (при загрузке программы или при выполнении eval), происходит связывание ссылок на переменные этого фрагмента с кодом. Значения переменных на этой стадии еще не известны. Доступ к ним осуществляется во время выполнения программного кода.

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

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

С другой стороны, конструкция local @R просто сохраняет копию глобальной переменной @R перед тем, как ее содержимое будет перезаписано массивом @_. Вполне возможно, что никакого предыдущего содержимого не существовало, но если глобальная переменная @R все же использовалась какой-то частью программы, мы на всякий случай сохраняем копию. При вычислении фрагмента посредством eval обозначение @R относится к той же глобальной переменной (что совершенно не связано с использованием local — поскольку не существует приватной my-версии @R, все ссылки на @R относятся к глобальной переменной, и наличие или отсутствие local для копирования данных несущественно).

При выходе из функции Build_MatchMany_Function восстанавливается сохраненная копия @R. Глобальная переменная @R — это та же переменная, которая использовалась при вызове eval, и та же переменная, с которой связана анонимная функция, но содержимое этой переменной становится другим. Таким образом, мы скопировали новые значения в @R, но ни разу не сослались на них! Все усилия пропали даром. Анонимная функция ожидает, что глобальная переменная @R содержит строки, используемые в качестве регулярных выражений, но мы уже потеряли скопированные значения. При первом использовании функции она «увидит» прежнее содержимое @R — что бы там ни хранилось, это не наши регулярные выражения, поэтому все решение перестает работать.

Остается сделать последнее замечание. Если бы перед выходом из Build_MatchMany_Function мы использовали анонимную функцию (и при этом ни одно из регулярных выражений не совпало в тексте), то модификатор /o зафиксировал бы регулярные выражения, хранящиеся в @R, и проблема была бы решена (если совпадение будет найдено, то зафиксируются только регулярные выражения, проверенные до настоящего момента). Такое решение обладает несомненными преимуществами — переменная @R нужна нам лишь до тех пор, пока регулярные выражения не будут зафиксированы в памяти. После этого необходимость в строках, использованных для их создания, отпадает, поэтому хранение их в отдельной переменной @R (удаляемой лишь после удаления анонимной функции!) лишь приводит к непроизводительным затратам памяти.

Внимание: приемы оптимизации, описанные ниже, основаны на знаниях внутренних механизмов работы Perl. Если эти приемы ускорят работу ваших программ — хорошо, но принципы, на которых основана их работа, не входят в спецификацию Perl[47] и могут измениться в будущих версиях (книга написана на основании версии 5.003). Если эти оптимизации неожиданно исчезнут, то это отразится только на эффективности — программы будут выдавать прежние результаты, поэтому особых причин для беспокойства нет.

Копирование при успешном поиске или подстановке осуществляется в трех ситуациях<$M[R7-121]> [23 с.<$R[P#,R7-120]>]:

l      если переменные $`, $& или &' встречаются в любом месте всего сценария;

l      если в регулярном выражении встречаются сохраняющие круглые скобки;

l      если модификатор /i используется в операторе поиска без модификатора /g.

Кроме того, дополнительные внутренние копии могут потребоваться для поддержки:

l      использования модификатора /i (при любом поиске или подстановке);

l      использования многих (но не всех) операторов подстановки.

Первые три случая рассматриваются ниже, а два последних — в следующем разделе.

Копирование при использовании $`, $& и &'

Для поддержки любого использования переменных $`, $& и &' Perl приходится создавать копии. На практике после большинства операций поиска эти переменные не используются, поэтому было бы желательно, чтобы копирование выполнялось только тогда, когда это действительно необходимо. Но поскольку эти переменные обладают динамической видимостью, их использование может происходить на большом расстоянии от найденного совпадения. Теоретически Perl может подробно проанализировать всю программу и определить, что любые случаи использования этих переменных не относятся к конкретной операции поиска (поэтому копирование для этой операции выполнять не нужно). На практике Perl этого не делает. По этой причине копирование обычно выполняется при каждом успешном совпадении каждого регулярного выражения во время работы программы.

Однако Perl замечает, если ссылки на $`, $& и &' вообще не встречаются в программе (а также во всех библиотеках, используемых сценарием!) Поскольку переменные в программе не используются, Perl может с полной уверенностью исключить все операции копирования, необходимые для их поддержки. Таким образом, если проследить за тем, чтобы переменные $`, $& и &' ни разу не упоминались в программе и всех используемых ей библиотеках, вам удастся избежать затрат, связанных с копированием (если необходимость в копировании не будет обусловлена двумя другими случаями).

Копирование при использовании сохраняющих круглых скобок

<$M[R7-4]>Если в регулярном выражении используются сохраняющие круглые скобки, Perl полагает, что вы собираетесь использовать сохраненный текст и выполняет копирование после поиска совпадения (наличие или отсутствие $1 в программе роли не играет — если сохраняющие круглые скобки встречаются в регулярном выражении, то копирование выполняется даже в том случае, если его результаты никогда не используются). В Perl4 не существовало круглых скобок, ограничивающихся группировкой элементов регулярного выражения, поэтому даже если вы не собирались сохранять текст, это приходилось рассматривать как отрицательный побочный эффект. С появлением конструкции [?:…] вам уже не придется сохранять текст, который вам не нужен[48]. Но если вы собираетесь использовать переменные $1, $2 и т. д., Perl выполнит копирование.

Копирование при использовании m/…/i

При использовании /i в операторе поиска без модификатора /g возникает необходимость в копировании. Почему? Честно говоря, не знаю. При взгляде на программную реализацию копирование кажется мне абсолютно излишним, но я, конечно, не являюсь экспертом по внутреннему устройству Perl — тем более, что использование /i имеет другие, более существенные последствия для быстродействия программы. Вскоре я вернусь к этой теме, но сначала я хочу привести результаты тестов, демонстрирующих последствия копирования при поддержке $&.

Результаты хронометража

<$M[R7-122]>Я провел простой тест, в котором конструкция m/c/ применялась к каждой из 50 000 с лишним строк программного кода C в исходных текстах Perl. В процессе хронометража я просто проверял, присутствует ли в строке буква — полученная информация никак не обрабатывалась, поскольку конечной задачей было определение последствий от копирования. Хронометраж проводился дважды: в первый раз ни одно из перечисленных выше условий не выполнялось, а во второй я позаботился о том, чтобы эти условия выполнялись. Таким образом, единственным отличием были затраты, связанные с дополнительным копированием.

Проведенная серия тестов показала, что дополнительное копирование стабильно увеличивало время выполнения программы более чем на 35 процентов. Конечно, эти данные следует рассматривать как своего рода «случай средней тяжести». Чем больше реальной работы выполняется в программе, тем меньшие (в процентном отношении) последствия будет иметь копирование. В моих тестах никакой реальной работы не выполнялось, поэтому эффект особенно хорошо заметен.

С другой стороны, в действительно тяжелых случаях большая часть времени работы программы может быть потрачена на дополнительное копирование. Я провел тот же тест для тех же данных, но на этот раз вместо 50 000 строк средней длины данные были организованы в виде одной большой строки объемом более мегабайта. На этом примере можно было оценить относительное быстродействие одной операции поиска. Без копирования операция выполнялась практически мгновенно, поскольку буква c находилась где-то неподалеку от начала строки. Как только буква была найдена, поиск завершался. Проверка с копированием работала так же, но с одним исключением — сначала создавалась копия строки, объем которой превышал мегабайт. Программа стала работать почти в 700 раз медленнее! Зная последствия применения некоторых конструкций, вы сможете добиться от своего кода максимальной эффективности.

Заключения и рекомендации по поводу переменной $& и ее друзей

Конечно, было бы замечательно, если бы Perl знал о намерениях программиста и создавал копии лишь в случае необходимости. Но следует учитывать, что копирование — это далеко не всегда плохо. Именно благодаря тому, что Perl берет на выполнение подобных второстепенных задач, мы выбираем этот язык вместо C или ассемблера. В самом деле, Perl был разработан как раз для того, чтобы пользователь мог избавиться от механических манипуляций с битами и сосредоточить все внимание на создании творческих решений.

К решению задач на Perl можно подходить разными способами, но следует помнить то, о чем я уже неоднократно упоминал. Если вы программируете на Perl так, как вы бы программировали на другом языке (например, на C), ваши Perl-программы получатся убогими и почти всегда малоэффективными. Как правило, построение программы в соответствии с идеологией Perl в значительной степени помогает встать на правильный путь, но здесь, как и в любой другой области, специальные меры могут обеспечить улучшение результата. Действительно, хотя в копировании нет ничего «принципиально плохого», мы все же хотим по возможности избавиться от лишних затртат. Ниже описаны некоторые действия, которые можно для этого предпринять.

Конечно, прежде всего следует полностью<$M[R7-129]> исключить использование переменных $`, $& и &' в вашей программе. Это также означает, что вы не должны использовать English.pm и любые другие библиотечные модули, использующие его, а также содержащие ссылки на эти переменные в табл. 7.10 приведен список стандартных библиотек Perl (версии 5.003), прямо или косвенно использующих эти нежелательные переменные. Обратите внимание: многие библиотеки испорчены использованием Carp.pm. Если заглянуть в этот файл, вы обнаружите в нем только одну ссылку:

$eval =~ s/[\\\']/\\$&/g;

Приведение этой строки к виду

$eval =~ s/([\\\'])/\\$1/g;

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

Если эти переменные ни разу не встречаются в программе, то копирование выполняется лишь при использовании в программе сохраняющих круглых скобок или m/…/i. Возможно, вам придется переписать некоторые выражения в программе. Например, $` часто имитируется конструкцией [(*?)] в начале регулярного выражения, $& — заключением всего регулярного выражения в [(…)], а &' — конструкцией<$M[R7-60]> [(?=(.*))] в конце.

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

Таблица 7.10. Стандартные библиотеки, содержащие ссылки на переменные семейства $&

C

AutoLoader

C

Fcntl

+C

Pod::Text

C

AutoSplit

 

File::Basename

C

POSIX

C

Benchmark

C

File::Copy

C

Safe

C

Carp

C

File::Find

C

SDBM_File

C

DB_File

C

File::Path

C

SelectSaver

+CB

diagnostics

C

FileCache

C

SelfLoader

C

DirHandle

C

FileHandle

C

Shell

 

dotsh.pl

C

GDBM_File

C

Socket

 

dumpvar.pl

 

Getopt::Long

C

Sys::Hostname

C

DynaLoader

C

IPC:Open2

C

Syslog

 

English

+C

IPC:Open3

C

Term::Cap

+CB

ExtUtils::Install

C

lib

C

Test::Harness

+CB

ExtUtils::LibList

C

Math::BigFloat

C

Test::ParseWords

C

ExtUtils::MakeMaker

C

MM_VMS

C

Text::Wrap

+CB

ExtUtils::Manifest

+CL

newgetopt.pl

C

Tie::Hash

C

ExtUtils::Mkbootstrap

C

ODBM_File

C

Tie::Scalar

C

ExtUtils::Mksymlists

 

open2.pl

C

Tie::SubtrHash

+CB

ExtUtils::MM_Unix

 

open3.pl

C

Time::Local

C

ExtUtils::testlib

 

perl5db.pl

C

vars

Не рекомендуется из-за использования: C — Carp; B — File::Basename; E — English; L — Getopt::Long

Влияние модификатора /i на эффективность

<$M[R7-68]>Здравый смысл подсказывает, что при поиске без учета регистра символов Perl выполняет какую-то дополнительную работу. Вероятно, вы даже не подозреваете, насколько в действительности увеличивается объем этой работы.

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

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

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

Способы реализации поиска без учета регистра

Существует по крайней мере два распространенных подхода к реализации поиска без учета регистра. Один из них, описанный выше, я называю «ориентированным на строку» (а также «необоснованно неэффективным», как говорилось выше). Другой, на мой взгляд — гораздо лучший способ, называется «ориентированным на регулярное выражение». Он работает с исходной строкой, содержащей символы разного регистра, и позволяет механизму принимать решения о различиях в регистре символов по мере необходимости.

проверка «загрязнения» программ использованием $&

Далеко не всегда можно уверенно определить, встречаются ли в вашей программе ссылки на $`, $& и &' — особенно при использовании библиотек. Я даже модифицировал свою версию Perl так, чтобы при первом обнаружении этих переменных она выдавала предупреждение (если вы хотите сделать то же самое, найдите три экземпляра sawampersand в файле gv.c из поставки Perl и добавьте соответствующий вызов warn).

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

sub CheckNaughtiness

{

local($_) = 'x' x 10000; # Большой фрагмент данных

 

# Вычислить $overhead для пустого цикла

local($start) = (times)[0];

for ($i = 0; $i < 5000; $i++)           {         }

local($overhead) = (times)[0] - $start;

 

# Вычислить $delta для того же количества итераций

$start = (times)[0];

for ($i = 0; $i < 5000; $i++)           { m/^/; }

local($delta) = (times)[0] - $start;

 

# Если $delta превышает $overhead в 10 раз и более -

# значит, программа загрязнена (оценка получена эвристическим путем)

printf "It seems your code is %s (overhead=%.2f, delta=%.2f)\n",

        ($delta > $overhead*10) ? "naughty":"clean", $overhead, $delta;

}

Конечно, такие функции не должны присутствовать в окончательной версии программы, однако вы можете временно вставить функцию в предварительную версию и вызвать ее в начале программы. Возможно, сразу же после вызова следует поставить exit и удалить функцию после получения ответа. Но даже если вы знаете, что ваша программа «чиста» по отношению к $&, остается возможность ее случайного загрязнения при вызове eval во время работы, поэтому для пущей надежности проверку стоит повторить в конце программы.

Многие подвыражения (а также полные регулярные выражения) не требуют специальной обработки. Программа анализа данных, разделенных запятыми, в начале главы (с. <$R[P#,R7-54]>); регулярное выражение для включения запятых в числа (с. <$R[P#,R7-59]>); даже огромное регулярное выражение объемом 4 724 байта, которое мы построим в разделе «Поиск адресов электронной почты» (с. <$R[P#,R7-15]>) — все эти выражения обходятся без специальной обработки регистра символов. Поиск без учета регистра в таких выражениях не должен отрицательно влиять на быстродействие.

Даже присутствие букв в символьном классе не должно приводить к снижению эффективности. Во время компиляции в символьный класс можно без труда включить версию всех букв из другого регистра (эффективность символьного класса не связана с количеством входящих в него символов — см. с. <$R[P#,R4-40]>). Следовательно, необходимость в дополнительной работе возникает только при вхождении букв в литеральный текст, а также при использовании обратных ссылок. Хотя и в таких ситуациях приходится что-то делать, существуют более эффективные решения, чем копирование всей целевой строки.

Кстати, я забыл упомянуть, что при использовании модификатора /g копирование выполняется при каждой попытке поиска. Утешает лишь то, что копия создается от начальной позиции поиска до конца строки — при использовании m/…/ig в длинных строках по мере продвижения к концу строки копии становятся все короче и короче.

Результаты хронометража для /i

Я провел несколько тестов, аналогичных описанным на с. <$R[P#,R7-122]>. При этом использовались те же тестовые данные — файл с исходным текстом Perl на языке C, состоящий из 52 011 строк и 1 192 395 байт.

Первый тест был самым низким и жестоким. Я загрузил весь файл в одну строку и провел сравнительные замеры для 1 while m/./g и 1 while m/./gi. Конечно, точка никак не связана с регистром символов, поэтому в данном примере поиск без учета регистра вроде бы не должен иметь отрицательных последствий. На моем компьютере первая команда выполнялась около 12 секунд. Простое добавление модификатора /i (в данном примере абсолютно бессмысленного) замедлило работу программы на четыре порядка, до полутора суток[49]! Я подсчитал, что из-за ненужного копирования Perl пришлось попусту передавать в памяти свыше 647 585 мегабайт. Это особенно обидно, поскольку во время компиляции можно было элементарно определить, что для регулярного выражения [.] поиск без учета регистра ни на что не влияет.

Несомненно, этот нереальный тест представлял худшую из возможных ситуаций. В более реалистичном случае совпадения встречаются реже, поэтому я провел тестирование команд m/\bwhile\b/gi и m/\b[wW][hH][iI][lL][eE]\b/g для той же строки. Здесь я попытался самостоятельно имитировать реализацию, ориентированную на регулярные выражения. Для подобных реализаций было бы верхом наивности преобразовывать литеральный текст в символьные классы[50], поэтому мы будем считать /i-эквивалент «худшей ситуацией» своего рода. Ручное преобразование [while] в [[wW][hH][iI][lL][eE]] также исключает оптимизацию проверки фиксированных строк (с. <$R[P#,R5-13]>) и делает применение study (с. <$R[P#,R7-5]>) бесполезным для данного выражения. После всего сказанного можно ожидать, что это решение будет работать очень медленно. Однако и при этом оно работает в 50 раз быстрее версии с /i!

Вероятно, и этот тест остается несправедливым — объем копирования, обусловленного наличием /i, от начала тестовых данных и после каждого из 412 совпадений [\bwhile\b] в моем примере, остается большим (помните: объем строки больше мегабайта!) Попробуем протестировать m/^int/i и m/^[iI][nN][tT]/ в каждой из 50 000 строк тестового файла. В этом случае /i приведет к копированию каждой строки перед попыткой поиска, но из-за небольшого размера строк затраты окажутся не столь сокрушительными, как прежде: версия с /i теперь работает всего на 77 процентов медленнее. В действительности замедление обусловлено и созданием дополнительных копий для каждого из 148 совпадений — вспомните, m/…/i без модификатора /g становится причиной копирования, обеспечивающего поддержку $&.

Несколько слов напоследок

Как показывают последние тесты, затраты, связанные с применением /i, не так страшны, как может показаться после первого теста. И все же это проблема, которую необходимо учитывать. Надеюсь, в будущих версиях Perl самые заметные недостатки будут устранены.

Главное правило: не используйте модификатор /i, если можете без него обойтись. Бездумное включение модификатора в регулярное выражение, где он не обязателен, лишь способствует напрасному расходованию ресурсов. Имитация решений, ориентированных на регулярные выражения, может обеспечить большой выигрыш в скорости — особенно при работе с большими строками. Два последних примера подтверждают сказанное.

Проблемы эффективности подстановок

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

Однако мне все же удалось немножко разобраться в работе оператора подстановки[51] и сформулировать ряд простых правил, которыми я хочу поделиться с вами. Заранее предупреждаю: сказанное нельзя обобщить в нескольких простых словах. Perl часто по крупицам собирает возможности для внутренних оптимизаций, а обилие правил и исключений, связанных с оператором подстановки, открывает широкие возможности для таких оптимизаций. Оказывается, копирование для поддержки $& исключает любые оптимизации, связанные с подстановками — тем больше причин для изгнания переменной $& и ее друзей из программ.

Начнем с возвращения к худшему случаю оператора подстановки.

«Медленный режим» оператора подстановки

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

s[(\d+(\.\d*)?)F\b]{sprintf "%.0fC",($1-32) * 5/9}eg

совпадает в подчеркнутых местах следующей строки:

Water boils at 212F, freezes at 32F.

Обнаружив первое совпадение, Perl создает временную пустую строку и копирует в нее все символы, предшествующие совпадению («Waterspcboilsspcatspc»). Затем вычисляется текст замены (в данном примере — 100C), который дописывается в конец временной строки. Кстати говоря, именно на этой стадии производится копирования для поддержки $&, если в нем возникает необходимость.

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

Water boils at 100C, freezes at 0C.

Исходная целевая строка, $_, удаляется и заменяется временной строкой (я полагал, что исходный текст может использоваться для поддержки $1, $& и т. д., но это не так — при необходимости создается отдельная копия).

На первый взгляд такой способ построения результата выглядит вполне разумно, поскольку в общем случае он действительно вполне разумен. Но представьте себе простую команду вида s/\s+$// для удаления завершающих пропусков. Нужно ли копировать всю (возможно — очень большую) строку только для того, чтобы отсечь несколько символов в самом конце? Теоретически — не нужно. На практике Perl этого и не делает… по крайней мере, делает не всегда.

Переменные семейства $& подавляют все оптимизации для оператора подстановки

У Perl хватает сообразительности на то, чтобы оптимизировать s/\s+$// простой регулировкой длины целевой строки. Необходимость в излишнем копировании при этом отпадает — все происходит очень быстро. Однако по причинам, смысл которых остается для меня загадкой, эта оптимизация (а также все остальные оптимизации подстановки, упоминаемые ниже) подавляется с копированием для поддержки $&. Почему? Не знаю, но на практике это еще одно из проявлений губительного влияния $& на эффективность ваших программ.

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

Замена, длина которой превышает длину совпадения, не оптимизируется

Команда s/\s+$// принадлежит к числу примеров, следующих определенному образцу: когда длина заменяющего текста не превышает длины заменяемого текста, его можно вставить прямо в строку, а необходимость в полном копировании всей строки отпадает. На рис. 7.2 изображена часть примера со с.<$R[P#,R2-10]> — применение команды s/<FIRST>/Tom/ к строке Dearspc<FIRST>,new. Новый текст копируется поверх заменяемого, а текст, следующий за совпадением, сдвигается и заполняет возможные промежутки.

 

Рис. 7.2. Применение команды s/<FIRST>/Tom/ к строке Dearspc<FIRST>,new

В примере с s/\s+$// нет необходимости копировать текст поверх строки и сдвигать последующие символы — после обнаружения совпадения длина строки попросту уменьшается так, чтобы совпавшая часть была удалена, вот и все. Все происходит очень быстро. Аналогичные оптимизации также могут использоваться и для совпадений в конце строки.

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

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

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

Все перечисленные оптимизации применяются лишь в том случае, если Perl заранее знает длину заменяющей строки. Это означает, что присутствие в заменяющей строке $1 или других переменных этого семейства исключает возможность оптимизаций[52]. Впрочем, к интерполяции других переменных это не относится. В приведенном выше примере исходная подстановка выглядела так:

$given = 'Tom';

$letter =~ s/<FIRST>/$given/g;

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

Конечно, при подстановке с модификатором /e размер текста замены становится известным лишь после поиска, а операнд замены вычисляется по правилам eval, поэтому при использовании этого модификатора оптимизация также не выполняется.

Комментарий напоследок

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

Хронометраж

Лучший способ оценки эффективности вашей программы — хронометраж. В Perl5 имеется модуль Benchmark, но он «загрязнен» использованием $&. Это весьма прискорбно, поскольку из-за снижения быстродействия результаты хронометража становятся бессмысленными. Я предпочитаю простое решение, при котором проверяемый код просто заключается в конструкцию вида:

$start = (times)[0];

$delta = (times)[0] - $start;

printf "took %.1f seconds", $delta

При проведении хронометража следует учитывать одно важное обстоятельство. Вследствие гранулярности часов (в большинстве систем — 1/60 или 1/100 секунды) тестируемый код должен выполняться хотя бы в течение нескольких секунд. Если код выполняется слишком быстро, перепишите программу и выполняйте его в цикле. Кроме того, постарайтесь исключить из тестируемой части все побочные операции. Например, следующий фрагмент:

$start = (times)[0]; # Запустить часы

$count = 0;

while (<>) {

        $count++ while m/\b(?:char\b|return|\b|void\b)/g;

}

print "found $count items.\n";

$delta = (times)[0] - $start; # Остановить часы

printf "the benchmark took %.1f seconds.\n", $delta;

лучше записать в виде:

$count = 0;  # Эту операцию незачем включать в измерения, убираем из цикла

@lines = <>; # Весь файловый ввод/вывод выполняется до начала цикла,

                  # чтобы медленные операции с диском не влияли на результаты

$start = (times)[0]; # Подготовка закончена, пускаем часы

foreach (@lines) {

        $count++ while m/\b(?:char\b|return|\b|void\b)/g;

}

$delta = (times)[0] - $start; # Остановить часы

print "found $count items.\n"; # Вывод тоже исключается из хронометража

printf "the benchmark took %.1f seconds.\n", $delta;

Самое большое изменение заключается в том, что из хронометрируемого фрагмента был исключен весь файловый ввод/вывод. Конечно, при нехватке памяти может начаться выгрузка на диск, и весь выигрыш теряется — проследите за тем, чтобы этого не случилось. Чтобы имитировать большой объем данных, можно воспользоваться меньшим набором реальных данных и обработать их несколько раз:

for ($i = 0; $i < 10; $i++) {

foreach (@lines) {

        $count++ while m/\b(?:char\b|return|\b|void\b)/g;

}

}

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

Отладочная информация регулярных выражений

Стремясь как можно быстрее найти совпадение для регулярного выражения, Perl выполняет феноменальное количество оптимизацией. Наименее таинственные разновидности оптимизаций перечислены в разделе «Внутренние оптимизации» главы 5 (с. <$R[P#,R5-17]>). Если ваша версия Perl была откомпилирована с включением отладочной информации (ключ -DDEBUGGING при сборке), появляется возможность использования ключа командной строки -D. Ключ -Dr (-D512 в Perl4) выдает информацию о том, как Perl компилирует ваше регулярное выражение и выдает подробную информацию о каждом приложении.

Большинство данных, выдаваемых ключом -Dr, выходит за рамки этой книги, однако некоторые из этих сведений вполне понятны. Рассмотрим простой пример (я использую Perl версии 5.003):

(1) jfriedl@tubby> perl -cwDr -e '/^Subject: (.*)/'

(2) rarest char j at 3

(3) first 14 next 83 offset 4

(4) 1:BRANCH(47)

(5) 5:BOL(9)

(6) 9:EXACTLY(23) <Subject: >

(7) 23:OPEN1(29)

(8) 47:END(0)

(9) start 'Subject: ' anchored minlen 9

В строке (1) я запускаю Perl из приглашения командного интерпретатора, используя аргументы командной строки -c (только проверить сценарий без выполнения), -w (выдавать предупреждения о конструкциях, подозрительных с точки зрения Perl — следует использовать практически всегда), -Dr (отладка регулярных выражений) и -e (в следующем аргументе передается фрагмент программного кода Perl). Эта комбинация ключей удобна для проверки регулярных выражений в режиме командной строки. Регулярное выражение ['/^Subject:spc(.*)/'] неоднократно встречалось на страницах этой книги.

Строки (4)-(8) описывают откомпилированную форму выражения в Perl. В основном эта информация не представляет интереса, но даже при беглом взгляде строка (6) выглядит знакомо.

Учет литерального текста

<$M[R7-124]>Многие оптимизации основаны на том, что Perl приходит к выводу о обязательном присутствии некоторого фиксированного литерального текста в любом возможном совпадении (я назову это обстоятельство «учетом литерального текста»). В данном примере это текст Subject:spc, однако во многих выражениях обязательного литерального текста нет, или способностей Perl не хватает на то, чтобы его вычислить (это одна из тех областей оптимизации, в которых Emacs превосходит Perl; с. <$R[P#,R6-5]>). В частности, Perl не может придти ни к какому выводу для следующих выражений: [-?[0-9]+)\.[0-9]*)?|\.[0-9]+)], [^\s*], [^(-?\d+)(\d{3})] и даже [int|void|while].

Анализируя [int|void|while], можно заметить, что символ i присутствует во всех альтернативах. Некоторые механизмы НКА замечают этот факт (а механизм ДКА знает о нем косвенно), но к сожалению, механизм регулярных выражений Perl к их числу не относится. В отладочных данных int, void и while будут выведены в строках, напоминающих (6), но это лишь локальные требования (подвыражения). Для учета литерального текста Perl необходимы глобальные знания на уровне регулярного выражения в целом; в общем случае Perl не сможет вычислить фиксированный текст по альтернативам, входящим в конструкцию выбора.

Во многих регулярных выражениях — например, [<CODE>(.*?)</CODE>] — содержится несколько фрагментов литерального текста. В таких случаях Perl выбирает один или два фрагмента и передает их для использования процедурам оптимизации. Выбранные фрагменты выводятся в строках, напоминающих строку (9).

Основные оптимизации, о которых сообщает ключ -Dr

В строке (9) может содержаться информация о разных видах оптимизаций. Ниже перечислены некоторые из часто встречающихся способов оптимизации<$M[R7-123]>:

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

must have "текст" back число — означает, что совпадение должно содержать обязательный фрагмент текста (как и в предыдущей оптимизации), но этот фрагмент не обязан находиться в начале литерального выражения. Если число не равно –1, Perl знает, что совпадение должно начинаться на указанное количеством символов раньше приведенного текста. Например, для выражения [[Tt]ubby] выводится сообщение must have "ubby" back 1; это означает, что если проверка фиксированных строк обнаруживает подстроку ubby, начинающуюся с некоторой позиции, то все совпадение должно начинаться на один символ раньше.

Для выражений вида [.*ubby] начальная позиция подстроки ubby несущественна, поскольку совпадение, в которое она входит, может начинаться в любой из предшествующих позиций, поэтому число в этом случае равно –1.

stclass ':тип' — означает, что совпадение должно начинаться с символа определенного типа. Для выражения [\s+] тип заменяется строкой SPACE, а для [\d+] — строкой DIGIT. Для символьных классов, как в приведенном выше примере [[Tt]ubby], используется строка ANYOF.

plus — означает, что к stclass или одному начальному символу start применяется квантификатор [+], поэтому исключение по первому символу не только находит начало потенциального совпадения, но и быстро обходит [\s+] и другие начальные конструкции, прежде чем полный (но более медленный) механизм регулярных выражений начнет проверку полного совпадения.

anchored — означает, что регулярное выражение начинается с якорного метасимвола ^. При этом появляется возможность использования оптимизации, обусловленной привязкой к границам логических строк/фрагментов (с. <$R[P#,R5-3]>).

implicit — означает, что Perl неявно включает в начало регулярного выражения метасимвол ^, поскольку регулярное выражение начинается с [.*] (с. <$R[P#,R5-25]>).

Другие оптимизации, связанные с учетом литерального текста

Еще одна разновидность оптимизации, связанная с учетом литерального текста, имеет отношение к функции study (см. следующий раздел). Perl в какой-то степени произвольно выделяет в выбранном литеральном тексте один символ, который он считает «редким». Если перед поиском строка была обработана функцией study, Perl немедленно узнает о присутствии этого символа в любой позиции строки. Если символ отсутствует, совпадение в принципе невозможно, и привлекать механизм регулярных выражений вообще не нужно. Этот прием позволяет быстро исключать некоторые невозможные совпадения. Выбранный символ сообщается в строке (2).

Для любителей всевозможных полезных и бесполезных сведений сообщу, что самым редким символом Perl считает \000, а за ним следуют \001, \013, \177 и \200. Среди отображаемых символов наиболее редкими являются ~, Q и Z, а наиболее частыми — e, пробел и t (в документации сказано, что этот факт был установлен анализом программ на языке C и английского текста).

Функция study

<$M[R7-5]>Функция study(…) оптимизирует не регулярное выражение, а доступ к информации о строке. В случае применения регулярного выражения (или нескольких регулярных выражений) можно достигнуть существенного выигрыша за счет наличия кэшированных сведений о строке. Понять принцип работы study несложно; значительно сложнее разобраться в том, обеспечивает она какой-нибудь выигрыш или нет в каждом конкретном случае. Функция абсолютно не влияет на значения, обрабатываемые или возвращаемые программой[53]. В результате ее применения Perl расходует больше памяти, а общее время работы программы может увеличиться, остаться прежним или уменьшиться (для чего, собственно, и предназначена эта функция).

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

Построенный список никогда не используется механизмом регулярных выражений; с ним работает только подсистема смещения текущей позиции поиск. Она просматривает данные, отображаемые в строках start и must have отладочной информации (см. с. <$R[P#,R7-123]>), и выбирает символ, который считается редким (см. выше). Редкий (но обязательно присутствующий в совпадении!) символ должен с меньшей вероятностью встречаться в строках при отсутствии совпадения, и если в результате быстрой проверки по списку study редкий символ не будет найден в строке, значит, строку можно отвергнуть без перебора всех символов.

Если редкий символ встречается в строке и если он занимает конкретную позицию по отношению к любому возможному совпадению (например, как символ h в выражении [..this], но не в [.?this]), подсистема смещения текущей позиции может использовать данные study для того, чтобы начать поиск поблизости от вычисленной позиции. Это приводит к экономии времени, поскольку позволяет обойти большие фрагменты строки[54].

Когда не следует использовать study

l      Не используйте study для коротких целевых строк. В таких случаях вполне достаточно обычной оптимизации с проверкой фиксированных строк.

l      Не используйте study при поиске небольшого количества совпадений в целевой строке (по крайней мере перед модификацией строки или вызовом study для другой строки). Общее ускорение более вероятно, если время, затраченное на анализ строки функцией study, распределяется по многим попыткам поиска.

Следует учитывать, что в текущей реализации конструкция m/…/g считается одной попыткой: список просматривается только в самом начале. Впрочем, при использовании m/…/g в скалярном контексте список проверяется для каждого совпадения, но каждый аз возвращается одна и та же позиция — для всех совпадений, кроме первого, эта позиция находится перед началом совпадения, поэтому такая проверка оказывается напрасной тратой времени.

l      Не используйте study в ситуациях, когда для регулярных выражений не может быть выполнен учет литерального текста (с. <$R[P#,R7-124]>). Без знания символов, которые должны присутствовать в любом совпадении, вызов study бесполезен.

Когда study может помочь

Наибольший эффект при вызове study достигается в ситуации, когда у вас имеется большая строка, в которой перед модификацией будет производиться многократный поиск. Хорошим примером является фильтр, написанный мной при подготовке этой книги. При написании книги я использую свой собственный стиль разметки, который преобразуется фильтром в SGML (который затем преобразуется в формат troff, который, в свою очередь, преобразуется в PostScript). Во время работы фильтра вся глава в конечном счете превращается в одну громадную строку (в этой главе ее объем превышал 650 килобайт). Перед завершением я выполняю ряд проверок, предназначенных для поиска возможных ошибок в разметке. Проверки не модифицируют строку и в них часто встречаются фиксированные строки — это как раз та ситуация, для которой создавалась функция study.

Практическое применение study

Функцию study с самого начала преследовал злой рок. Во-первых, многие программисты так и не разобрались, для чего она нужна. Затем из-за ошибок в Perl версий 5.000 и 5.001 эта функция стала абсолютно бесполезной. В последующих версиях ошибка была исправлена, но зато появилась новая ошибка, иногда приводящая к неудаче при поиске существующих совпадений в $_ (не имеющих никакого отношения к строке, для которой вызывалась функция study!) Я обнаружил эту ошибку, выясняя, почему не работает мой фильтр преобразования разметки — причем это произошло во время написания раздела, посвященного функции study! Совпадение по меньшей мере выдающееся.

Проблему можно обойти явным присваиванием undef или иной модификацией строки, для которой вызывалась функция study (разумеется, после завершения работы с ней). Автоматического присваивания $_ в конструкции while(<>) недостаточно.

Но даже если функция study работает, она часто не используется в полную силу — либо из-за простых ошибок, либо из-за реализации, которая по своим темпам развития отстает от остальных компонентов Perl. На данный момент я рекомендую избегать применения study, если у вас нет твердой уверенности в том, что в вашей конкретной ситуации она принесет пользу. Если вы используете study для целевой строки $_, не забудьте присвоить ей неопределенное значение после завершения поиска.

Все вместе

<$M[R7-14]>В этой главе мы подробно рассмотрели диалект регулярных выражений и операторы Perl. Возможно, сейчас вы чувствуете себя в некоторой растерянности — для практического усвоения этой информации необходим немалый опыт ее практического применения.

Давайте вернемся к задаче с анализом данных, разделенных запятыми. Ниже приведено мое решение для Perl5, которое, как нетрудно заметить, несколько отличается от первоначального варианта на с. <$R[P#,R7-125]>:

@fields = ();

push(@fields, $+) while $text =~ m{

    "([^"\\]*(?:\\.[^"\\]*)*)",?  # Строка в кавычках (возможно, с запятой)

| ([^,]+),?                                # Все остальное (возможно, с запятой)

| ,                                        # Отдельная запятая

}gx;

 

# Добавить последнее пустое поле перед завершающей запятой

push(@fields, undef) if substr($text,-1,1) eq ',';

Здесь, как и в первой версии, перебор полей реализуется конструкцией m/…/g в скалярном контексте с циклом while. Мы хотим обеспечить позиционную синхронизацию и поэтому убеждаемся в том, что хотя бы одна из трех альтернатив совпадает с позиции, в которой может начинаться совпадение. Возможны три типа полей, поэтому конструкция выбора включает три альтернативы.

Поскольку Perl5 позволяет точно указать, какие скобки являются сохраняющими, а какие — нет, можно быть уверенным в том, что после любого совпадения переменная $+ содержит текст поля. Для пустых полей совпадает третья альтернатива, и из-за отсутствия сохраняющих круглых скобок переменная $+ заведомо имеет неопределенное значение — это нам и нужно (значение undef отличается от пустой строки — возврат разных значений для пустого поля и "" учитывает это обстоятельство).

Завершающая команда push предназначена для строк, завершающихся запятой (признаком завершающего пустого поля). Обратите внимание: я не использую m/,$/, как прежде. Раньше это делалось для демонстрации работы с регулярными выражениями, но в действительности нет необходимости использовать регулярные выражения там, где существует более простое и быстрое решение.

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

Удаление начальных и конечных пропусков

Самое лучшее и надежное решение просто и очевидно:

s/^\s+//;

s/\s+$//;

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

l      s/\s*(.*?)\s*$/$1/ — это выражение часто приводится как замечательный пример минимализма квантификаторов, появившегося в Perl5. В действительности пример не так уж хорош, поскольку он работает гораздо медленнее большинства других решений (в моих тестах — примерно в три раза). Причина заключается в том, что прежде чем разрешить совпадение точки с каждым символом, конструкция *? должна проверить, совпадают ли последующие символы. Это приводит к многочисленным возвратам и постоянным входам/выходам из круглых скобок (с. <$R[P#,R5-21]>).

l      s/^\s(.*\S)?\s*$/$1/ — гораздо более прямолинейное решение. Начальная конструкция [.*] совпадает до конца строки, а \S обеспечивает возврат до позиции после конечного пропуска в завершающей последовательности не-пропусковых символов. Если строка не содержит ничего, кроме пропусков, [(.*\S)?] не совпадает (что допустимо), а завершающая конструкция [\s*] распространяется до конца строки.

l      $_ = $1 if m/^\s*(.*\S)?/ — более или менее похоже на предыдущее решение. На этот раз вместо подстановки используется поиск и присваивание. В моих тестах этот вариант работал на 10 процентов быстрее.

l      s/^\s*|\s*$//g — это решение предлагается очень часто. Хотя оно работает, присутствие конструкции выбора исключает многие оптимизации, которые могли бы использоваться в данном случае. Модификатор /g разрешает совпадение для всех альтернатив, но применение /g расточительно, поскольку нам нужны максимум два совпадения, притом для разных подвыражений. Получается неэффективно.

Скорость часто зависит от характера данных. Например, в редких случаях, когда строка имеет очень большую длину с относительно малым количеством пропусков на обоих концах, обработка s/^\s+//; s/\s+$// может занять вдвое больше времени, чем для $_ = $1 if m/^\s*(.*\S)?/. И все же в своих программах я использую только s/^\s+//; s/\s+$//, поскольку это решение почти всегда является самым быстрым, и уж конечно — самым простым для понимания.

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

<$M[R7-59]>Часто возникает вопрос, как выводить числа с запятыми — например, 12,345,678. В настоящее время FAQ предлагает следующее решение:

1 while s/^(-?\d+)(\d{3})/$1,$2/;

Команда в цикле сканирует число до последней цифры (то есть символа, отличного от запятой) при помощи [\d+], возвращается на три цифры, чтобы совпадало подвыражение [\d{3}], и вставляет запятую при помощи подвыражения [$1,$2]. Поскольку обработка числа происходит «справа налево» вместо обычного направления «слева направо», модификатор /g в данном случае бесполезен. Таким образом, многократное включение запятых обеспечивается при помощи цикла while.

Это решение можно усовершенствовать, воспользовавшись стандартной оптимизацией из главы 5 (с. <$R[P#,R5-26]>), и заменить [\d{3}] последовательностью [\d\d\d]. Зачем заставлять механизм регулярных выражений подсчитывать экземпляры, когда с таким же успехом можно просто сказать, что вам нужно? В моих тестах это простое изменение обеспечило громадную экономию в целых три процента! (А впрочем, курочка по зернышку клюет…)

Другое усовершенствование — удаление якоря начала строки. Это позволит вам включать запятые в число (или числа), расположенные где-то внутри строки. Заодно можно смело удалить [-?], поскольку она нужна лишь для привязки первой цифры к якорю. Если вы не знаете природы целевых данных, подобные изменения бывают рискованными, поскольку 3.14159265 может превратиться в 3.14,159,265. Так или иначе, если вся строка содержит только одно число, лучше использовать версию с якорем.

В абсолютно непохожем, но практически эквивалентном решении используется одна подстановка с модификатором /g:

s<

    (\d{1,3})           # До запятой: от одной до трех цифр

    (?=                    # Затем следуют (не включаемые в совпадение!)

           (?:\d\d\d)+  #  ...несколько триплетов...

           (?!\d)       #  ...за которыми следует не цифра...

    )                          #  (на этом число заканчивается)

><$1,>gx;

Из-за комментариев и форматирования может показаться, что это решение сложнее приведенного в FAQ, но на самом деле все не так плохо — оно работает примерно на треть быстрее. Тем не менее, поскольку это выражение не привязано к началу строки, возникают те же проблемы, как для 3.1415926. Чтобы эта команда работала так же, как и решение в FAQ для всех строк, замените [(\d{1,3})] на [\G((?:^-)?\d{1,3})]. \G привязывает все совпадение к началу строки, а каждое последующее совпадение, обусловленное модификатором /g — к предыдущему совпадению. [(?:^-)?] разрешает наличие минуса в начале строки, как в решении FAQ. Эти изменения чуть замедляют работу приведенного решения, но оно все равно работает на 30 процентов быстрее решения, приведенного в FAQ.

Удаление комментариев C

Интересно посмотреть, как решается задача удаления комментариев C из текста. В главе 5 мы потратили довольно много времени на построение обобщенного выражения для поиска комментариев [/\*[^*]*\*+([^/*][^*]*\*+)*/], и на написание программы Tcl для удаления комментариев. Давайте сделаем то же<$M[R7-7]> самое в Perl.

В главе 5 рассматривались традиционные механизмы НКА, поэтому регулярное выражение будет работать и в Perl. Для повышения эффективности я воспользуюсь не-сохраняющими круглыми скобками, но это практически единственное непосредственное изменение. Впрочем, более простое выражение [/\*.*?\*] из FAQ не всегда нежелательно — решение из главы 5 заметно повышает эффективность поиска, но для тех приложений, в которых время не критично, [/\*.*?\*] вполне подходит. Конечно, это выражение выглядит гораздо понятнее, поэтому я воспользуюсь им для упрощения первой, черновой версии выражения, предназначенного для удаления комментариев.

Итак, вернемся к нашему выражению:

s{

    # Сначала перечисляется то, что совпадает, но не удаляется

    (

           " (\\.|[^"\\])* " # Строка в кавычках

        |                             # - или -

           ' (\\.|[^'\\])* ' # строка в апострофах

    )

| # ИЛИ...

    # ...комментарий. Поскольку он не совпал

    # с круглыми скобками $1 (см. выше), комментарии

    # исчезнут, когда мы используем $1 в качестве текста замены.

    /\* .*? \*/          # Традиционный комментарий C

    |                                # -или-

    //[^\n]*                      # Комментарии C++ ( // )

}{$1}gsx;

После применения модификаций, описанных для решения на Tcl, и объединения двух регулярных выражений для комментариев в одну альтернативу верхнего уровня (это легко, поскольку мы пишем регулярное выражение сразу, а не строим его из отдельных компонентов $COMMENT и $COMMENT1), наша Perl-версия принимает следующий вид:

s{

    # Сначала перечисляется то, что совпадает, но не удаляется

    (

           [^"'/]+                                                    # Все остальное

        |                                                                   # - или -

        (?:"[^"\\]*(?:\\.[^"\\]*)*" [^"'/]*)+ # строка в кавычках

        |                                                             # - или -

           (?:'[^"\\]*(?:\\.[^'\\]*)*' [^"'/]*)+ # строка в апострофах

    )

| # ИЛИ...

    # ...комментарий. Поскольку он не совпал

    # с круглыми скобками $1 (см. выше), комментарии

    # исчезнут, когда мы используем $1 в качестве текста замены.

 

    / (?:                      # Любой комментарий начинается с косой черты

        \*[^*]*\*+(?:[^/*][^*]*\*+)*/ # Традиционный комментарий C

    |                                                    # -или-

    /[^\n]*                                           # Комментарии C++ ( // )

        )

}{$1}gsx;

Тест, аналогичный приведенному в главе 5 для Tcl-версии, работает около 1,45 секунды. Для сравнения: основная Tcl-версия работала 2,3 секунды, первая Perl-версия — около 12 секунд, а первая Tcl-версия — около 36 секунд.

Чтобы превратить эту команду в полноценную программу, достаточно вставить ее в следующую конструкцию:

undef $/;                      # Режим поглощения всего файла

$_ = join ('', <>);     # Функция join(…) работает с несколькими файлами

# ...Вставить команду замены, приведенную выше...

print;

Да, перед вами вся программа.

Поиск адресов электронной почты

<$M[R7-15]>Я хочу завершить эту книгу большим примером, в котором задействованы многие приемы работы с регулярными выражениями, встречавшиеся в нескольких последних главах, а также ряд исключительно полезных навыков построения регулярных выражений из переменных. Проверка правильности синтаксиса адресов электронной почты Интернета — задача весьма распространенная, но к сожалению, из-за сложности стандарта[55] найти простое решение очень трудно. В сущности, с помощью регулярного выражения это сделать невозможно, поскольку комментарии могут быть вложенными (да, адреса электронной почты могут содержать комментарии — символы, заключенные в круглые скобки). Если вы согласны на компромисс (например. разрешить только один уровень комментариев — во всех адресах, которые я видел, этого было вполне достаточно) — давайте попробуем.

И все же задача не для слабонервных. Регулярное выражение, которое у нас получится, будет состоять из 4724 байт! На первый взгляд кажется, что задача решается простым выражением типа [\w+\@[.\w]+], но на самом деле ситуация гораздо более сложная. Например следующая строка абсолютно допустима с точки зрения спецификации[56]:

Jeffy <"That Tall Guy"@ora.com (this address no longer active)>

Итак, что же следует считать лексически правильным адресом? В табл. 7.11 приведена лексическая спецификация адреса электронной почты Интернета в смешанной записи, в которой можно разобраться без особого труда. Кроме того, большинство элементов может разделяться комментариями (элемент 22) и пропусками (пробелы или табуляции). Наша задача — по возможности преобразовать эту спецификацию в регулярное выражение. Для этого потребуется вспомнить все, чему вы научились, и все же это возможно[57].

Таблица 7.11. Слегка формализованное описание адреса электронной почты

 

Элемент

Описание

1

mailbox

addr-spec | phrase route-addr

2

addr-spec

local-part@domain

3

phrase

(word)+

4

route-addr

<( route )?addr-spec>

5

local-part

word(.word)*

6

domain

sub-domain(.sub-domain)*

7

word

atom|quoted-string

8

route

@domain(,@domain)

9

sub-domain

domain-ref | domain-literal

10

atom

(любой char, кроме specials, space и ctls)+

11

quoted-string

"(qtext | quoted-pair)*"

12

domain-ref

atom

13

domain-literal

[(dtext | quoted-pair)*]

14

char

любой ASCII-символ (000-177 восьм.)

15

ctl

любой управляющий ASCII-символ (000-037 восьм.)

16

space

ASCII-символ «пробел» (040 восьм.)

17

CR

ASCII-символ «возврат курсора» (015 восьм.)

18

specials

любой из символов: ()<>@,;:\".[]

19

qtext

любой символ, кроме ", \ и CR

20

dtext

любой символ, кроме [, ], \ и CR

21

quoted-pair

\char

22

comment

((ctext | quoted-pair | comment)*)

23

ctext

любой символ, кроме (, ), \ и CR

Уровни интерпретации

Строя регулярное выражения из переменных, необходимо особенно внимательно разобраться в происходящей обработке строк в кавычках и апострофах, интерполяции и экранировании. Например, выражение [^\w+\@[.\w]+$] можно наивно воспроизвести в виде

$username = "\w+";

$hostname = "\w+(\.\w+)+";

$email     = "^$username\@$hostname$";

...

... m/$email/o ...

Но все не так просто. При вычислении строк в кавычках в процессе присваивания переменным символы \ интерполируются и удаляются: в Perl4 итоговая переменная $email будет содержать ^w+@w+(.w+)+$, а в Perl5 этот фрагмент вообще не компилируется из-за завершающего символа $. Приходится либо экранировать символы \, чтобы сохранить их в регулярном выражении, либо использовать строки в апострофах. Строки в апострофах не могут использоваться в некоторых ситуациях — например, в третьей строке приведенного выше фрагмента действительно необходима интерполяция переменных, обеспечиваемая строками в кавычках:

$username  = '\w+';

$hostname  = '\w+(\.\w+)+';

$email     = '^$username\@$hostname\$';

Начиная построение регулярного выражения, давайте повнимательнее присмотримся к элементу 16 в табл. 7.11. Простейшая команда $space = "spc" не подходит, потому что при использовании модификатора /x при использовании регулярного выражения (а это будет происходить!) пробелы за пределами символьных классов, как в этом случае, исчезают. Пробел в регулярном выражении можно представить в виде [\040] (40 — восьмеричный код пробела в кодировке ASCII), поэтому возникает искушение присвоить "\040" переменной $space. Это было бы ошибкой, поскольку при вычислении строки в апострофах \040 превращается в пробел. Регулярное выражение увидит этот пробел, так что мы возвращаемся на старое место. Мы хотим, чтобы регулярное выражение видело код \040 и само превращало его в пробел; следовательно, необходимо использовать "\\040" или '\040'.

Совпадение для литерала \ — задача особенно непростая, потому что этот символ также является метасимволом регулярного выражения. Для совпадения с одним литералом \ в регулярное выражение необходимо включить [\\]. Чтобы присвоить его, скажем, переменной $esc, нам хотелось бы использовать '\\', но поскольку комбинация \\ особым образом интерпретируется даже в строках, заключенных в апострофы[58], нам приходится использовать команду вида $esc = '\\\\' — только для того, чтобы окончательное регулярное выражение совпало с одним символом \. Именно по этой причине я создаю переменную $esc один раз и затем использую ее всюду, где в регулярном выражении потребуется литерал \. Эта переменная будет неоднократно использована при построении регулярного выражения для адреса. Ниже приведены некоторые вспомогательные переменные, которые будут использоваться аналогичным образом:

# Вспомогательные переменные, позволяющие избавиться от лишних символов \.

$esc          = '\\\\';                  $Period          = '\.';

$space        = '\040';                     $tab          = '\t';

$OpenBR    = '\[';                    $CloseBR     = '\]';

$OpenParen    = '\(';                    $CloseParen  = '\)';

$NonASCII     = '\x80-\xff';         $ctrl        = '\000-\037';

$Crlist       = '\n\015'; # Примечание: должно быть \015

Переменная $CRList заслуживает отдельного упоминания. В спецификации упоминается только ASCII-символ возврата курсора (015 восьм.). С практической точки зрения это регулярное выражение, скорее всего, будет применяться к тексту, уже преобразованному к системной комбинации для новой строки. Этот формат может совпадать с ASCII-символом возврата курсора или отличаться от него (как, например, в MacOS, но не в Unix; см. с. <$R[P#,R3-28]>). Исходя из этого, я решил учесть оба случая.

Определение базовых типов

В следующем фрагменте приведены вспомогательные символьные классы, представляющие элементы 19, 20, 23 и начало элемента 10:

# Элементы 19, 20, 21

$qtext = qq/[^$esc$NonASCII$CRlist\"]/;                  # внутри "..."

$dtext = qq/[^$esc$NonASCII$CRlist$OpenBR$CloseBR]/; # внутри [...]

$quoted_pair = qq< $esc [^$NonASCII] >; # экранированный символ

 

# Элемент 10: atom

$atom_char = qq/[^($space)<>\@,;:\".$esc$OpenBR$CloseBR$ctrl$NonASCII]/;

$atom = qq<

$atom_char+      # Некоторое количество символов атома...

(?!$atom_char) # ..за которыми не следует нечто,

                     # не являющееся частью атома

>;

Последний элемент, $atom, требует некоторых разъяснений. Сама по себе переменная $atom представляет собой всего лишь $atom_char+ — но взгляните на элемент 3 таблицы 7.11, phrase. Комбинация образует выражение [($atom_char+)+] — прекрасный пример «бесконечного» поиска (с. <$R[P#,R5-27]>). При построении регулярных выражений из переменных часто возникают подобные ошибки, поскольку программисту трудно предусмотреть все возможные последствия. Именно из-за таких проблем я использовал выше команду $NonASCII = '\x80-\xff'. Вместо этого можно было воспользоваться строкой "\x80-\xff", но я хотел иметь возможность вывести частично построенное регулярное выражение в любой момент тестирования. В последнем случае регулярное выражение содержит «сырые» коды, вполне подходящие для механизма регулярных выражений — но не для вывода выражения в процессе отладки.

Вернемся к [($atom_char+)+]. Я не могу ограничивать атом во внутреннем цикле при помощи [\b], поскольку концепция слова в Perl полностью отличается от концепции атома в адресе электронной почты. Например, --genki-- — вполне допустимый атом, который не совпадает с [\b$atom_char+\b]. Таким образом, чтобы убедиться, что в процессе возврата атом не закончится в середине последовательности, с которой он должен совпасть, я при помощи (?!…) убеждаюсь в том, что $atom_char не совпадает за концом atom (перед вами одна из ситуаций, где пригодились бы неуступчивые квантификаторы — см. сноску на с. <$R[P#,R4-41]>).

Хотя мы работаем со строками в кавычках, а не с регулярными выражениями, я использую произвольную расстановку пропусков и комментарии (за исключением символьных классов), поскольку эти строки в конечном счете будут использованы для построения регулярного выражения с модификатором /x. Однако я специально слежу за тем, чтобы каждый комментарий завершался символом новой строки, потому что я не хочу столкнуться с проблемой лишних удалений (с. <$R[P#,R7-126]>).

Комментарии в адресах

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

# Элементы 22 и 23 - комментарий.

# Реализовать общий случай при помощи регулярного выражения

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

$ctext     = qq< [^$esc$NonASCII$CRlist()] >;

$Cnested   = qq< $OpenParen (?: $ctext | $quoted_pair )* $CloseParen >;

$comment   = qq< $OpenParen

                           (?: $ctext | $quoted_pair | $Cnested )*

                  $CloseParen >;

 

$X         = qq< (?: [$space$tab] | $comment )+ >; # Необязательный

                                                                                         # разделитель

$sep       = qq< (?: [$space$tab] | $comment )* >; # Обязательный

                                                                                         # разделитель

 

Обратите внимание: элемент 22 нигде больше в таблице 7.11 не встречается. Правда, в таблице не показано, что спецификация позволяет свободно включать комментарии, пробелы и символы табуляции между большинством лексических элементов. По этой причине мы создаем переменную $X для необязательных пробелов и комментариев, и переменную $sep — для обязательных.

Основная часть решения

Большинство элементов из табл. 7.11 реализуется очень просто. Единственный фокус заключается в том, чтобы переменная $X использовалась там, где она может использоваться в соответствии со спецификацией, но по соображениям эффективности — не чаще необходимого. Мой способ заключается в использовании $X только между элементами одного подвыражения. Определение большинства лексических элементов приведено ниже.

# Элемент 11: строка в кавычках, возможно - с экранированными символами

$quoted_str = qq<

        \" (?:                           # Открывающая кавычка...

                  $qtext                 #   все, кроме \ и "

                  |                             #  или

                  $quoted_pair        #  экранированное "нечто"(!= CR)

                                     )* \" # закрывающая кавычка

>;

 

# Элемент 7: word - atom или quoted_str

$word = qq< (?: $atom | $quoted_str ) >;

 

# Элемент 12: domain-ref - просто atom

$domain_ref = $atom;

 

# Элемент 13: domain-literal - аналог строки в кавычках-, но вместо

# "..." используется [...]

$domain_lit = qq<  $OpenBR                                      # [

                            (?: $dtext | $quoted_pair )* #     содержимое

                            $CloseBR                                             #                   ]

>;

 

# Элемент 9: sub-domain - domain-ref или domain-literal

$sub_domain = qq< (?: $domain_ref | $domain_lit ) >;

 

 

 

 

# Элемент 6: domain - список субдоменов (subdomain), разделенных точками.

$domain = qq< $sub_domain                             # Начальный субдомен

                  (?:                                     #

                     $X $Period                    # Если дальше идет точка...

                     $X $sub_domain                   #  ...следующий субдомен

                  )*

>;

 

# Элемент 8: route. Последовательность "@ $domain", разделенных запятыми,

# после которой следует точка.

$route = qq< \@ $X $domain

           (?: $X , $X \@ $X $domain )* # Домены перечисляются через запятую

        :                                                 # закрывающее двоеточие

>;

 

 

# Элемент 5: local-part - последовательность $word, разделенных запятыми

$local_part_part = qq< $word                       # Начальное слово

        (?: $X $Period $X $word )*    # Слова перечисляются через запятую

>;

 

# Элемент 2: addr-spec - local@domain

$addr_spec  = qq< $local_part $X \@ $X $domain >;

 

# Элемент 4: route-addr - <route? addr-spec>

$route_addr = qq[ < $X                               # Открывающая скобка <

                           (?: $route $X )?       #      необязательный маршрут

                                     $addr_spec          #                  спецификация адреса

                                                         $X >  #                            закрывающая >

];

Элемент 3 — phrase

При определении фразы (phrase) возникают определенные сложности. В соответствии с табл. 7.11, это одно или несколько слов (word), однако мы не можем использовать конструкцию (?:$word)+, поскольку компоненты могут разделяться $sep. Конструкция (?:$word|$sep)+ тоже не годится, поскольку она не требует присутствия $word, а лишь разрешает его. Возникает заманчивая мысль — использовать $word(?:$word|$sep)*, и здесь придется хорошенько подумать. Вспомните, как мы конструировали $sep. Часть, не являющаяся комментарием, фактически описывается выражением [[$space$tab]+], и ее включение в (…)* подозрительно напоминает ситуацию бесконечного перебора (с. <$R[P#,R5-27]>). Аналогичная проблема возникала бы и для $atom внутри $word, если бы не конструкция (?!…), использованная для проверки границ совпадения. Нечто похожее можно было бы сделать и для $sep, но у меня есть идея получше.

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

$word (?: [$atom_char$space$tab] | $quoted_string | $comment )+

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

# Элемент 3: phrase

$phrase_ctrl = '\000-\010\012-\037'; # Аналог ctrl, но без табуляции

 

# Аналог atom-char, но без пробела и с phrase_ctrl.

# Поскольку класс является инвертированным, он совпадает с теми же

# символами, что и atom-char, а также с пробелом и табуляцией

$phrase_char =

   qq/[^()<>\@,;:\".$esc$OpenBR$CloseBR$NonASCII$phrase_ctrl]/;

 

$phrase = qq< $word        # Одно слово, за которым могут следовать....

                  (?:

                           $phrase_char | # Атомы и пробелы, или ...

                           $comment         | # комментарии, или...

                           $quoted_str         # строки в кавычках

                  )*

>;

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

Итоговое определение mailbox

Остается лишь определить элемент 1 несложной конструкцией следующего вида:

# Элемент 1: mailbox - addr_spec или phrase/route_addr

$mailbox = qq< $X                           # Необязательный начальный комментарий

                  (?: $addr_spec         # адрес

                           |                   #  или

                           $phrase $route_addr # имя или адрес

                  ) $X                   # необязательный конечный комментарий

>;

Готово!

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

die "invalid address [$addr]\n" if $addr !~ m/^$mailbox/xo;

(я настоятельно не советую забывать модификатор /o в подобных регулярных выражениях[59]!)

Интересно взглянуть на итоговое регулярное выражение, то есть непосредственное содержимое $mailbox. Ниже приведены первые из 60 с лишним строк этого выражения, разбитого на строки для вывода, после удаления комментариев с пробелами:

(?:[\040\t]|\((?:[^\\\x80-\xff\n\015()]|\\[^\x80-\xff]|\((?:[^\\\x80-

\xff\n\015()]|\\[^\x80-\xff])*\))*(?:(?:[^(\040)<>@,;:".\\\[\]\000-\0

37\x80-\xff]+(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff])|"(?:[^\\\x

80-\xff\n\015"]|\\[^\x80-\xff])*")(?:(?:[\040\t]|\((?:[^\\\x80-\xff\n

\015()]|\\[^\x80-\xff]|\((?:[^\\\x80-\xff\n\015()]|\\[^\x80-\xff])*\)

)*\))*\.(?:[\040\t]|\(?:[^\\\x80-\xff\n\015()]|\\[^\x80-\xff]|\(?:[^\

\\x80-\xff\n\015()]|\\[^\x80-\xff])*\))*\))*(?:[^(\040)<>@,;:".\\\[\]

\000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff])|"(?

Однако! На первый взгляд кажется, что такое гигантское регулярное выражение просто не может работать эффективно, но размер регулярного выражения не имеет никакого отношения к эффективности. Главный определяющий фактор — количество возвратов. Не содержит ли выражение большое количество конструкций выбора? А ситуации бесконечного поиска? Даже очень большое регулярное выражение может быстро приводить механизм к найденному совпадению или выводу о его отсутствии.

Недостатки и ограничения

При работе с подобными выражениями необходимо хорошо понимать их возможности. Так, наше выражение опознает только адреса электронной почты Интернета и не работает с локальными адресами. Например, на моем компьютере строка jfriedl является допустимым адресом электронной почты, но она не соответствует формату почтового адреса Интернета (впрочем, эта проблема связана не с регулярным выражения, а скорее с его неправильным применением). Кроме того, адрес может быть лексически правильным, но при этом никуда не указывать (как в приведенном выше примере That Tall Guy). Для решения некоторых проблем можно потребовать, чтобы имя домена завершалось субдоменом, состоящим из двух или трех символов (например, .com или .jp). Задачу можно решить простым присоединением $esc . $atom_char {2,3} к $domain, или же более строго — прямым перечислением возможных субдоменов:

$esc . (?: com | edu | gov | … | ca | de | jp | u[sk] …)

Если уж на то пошло, абсолютно невозможно гарантировать, что отправленное по некоторому адресу письмо до кого-то дойдет. Точка. Отправка тестового сообщения — хороший способ, если кто-нибудь удосужится на него ответить. Также в заголовок сообщения желательно включить поле Return-Receipt-To, поскольку оно заставит удаленную систему сгенерировать короткий ответ о прибытии вашего сообщения в почтовый ящик.

Оптимизация — раскрутка циклов

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

$quoted_str = qq<

    \"                                                         # "

        $qtext *                                          #      норм

        (?: $quoted_pair $qtext * )*        #  ( спец норм* )*

    \"                                                         #                 "

>;

При этом определение $phrase принимает следующий вид:

$phrase = qq<

$word                                   # Начальное слово

    $phrase_char *                   # "нормальные" атомы и/или пробелы

(?:

        (?: $comment | $quoted_str ) # "специальные" комментарии или

                                               # строки в кавычках

        $phrase_char *                   #  снова "нормальные" элементы

)*

>;

Такие элементы, как $Cnested, $comment, $phrase, $domain_lit и $X, оптимизируются аналогичным образом, но будьте внимательны — иногда возникают затруднения. За примером обратимся к переменной $sep. Она требует минимум одного совпадения, но в результате стандартной раскрутки создается регулярное выражение, для которого совпадение не является обязательным.

Возвращаясь к общему шаблону раскрутки (с. <$R[P#,R5-28]>), если вы хотите, чтобы элемент спец был обязательным, внешнюю конструкцию (…)* можно заменить на (…)+, но для $sep этот вариант не подходит. Совпадение должно присутствовать, но совпадать может как спец, так и норм.

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

$sep = qq< (?:

                  [$space$tab]+                           # Начинается с пробела

                  (?: $comment [$space$tab]* )*

                  |

                  (?: $comment [$space$tab]* )+ # Начинается с комментария

                  )

>;

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

$comment (?: [$space$tab]+ $comment )*

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

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

Оптимизация обработки пробелов

Другая разновидность оптимизация связана с использованием $X. Посмотрим, как часть нашего регулярного выражения, соответствующая $route, совпадает с @spcgatewayspc:. Встречаются ситуации, когда поиск совпадения для необязательной части завершается неудачей, но лишь после обнаружения одного или нескольких внутренних совпадений для $X. Давайте вспомним, как выглядят наши определения $domain и $route:

# Элемент 6: domain - список субдоменов (subdomain), разделенных точками.

$domain = qq< $sub_domain                             # Начальный субдомен

                  (?:                                     #

                    $X $Period                    # Если дальше идет точка...

                     $X $sub_domain                   # ...следующий субдомен

                  )*

>;

 

# Элемент 8: route. Последовательность "@ $domain", разделенных запятыми,

# после которой следует точка.

$route = qq< \@ $X $domain

        (?: $X , $X \@ $X $domain )* # Домены перечисляются через запятую

         :                                                # закрывающее двоеточие

>;

После того, как $route находит в начале строки совпадение @spcgatewayspc:, а первый компонент $sub_domain переменной $domain совпадает с @spcgatewayspc:, регулярное выражение ищет точку и следующий компонент $sub_domain (с возможным присутствием $X на каждом стыке). При первой попытке поиска для подвыражения $X $Period $X $sub_domain первый экземпляр $X совпадает с пробелом @spcgatewayspc:, но поиск точки завершается неудачей. Это приводит к выходу из круглых скобок с возвратом, а поиск совпадения для $domain на этом завершается.

Поиск совпадения для $route продолжается. После обнаружения совпадения для первого $domain механизм пытается найти следующее совпадение, отделенное запятой. В подвыражении $X , $X \@… первый экземпляр $X совпадает с тем же пробелом, совпадение для которого было найдено (и снова отменено!) ранее. И на этот раз поиск завершается неудачей.

Расточительно тратить время на поиски совпадения для $X, когда поиск совпадения для всего подвыражения завершается неудачей. Поскольку $X может совпасть практически везде, эффективнее искать совпадение лишь тогда, когда заведомо существует совпадение для всего подвыражения.

Рассмотрим следующий фрагмент, в котором изменилось только расположение $X:

$domain = qq<

                  $sub_domain $X

                  (?:                                     #

                     $Period $X $sub_domain $X

                  )*

>;

 

$route = qq<

        \@ $X $domain

        (?: , $X \@ $X $domain )*

        : $X

>;

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

В результате всех изменений итоговое выражение (после удаления всех комментариев и произвольных пропусков) удлиняется почти на 50 процентов, но в моих тестах оно работало на 9–19 процентов быстрее (9 процентов — для тестов с неудачным поиском, 19 процентов — для тестов, в которых совпадение существует). Еще раз напомню о важности модификатора /o. Окончательная версия регулярного выражения приведена в приложении Б.

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

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

l      Разберитесь, кто и что интерполирует, и когда это происходит. Особое внимание следует обратить на многочисленные превращения экранирующего префикса \.

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

$word         = qq< $atom | $quoted_str >;

$local_part = qq< $word (?: $X $Period $X $word) * >

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

$field =   "Subject|From|Date";

$line =    "^$field: (.*)";

Выражение [^Subject|From|Date:spc(.*)] заметно отличается от [^(Subject|From|Date):spc(.*)] — и конечно, пользы от него гораздо меньше.

l      Помните о том, что действие модификатора /x не распространяется на символьные классы, поэтому в классах нельзя использовать произвольные пропуски и комментарии.

l      Комментарии внутри регулярных выражений (после #) продолжаются до конца строки или регулярного выражения. Рассмотрим следующий фрагмент:

$domain_ref = qq< $atom # Просто атом >

$sub_domain = qq< (?: $domain_ref | $domain_lit) >

Конец переменной $domain_ref совсем не означает конца комментария, вставленного из этой переменной в регулярное выражение. Комментарий продолжается до конца регулярного выражения или символа новой строки, поэтому в данном случае комментарий выходит за пределы $domain_ref, поглощает конструкцию выбора, остаток $sub_domain и все последующие символы до конца регулярного выражения или ближайшего символа новой строки. Проблему можно решить ручной вставкой символа новой строки (с. <$R[P#,R7-127]>): $domain_ref = qq< $atom # Просто атом\n >.

l      Поскольку в процессе отладки часто возникает необходимость выводить регулярные выражения командой print, подумайте о замене "\0xff" на '\0xff'.

l      Разберитесь в отличиях между следующими командами:

$quoted_pair = qq< $esc[^$NonASCII] >;

$quoted_pair = qq< $esc [^$NonASCII] >;

$quoted_pair = qq< ${esc}[^$NonASCII] >;

Первая команда очень сильно отличается от двух остальных. Она интерпретируется как попытка индексации элемента массива $esc, что, конечно, отличается от задуманного (с. <$R[P#,R7-128]>).

Последний комментарий

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

Впрочем, не считайте меня бездумным фанатиком — Perl не обладает многими возможностями, которые я бы хотел в нем видеть. Наиболее очевидное упущение поддерживается в других диалектах, в частности в Tcl, Python и GNU Emacs: речь идет о возможности получения индексов начала и конца совпадения (а также частичных совпадений $1, $2 и т. д.). При помощи переменных можно получить копию текста, совпадающего с подвыражениями в круглых скобках, но в общем случае невозможно определить, где именно в строке расположен этот текст. Предположим, вы пишете программу, обучающую работе с регулярными выражениями. Вам хотелось бы вывести исходную строку и показать: «Здесь совпадает первая пара круглых скобок, здесь — вторая, и так далее», но пока в Perl это сделать невозможно.

Другая возможность, которая мне иногда нужна — массив ($1, $2, $3, …), аналог функции match-data в Emacs (с. <$R[P#,R6-6]>). Конечно, я могу самостоятельно сконструировать нечто подобное:

$parens[0] = $&;

$parens[1] = $1;

$parens[2] = $2;

$parens[3] = $3;

$parens[4] = $4;

И все же было бы удобнее, если бы эта возможность была встроенной.

Также вспоминаются неуступчивые квантификаторы, упомянутые в сноске на с. <$R[P#,R4-41]>. С ними многие выражения работали бы гораздо эффективнее.

Наконец, существует масса экзотических возможностей, о которых можно только мечтать (чем я, собственно, и занимаюсь). Однажды я дошел до того, что реализовал на своем компьютере специальную форму записи, при которой регулярное выражение могло во время поиска совпадения обращаться к ассоциативному массиву, используя [\1] и т. д. в качестве индексов. Благодаря этому появилась возможность расширить выражения типа [(['"]).*?\1] и реализовать в них поддержку <…>, {…} и т. д.

Другая возможность, которую мне хотелось бы видеть в Perl — именованные подвыражения, аналоги имен символических групп языка Python. Речь идет о сохраняющих круглых скобках, с которыми (каким-то образом) связывается переменная. При успешном поиске совпадения переменной присваивается значение. Например, это позволило бы анализировать телефонные номера конструкциями следующего вида (с использованием фиктивного синтаксиса ?<переменная>):

[(?<$area>\d\d\d)-(?<$exchange>\d\d\d)-(?<$num>\d\d\d\d)]

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

Но он очень близок к идеалу.

 

Примечания, относящиеся к Perl4

Как правило, при переходе от Perl4 к Perl5 проблем с регулярными выражениями не возникает. Вероятно, самое серьезное расхождение заключается в том, что @ теперь интерполируется в регулярных выражениях (а также строках в кавычках). И все же при работе на Perl4 необходимо учитывать некоторые тонкие (и не очень) обстоятельства.

1

С. <$R[P#,R7-38]>. <$M[R7-37]>Специальные переменные $&, $1 и т. д. в Perl4 не ограничиваются доступом только для чтения, как в Perl5. Модификация этих переменных не приводит к волшебному изменению исходной строки, из которой они были скопированы (хотя такая возможность была бы нелишней). В основном это обычные переменные с динамической видимостью, которым при каждом успешном совпадении присваиваются новые значения.

2

С. <$R[P#,R7-40]>. <$M[R7-39]>В Perl4 переменная $` иногда ссылается на текст, начинающийся от начала совпадения (а не от начала строки). Ошибка, исправленная в новых версиях, приводила к тому, при каждой компиляции регулярного выражения значение $` сбрасывалось. Если регулярное выражение-операнд использовало интерполяцию переменных и входило в конструкцию m/…/g в скалярном контексте (как в итераторе цикла while), то перекомпиляция, приводившая к сбросу $`, выполнялась при каждой итерации.

3

С. <$R[P#,R7-42]>. <$M[R7-41]>В Perl4 при отсутствии круглых скобок в регулярном выражении переменная $+ волшебным образом превращалась в копию $&.

4

С. <$R[P#,R7-45]>. <$M[R7-44]>Perl4 интерполирует $MonthName[…] как ссылку на массив лишь в том случае, если массив @MonthName заведомо существует. В Perl5 такая интерпретация используется всегда.

5

С. <$R[P#,R7-47]>. <$M[R7-46]>В отличие от Perl5, префикс экранированного закрывающего ограничителя в Perl4 не удаляется. Это существенно, если ограничитель является метасимволом. Например, несколько надуманная подстановка s*2\*2*4* в Perl5 работает не так, как следовало бы ожидать.

6

С. <$R[P#,R7-72]>. <$M[R7-71]>Perl4 позволяет использовать пропуски в качестве ограничителя операнда при поиске. Хотя иногда в таких ситуациях бывало удобно использовать символ новой строки, в основном подобные фокусы хороши лишь для конкурса на Самую Загадочную Программу на Perl.

7

С. <$R[P#,R7-74]>. <$M[R7-73]>Помните: в Perl4 пропускаются все экранирующие префиксы (см. примечание 5).

8

С. <$R[P#,R7-76]>. <$M[R7-75]>Для оператора поиска в Perl4 четыре специальных ограничителя m{…} не поддерживаются. С другой стороны, они поддерживаются для оператора подстановки.

9

С. <$R[P#,R7-78]>. <$M[R7-77]>Perl4 поддерживает особый синтаксис поиска с использованием ?, но только в форме ?…?. Конструкция ?…? не является специальной.

10

С. <$R[P#,R7-80]>. <$M[R7-79]>В Perl4 любой вызов reset в программе приводит к сбросу всех операций поиска с ограничителями ?. В Perl5 вызов reset относится только к текущему пакету.

11

С. <$R[P#,R7-83]>. <$M[R7-82]>При вызове оператора поиска с пустым операндом Perl4 заново использует последнее успешно примененное регулярное выражение без учета области видимости. В Perl5 используется последнее успешно примененное регулярное выражение в текущей динамической области видимости.

Сказанное проще пояснить на примере. Рассмотрим следующий фрагмент:

"5" =~ m/5/;

{ # Создать новую область видимости…

    "4" =~ m/4/;

} # … завершить новую область видимости

"45" =~ m//; # Использовать выражение по умолчанию. Совпадает 4 или 5

           # в зависимости от того, какое выражение было использовано.

print 'this is Perl $&\n";

Perl4 выводит строку this is Perl4, а Perl5 — this is Perl5.

12

С. <$R[P#,R7-89]>. <$M[R7-88]>В любой версии списковая форма m/…/g возвращает список текстовых фрагментов, совпавших с подвыражениями в круглых скобках. Тем не менее, Perl4 при этом не присваивает значения $1 и других переменных этого семейства, а Perl5 это делает.

13

С. <$R[P#,R7-91]>. <$M[R7-90]>В Perl5 элементы списка, соответствующие несовпадающим парам круглых скобок в конструкции m/…/g, имеют неопределенное значение, а в Perl4 они содержат пустые строки. Оба значения интерпретируются как логическая ложь, но в остальном заметно отличаются.

14

С. <$R[P#,R7-93]>. <$M[R7-92]>В Perl5 модификация целевого текста конструкции m/…/g в скалярном контексте приводит к сбросу данных pos. В Perl4 позиция /g связывается с регулярным выражением-операндом. Таким образом, модификация текста, используемого в качестве целевых данных, не влияет на позицию /g (это может быть как хорошо, так и плохо — в зависимости от того, как посмотреть). В Perl5 позиция /g связывается с целевой строкой, поэтому при любой модификации последней происходит сброс данных pos.

15

С. <$R[P#,R7-97]>. <$M[R7-96]>Хотя в Perl4 оператор поиска не позволяет использовать парные ограничители (например, m[…]), такая возможность существует для оператора подстановки. По аналогии с Perl5, если регулярное выражение-операнд заключается в парные ограничители, операнд-замена заключается в другую пару. В отличие от Perl5, эти две пары не могут разделяться пропусками (поскольку в Perl4 пропуски считаются допустимыми ограничителями).

16

С. <$R[P#,R7-100]>. <$M[R7-99]>Как ни странно, в Perl4 конструкция s'…'…' обеспечивает апострофный контекст для операнда-выражения, но не для операнда-замены — последний обрабатывается по стандартным правилам строк, заключенных в кавычки.

17

С. <$R[P#,R7-102]>. <$M[R7-101]>В Perl4 операнд-замена интерпретируется по правилам строк в апострофах, поэтому в \` и \\ начальная косая черта удаляется еще до передачи eval. В Perl5 eval получает все данные так, как есть.

18

С. <$R[P#,R7-105]>. <$M[R7-104]>При отсутствии подстановок Perl5 возвращает пустую строку. Perl4 возвращает число 0 (оба значения интерпретируются как логическая ложь).

19

С. <$R[P#,R7-108]>. <$M[R7-107]>Количество фрагментов по умолчанию, предоставляемое Perl для конструкции ($filename, $size, $date) = split(…), влияет на значение, присваиваемое @_, при использовании операнда в форме ?…?. В Perl5 подобной проблемы не существует, поскольку форсированное разбиение в @_ не поддерживается.

20

С. <$R[P#,R7-112]>. <$M[R7-111]>В Perl4 поддерживается особая разновидность оператора split: если в операторе поиска в списковом контексте используется конструкция ?…? (но не m?…?), split заполняет @_ так, как это делается в списковом контексте. В Perl5 данная возможность не поддерживается, хотя в документации утверждается обратное.

21

С. <$R[P#,R7-114]>. <$M[R7-113]>В Perl4 специальная интерпретация распространяется и на выражения (например, вызовы функции), вычисление которых дает скалярный результат в виде одиночного пробела. В Perl5 специальная интерпретация распространяется лишь на литеральные строки, состоящие из одного пробела.

22

С. <$R[P#,R7-116]>. <$M[R7-115]>В Perl4 по умолчанию вместо 'spc' используется операнд m/\s+/. Отличие влияет на обработку начальных пропусков.

23

С. <$R[P#,R7-121]>. <$M[R7-120]>В Perl4 копирование при каждом успешном совпадении также происходит в том случае, если где-либо в программе используется eval.



[1] Недоступен до версии 5.000.

[2] Поддерживается, начиная с версии 5.000.

[3] Поддерживается, начиная с версии 5.000.

[4] Надежно поддерживается, начиная с версии 5.002.

[5] Поддерживаются, начиная с версии 5.000.

[6] \b — якорный метасимвол вне символьного класса, сокращенное обозначение символа внутри класса.

[7] Поддерживается, начиная с версии 5.000.

[8] Поддерживается, начиная с версии 5.000.

[9] \b — якорный метасимвол вне символьного класса, сокращенное обозначение символа внутри класса.

[10] Поддерживаются, начиная с версии 5.000.

[11] В Perl ситуация не столь однозначная, поскольку функции и процедуры тоже могут получать сведения о контексте вызова и даже модифицировать свои аргументы. Я пытаюсь провести границу между операторами и функциями, чтобы особо выделить некоторые характеристики, из-за которых часто возникают недоразумения.

[12] Работает с $_, если целевая строка не задается при помощи =~ или !~.

[13] Работает с $_, если целевая строка не задается при помощи =~ или !~.

[14] Поддерживается, начиная с версии 5.000.

[15] Поддерживается, начиная с версии 5.000.

[16] Поддерживается, начиная с версии 5.000.

[17] См. http://tpj.com/ или staff@tpj.com.

[18] Не хочу противоречить себе, но я должен сообщить, что на момент написания книги в функции quotewords содержались ошибки. Эта функция пропускает последнее поле, если оно равно числу 0, не распознает завершающие пустые строки, а поля в кавычках не могут содержать экранированных символов (кроме экранированных кавычек). Кроме того, она отрицательно влияет на эффективность всего сценария (с. <$R[P#,R7-129]>). Я связался с автором, и эти ошибки, вероятно, будут исправлены в будущих версиях.

[19] В действительности раньше, но в тех версиях оно работало ненадежно.

[20] Разумеется, речь идет об оригинале — Примеч. перев.

[21] Раньше списковый контекст назывался «контекстом массива». Изменение связано с тем, что данные в этом контексте могут относиться к массиву, хэшу и списку с явным перечислением.

[22] Perl позволяет объединять имена глобальных переменных в группы, называемые пакетами, но переменные все равно остаются глобальными.

[23] Perl не позволяет использовать my с именами специальных переменных, поэтому сравнение чисто теоретическое.

[24] Как упоминалось выше, пометка [1 с. <$R[P#,R7-37]>] относится к примечанию 1 для Perl4, находящемуся на с. <$R[P#,R7-37]>.

[25] В действительности, если исходный текст представляет собой переменную с неопределенным значением, но совпадение будет успешно найдено (маловероятная, но возможная ситуация), результат "$`$&$'" будет представлять собой пустую строку, а не неопределенную величину. Это единственное исключение из этого правила.

[26] На рис. 7.1 изображена модель, описывающая многоуровневый процесс обработки регулярных выражений в Perl. Читатели, разбирающиеся во внутреннем устройстве Perl, заметят, что в действительности обработка организована несколько иначе. Например, то, что я называю фазой C, в действительности является не отдельным этапом, а составной частью фаз B и D.

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

Рассмотрим оператор вида m/regex #comment $var/. В моей модели $var интерполируется в операнд в фазе B, и результаты интерполяции удаляются с остатком комментария в фазе C. На В действительности комментарий и ссылка на переменную будут удалены в фазе B еще до интерполяции переменной. Это приводит к тому же конечному результату… почти всегда.  Если переменная, о которой идет речь, содержит символ новой строки, этот символ не будет интерпретироваться как признак конца комментария, как видно из моей модели. Кроме того, пропадают все побочные эффекты интерполяции переменной (вызовы функций и т. д.), поскольку на самом деле переменная не интерполируется.

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

[27] Поддерживаются, начиная с Perl5.

[28] По форме записи ничем не отличается от предыдущего квантификатора. Обе версии абсолютно идентичны и различаются только по эффективности — их совместное существование объясняется лишь удобством записи и ее логичностью.

[29] Еще лучше было бы использовать именованные подвыражения вроде тех, которые поддерживаются в Python при помощи имен символических групп, однако в Perl такая возможность не поддерживается… пока.

[30] Существуют разные варианты реализации «[this] и [that]» и столько же способов сравнения этих вариантов. Как правило, главными факторами при сравнении является точное понимание того, что же именно совпадает, эффективность поиска и наглядность. Я написал для августовского номера «The Perl Journal» за 1996 год статью с подробным анализом этой темы. Надеюсь, эта статья даст вам дополнительную информацию о том, какой же реальный смысл стоит за операциями, выполняемыми механизмом. Кстати говоря, приведенное здесь изобретательное решение я получил от Рэндала Шварца (Randal Schwartz), и при тестировании оно показало довольно высокие результаты.

[31] Комментарии (?#…) удаляются на очень ранней стадии обработки регулярного выражения, где-то между фазами A и B на рис. 7.1 (с. <$R[P#,R7-117]>). Интересный факт: насколько мне известно, закрывающая круглая скобка комментария (?#…) является единственным элементом языка регулярных выражений Perl, который не может экранироваться. Первая закрывающая круглая скобка после (?# однозначно заканчивает комментарий.

[32] Знак $ также может быть признаком интерполяции переменной, однако его интерпретация редко вызывает какие-либо неоднозначности. Подробности приведены в разделе «Предварительная обработка и интерполяция переменных» (с. <$R[P#,R7-130]>).

[33] Строго говоря, в двух многострочных режимах оптимизация по границе логических строк становится чуть менее эффективной, поскольку механизм смещения текущей позиции должен применять регулярное выражение после каждого внутреннего символа новой строки (с. <$R[P#,R5-3]>).

[34] Небольшое замечание, не связанное с регулярными выражениями. Существует распространенное заблуждение, будто присваивание $/ значения undef заставляет следующую конструкцию <> вернуть «весь остаток ввода» (или если она используется в начале сценария — весь ввод).  В термине «режим поглощения файла» ключевым является слово файл. Если входные данные состоят из нескольких файлов, для каждого файла понадобится отдельный вызов <>. Если вы действительно хотите получить весь ввод, воспользуйтесь конструкцией join('',<>).

[35] Эти две разновидности метасимволов не являются взаимоисключающими. Скажем, в GNU Emacs поддерживаются оба типа.

[36] Причем в последний раз — как раз перед тем, как я решил включить в книгу этот раздел!

[37] В таблицу не входит символ [\v] (вертикальная табуляция). В течение нескольких лет этот символ описывался в страницах руководства и в прочей документации, но он так и не был включен в язык! Полагаю, теперь, когда символ вертикальной табуляции был убран из документации, никто не пожалеет о нем.

[38] Для программирования на Perl я обычно использую GNU Emacs в режиме cperl-mode, обеспечивающем автоматическую расстановку отступов и цветовое выделение. Я часто экранирую кавычки в регулярных выражениях, поскольку в противном случае они сбивают с толку режим cperl-mode, не понимающий, что кавычки в регулярном выражении не являются началом строк.

[39] Если \E отсутствует  — то до конца строки или регулярного выражения.

[40] В Perl5 игнорируется в конструкциях \L…\E и \U…\E, если только не следует непосредственно за \L и \U.

[41] Поскольку модификаторы оператора поиска могут следовать в любом порядке, программисты часто тратят немало времени на то, чтобы добиться наибольшей выразительности. Например, learn/by/osmosis является допустимой командой (при условии, что у вас имеется функция learn). Слово osmosis составлено из модификаторов — повторение модификаторов оператора поиска (но не модификатора /e оператора подстановки!) допускается, хотя и не имеет смысла.

[42] Я благодарен Хансу Малдеру (Hans Mulder), предоставившему исторические сведения по этому вопросу, а также Рэндалу за его чувство юмора.

[43] Если вы тоже захотите узнать, сколько дней осталось до моего дня рождения, загрузите страницу

http://omrongw.wg.omron.co.jp/cgi-bin/j-e/jfriedl.html

или одну из зеркальных страниц (см. приложение А).

[44] Существует один особый случай, когда это не соответствует истине. Он описан ниже, при рассмотрении нетривиального использования первого операнда split.

[45] Возможно, вы узнали частично раскрученную версию [<("[^>"]*"|[^>"])*>], где нормальным элементом является [[^>"]], а специальным — ["[^"]*"]. Конечно, вы можете самостоятельно провести дополнительную раскрутку специального элемента.

[46] По неизвестным мне причинам в Perl4 исходное выражение работает быстрее всех остальных. Впрочем, я не мог тестировать решения с модификатором /e, поскольку утечка памяти в Perl4 делала результат бессмысленным.

[47] Точнее, не входили бы, если бы в природе существовала спецификация Perl.

[48] Сохраняющие круглые скобки также могут использоваться в обратных ссылках, поэтому существует вероятность того, что сохраняющие круглые скобки будут использоваться без применения $1 и других переменных. На практике подобные ситуации встречаются редко.

[49] На самом деле я не дождался конца тестирования. На основании других тестов я вычислил, что выполнение программы должно занимать примерно 36,4 часа. Впрочем, если хотите — можете проверить.

[50] Хотя именно это и происходило в первой реализации grep!

[51] Я модифицировал свою копию версии 5.003 так, чтобы по мере выполнения разных внутренних операций она выдавала массу красивых сообщений с цветовой кодировкой. Это позволило мне представить картину в целом, не отвлекаясь на мелкие детали.

[52] Вероятно, это замечание излишне. Вспомните: присутствие сохраняющих круглых скобок в регулярном выражении активизирует копирование для поддержки $&, а это копирование само по себе подавляет все оптимизации замены. А использовать $1 без сохраняющих круглых скобок глупо, поскольку значение этой переменной заведомо будет неопределенным (с. <$R[P#,R7-131]>)

[53] Во всяком случае, теоретически. В Perl версии 5.003 присутствует ошибка, из-за которой применение study может привести к тому, что механизм регулярных выражений перестает находить правильные совпадения. Эта тема рассматривается ниже.

[54] В текущей реализации имеется ошибка, которая приводит к подавлению этой оптимизации в тех случаях, когда регулярное выражение начинается с литерального текста. Это очень обидно, поскольку обычно именно такие выражения обеспечивают наибольший выигрыш от применения study.

[55] Internet RFC 822. Документ доступен по адресу ftp://ftp.rfc-editor.org/in-notes/rfc822.txt.

[56] Конечно, отправленная по этому адресу почта будет возвращена из-за отсутствия активного пользователя с таким именем, но это совершенно другой вопрос.

[57] Программа, которую мы напишем в следующем разделе, имеется на моей домашней странице — см. приложение А.

[58] В строках, заключенных в апострофы, \\ и экранированный закрывающий ограничитель (обычно \') имеют особый смысл. Другие экранированные символы передаются без изменений, поэтому \040 остается \040.

[59] Во время исходного тестирования меня ждал неприятный сюрприз — оптимизированная версия программы (см. ниже) почему-то работала медленнее исходной. Я был совершенно потрясен, пока не выяснилось, что я забыл указать модификатор /o! В результате громадное регулярное выражение-операнд обрабатывалось заново для каждого совпадения (с. <$R[P#,R7-85]>). Оптимизированное выражение имеет существенно большую длину, поэтому дополнительные затраты времени на обработку полностью затмевали весь выигрыш от эффективности. При включении /o оптимизированная версия не только обогнала исходную, но и весь тест стал выполняться на порядок быстрее.