ЭЛЕКТРОННАЯ БИБЛИОТЕКА КОАПП |
Сборники Художественной, Технической, Справочной, Английской, Нормативной, Исторической, и др. литературы. |
Поиск по шаблонуВведениеВ большинстве современных языков программирования существуют примитивные средства поиска по шаблону (обычно вынесенные в дополнительные библиотеки), но шаблоны Perl интегрируются на уровне самого языка. Они обладают возможностями, которыми не могут похвастаться другие языки; возможностями, которые позволяют взглянуть на данные с принципиально новой точки зрения. Подобно тому, как шахматист воспринимает расположение фигур на доске как некий образ, адепты Perl рассматривают данные с позиций шаблонов. Шаблоны записываются на языке регулярных выражений1, богатом знаками препинания, и позволяют работать с замечательными алгоритмами, обычно доступными лишь экспертам в области компьютерных технологий. "Если поиск по шаблону - такая потрясающая и мощная штука, - спросите вы, - то почему же эта глава не содержит сотни рецептов по применению регулярных выражений?" Да, регулярные выражения обеспечивают естественное решение многих проблем, связанных с числами, строками, датами, Web-документами, почтовыми адресами и буквально всем, что встречается в этой книге. В других главах поиск по шаблону применяется свыше 100 раз. А в этой главе в основном представлены те рецепты, в которых шаблоны являются частью вопроса, а не ответа. Обширная и тщательно проработанная поддержка регулярных выражений в Perl означает, что в вашем распоряжении оказываются не только те средства, которые не встречаются ни в одном другом языке, но и принципиально новые возможности их использования. Программисты, недавно познакомившиеся с Perl, часто ищут в нем функции поиска и подстановки: 1 Точнее, регулярные выражения в классическом смысле не содержат обратных ссылок, присутствующих в шаблонах Perl.match( $строка, $шаблон); subst( $строка, $шаблон, $замена); Однако поиск и подстановка - настолько распространенные задачи, что они заслуживают собственного синтаксиса: $meadow =" m/sheep/; # Истинно, если $meadow содержит "sheep"
Поиск по шаблону даже в упрощенном виде не похож на обычные строковые
сравнения. Он больше похож на поиск строк с применением универсальных символов-мутантов,
к тому же накачанных допингом. Без специального "якоря" позиция, в которой
ищется совпадение, свободно перемещается по всей строке. Допустим, если
вы захотите найти слово 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 шаблон должен выглядеть примерно так:
/i Игнорировать регистр (с учетом национальных алфавитов).
Наиболее распространены модификаторы /i и /д. Шаблон /ram/i совпадает со строками " ram", "RAM", "Ram" и т. д. При наличии этого модификатора обратные ссылки проверяются без учета регистра (пример приведен в рецепте 6.16). При вызове директивы use locale в сравнениях будет учитываться состояние текущих локальных настроек. В текущей реализации модификатор /i замедляет поиск по шаблону, поскольку подавляет некоторые оптимизации скорости. Модификатор /д используется с s/// для замены всех найденных совпадений, а не только первого. Кроме того, /д используется с т// в циклах поиска (но не замены!) всех совпадений: while (m/(\d+)/g) {
В списковом контексте /g извлекает все совпадения в массив:
Модификаторы /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 -
в зависимости от того, какая из этих неременных была заполнена). Переменная
$& содержит полный текст совпадения при последнем успешном поиске.
В переменных $' и $' хранятся строки соответственно до и после совпадения
при успешном поиске:
Переменные $', $& и $' соблазнительны, но опасны. Само их присутствие в любом месте программы замедляет поиск по шаблону, поскольку механизм должен присваивать им значения при каждом поиске. Сказанное справедливо даже в том случае, если вы всего один раз используете лишь одну из этих переменных, - или даже если они совсем не используются, а лишь встречаются в программе. В версии 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 и занести в $ количество подстановок > Смотри также ----------------------------
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;
> Смотри также -------
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]; # Выбрать одно,
В другом примере находятся все нечетные совпадения:
При подстановке заменяющая строка должна представлять собой программное выражение, которое возвращает соответствующую строку. Не забывайте возвращать оригинал как заменяющую строку в том случае, если замена не нужна. В следующем примере мы ищем четвертый экземпляр "fish" и заменяем предшествующее слово другим: $count = 0;
Задача поиска последнего совпадения также встречается довольно часто.
Простейшее решение - пропустить все начало строки. Например, после /. *\b(\w+)\s+
fish\b/ переменная $1 будет содержать слово, предшествующее последнему
экземпляру "fish". Другой способ - глобальный поиск в списковом контексте
для получения всех совпадений и последующее извлечение нужного элемента
этого списка:
A # Найти некоторый шаблон
В результате поиск последнего экземпляра "fish" принимает следующий
вид:
> Смотри также -------------------------------
|