Изучаем Perl, часть 2

  Автор: © Ben Okopnik
Перевод: © Владимир Меренков.


 

"И тут я понял, что существовала большая экологическая ниша между языком C и UNIX-shell. С был хорош для манипулирования сложными комплексными проектами - назовем это "манипулексией". А shell-ы идеально подходили для для быстрого решения повседневных проблем - я называю это 'whipupitude' (от англ. whip up - быстро решать проблему). Но была обширная пустая область, где ни C, ни shell не были столь хороши, и вот туда-то я и нацелил Perl."
 -- Larry Wall, автор Perl

Обзор

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

Механизм действия кавычек

Большинство из вас будут на "ты" со стандартным механизмом кавычек в Unix: одинарные и двойные кавычки, о которых я упомянул в предыдущей статье, имеют такую же функциональность в Perl, как и в shell. Хотя иногда экранирование всех метасимволов в строке может быть немного затруднительным. Представьте попытку напечатать строку похожую на эту:

``/// Don't say "shan't," "can't," or "won't." ///''

Вот не было печали! Что мы можем сделать с такой неприятностью?

Ну, мы могли бы всунуть целую связку "экранов" ("\"), но это будет болезненно - как в случае с ССИДДФР (Синдром склонности использования дубины для филигранной работы):

print '\`\`\/\/\/ Don\'t...

Бр-р-р.. Очевидно - это не есть хороший ответ. Для случаев, похожих на этот, Perl предлагает альтернативный механизм кавычек:

q//        # Одиночные кавычки
qq//       # Двойные кавычки
qx//       # Обратные кавычки для команд shell
qw//       # Список слов - полезен при начальной загрузке массивов

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

print q-``/// Don't say "shan't," "can't," or "won't." ///''-;

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

Вызов Perl

"Услышь мои мольбы, о Perl, Великий и Мудрейший!" Ладно, не берите в голову; я думаю, такие реплики были стандартом при работе с Perl3, но теперь они обесценились... :)

Наиболее часто используемый ключ при вызове Perl, если вы вызываете его из командной строки - это '-e'; он говорит Perl, что нужно выполнить команды расположенные сразу после него. Фактически, '-e' должен быть последним переключателем, используемым в командной строке, потому что все, что после него, рассматривается как часть скрипта!

perl -we 'print "Боги начали этот трэд в конференции, чтобы родился Web.\n"'

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

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

perl -wne 'print if /holiday/' schedule.txt

Perl "пройдет" через весь "schedule.txt" и напечатает все строки, которые содержат слово "holiday". Будьте осторожны: так вы можете впасть в депрессию, узнав о том, как мало праздников у вас намечается.

"-p" вызов "цикла печатания", действует точно как "-n" за исключением того, что печатает каждую строку, которую обрабатывает Perl. Это очень полезно для "sed"-подобных операций, например модификация файла и вывод его (мы обсудим 's///', оператор замены, только вскользь):

perl -wpe 's/holiday/Party time!/' schedule.txt

Эта команда выполнит замену первого вхождения слова 'holiday' на 'Party time!' в каждой строке файла schedule.txt (см. "perldoc perlre" для рассмотрения модификаторов, используемых с 's///', таких как 'g'- глобальный.)

"-i" - этот переключатель хорошо работает в комбинации с любым из вышеупомянутых, в зависимости от желаемого действия; это позволяет выполнять "замещающее" редактирование, т.е. делать изменения в определенном файле (при желании можно сохранить копию, содержащую первоначальные данные), а не печатать его на экране. Учтите, что мы не можем просто добавить "i" в строку "wpe": требуется необязательный параметр - раcширение, которое будет добавлено к файлу, содержащему резервную копию обрабатываемого файла.

perl -i~ -wpe 's/holiday/Party time!/' schedule.txt

Эта строка произведет файл файл "schedule.txt", содержащий модифицированный текст, и "schedule.txt~", содержащий первоначальные данные. "-i" без всякого расширения перепишет оригинальный файл; это гораздо более удобно, чем создание измененного файла и переименование его, но при этом будьте уверены в правильности вашего кода или вы попрощаетесь с вашими первоначальными данными!
 
 

Регулярные выражения или "Опять по моей клавиатуре кошка гуляла?"

Одним из мощнейших инструментальных средств, доступных в Perl, является регулярное выражение - это способ выразить соответствие почти любой символьной последовательности. Здесь (по необходимости) я обьясню только самые основы; если вам покажется, что вы нуждаетесь в большем количестве информации, копайте в "perlre" manpage, которая поставляется вместе с Perl. Это займет вас на некоторое время. :)

