Приглашаем посетить
Культура (niv.ru)

Поиск по шаблону

Назад
Глава 6 Поиск по шаблону
Вперед

Поиск по шаблону

Введение

В большинстве современных языков программирования существуют примитивные средства поиска по шаблону (обычно вынесенные в дополнительные библиотеки), но шаблоны Perl интегрируются на уровне самого языка. Они обладают возможностями, которыми не могут похвастаться другие языки; возможностями, которые позволяют взглянуть на данные с принципиально новой точки зрения. Подобно тому, как шахматист воспринимает расположение фигур на доске как некий образ, адепты Perl рассматривают данные с позиций шаблонов. Шаблоны записываются на языке регулярных выражений1, богатом знаками препинания, и позволяют работать с замечательными алгоритмами, обычно доступными лишь экспертам в области компьютерных технологий. "Если поиск по шаблону - такая потрясающая и мощная штука, - спросите вы, - то почему же эта глава не содержит сотни рецептов по применению регулярных выражений?" Да, регулярные выражения обеспечивают естественное решение многих проблем, связанных с числами, строками, датами, Web-документами, почтовыми адресами и буквально всем, что встречается в этой книге. В других главах поиск по шаблону применяется свыше 100 раз. А в этой главе в основном представлены те рецепты, в которых шаблоны являются частью вопроса, а не ответа. Обширная и тщательно проработанная поддержка регулярных выражений в Perl означает, что в вашем распоряжении оказываются не только те средства, которые не встречаются ни в одном другом языке, но и принципиально новые возможности их использования. Программисты, недавно познакомившиеся с Perl, часто ищут в нем функции поиска и подстановки: 1 Точнее, регулярные выражения в классическом смысле не содержат обратных ссылок, присутствующих в шаблонах Perl.
match( $строка, $шаблон);
subst( $строка, $шаблон, $замена);
Однако поиск и подстановка - настолько распространенные задачи, что они заслуживают собственного синтаксиса:

$meadow =" m/sheep/; # Истинно, если $meadow содержит "sheep"
$meadow !~ m/sheep/; # Истинно, если $meadow не содержит "sheep"
$meadow ="" s/old/new; # Заменить в $meadow "old" на "new"


Поиск по шаблону даже в упрощенном виде не похож на обычные строковые сравнения. Он больше похож на поиск строк с применением универсальных символов-мутантов, к тому же накачанных допингом. Без специального "якоря" позиция, в которой ищется совпадение, свободно перемещается по всей строке. Допустим, если вы захотите найти слово ovine или ovines и воспользуетесь выражением $meadow =~ /ovine/, то в каждой из следующих строк произойдет ложное совпадение: Fine bovines demand fine toreadors, Muskoxen are a polar ovibovine species. Grooviness went out of fashion decades ago. Иногда нужная строка находится прямо у вас перед глазами, а совпадение все равно не происходит: Ovines are found typically in oviaries. Проблема в том, что вы мыслите категориями человеческого языка, а механизм поиска по шаблону - нет. Когда этот механизм получает шаблон /ovine/ и другую строку, в которой происходит поиск, он ищет в строке символ "о", за которым сразу же следует "v", затем "i", "п" и "е". Все, что находится до этой последовательности символов или после нее, не имеет значения. Итак, выясняется, что шаблон находит совпадения там, где они не нужны, и не узнает то, что действительно нужно. Придется усовершенствовать его. Например, для поиска последовательности ovine или ovines шаблон должен выглядеть примерно так:
if ($meadow =~ /\bovines?\b/i) { print "Here be sheep!" }
Шаблон начинается со метасимвола \Ь, который совпадает только с границей i лова. s? обозначает необязательный символ s - он позволяет находить как ovine, так и ovines. Модификатор /i в конце шаблона означает, что поиск осуществляется без учета регистра. Как видите, некоторые символы и последовательности символов имеют особый смысл для механизма поиска но шаблону. Метасимволы фиксируют шаблон в начале или конце строки, описывают альтернативные значения для частей шаблона, организуют повторы и позволяют запомнить часть найденной подстроки, чтобы в дальнейшем использовать ее в шаблоне или программном коде. Освоить синтаксис поиска по шаблону не так уж сложно. Конечно, служебных символов много, но существование каждого из них объясняется вескими причинами. Регулярное выражение - это не просто беспорядочная груда знаков... это тщательно продуманная груда знаков! Если вы что-нибудь забыли, всегда можно заглянуть в документацию. Сводка по синтаксису регулярных выражений имеется в страницах руководства perlre(1) и реrlор(1), входящих в любую поставку Perl. Три затруднения Но синтаксис регулярных выражений - это еще цветочки по сравнению с их хитроумной семантикой. Похоже, большинство трудностей вызывают три особенности поиска по шаблону: жадность, торопливость (а так же то, как эти три аспекта взаимодействуют между собой) и возврат. Принцип жадности: если квантификатор (например, *) может совпасть в нескольких вариантах, он всегда совпадает со строкой наибольшей длины. Объяснения приведены в рецепте 6.15. Принцип торопливости: механизм поиска старается обнаружить совпадение как можно скорее, иногда даже раньше, чем вы ожидаете. Рассмотрим конструкцию "Fred" =~ /х*/. Если попросить вас объяснить ее смысл, вы, вероятно, скажс те: "Содержит ли строка "Fred" символы х?" Вероятно, результат поиска окажет ся неожиданным - компьютер убежден, что символы присутствуют. Дело в том, что /х*/ означает не просто "символы х", а "любое количество символов х". Или более формально - ноль и более символов. В данном случае нетерпеливый механизм поиска удовлетворяется нулем. Приведем более содержательный пример:
$string = "good food":
$string =~ s/o*/e/:

