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

  Автор: © Ben Okopnik
Перевод: © Александр Михайлов.


 

Интернет революция была основана на открытых системах; открытая система - это система, на программное обеспечение которой вы можете взглянуть, коробочка, которую вы можете открыть и "поиграть" с её содержимым. Это не секретные бинарные файлы, программы с обрезанной функциональностью или дай-монету-брат shareware. Если бы все всегда прятали программное обеспечение, у вас бы сейчас не было и сотой доли того полезного программного обеспечения, что есть у вас сейчас.

И у вас бы не было Perl.
   -- Tom Christiansen

Обзор

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

Упражнения

Последний раз, я упомянул написание нескольких скриптов, для практики. Давайте посмотрим на несколько возможных путей, сделать это.

Первым был скрипт, который должен принимать на входе число, и печатать "Hello!" ("Привет!") указанной число раз. Он должен также проверять входное число на наличие "неправильных" (не цифр) знаков. Вот хороший пример, присланный David Zhuwao:

#! /usr/bin/perl -w

#@автор David Zhuwao
#@с Apr/19/'01

print "Enter number of times to loop: ";

#Получим ввод и сохраним его в переменной.
chomp ($input = <>);

# проверим ввод на наличие не-числовых символов.
if ($input !~ m/\D/ && length($input) > 0) {
    for ($i = 0; $i < $input; $i++) {
        print "Hello!\n";
    }
} else {
    print "Non-numeric input.\n";
}

Для начала укажу на пример хорошего стиля программирования : David использовал ключ "-w", чтобы Perl предупреждал его о любых ошибках во время компиляции - это прекрасная привычка. Он также эффективно использовал пустое пространство (пустые линии и табуляции), чтобы сделать код легкочитаемый, а также свободно использовал комментарии. Также, вместо того, чтобы проверять наличие числа (что создало бы проблемы с входными данными вроде "1A"), о проверяет наличие не числовых символов, и не-нулевую длину данных - правильное мышление!

К недочетам (заметьте что не один из них не является серьезной проблемой, просто наблюдения): можно отнести использование оператора соответствия (match), "m//", "m" является обязательным , только если разделитель - это символ отличный от "/". Также цикл Perl "for/foreach" будет более компактным, чем в цикл "for" в стиле C, в то же время продолжая выполнять его функцию.

print "Hello!\n" for 1 .. $input;

Это также сделает "$i" не нужным. Все остальное мелкие недочеты - отличная работа, David!
 

Вот другой путь:

#!/usr/bin/perl -w

print "Please enter a number: ";
chomp ( $a = <> );

print "Hello!\n" x $a if $a =~ /^\d+$/;

В отличии от версии David'а, моя не печатает сообщение об ошибке; она просто возвращается к запросу, если ввод не цифровой. Также, вместо проверки наличия не-цифровых символов, я проверяю строку от её начала до конца на наличие только цифровых символов. Любая из этих методик будет работать. Также, вместо использования явного цикла, я использую оператор "x", который просто повторяет предшествующую ему команду печати "$a" раз.
 

...И, Еще Раз...

Давайте разберем еще один скрипт, второе предложение предыдущего месяца: скрипт, который берет значение часа (0-23) в качестве входных данных и говорит "Good morning", "Dobriy den" :), "Guten Abend" или "Buenas noches" в качестве результата (сдесь я схитрил и использовал только английский, чтобы избежать путаницы.).

#!/usr/bin/perl -w

$_ = <>;

if    ( /^[0-6]$/          )   { print "Good night\n";     }
elsif ( /^[7-9]$|^1[0-2]$/ )   { print "Good morning\n";   }
elsif ( /^1[3-8]$/         )   { print "Good day\n";       }
elsif ( /^19$|^2[0-3]$/    )   { print "Good evening\n";   }
else                           { print "Invalid input!\n"; }

С первого взгляда, этот скрипт кажется достаточно простым - так и есть на самом деле - но он содержит несколько скрытых соображений, о которых я хочу поговорить. Для начала, зачем нам везде проверки на начало и конец строки? Очевидно что мы хотим избежать перепутывания "1" и "12" - но что может быть не так с /1[3-8]/?.

Что может случиться, так это опечатка. Не то, чтобы это имело особое значение в данном случае, но быть параноидально настроенным в отношении проверок - это в целом хорошая идея :). Что если пользователь, пытаясь ввести "14", набрал "114"? Без этих "ограничений", это будет соответствовать "11" - и мы получим неверный ответ.