RE (regular expressions = регулярные выражения) используются для сопоставления с шаблоном, обычно, в операторах "m//" (соответствие) и "s///" (замена). Учтите, что разделители в них, точно также как и в правилах кавычек, не ограничены только символом '/'; фактически, первая 'm' в операторе сопоставления требуется только если используется разделитель не определенный по умолчанию. Другими словами, просто "//" - достаточно.

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

.        соответствует любому символу, кроме '/n' -конец строки
^        соответствует началу строки
$        соответствует концу строки
|        альтернатива (или) (соответствует "left|right|up|down|sideways")
*        соответствует 0 или больше раз
+        соответствует 1 или больше раз
?        соответствует 0 или 1 раз
{n}      соответствует точно n раз
{n,}     соответствует по крайней мере n раз
{n,m}    соответствует по крайней мере n но не больше чем m раз
 

Скажем, у нас есть файл со списком имен:

Anne Bonney
Bartholomew Roberts
Charles Bellamy
Diego Grillo
Edward Teach
Francois Gautier
George Watling
Henry Every
Israel Hands
John Derdrake
KuoHsing Yeh
...

и мы хотим заменить первое имя словом 'Captain'. Очевидно, мы должны пройтись по файлу с "циклом печати" и сделать замену соответствие критерию:

s/^.+ /Captain /;

Символ ('^') соответствует началу строки, ".+" говорит "любой символ, поторенный 1 или больше раз", и пробел соответствует пробелу. Как только мы найдем, что ищем, мы заменим это на слово 'Captain' со следующим за ним пробелом - так как исходная строка, которую мы заменяем, содержит пробел, мы должны вернуть его.

Скажем, что мы также знали, что где-то в файле есть пара имен , содержащих апострофы (Francois L'Ollonais), и мы бы хотели пропустить их, а также еще все те, что содержит 'не-буквенные' символы. Давайте немного расширим наше регулярное выражение:

s/^[A-Z][a-z]* /Captain /;

Мы использовали спецификатор "класс символов" - "[]", первому соответствует символ между 'A' и 'Z' - учтите только один символ, очень важное ограничение! - следующему соответствует один символ от 'a' до 'z', и астериск, который говорит опять "ноль и или большее число предшествующих символов".

Оп-ля, подождите! А как насчет "KuoHsing"? На букве 'H' произойдет ошибка сопоставления, так как символы верхнего регистра не включены в определенный диапазон. OK, мы модифицируем рег. выражение:

s/^\w* /Captain /;

'\w' - это "буквенный символ" - еще раз, он соответствует только одному символу, который может быть: 'A-Z', 'a-z', и '_'. Это предпочтительнее чем [A-Za-z_], потому что используется значение $LOCALE (системное значение), чтобы определить какие символы могут являтся частями слов - это важно для языков, отличных от английского. К тому же, '\w' напечатать легче, чем '[A-Za-z_]'.

Давайте попытаемся сделать нечто немножечко другое: Что если мы захотели найти имена, но теперь, вместо того, чтобы заменить их, мы пожелали поменять их местами с фамилиями, разделив запятыми, и перед фамилиями вставить слово 'Captain'? С регулярными выражениями в нашей команде - это не проблема:

s/^(\w*) (\w*)$/Captain $2, $1/;

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

Стартуем с начала строки, (начинаем "наполнять" переменную $1) ищем соответствие любому "буквенному символу", встречающегося ноль или больше раз (конец) и следующий затем пробел, (начинаем "наполнять" переменную $2) следующий затем любой "буквенный символ", повторенный ноль или более раз (конец) пока не достигнем конца строки. Возвращаем слово 'Captain', затем пробел, за которым следует значение переменной $2, запятая, пробел и значение переменной $1.

Я бы сказал, что регулярные выражения очень компактный способ для выражения всего этого. В подобных случаях становится довольно очевидно, что Larry Wall - профессиональный лингвист. :)

Это только простые примеры подхода к построению регулярных выражений. Я должен признаться в небольшом обмане: разбор имен, вероятно, одна из широчайших задач, и я мог бы плести этот пример так долго, как хотел бы. Рассмотрение возможностей включения "John deJongh", "Jan M. van de Geijn", "Kathleen O'Hara-Mears", "Siu Tim Au Yeung", "Nang-Soa-Anee Bongoj Niratpattanasai", и "Mjölby J. de Wærn" (не забудьте об использовании LOCALE соответствий, правильно?), поле довольно широко и очень неоднородно. (Мисс Niratpattanasai, посмотрев на что-нибудь типа "John Smith". вероятно согласилась бы. :)
 

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

Acciones son amores, no besos ni apachurrones

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

/A.*es/

получим следующее соответствие:

Acciones son amores, no besos ni apachurrones
|___________________________________________|

