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

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

Введение

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

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


Поиск по шаблону даже в упрощенном виде не похож на обычные строковые сравнения. Он больше похож на поиск строк с применением универсальных символов-мутантов, к тому же накачанных допингом. Без специального "якоря" позиция, в которой ищется совпад 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", зате Итак, выясняется, что шаблон находит совпадения там, где они не нужны, и не узнает то, что действительно нужно. Придется усовершенствовать его. Например, для поиска последовательности 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 Правильный ответ - последний, поскольку первая точка, в которой встречается ноль и более экземпляров "о", находится прямо в начале строки. Удивлены? С регулярными выражениями это бывает довольно часто. А теперь попробуйте угадать, как будет выглядеть результат при добавлении модификатора /д, который делает подстановку глобальной? Строка содержит много мест, в которых встречается ноль и более экземпляров "о", - точнее, восемь. Итак, правильный ответ - "e Приведем другой пример, в котором жадность уступает место торопливости:
% 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, используют регулярные выражения и возвращают либо логическое "да/не Последняя и самая интересная из трех особенностей - возврат. Чтобы шаблон совпал, должно совпасть все регулярное выражение, а не лишь его отдельная часть. Следовательно, если начало шаблона с квантификатором совпадает, а одна из последующих частей шаблона Модификаторы Модификаторы, используемые при поиске по шаблону, намного проще перечисли 11. и понять, чем другие метасимволы. Ниже приведена краткая сводка:

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

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

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


В списковом контексте /g извлекает все совпадения в массив:
@numbers = m/(\d+)/g;
vВ этом случае будут найдены только неперекрывающиеся совпадения. Для поиска перекрывающихся совпадений придется идти на хитрость - организовать опережающую проверку нулевой ширины с помощью конструкции (?=...). Раз ширина равна нулю, механизм поиска вооб Продемонстрируем отличия на примере:
$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 преобразует все числа в шестнадцатеричную систему счислени В разных странах существуют разные понятия об алфавите, поэтому стандарт POSIX предоставляет в распоряжение систем (а следовательно, и программ) стандартные средства для представления алфавитов, упорядочения наборов символов и т. д. Директива Perl use loc дополнительную информацию можно найти в странице руководства perllocale. При действующей директиве use locale в символьный класс \w попадают символы с диакритическими знаками и прочая экзотика. Служебные символы изменения регистра \u, \U, \1 и \1_ (а такж Специальные переменные В результате некоторых операций поиска по шаблону Perl устанавливает значения специальных переменных. Так, переменные $1, $2, $3 и т. д. до бесконечности (Perl не останавливается на $9) устанавливаются в том случае, если шаблон содержит обратные ссылки (т
$string = "And little lambs eat ivy";
$string =~ /1["s]"s/;
print "($o) ($&) ($')\n";
(And ) (little lambs) ( eat ivy)


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

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 не является одним

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 не существует встроенного определения слов. Слова приходится собирать вручную из символьных классов и квантификаторов, как это сделано выше. Во втором примере мы пытаемся сде У большинства реализации имеются ограничения, связанные с вольностями письменного языка. Например, хотя второй шаблон успешно опознает слова "spank'd" и "counter-clockwise", он выдернет "rd" из строки "23rd Psalom". Чтобы повысить точность идентификации слов в строке, можно указать то, что окружает слово. Как правило, указываются метасимволы границ1, а не пропусков: /\b([A-Za-z]+\b/ # Обычно наилучший вариант /\s([A-Za-z]+)\s/ # He работает в конце строки или без знаков препинания В Perl существует метасимвол \w, который совпадает с одним символом, разрешенным в идентификаторах Perl. Однако идентификаторы Perl редко отвечают нашим представлениям о словах - обычно имеется в виду последовательность алфавитно-цифровых символов и подче И все же метасимволы \Ь и \В могут пригодиться. Например, шаблон /\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 - улучшенное форматирование Для эстетов в этом примере использованы альтернативные ограничители. Когда шаблон поиска или замены растягивается на несколько строк, наличие парных скобок делает его более понятным. Другая частая причина для использования альтернативных ограничителей - п При наличии модификатора /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 посредст Если вам не хочется брать на себя дополнительные хлопоты, всегда можно извлечь все совпадения и затем выбрать из них то, что вас интересует.
$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.


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

$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).


© copyright 2000 Soft group
Используются технологии uCoz