Как вы думаете, какое из следующих значений примет $string после подстановки? goof food geod food geed food geed feed ged food ged fed egood food Правильный ответ - последний, поскольку первая точка, в которой встречается ноль и более экземпляров "о", находится прямо в начале строки. Удивлены? С регулярными выражениями это бывает довольно часто. А теперь попробуйте угадать, как будет выглядеть результат при добавлении модификатора /д, который делает подстановку глобальной? Строка содержит много мест, в которых встречается ноль и более экземпляров "о", - точнее, восемь. Итак, правильный ответ - "egeede efeede". Приведем другой пример, в котором жадность уступает место торопливости:
% echo ababacaca | perl -ne 'print "$&\n" if /(a|ba|b)+(a|ac)+/' ababa

Это объясняется тем, что при поиске в Perl используются так называемые традиционные неопределенные конечные автоматы (в отличие от неопределенных конечных автоматов POSIX). Подобные механизмы поиска гарантируют возврат не самого длинного общего совпадения, а лишь самого длинного левого совпаде- ния. Можно считать, что жадность Perl проявляется лишь слева направо, а не в глобальном контексте. Но дело не обязательно обстоит именно так. В следующем примере используется awk - язык, от которого Perl позаимствовал немало:
% echo ababacaca awk omatch($0,/(a|ba|b)+(a|ac)+/) { print substr($0, RSTART, RLENGTH) }o ababacaca Выбор реализации поиска по шаблону в основном зависит от двух факторов: нерегулярности выражений (то есть наличия в них обратных ссылок) и типа возвращаемой величины (логическое "да/нет", все совпадение, подвыражения). Такие инструменты, как awk, egrep и lex, используют регулярные выражения и возвращают либо логическое "да/нет", либо все совпадение. Подобные возможности поддерживаются определенными конечными автоматами; поскольку определенные конечные автоматы работают быстрее и проще, реализация в перечисленных инструментах основана именно на них. Поиск по шаблону в таких программах и библиотеках, как ed, regex или perl, - совсем другое дело. Обычно приходится поддерживать нерегулярные выражения и знать, какие части строки совпали с различными частями шаблона. Эта задача намного сложнее и отличается экспоненциальным ростом времени выполнения. Естественный алгоритм ее реализации основан на неопределенных конечных автоматах; в этом заключается и проблема, и возможности. Проблема - в том, что неопределенные конечные автоматы работают медленно. Возможности - в том, что формулировка шаблона с учетом особенностей конкретной реализации позволяет существенно повысить быстродействие. Последняя и самая интересная из трех особенностей - возврат. Чтобы шаблон совпал, должно совпасть все регулярное выражение, а не лишь его отдельная часть. Следовательно, если начало шаблона с квантификатором совпадает, а одна из последующих частей шаблона - нет, механизм поиска возвращается к началу и пытается найти для него другое совпадение - отсюда и термин "возврат". Фактически это означает, что механизм поиска должен систематически перебирать разные возможности до тех пор, пока не найдет полное совпадение. В некоторых реализациях поиска возврат используется для поиска других совпадающих компонентов, которые могли бы увеличить длину найденного совпадения. Механизм поиска Perl этого не делает; найденное частичное совпадение используется немедленно, - если позднее другая часть шаблона сделает полное совпадение невозможным, происходит возврат и поиск другого частичного совпадения (см. рецепт 6.16). Модификаторы Модификаторы, используемые при поиске по шаблону, намного проще перечисли 11. и понять, чем другие метасимволы. Ниже приведена краткая сводка:

