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

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


 
Как звучит Perl? Не так как звучит стена, об которую люди бьются головой?
 -- Larry Wall

Обзор

В этом месяце мы охватим несколко общих моментов в Perl, рассмотрим путь использования его в Real Life(tm) (Реальной жизни), и окинем быстрым взором механизм, который позволяет вам овладеть мощью O.P.C - Other People's Code (код посторонних людей) :) Modules (модули) позволяют вставлять целые куски уже написанного кода в ваши собственные скрипты, сохраняя часы или даже дни программирования. На этом заканчивается предварительная серия, в надежде, что у вас есть достаточно идей, как на Perl написать несколько начальных скриптов и, может быть, с желанием использовать его в дальнейшем.
 

Быстрая поправка

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

Упражнения

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

Идея была такая: написать скрпт, который читает "/etc/services", подсчитывает UDP и TCP порты и записывает их в раздельные файлы. Здесь решение Тябо (мои комментарии предваряются '###'):


#!/usr/bin/perl -w
### Хорошо сделано; пусть компьютер отлаживает ваш скрипт!

$udp = $tcp = 0;
### Не явл. необходимым: Perl не требует объявления переменных.

# open target files:
open (TCP, ">>tcp.txt") or die "Arghh #1 !";
open (UDP, ">>udp.txt") or die "Arghh #2 !";

### Здесь, мой недочет: в предыдущей статье я показал быстрый hack, в котором
### я использовал похожие словосочетания для строки "die". Вот правильный способ
### использования:
###
### open TCP, ">tcp.txt" or die "Can't open tcp.txt: $!\n";
###
### Переменная '$!' содержит ошибку, возвращаемую системой, и определенно
### должна быть использована; "\n" в конце строки "die" предотвращает
### слипание строк при дальнейшей печати. Так же не правильно применен модификатор ">>" (добавить):
### может случиться более чем один запуск скрипта
### и он сделает добавление (вместо замены) к содержимому файлов
###

# open data source:
open (SERV, "</etc/services") or die "Arghh #3 !";