Хммм. Все от первой 'A'(следующими за ней нулем или больше символами) до последней 'es'. Как найти первое же совпадение, а? Чтобы противодействовать жадности, Perl предоставляет модификатор "щедрости" к кванторам типа '*', '+', и '?':

/A.*?es/

Acciones son amores, no besos ni apachurrones
|______|

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

Заданный по умолчанию буфер/переменная

Некоторые из вас, особенно те, кто в прошлом программировал, вероятно были озадачены некоторыми конструкцями в приведенном выше коде, например

print if /holiday/;

"Напечатать что? если что? Где та переменная, которую мы проверяем на соответствие? Разве это не должно выглядеть как-то так 'if $x == /holiday/'?"

Я рад, что вы задали этот вопрос. :)

Perl использует интересную концепцию, найденную также в нескольких других языках, буфера по умолчанию - также известному нам как переменная по умолчанию и default pattern space. Не удивительно, что это используется в конструкциях выполнения цикла, когда мы используем синтаксис "-n/-p" при вызове Perl, это переменная использована для "содержания" текущей строки - так же как при замене и поиска соответствия и в некоторых других местах. Переменная '$_' - является переменной по умолчанию для всего вышеперечисленного; когда переменная не определена там, где вы ожидаете, "виновником" обычно является '$_'. Фактически применение '$_' довольно трудно объяснить - она появляется в таком количстве мест, что алгоритм выглядит невозможным - но само использование чудесно легко и интуитивно понятно, как только у вас появляется идея.

Рассмотрим следующее:

perl -wne 'if ( $_ =~ /Henry/ ) { print $_; }' pirates

Если в строке в файле "pirates" есть соответствие "Henry", то она будет напечатана. Прекрасно; а теперь, давайте сыграем в любителя "Perl Golf" - это конкурс среди хакеров Perl, чтобы посмотреть сколько (ключевых) штрихов можно удалить из этого куска кода и оставить ту же его функциональность.

Так как мы уже знаем, что Perl читает каждую строку в '$_', мы просто удалим все явные объявления этой переменной:

perl -wne 'if ( /Henry/ ) { print; }' pirates

Perl "знает" что мы ищем соответствие в переменной по умолчанию, и он "знает", что оператор "print" применяется к ней же. Теперь применим маленькую Perl идиому:

perl -wne 'print if /Henry/' pirates

Здорово, правда? Perl фактически позволяет вам писать код с условием, следующим за действием; это почти также, как вы бы сказали это по-английски. О, и мы отсекли точку с запятой в конце, потому что она нам не нужна: это разделитель операторов, а никакого следующего оператора нет
"/Henry/".

<grin> Для тех кто играет один дома, попытайтесь разобраться

perl -ne'/Henry/&&print' pirates

Не нужно быть слишком крутым чтобы понять; оператор '&&' в Perl работает также как, он работает в shell. Perl Golf - это забава для игры, но будьте осторожны: легко написать код, который будет работать, но требующий поломать голову, чтоб понять его. Не Делайте Так. Может быть завтра я буду сопровождать ваш код... точно также, как вам, быть может, придется сопровождать мой.
 

В первом примере обратите внимание на "связывающий оператор" '=~', который проверяет на соответствие переменную. Это то , что вы бы использовали, если бы искали соответствие не с "$_", а с другой переменной. Оператор "не-соответствия" - '!~', который возвращает истину, если совпадения не было найдено (инверсный '=~'.)

Учтите также, что доступные операторы для простых выражений, типа упомянутых выше, включают не только "if", но также и "unless", "while", "until", и "for". О них и многом другом в Части 3...
 
 

Ben Okopnik
perl -we '$perl=0;JsP $perl "perl"; $perl->perl(0)'\
 2>&1|perl -ne '{print ((split//)[19,29,20,4,5,1,2,
15,13,14,12,52,5,21,12,52,8,5,14,1,6,37,12,52,75])}'



[1]. Фактически "Мастерство регулярных выражений" by Jeffrey E. Friedl может быть справочником по предмету. Она включает некоторые замечательные примеры, и буквально учит читателя "думать по-регулярному".
 

Ссылки:

Relevant Perl man pages (available on any pro-Perl-y configured system):

perl      - overview              perlfaq   - Perl FAQ
perltoc   - doc TOC               perldata  - data structures
perlsyn   - syntax                perlop    - operators/precedence
perlrun   - execution             perlfunc  - builtin functions
perltrap  - traps for the unwary  perlstyle - style guide

"perldoc", "perldoc -q" and "perldoc -f"

 


Copyright © 2001, Ben Okopnik.
Copying license http://www.linuxgazette.com/copying.html
Published in Issue 64 of Linux Gazette, March 2001

Вернуться на главную страницу