/i Игнорировать регистр (с учетом национальных алфавитов).
/х Игнорировать большинство пропусков в шаблонах и разрешить комментарии.
/g Глобальный модификатор - поиск/замена выполняются всюду, где это возможно.
/gс Не сбрасывать позицию при неудачном поиске.
/s Разрешить совпадение . с переводом строки; кроме того, игнорировать устаревшее значение $*.
/т Разрешить совпадение " и $ соответственно для начала и конца строки во внутренних переводах строк.
/о Однократная компиляция шаблонов. /е Правая часть s/// представляет собой выполняемый код.
/ее Правая часть s/// выполняется, после чего возвращаемое значение ин-терпретируеся снова.

Наиболее распространены модификаторы /i и /д. Шаблон /ram/i совпадает со строками " ram", "RAM", "Ram" и т. д. При наличии этого модификатора обратные ссылки проверяются без учета регистра (пример приведен в рецепте 6.16). При вызове директивы use locale в сравнениях будет учитываться состояние текущих локальных настроек. В текущей реализации модификатор /i замедляет поиск по шаблону, поскольку подавляет некоторые оптимизации скорости. Модификатор /д используется с s/// для замены всех найденных совпадений, а не только первого. Кроме того, /д используется с т// в циклах поиска (но не замены!) всех совпадений:

while (m/(\d+)/g) {
print "Found number $1\n";
}


В списковом контексте /g извлекает все совпадения в массив:
@numbers = m/(\d+)/g;
vВ этом случае будут найдены только неперекрывающиеся совпадения. Для поиска перекрывающихся совпадений придется идти на хитрость - организовать опережающую проверку нулевой ширины с помощью конструкции (?=...). Раз ширина равна нулю, механизм поиска вообще не смещается вперед. При этом найденные данные сохраняются внутри скобок. Однако Perl обнаруживает, что при наличии модификатора /g мы остались на прежнем месте, и перемещается на один символ вперед. Продемонстрируем отличия на примере:
$digits = "123456789";
@nonlap = $digits =~/(\d\d\d)/g;
@yeslap = $digits =~/(?=(\d\d\d))/g;
print "Non-overlapping: @nonlap\n";
print "Overlapping: @yeslap\n";
Non-overlapping:
123 456 789
Overlapping:
123 234 345 456 567 678 789