while( <SERV> ) {
  if (/^ *([^# ]+) +(\d+)\/([tcpud]+)/) {

### У приведенного рег. выражения есть несколько проблем, некоторые не важные
### (не необходимые элементы) и одна критическая: она даже пропускает
### большинство строк в файле "/etc/services". Виновником является ' +',
### который стоит за первой частью выражения: "/etc/services" использует смесь пробелов
### и *tabs* (табуляций) для разделения элементов.

    $name   = $1;
    $port   = $2;
    $tcpudp = $3;
    $tmp = "$name ($port)\n";

### Вышеприведенные выражения лишние; $1, $2, и т.д. будут хранить свои значения
### до следующего успешного совпадения. Удаление всего этого
### и изменение оператора "if", который идет далее на
###
### if ( $3 eq "udp" ) { print UDP "$1 ($2)\n"; $udp++; }
###
### будет работать просто прекрасно.

    if ($tcpudp eq "udp") {
      print UDP $tmp;
      $udp++;
    }

    if ($tcpudp eq "tcp") {
      print TCP $tmp;
      $tcp++;
    }
  }
}

# очень кстати :-) :
for ( qw/SERV TCP UDP/ ) { close $_ or die "can't close $_: $!\n"; }

print "TCP: $tcp, UDP: $udp\n";


Вышеприведенный скрипт насчитал 14 TCP и 11 UDP в моем "/etc/services" (который содержит их 185 и 134 соответственно). Давайте посмотрим, сможем ли мы немного его улучшить:



#!/usr/bin/perl -w

open SRV, "</etc/services" or die "Can't read /etc/services: $!\n";
open TCP, ">tcp.txt"       or die "Can't write tcp.txt: $!\n";
open UDP, ">udp.txt"       or die "Can't write udp.txt: $!\n";

for ( <SRV> ) {
    if ( s=^([^# ]+)(\s+\d+)/tcp.*$=$1$2= ) { print TCP; $tcp++; }
    if ( s=^([^# ]+)(\s+\d+)/udp.*$=$1$2= ) { print UDP; $udp++; }
}

close $_ or die "Failed to close $_: $!\n" for qw/SRV TCP UDP/;

print "TCP: $tcp\t\tUDP: $udp\n";



В цикле "for", где выполняется вся 'настоящая' работа, я сделал следующие совпадения/подстановки:

Стартуем с начала строки,(начало 'наполнения' переменной $1) ищем совпадения с любым символом кроме '#' или пробела и повторенного один или более раз (конец 'наполнения'). (Начинаем 'наполнять' переменную $2) соответствие одному или более пробелам с последующими за ними цифрами (конец), прямой слэш и строка 'tcp' с последующим за ней любым
числом любых символов до конца строки. Заместить найденную строку содержимым переменных $1$2 (которые содержат имя сервиса, пробел и номер порта). Записать результат в файл для TCP и инкрементировать переменную "$tcp".

Повторить для 'udp'.

Учтите, что я использовал символ '=' в качестве разделителя в функции 's///'. '=' не обладает никакой собственной магией, просто я пытался избежать конфликта с символами '\' и '#' которые появляются в составной части шаблона (и которые являются общеупотребительными разделителями), а на ближайшем базаре как раз продавался '='. :) Любая другая буква или символ подошли бы не хуже.
 

Теперь пара простых решений для двух других проблем:

1. Открыть два файла и поменять их содержимое.



#!/usr/bin/perl -w
# Файлы, содержимое которых поменяется друг с другом, названы "a" и "b".

for ( A, B ) { open $_, "<\l$_" or die "Can't open \l$_: $!\n"; }
@a = <A>; @b = <B>;

for ( A, B ) { open $_, ">\l$_" or die "Can't open \l$_: $!\n"; }
print A @b; print B @a;



Достаточно консервативное решение, с использованием базовых инструментов. Второстепенное замечание: я использовал модификатор '\l' для установки имени файла в нижний регистр. Учтите, что повторное открытие файлового дескриптора закрывает его автоматически - вы не должны закрывать дескриптор между различными "open"-ами. Так же не всегда необходимо явное закрытие файла: Perl закроет их для вас по выходу из скрипта (но знайте, что некоторые ОС выдадут сообщение о не закрытых файлах). Попутно, текущая версия Perl (5.6.1) имеет лаконичный механизм, который поможет вам сделать то, что я сделал выше, гораздо более изящно:


...
$FN = "/usr/X11R6/include/X11/Composite.h";
open FN or die "I choked on $FN: $!\n";
# "FN" теперь открывается как файловый дескриптор для "Composite.h".
...


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

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



#!/usr/bin/perl -w
%h = qw/a $$.$$ b a $$.$$ b/;
rename $x, $y while ($x, $y) = each %h


Я создал хэш, используя список имен файлов и временную переменную $$ в Perl, так же как и в shell - это ID текущего поцесса, и "$$.$$" - почти наверняка уникальное имя файла, прошелся по нему циклом командой "each", которая возвращает из кэша пары ключ/значение. Я предполагаю, что вы могли бы назвать это "кольцевое переименование"...
 

2. Прочитать файл "/var/log/messages" и вывести на печать все строки содершащие в себе слова "fail", "terminated/terminating", или " no ". Сделать это независимо от регистра.

Это простое однострочное решение:



perl -wne 'print if /(fail|terminat(ed|ing)| no )/i' /var/log/messages


Здесь есть один интересный мехенизм - "чередование" при нахождении совпадений: это позволяет задав "(abc|def|ghi)", найти строки, содержащие все перечисленные комбинации.
 

Построение быстрых инструментальных средств

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

1)Я взял копию фонетического алфавита с Web и сохранил в виде файла. Я назвал его "phon", а выглядел этот файл как:

Alpha
Bravo
Charlie
Delta
Echo
Foxtrot
Golf
...

2) Затем я ввел следующую команду:



perl -i -wple's/^(.)(.*)$/\t"\l$1" => "$1$2",/' phon


Вуаля! Свершилось чудо. (Ниже представлен результат операции замены). Сейчас файл выглядит похожим на это:

        "a" => "Alpha",
        "b" => "Bravo",
        "c" => "Charlie",
        "d" => "Delta",
        "e" => "Echo",
        "f" => "Foxtrot",
        "g" => "Golf",
        ...

3) Несколько секунд спустя у меня был нужный мне инструмент - скрипт, содержащий только одной функцию и одну структуру данных:



#!/usr/bin/perl -wlp
# Created by Benjamin Okopnik on Sun May 27
13:07:49 2001

s/([a-zA-Z])/$ph{"\l$1"} /g;

BEGIN {
    %ph = (
        "a" => "Alpha",
        "b" => "Bravo",
        "c" => "Charlie",
        "d" => "Delta",
        "e" => "Echo",
        "f" => "Foxtrot",
        "g" => "Golf",
        "h" => "Hotel",
        "i" => "India",
        "j" => "Juliet",
        "k" => "Kilo",
        "l" => "Lima",
        "m" => "Mike",
        "n" => "November",
        "o" => "Oscar",
        "p" => "Papa",
        "q" => "Quebec",
        "r" => "Romeo",
        "s" => "Sierra",
        "t" => "Tango",
        "u" => "Uniform",
        "v" => "Victor",
        "w" => "Whisky",
        "x" => "X-ray",
        "y" => "Yankee",
        "z" => "Zulu",
    );
}



Вышеприведенный скрипт будет обрабатывать либо ввод с клавиатуры, либо файл, заданный как аргумент, и возвращает алфавитное представление текста.

Это один из наиболее частых случаев, когда я использую Perl - построение быстрых инструментальных средств, которые мне нужны, чтобы сделать специфичную работу. Вообще-то другие люди могут по-другому использовать его - TMTOWTDI [1] - но для меня компьютер без Perl является наполовину неиспользуемым. Еще дальше пошла группа Мастеров Perl, которые переписали большинство системных утилит на Perl - см. <http://language.perl.com/ppt/> - и устранили ряд раздражающих причуд в процессе портирования. Это, как я понимаю, мотивировалось тремя главными достоинствами программиста: Ленью, Нетерпением и
Гордостью (если вас это смущает см. для объяснения Camel Book ["Programming Perl, Third Edition"]). Если вы хотите увидеть хорошо написанный Perl код, имеется много гораздо лучших мест. Учтите, что проект еще не закончен, но ряд версий UNIX уже находят ему применение: в Solaris 8 имеется большое число скриптов Perl в качестве системных исполняемых файлов, а выполнение команды

file /sbin/* /usr/bin/* /usr/sbin/*|grep -c perl
покажет, что дистрибутив Debian "potato" имеет в вышеприведенных директориях 82 скрипта Perl.
 

OK, сейчас объяснение двух вышеприведенных функций s///'. Первый, "волшебный" конвертер:

perl -i -wple's/^(.)(.*)$/\t"\l$1" => "$1$2",/' phon

Переключатели "-i", "-w", "-p" и "-e" были объяснены во второй части этой серии; в качестве быстрого обзора - будет редактироваться содержимое файла проходом по нему и обрабатывая каждую строку. Доступен механизм предупреждений, и скрипт будет запущен на исполнение из командной строки. "-l" - допускается обработка концов строк, заключающаяся в добавлении CR к строкам, которые их не имели Замена идет похоже на:

Старт на начале строки (начало сбора данных в переменную $1) соответствует одному символу (конец, начало сбора данных в $2). Собрать любое количество любых символов (конец) до .

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

Напечатать tab, за ним содержимое переменной $1 в нижнем регистре* и в двойных кавычках. Напечатать пробел, стрелку '=>', еще пробел, $1$2 в двойных кавычках, сопровождаемые запятой.

* Это сделано с помощью оператора "\l" 'следующий символ в нижнем регистре'(см. 'Quote and Quote-like Operators' на странице "perlop".)
 

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

s/([a-zA-Z])/$ph{"\l$1"} /g;

Первое, регулярное выражение:

(Начало сбора данных в $1) Совпадение с любым символом в диапазоне 'a-zA-Z' (конец).

Второе, строка подстановки:

Возвратить значение из хэша "%ph", используя "нижнерегистровую" версию содержимого переменной $1 в качестве ключа, затем пробел.
 

Блок BEGIN { ... } делает содержимое хэша "одноразово-используемым", несмотря на то, что скрипт может сделать тысячи проходов. Здесь тот же механизм, что и в Awk, о нем было упомянуто в предыдущей статье. Итак, все, что мы делаем - это используем каждый символ в качестве ключа в "%ph", и выводим на печать значение, ассоциированное с этим ключом.

Хэши - очень полезная структура в Perl, и мы очень ценим его изучение и понимание.
 

Модульное конструирование

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

Представьте программу, которая "идет" в Web, подключается к серверу, добывает данные о погоде для вашего города, либо текущие либо прогноз, выдает результат на экран вашего компьютера. Теперь, представьте, что весь скрипт на Perl будет состоять всего из одной строки.

perl -MGeo::WeatherNOAA -we 'print print_forecast( "Denver", "CO" )'

Вот так. Это вся программа. Как это возможно?

( Учтите, что скрипт не будет работать пока вы не установите на свой компьютер модуль 'Geo::WeatherNOAA'. )

CPAN (Comprehensive Perl Archive Network) является вашим другом. :) Если вы пойдете на <http://cpan.org/>, вы найдете там множество и множество (и МНОЖЕСТВО) модулей, разработанных для выполнения большинства программистских задач, которые вы себе могли бы представить. Захотелось перевести ваш Perl скрипт на азбуку Морзе или Клингона? Как пить дать. Неплохо было бы выдернуть из денежных потоков Deutsche Bank Gruppe информацию, о том как обстоят дела у ваших акций? Легко. Озабочены посылкой текстовых SMS сообщений? Нет проблем! Благодаря модулями - это короткие, легкие задачи, которые могут быть представлены в виде кода за считанные секунды.

Стандартный дистрибутив Perl приходит с множеством модулей (для краткого пояснения того, что мы делаем см. "Standard Modules" in 'perldoc perlmodlib'); один из них - модуль CPAN, который автоматизирует процесс загрузки, распаковки, построения и установки. Чтобы его использовать, просто напечатайте

perl -MCPAN -eshell

и следуйте за указаниями. Ручной процесс, понимание которого вам потребуется в более сложных случаях объяснен на странице "How to install" на CPAN <http://http://cpan.org/modules/INSTALL.html>. Я настоятельно рекомендую прочитать ее. Между прочим, разница между двумя процессами в точности похожа на разницу использования "apt" (Debian) или "rpm" (RedHat), и попыткой установки тарбола вручную: 'CPAN' возьмет все дополнительно необходимые модули для модуля, запрошенного вами, выполнит все тесты и инсталляцию, в то время как проделывание этого вручную может быть до некоторой степени болезненным. Для определения того как использовать CPAN модуль (хотя вышеприведенный синтаксис вы будете использовать 99.9 % времени) просто напечатайте

perldoc CPAN

Полная информация о любом модуле, установленном на вашей машине может быть добыта тем же способом.

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



#!/usr/bin/perl -w

use Finance::Quote;

$q = Finance::Quote->new;
my %stocks = $q->fetch("nyse","LNUX");
print "$k: $v\n" while ($k, $v) = each %stocks;



Вышеприведенная программа (для ее работы вы должны установить модуль "Finance::Quote") рассказывает мне все о положении VA Linux на Нью-Йоркской фондовой бирже. Неплохо для пяти строк кода.

Это пример модуля объектно-ориентированного стиля, этот тип становится общеупотребительным. После указания Perl использовать модуль, мы создаем новый экземпляр объекта из класса "Finance::Quote" и назначаем его переменной $q. Затем мы вызываем метод "fetch" (все методы перечислены в документации модуля) с переменными "nyse" и "LNUX", и распечатываем результат, сохраненный в хэше.

Много модулей так называемого экспортного стиля; они просто обеспечивают вашу программу дополнительными "вставленными" функциями.



#!/usr/bin/perl -w
use LWP::Simple;

$code = mirror( "http://slashdot.org", "slashdot.html" );
print "Slashdot returned a code of $code.\n";



В этом случае, "mirror" - это новая функция, которая "пришла" из модуля LWP::Simple. Довольно очевидно, что программа будет копировать ("mirror") данную страницу в определенный файл, и возвращать код (например '404' для 'RC_NOT_FOUND').
 

Подведение итогов

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

Learning Perl, 3rd Edition (coming out in July)
Randal Schwartz and Tom Phoenix

Programming Perl, 3rd Edition
Larry Wall, Tom Christiansen & Jon Orwant

Perl Coookbook
By Tom Christiansen & Nathan Torkington

Data Munging with Perl
By David Cross

Mastering Algorithms with Perl
By Jon Orwant, Jarkko Hietaniemi & John Macdonald

Mastering Regular Expressions
By Jeffrey E. F. Friedl

Elements of Programming with Perl
by Andrew Johnson
 

Удачи в программировании на Perl и вообще в Linux!
 

Ben Okopnik
perl -we'print reverse split//,"rekcah lreP rehtona tsuJ"'



1. "Есть более чем один способ сделать это" - девиз Perl. Я нашел, что он применим ко всему в UNIX.

Ссылки:

Perl man (доступны на всех системах с
Perl):

perl      - обзор              perlfaq   - Perl FAQ
perltoc   - документация TOC               perldata  - структуры данных
perlsyn   - синтаксис                perlop    - опрераторы/precedence
perlrun   - выполнение             perlfunc  - встроенные функции
perltrap  - ловушки для неосторожных  perlstyle - руководство по стилю

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

Ben Okopnik

Кибер-мастер на все руки, Ben странствуют по миру в его 38' парусной шлюпке, создавая сети и , "взламывая" железо и софт в промежутках между зарабатыванием денег. Он работает с компьютерами со стародавних дней (кто нибудь помнит Elf II?), и не собирается останавливаться в ближайшее время.

 


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

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