ОК - почему я не использую численные проверки вместо проверки на совпадение? Я имею ввиду, что, в конце концов, мы работаем всего лишь с числами.... не будет ли так проще и очевиднее? Да, но, что произойдет если мы проводим численные проверки, а пользователь вводит "joe"? Мы получим ошибку вмести с нашим "Invalid input!" ("Некорректные данные!"):

Argument "joe\n" isn't numeric in gt at -e line 5, <> chunk 1.

Это дело хорошего стиля программирования, мы хотим чтобы пользователь видел только вывод сгенерированный нами (или ожидаемый); не должно быть каких либо ошибок, вызванных самой программой. Проверка при помощи регулярного выражения, не будет "удивлена" не числовым вводом; она просто вернет 0 (нет совпадения) и передаст управление на следующий "elsif" или "else", который будет ловушкой для ошибки. Все, что не удовлетворяет одному из первых четырех тестов - это неверный ввод - и мы хотим чтобы об этом было выдано соответствующее сообщение.
 

Работа с файлами

Важная возможность любого языка - это работа с файлами. В Perl, это относительно просто, но есть несколько мест, в которых надо быть осторожными.

# Правильный путь
open FILE, "/etc/passwd" or die "Can't open /etc/password: $!\n";

Вот несколько неправильных или неоднозначных путей сделать это:

# Не проверяет возвращаемый результат
open FILE, "/etc/passwd";

# Игнорирует ошибку возвращаемую возвращаемую командным интерпретатором в переменной '$!'
open FILE, "/etc/passwd" or die "Can't open /etc/password\n";

# Использует "логическое ИЛИ" для проверки - могут возникнуть проблемы с порядком исполнения.
open FILE, "/etc/passwd" || die "Can't open /etc/password: $!\n";

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

# Открыть для записи - любая запись будет перекрывать содержимое файла
open FILE, ">/etc/passwd" or die "Can't open /etc/password: $!\n";

# Открыть для добавления - данные будут добавляться в конец файла
open FILE, ">>/etc/passwd" or die "Can't open /etc/password: $!\n";

# Открыть для записи и чтения
open FILE, "+>/etc/passwd" or die "Can't open /etc/password: $!\n";

# Открыть для чтения и добавления
open FILE, "+>>/etc/passwd" or die "Can't open /etc/password: $!\n";

Создав указатель файла ("FILE", в нашем случае), вы можете использовать его следующим образом:

while ( <FILE> ) {
    print;      # Это будет циклически считывать содержимое файла и печатать каждую линию
}

print FILE, "Эта строка будет записана в файл.\n";

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

open FILE, "</etc/passwd" or die "Can't open /etc/password: $!\n";

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

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

close FILE or die "Can't close FILE: $!\n";

Кстати, эффект функции "die" должен быть очевидным: она печатает указанную строку и выходит из программы.

Не делайте этого, если вы не на последней строке вашей программы:

close;

Это закрывает все указатели файлов ... включая STDIN, STDOUT и STDERR (стандартные потоки), что оставляет вашу программу немой, глухой и слепой. Также, вы не можете указывать множество указателей в одной операции close, так что вам придется вместо этого закрывать их по одиночке:

close Fh1 or die "Can't close Fh1: $!\n";
close Fh2 or die "Can't close Fh2: $!\n";
close Fh3 or die "Can't close Fh3: $!\n";
close Fh4 or die "Can't close Fh4: $!\n";

Вы конечно можете сделать так:

for ( qw/Fh1 Fh2 Fh3 Fh4/ ) { close $_ or die "Can't close $_: $!\n"; }

Вот что такое Perl для вас; Всегда есть более чем один путь сделать что-то ....
 

Использование указателей

Предположим, что у вас есть два файла с финансовой информацией - ставки займов в одном, тип и размер ваших займов в другом - и вы хотите расчитать, сколько процентов вы будете платить, и записать результат в файл. Вот данные:

rates.txt

Дом    9%
Машина     16%
Лодка    19%
Прочие    21%
loans.txt

Chevy   машина     8000
BMW     машина     22000
Scarab  Лодка    150000
Pearson лодка    8000
Пианино   прочие    4000

Итак, давайте приступим:

#!/usr/bin/perl -w

open Rates, "<rates.txt" or die "Can't open rates.txt: $!\n";
open Loans, "<loans.txt" or die "Can't open loans.txt: $!\n";
open Total, ">total.txt" or die "Can't open total.txt: $!\n";

while ( <Rates> ) {
    # Избавимся от символов '%'
    tr/%//d;
    # Расщепим каждую строку в массив
    @rates = split;
    # Создадим хэш (ассоциативный массив) с типами займов в качестве ключей и процентными ставками в качестве значений
    $r{lc $rates[0]} = $rates[1] / 100;
}