Модификаторы /s и /т используются для поиска последовательностей, содержащих внутренний перевод строки. При указании /s точка совпадает с "\n" - в обычных условиях этого не происходит. Кроме того, при поиске игнорируется значение устаревшей переменной $*. Модификатор /т приводит к тому, что " и $ совпадают в позициях до и после "\п" соответственно. Он полезен в режиме поглощения файлов, о котором говорится во введении к главе 8 "Содержимое срайлов" и рецепте 6.6. При наличии модификатора /е правая часть выполняется как программный код, и затем полученное значение используется в качестве заменяющей строки. Например, подстановка s/(\d+)/sprintf("%#x", $1)/ge преобразует все числа в шестнадцатеричную систему счисления - скажем, 2581 превращается в ОхЬ23. В разных странах существуют разные понятия об алфавите, поэтому стандарт POSIX предоставляет в распоряжение систем (а следовательно, и программ) стандартные средства для представления алфавитов, упорядочения наборов символов и т. д. Директива Perl use locale предоставляет доступ к некоторым из них; дополнительную информацию можно найти в странице руководства perllocale. При действующей директиве use locale в символьный класс \w попадают символы с диакритическими знаками и прочая экзотика. Служебные символы изменения регистра \u, \U, \1 и \1_ (а также соответствующие функции uc, ucfirst и т. д.) также учитывают use locale, поэтому \u превратит ст в ?, если этого потребует локальный контекст. Специальные переменные В результате некоторых операций поиска по шаблону Perl устанавливает значения специальных переменных. Так, переменные $1, $2, $3 и т. д. до бесконечности (Perl не останавливается на $9) устанавливаются в том случае, если шаблон содержит обратные ссылки (то есть часть шаблона заключена в скобки). Каждая открывающая скобка, встречающаяся в шаблоне слева направо, начинает заполнение новой переменной. Переменная $+ содержит значение последней обратной ссылки для последнего успешного поиска. Это помогает узнать, какой из альтернативных вариантов поиска был обнаружен (например, при обнаруженном совпадении для /(x,*y)|(y.*z)/B переменной $+ будет находиться содержимое $1 или $2 - в зависимости от того, какая из этих неременных была заполнена). Переменная $& содержит полный текст совпадения при последнем успешном поиске. В переменных $' и $' хранятся строки соответственно до и после совпадения при успешном поиске:
$string = "And little lambs eat ivy";
$string =~ /1["s]"s/;
print "($o) ($&) ($')\n";
(And ) (little lambs) ( eat ivy)


Переменные $', $& и $' соблазнительны, но опасны. Само их присутствие в любом месте программы замедляет поиск по шаблону, поскольку механизм должен присваивать им значения при каждом поиске. Сказанное справедливо даже в том случае, если вы всего один раз используете лишь одну из этих переменных, - или даже если они совсем не используются, а лишь встречаются в программе. В версии 5.005 переменная $& перестала обходиться так дорого. После всего сказанного возникает впечатление, что шаблоны могут все. Как ни странно, это не так .(во всяком случае, не совсем так). Регулярные выражения в принципе не способны решить некоторые задачи. В этом случае на помощь при- ходят специальные модули. Скажем, регулярные выражения не обладают средствами для работы со сбалансированным вводом, то есть любыми данными произвольной вложенности - например, парными скобками, тегами HTML и т. д. Для таких целей приходится строить настоящий анализатор наподобие HTML::Parser из рецептов главы 20 "Автоматизация в Web". Еще одна задача, не решаемая шаблонами Perl, - неформальный поиск. В рецепте 6.13 показано, как она решается с помощью специального модуля.

6.1, Копирование с подстановкой

Проблема

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

Решение

Замените фрагменты вида:
$dst = $src;
$dst =~ s/this/that/;
следующей командой:
($dst = $src) =" s/this/that/;

Комментарий

Иногда подстановка должна выполняться не в исходной строке, а в ее копии, однако вам не хочется делить ее на два этапа. Например: # Выделить базовое имя
($progname = $0) =~ s!" */!!; # Начинать Все Слова С Прописной Буквы
($capword = $word) =~ s/(\w+)/\u\L$1/g;
# /usr/man/manS/foo.1 заменяется на /usr/man/man/catS/foo.1
($catpage = $manpage) =~ s/man(?=\d)/cat/;
Подобная методика работает даже с массивами:
@bindirs = qw( /usr/bin /bin /usr/local/bin );
for (olibdirs = (Sbindirs) { s/bin/lib/ } print "@libdirs\n";
/usr/lib /lib /usr/local/lib
Если подстановка должна выполняться для правой переменной, а в левую заносится результат, следует изменить расположение скобок. Обычно результат подстановки равен либо "" в случае неудачи, либо количеству выполненных замен. Сравните с предыдущими примерами, где в скобки заключалась сама операции присваивания. Например:
($а = $b) =~ s/x/y/g; # Скопировать $b и затем изменить $а
$а = ($b =~ s/x/y/g); # Изменить $b и занести в $ количество подстановок


Смотри также: Раздел "Assignment Operators" perlop(1).

6.2. Идентификация алфавитных символов

Проблема

Требуется узнать, состоит ли строка только из алфавитных символов.

Решение

Наиболее очевидное решение не подходит для общего случая:
if ($var =~ /"[a-za-z]+$/) {
# Только алфавитные символы }
Дело в том, что такой вариант не учитывает локальный контекст пользователя. Если наряду с обычными должны идентифицироваться символы с диакритическими знаками, воспользуйтесь директивой use locale и инвертированным символьным классом:
use locale;
if ($var =- /t\w\d_]+$/) {
print "var is purely alphabetic\n";
}

Комментарий

В Perl понятие "алфавитный символ" тесно связано с локальным контекстом, поэтому нам придется немного схитрить. Регулярное выражение \w совпадает с одним алфавитным или цифровым символом, а также символом подчеркивания. Следовательно, \W не является одним из этих символов. Инвертируемый символьный класс [ "\W\d_] определяет байт, который не является алфавитным символом, цифрой или подчеркиванием. После инвертирования остаются одни алфавитные символы, которые нас и интересуют. В программе это выглядит так:

use locale;
use POSIX 'locale_h'
# На вашем компьютере строка локального контекста может выглядеть иначе unless
(setlocale(LC_ALL, "fr_CA.IS08859-1")) { die "couldn't set locale to French Canadian\n";
}
while () {
chomp;
if (/"["\W\d_]+$/) }
print "$_: alphabetic\n";
} else {
print "$_: line noise\n";
}
}


