Парсинг на Perl
🕛 25.10.2006, 12:53
Основы анализа. 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, как я.