while ( <Loans> ) {
    # Расщепим строку в массив
    @loans = split;
    # Напечатаем заем и процентную ставку в указатель "Total"
    # вычислим, умножив полное количество на возвращаемое значение
    #по ключу хэша.
    print Total "$loans[0]\t\t\$", $loans[2] * $r{lc $loans[1]}, "\n";
}

# Закроем указатели файлов - не обязательно, но может оказаться нужным
for ( qw/Rates Loans Total/ ) {
    close $_ or die "Can't close $_: $!\n";
}


Объективно, Perl очень хорош в таких вещах: мы проделали необходимую работу в дюжине строк кода. Комментарии занимают большую часть листинга :).
 

Вот другой пример, который явился результатом моей статьи о procmail ( "Нет спаму!" в LG#62. Оригинальный скрипт "черного списка" который вызывался из Mutt, вытаскивал e-mail спаммера при помощи "formail", затем обрабатывал результат вплоть до конкретного "пользователь@хост" адреса при помощи одно-строчного Perl скрипта. Он принимал все письма со спамом как данные входного канала (pipe). Martin Bock, предложил проделывать все это при помощи Perl; после короткой переписки с ним, я создал следующий скрипт, основанный на его идеи:

#!/usr/bin/perl -wln
# Ключ '-n' заставляет скрипт считывать входные данные по 1й строке за раз. time--
# для каждой строки исполняется весь скрипт;
# ключ '-l' включает обработку строк, которая добавляет символ возврата каретки к
# строкам выводимым на печать.

# Если строка соответствует регулярному выражению, тогда...
if ( s/^From: .*?(\w\S+@\S+\w).*/$1/ ) {
    # Открываем "черный список" с указателем файла "OUT" в режиме добавления
    open OUT, ">>$ENV{HOME}/.mutt/blacklist" or die "Aargh: $!\n";
    # Печатаем $_ в этот указатель файла
    print OUT;
    # Закрываем
    close OUT or die "Aargh: $!\n";
    # Выходим из цикла
    last;
}


Оператор замены, в первой строке, записан не идеально - я могу написать некоторые достаточно "накрученные" e-mail адреса, которые не будут правильно обработаны - но это работает хорошо с вариантами вроде
[email protected]
<[email protected]>
[email protected] (Joe Blow)
Joe Blow <[email protected]>
[ The artist formerly known as squiggle ] <[email protected]>
(Joe) [email protected] ["Wildman"]

Чтобы "расшифровать", что означает регулярное выражение, проконсультируйтесь с man-страницей "perlre". Это не так сложно. :) Подсказка: ищите слово "greed" что понять что значит ".*?", и слово "capture" чтобы понять конструкцию "(...) / $1". Оба из них представляют очень важные концепции, и оба были упомянуты в этой серии статей.

Вот более компактная (и поэтому гораздо менее читаемая) версии приведенного выше фрагмента; заметьте, что механизм кое в чем различен:

#!/usr/bin/perl -wln
BEGIN { open OUT, ">>$ENV{HOME}/.mutt/blacklist" or die "Aargh: $!\n"; }
if ( s/^From: .*?(\w\S+@\S+\w).*/$1/ ) { print OUT; close OUT; last; }

Блок BEGIN на первой строке скрипта запускается только 1 раз в ходе исполнения скрипта, несмотря на то, что скрипт циклически исполняется много раз; это очень похоже на аналогичную конструкцию в Awk.
 

В следующий раз

В следующем месяце, мы рассмотрим несколько отличных способов избавить себя от лишней работы используя модули: полезный код, который другие люди написали для Comprehensive Perl Archive Network (CPAN). Мы также посмотрим, как можно использовать Perl для GGI, Common Gateway Interface - механизм, который может "рубить лес и носить воду" за кулисами Web. А до того времени вот несколько вещей, с которыми можно "поиграть":

Напишите скрипт, который открывает "/etc/services" и подсчитывает сколько портов поддерживает UDP, и сколько TCP. Запишите названия сервисов в файлы называемые "udp.txt" и "tcp.txt", и выведите все на экран.

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

Считайте "/var/log/messages" и выведите число строк, которые содержат слово "fail", "terminated/terminating", или " no ". Сделайте его
регистро-независимым.
 

А сейчас -

perl -we 'print "Увидимся в следующем месяце!"'
 

Ben Okopnik
perl -we'print reverse split//,"rekcah lreP rehtona tsuJ"'
Ссылки:

Man страницы Perl (доступные в любой pro-Perl-y (правильно) сконфигурированной
системе):

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

"perldoc", "perldoc -q" и "perldoc -f"

Ben Okopnik

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

 


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

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