Смотри также: Описание работы с локальным контекстом в perllocale{\)\ страница руководства /оса/е(3) вашей системы; рецепт 6.12.

6.3. Поиск слов

Проблема

Требуется выделить из строки отдельные слова.

Решение

Хорошенько подумайте, что должно считаться словом и как одно слово отделяется от остальных. Затем напишите регулярное выражение, в котором будут воплощены ваши решения. Например: /\S+/ # Максимальная серия байтов, не являющихся пропусками /[A-Za-z'-]+/ # Максимальная серия букв, апострофов и дефисов

Комментарий

Концепция "слова" зависит от приложения, языка и входного потока, поэтому в Perl не существует встроенного определения слов. Слова приходится собирать вручную из символьных классов и квантификаторов, как это сделано выше. Во втором примере мы пытаемся сделать так, чтобы "shepherd's" и "sheep-sheering" воспринимались как отдельные слова. У большинства реализации имеются ограничения, связанные с вольностями письменного языка. Например, хотя второй шаблон успешно опознает слова "spank'd" и "counter-clockwise", он выдернет "rd" из строки "23rd Psalom". Чтобы повысить точность идентификации слов в строке, можно указать то, что окружает слово. Как правило, указываются метасимволы границ1, а не пропусков: /\b([A-Za-z]+\b/ # Обычно наилучший вариант /\s([A-Za-z]+)\s/ # He работает в конце строки или без знаков препинания В Perl существует метасимвол \w, который совпадает с одним символом, разрешенным в идентификаторах Perl. Однако идентификаторы Perl редко отвечают нашим представлениям о словах - обычно имеется в виду последовательность алфавитно-цифровых символов и подчеркиваний, но не двоеточий с апострофами. Поскольку метасимвол \Ь определяется через \w, он может преподнести сюрпризы при определении границ английских слов (и тем более - слов языка суахили). И все же метасимволы \Ь и \В могут пригодиться. Например, шаблон /\Bis\B/ совпадает со строкой "is" только внутри слова, но не на его границах. Скажем, в "thistle" совпадение будет найдено, а в "vis-a-vis" - нет.

Смотри также: Интерпретация \b, \w и \s в perlre(1) шаблоны для работы со словами из рецепта 6.23.

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

Проблема

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

Решение

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

Комментарий

Во фрагменте из примера 6.1 использованы все четыре способа. Начальный комментарий описывает, для чего предназначено регулярное выражение. Для относительно простых шаблонов ничего больше не потребуется. В сложных шаблонах (вроде приведенного) желательно привести дополнительные комментарии. Пример 6.1. resname #!/usr/bin/perl -p # resname - заменить все имена в стиле "foo.bar.com" во входном потоке Хотя метасимвол \b выше был назван "границей слова", в действительности он определяется как полиция между двумя символами, по одну сторону которой располагается \w, а по другую - \W (в любом порядке). - Примем, перев. и на "foo.bar.com [204.148.40.9]" (или аналогичными) use Socket; # Загрузить inet_addr s{ # ( # Сохранить имя хоста в $1 (?: # Скобки только для группировки (9! [-_] ) # Ни подчеркивание, ни дефис [\w-] + # Компонент имени хоста \. # и точка домена ) + # Повторяется несколько раз [A-Za-z] # Следующий символ должен быть буквой [\w-] + #Завершающая часть домена ) # Конец записи $1 }{ # Заменить следующим: "$1 " . # Исходная часть плюс пробел ( ($addr = gethostbynarne($1)) # Если имеется адрес ? "[" . inet_ntoa($addr) . "]" # отформатировать : "[???]" # иначе пометить как сомнительный o ) }gex; # /g - глобальная замена # /e - выполнение # /x - улучшенное форматирование Для эстетов в этом примере использованы альтернативные ограничители. Когда шаблон поиска или замены растягивается на несколько строк, наличие парных скобок делает его более понятным. Другая частая причина для использования альтернативных ограничителей - присутствие в шаблоне символов / (например, s/\/\//\/..\//g). Альтернативные ограничители упрощают чтение такого шаблона (например, s!//!/. ./!g или s{//}{/. ./}g). При наличии модификатора /x Perl игнорирует большинство пропусков в шаблоне (в символьных классах они учитываются) и интерпретирует символы # и следующий за ними текст как комментарий. Такая возможность весьма полезна, однако у вас могут возникнуть проблемы, если пропуски или символы # являются частью шаблона. В таких случаях снабдите символы префиксом \, как это сделано в следующем примере: s/ # Заменить \# # знак фунта (\w+) # имя переменной \# # еще один знак фунта /${$1}/xg; # значением глобальной переменной Помните: комментарий должен пояснять программу, а не пересказывать ее. Комментарии типа "$i++ # Увеличить $i на 1" станут причиной плохих оценок на курсах программирования или подорвут вашу репутацию среди коллег. Остается модификатор /e, при котором заменяющая строка вычисляется как полноценное выражение Perl, а не как (заключенная в кавычки и интерполированная) строка. Результат выполнения этого кода используется в качестве заменяю- щей строки. Поскольку выражение будет интерпретировано как программный код, оно может содержать комментарии. Это несколько замедляет работу программы, но не так сильно, как может показаться (пока вы не начали писать собственные тесты, желательно представлять себе эффективность тех или иных конструкций). Дело в том, что правая сторона подстановки проверяется и компилируется на стадии компиляции вместе со всей программой. Для простой замены строк это, пожалуй, перебор, но в более сложных случаях работает просто замечательно. Удвоение /е напоминает конструкцию eval "STRING". Это позволит применить лексические переменные вместо глобальных в предыдущем примере с заменой. s/ # Заменить \# # знак фунта (\w+) # имя переменной \й # еще один знак фунта /'$' . $1/хеед; и значением *любой* переменной После подстановки /ее проверьте переменную $@. Она содержит сообщения об ошибках, полученные в результате работы вашего кода, - в отличие от /е, в данном случае код действительно генерируется во время работы программы.

Смотри также: Описание модификатора /х в perlre(1).

6.5. Поиск N-го совпадения

Проблема

Требуется найти не первое, a N-e совпадение шаблона в строке. Допустим, вы хотите узнать, какое слово предшествует третьему экземпляру слова fish: One fish two fish red fish blue fish

Решение

Воспользуйтесь моди4)икатором /g и считайте совпадения в цикле while:
$WANT = 3;
$count = 0;
while (/(\w+)\s+fish\b/gi) { if (++$count - $WANT) {
print "The third fish is a $1 one.\n";
# Предупреждение: не выходите из этого цикла с помощью last
}
}

