Ключевые слова:perl, parser, (найти похожие документы)
From: Иван Рак <rak at tut.by>
Newsgroups: http://rak.at.tut.by
Date: Mon, 20 Sep 2004 18:21:07 +0000 (UTC)
Subject: Парсинг на Perl
Оригинал: http://rak.at.tut.by/col01r.txt
Unix Review Column 1 (Март 1995) Basic Parsing
Randal L. Schwartz
http://www.stonehenge.com/merlyn/UnixReview/col01.html
Перевод: Иван Рак ([email protected])
Основы анализа.
Perl быстро становится ключевым инструментом обычного системного
администратора и волшебной шляпой системного программиста.
Легко, однако, испугаться 211 страниц документации, которая прилагается
к последнему (пятому) релизу Perl. Быть может, вы уже спрашиваете себя
"с чего начинать?" и "сколько всего надо знать, чтобы писать программы
на Perl?"
Легче всего - посмотреть, как кто-то другой решает простую проблему.
Возьмем для примера типичную задачу системного администрирования -
присвоение новому пользователю уникального ID номера. Для этого нужно
определить наибольший из имеющихся в вашей системе ID, и выбрать
следующее большее число.
Разберем подвернувшуюся нам задачу на простые задачи и их решения. [?
We'll build up to the task at hand by looking at some simpler problems
and their solutions.]
Во-первых, посмотрим, как распечатывается первая колонка вывода команды
who, [just for grins].
who | perl -ne '@F = split; print "$F[0]\n";'
Вывод who передается на ввод Perl. Ключ -n позволяет выполнить некоторый
код, помещая каждую входящую строку в переменную $_. Ключ -e задает код,
и мы можем (и часто будем) совмещать ключи показанным образом.
В нашем случае вы имеем два выражения: операции split и print. split
разбивает содержимое $_ на список слов (подразумевая разделителем между
словами пробел). Результат получает массив @F.
Затем операция [? operation. операция - плохо, потому что похоже на
оператор, а это функция] print отображает значение первого элемента
масства, завершенное переводом строки (\n). Заметим, что доступ к
первому элементу @F происходит через $F[0], потому что элементы
нумеруются, начиная от нуля (как в массивах C).
Можно немного сэкономить на наборе, если вынести разделение в аргументы
командной строки:
who | perl -ane 'print "$F[0]\n"'
Заметим, что здесь мы добавили ключ -a, который заставляет Perl
разбивать содержимое $_ в @F автоматически, так же, как в предыдущем
примере мы сделали это явно.
Чтобы набирать еще чуть меньше, можно добавить ключ -l, который делает
две вещи сразу:
- удаляет перевод строки из переменной $_ перед тем, как ее увидит наш
код (на самом деле его (код) не волнует, есть ли он (перевод) там (в
строке)), и
- приклеивает перевод строки обратно на выходе.
После этого наш маленький командно-строковый пример будет выглядеть так:
who | perl -lane 'print $F[0]'
И, чтобы сократить еще чуть-чуть, заменим ключ -n ключом -p, который
позволяет печатать то, что получилось в $_ в конце кода:
who | perl -lape '$_ = $F[0]'
Да, действительно, мы выиграли только один символ. Но это все таки один
символ, и, может, это даст большие сбережения, если вы будете экономить
по символу каждый день в ближайшие пять лет. Может и нет.
Скрипт, эквивалентный предыдущему вызову Perl, будет выглядеть примерно так:
#!/usr/bin/perl
$\ = $/; # from -l
while (<>) { # from -p
chop; # from -l
@F = split; # from -a
$_ = $F[0]; # argument to -e
print; # from -p
}
Как вы видите, немаленький кусок кода [] можно задать несколькими
символами в командной строке.
Переменная $\ определяет заканчивающий суффикс для каждой операции
print, примерно так, как это делает переменная ORS а awk. По умолчанию
она пуста, то есть выводимая строка будет выглядеть так, как задано.
Здесь мы придаем этой переменной значение $/, разделителя входящих
записей (как RS в awk). По умолчанию это "\n". То есть разделитель
вывода такой же, как разделитель ввода, и к печатаемому будет
добавляться перевод строки.
Закончим, наконец, с командой who. Перейдем к реальной задаче: проход по
файлу паролей для получения наибольшего пользовательского ID.
Файл паролей отличается от вывода команды who - здесь колонки
разделяются не пробелами, а двоеточием. Нет проблем - укажем другой
символ-разделитель:
perl -aF: -lne 'print $F[0]' /etc/passwd
и мы получим список пользователей на стандартном выводе. Ключ -F задает
двоеточие как разделитель. Заметим, что мы поставили ключ -a перед -F,
что, я думаю, вполне логично -- разделитель полей не имееет смысла, если
их не разделять.
Если у вас запущены Желтые Страницы [Yellow Pages], то есть, я хотел
сказать, Network Information Services, вам, возможно, понадобится
вытягивать пароли отсюда, а не из файла, чтобы получить что-то полезное:
ypcat passwd | perl -aF: -lne 'print $F[0]'
Здесь команда ypcat выдает пароле-подобный файл на стандартный вывод,
где команда Perl радостно его слизывает, как если бы это был локальный
файл etc/password.
Но это имена пользователей, не пользовательские ID. Они в третьем
столбце, в $F[2] (опять же, сдвинуто на один, потому что отсчет
начинается с нуля). Немного подредактируем, и:
perl -aF: -lne 'print $F[2]' /etc/passwd
Теперь у нас есть список чисел. Уже лучше. Нам нужно определить
наибольшее число, и распечатать еще большее.
Для этого используем скалярную переменную $max. Изначально $max не
определена, и при сравнении с другими числами будет выглядеть как ноль.
Итак, работа состоит в том, чтобы сравнить номер каждого пользователя с
$max, и присвоить $max этот номер, если он больше.
perl -aF: -lne '$max = $F[2] if $max < $F[2]; print $max' /etc/passwd
Здесь мы присваиваем $max значение, если выполняется условие. В данном
случае условие
$max < $F[2]
вычисляется на каждой итерации цикла, и, если результат истина,
происходит присваивание. Это единственное место в Perl, где логическая
последовательность идет справа налево, а не слева направо.
Теперь это все становится неудобно длинным, так что лучше развернуть в
скрипт:
#!/usr/bin/perl
$\ = $/;
while (<>) {
chop;
@F = split /:/;
$max = $F[2] if $max < $F[2];
print $max;
}
Еще лучше. Однако нам все еще нужно скормить скрипту /etc/passwd, что
несколько обременительно для вызывающего. Так что откроем файл
/etc/passwd прямо в программе.
#!/usr/bin/perl
open(PASSWD,"/etc/passwd");
$\ = $/;
while (<PASSWD>) {
chop;
@F = split /:/;
$max = $F[2] if $max < $F[2];
print $max;
}
open() создает дескриптор [? filehandle. глупо, на самом деле,
переводить handle как "дескриптор"] для чтения файла /etc/passwd.
Ваше, желтостраничники [YP'ers], решение будет на пару символов
длиннеее:
#!/usr/bin/perl
open(PASSWD,"ypcat passwd|");
$\ = $/;
while (<PASSWD>) {
chop;
@F = split /:/;
$max = $F[2] if $max < $F[2];
print $max;
}
Perl чУдно использует вывод команды как файл. О том, что это команда, а
не файл, свидетельствует завершающая вертикальная черта. Это напоминание
о потоке [? pipe. не уверен.], который используется, когда мы пишем
программу в командной строке.
На выходе этих последних программ будет серия чисел с наибольшим
найденным пользовательский ID. На самом деле нам нужно распечатать самый
последний номер. Нет, еще раз. На самом деле нам нужен номер, больший на
единицу. Как это сделать в программе? Просто. Просто вынесем печать из
цикла:
#!/usr/bin/perl
open(PASSWD,"/etc/passwd"); # or YP equivalent
$\ = $/;
while (<PASSWD>) {
chop;
@F = split /:/;
$max = $F[2] if $max < $F[2];
}
print $max + 1;
Не забудьте + 1, чтобы получить больше, чем прошлое наибольшее.
Whew! Мы можем набить этот скрипт в файл, преобразовать в исполнимый
код, поместить его где-нибудь в $PATH, и, когда нам нужен новый номер
пользователя, просто вызовем его в обратных кавычках [уточнить], и
получим правильное значение.
Или что-то около правильного номера. Как оказалось, некоторые системы
(например, SunOS, на которой я это тестировал), имеют пользователя
nobody, с очень-очень большим ID. Если вы запустите эту программу в
своей системе и получите что-то вроде 65535, у вас такой тоже есть.
Так что нам нужно исключить из нашего подсчета все, что выше какого-то
порога. Как же это сделать?
Допустим, $max не нужно устанавливать, если $F[2] превышает наш порог
(скажем, 30000). Что делает if чуть более сложным:
#!/usr/bin/perl
open(PASSWD,"/etc/passwd"); # or YP equivalent
$\ = $/;
while (<PASSWD>) {
chop;
@F = split /:/;
$max = $F[2] if $F[2] < 30000 and $max < $F[2];
}
print $max + 1;
На этом можно остановиться (надеюсь). Во всяком случае, в SunOS
работает.
В конце концов задачка оказалась не совсем крошечной, но, по крайней
мере, мы уложились в дюжину строк кода. Если длина командных строк вас
не пугает, мы можем предложить такой вариант:
perl -aF: -lne '$m=$F[2] if $F[2]<30000 and $m<$F[2];
END { print $m+1 }' /etc/passwd
Интересный момент: блок выражений END выносится за пределы
подразумеваемого цикла, туда, куда мы поставили его в развернутом
скрипте.
Если вы не знакомы с Perl, возможно, вам пригодится хорошая книга. Я
могу порекомендовать две, хотя я несколько пристрастен, потому что
причастен к написанию обеих.
Learning Perl (O'Reilly and Associates, ISBN 1-56592-042-2) - нежное
введение [? gentle introduction] в язык, с примерами и развернутыми
ответами. Это книга для тех, кто "знаком с UNIX, но никак не гуру". Но
требует некотрого знания снов программирования.
Programming Perl (O'Reilly and Associates, ISBN 0-937175-64-1) -
здоровенный всесторонний справочник по языку, в соавторстве с создателем
Perl, Ларри Уоллом. Здесь вы найдете немного поверхностной обучающей
информации, и массу длинных практических примеров. Однако это скорее
книга для гуру, и может пролететь мимо головы, если вы на хакаете UNIX с
1977, как я.
Еще есть очень интересная Usenet-группа comp.lang.perl, с большим
участием волшебников Perl, включая Ларри Уолла (и вашего покорного
слугу). Если у вас нет доступа к Usenet, пошлите е-мэйл на
[email protected], и попросите включить вас в список
рассылки.