The third fish is a red one.
Или воспользуйтесь счетчиком и шаблоном следующего вида:
/(?:\w+\s+fish\s+){2}(\w+)\s+fish/i;

Комментарий

Как объяснялось во введении к этой главе, при наличии модификатора /д в скалярном контексте происходит многократный поиск. Его удобно использовать в циклах while - например, для подсчета совпадений в строке:
# Простой вариант с циклом
while $count = 0;
while($string =~ /pat/g) {
$count++; # Или что-нибудь другое }
# То же с завершающим циклом while $count = 0;
$count++ while $string =~ /pat/g;
# С циклом for
for ($count = 0; $string =~ /pat/g; $count++) { }
# Аналогично, но с подсчетом перекрывающихся совпадений $
count++ while $string =~
/(?=pat)/g;
Чтобы найти N-й экземпляр, проще всего завести отдельный счетчик. Когда он достигнет N, сделайте то, что считаете нужным. Аналогичная методика может применяться и для поиска каждого N-го совпадения - в этом случае проверяется кратность счетчика N посредством вычисления остатка при делении. Например, проверка (++$count % 3) == 0 находит каждое третье совпадение. Если вам не хочется брать на себя дополнительные хлопоты, всегда можно извлечь все совпадения и затем выбрать из них то, что вас интересует.
$pond = 'one fish two fish red fish blue fish';
# С применением временного массива

@colors = ($pond =~ /(w+)\s'+fish\b\gi); # Найти все совпадения

$color = $colors[2]; # Выбрать одно,
# интересующее нас
# Без временного массива
$соlоr = ( $pond =~ /(\w+)\s+fish\b/gi )[2]; # Выбрать третий элемент
print "The third fish is the pond is $color.\n";
The third fish in the pond is red.

В другом примере находятся все нечетные совпадения:
$count = 0;
$_ = 'one fish two fish red fish blue fish';
(Sevens = grep {$count++ % 2 == 1} /(\w+)\s+fish\b/gi;
print "Even numbered fish are @evens.\n";
Even numbered fish are two blue.


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

$count = 0;
s{\b ( \w+) (\s+ fish \b) }{
if (++$count ^= 4) { "sushi" . $2;
} else {
$1 . $2;
} }gex;
One fish two fish red fish sushi fish


Задача поиска последнего совпадения также встречается довольно часто. Простейшее решение - пропустить все начало строки. Например, после /. *\b(\w+)\s+ fish\b/ переменная $1 будет содержать слово, предшествующее последнему экземпляру "fish". Другой способ - глобальный поиск в списковом контексте для получения всех совпадений и последующее извлечение нужного элемента этого списка:
$pond = 'one fish two fish red fish blue fish swim here.';
$color = ( $pond =o" /\b(\w+)\s+fish\b/gi )[-1];
print "Last fish is $color.\n";
Last fish is blue.
Если потребуется найти последнее совпадение без применения /g, то же самое можно сделать с отрицательной опережающей проверкой (?! НЕЧТО). Если вас интересует последний экземпляр произвольного шаблона А, вы ищете А, сопровождаемый любым количеством "не-А", до конца строки. Обобщенная конструкция имеет вид А(?! . *А)*$, однако для удобства чтения ее можно разделить:

A # Найти некоторый шаблон
А (?! # При этом не должно находиться
.* # что-то другое
А #
# А
) $ # До конца строки


В результате поиск последнего экземпляра "fish" принимает следующий вид:
$pond = 'one fish two fish red fish blue fish';
if ($pond =~ m{
\b ( \w+) \s+ fish \b (?! .* \b fish \b ) }six ) {
print "Last fish is $1/\n";
} else {
print "Failed!\n";
} Last fish is blue.
Такой подход имеет свои преимущества - он ограничивается одним шаблоном и потому подходит для ситуаций, аналогичных описанной в рецепте 6.17. Впрочем, имеются и недостатки. Он однозначно труднее записывается и воспринимается - впрочем, если общий принцип понятен, все выглядит не так плохо. К тому же это решение медленнее работает - для протестированного набора данных быстродействие снижается примерно в два раза.

Смотри также: Поведение конструкции т//g в скалярном контексте описано в разделе "Regexp Quote-like Operators" perlop(1). Отрицательные опережающие проверки нулевой ширины продемонстрированы в разделе "Regular Expressions" perlre(1).


Назад
Вперед