Перевод Anton Petrusevich <casus@mail.ru> и Alex Ott <ott@phtd.tpu.edu.ru>
Сокращение ``PERL'' оригинально замышлялось для обозначения ``Practical Extraction and Report Language (язык практических извлечений и отчетов)''. Хотя Perl расширил свою сферу применения и возможности за последние 10 лет своего существования, но все равно основы получения отчетов созданных из извлеченных данных являются основой большинства проблем программирования на Perl.
Давайте взглянем на типичную проблему: анализ файлов протокола. В наши дни файлы протокола создаются во всех местах. Например, записи входа/выхода в систему, запуск команд, перекачка файлов, почтовые системы, сервера gopher и даже увеличившееся использование серверов web, всё это создает строки и строки и страницы кажется бесконечных потоков данных. Каждая отдельная транзакция не заслуживает исследования (до тех пор, пока вы ищите недавние нарушения безопасности), но сведение этих данных в отчеты является частой задачей.
Давайте взглянем на гипотетический ``файл протокола передачи файлов'', который выглядит примерно так:
fred wilma 08:50 730 barney betty 06:15 190 betty barney 22:27 993 barney wilma 23:47 504 fred wilma 04:29 836 betty betty 14:37 738 wilma barney 18:47 825
и состоит из четырех разделенных пробелами колонок, содержащих исходный сервер, сервер назначения, время в 24-часовом формате и число переданных байт.
Я создаю некоторые данные с помощью небольшой тестовой программы:
my @hosts = qw(fred barney betty wilma); srand; sub randhost { $hosts[rand @hosts]; } for (my $n = 0; $n <= 999; $n++) { printf "%s %s %02d:%02d %d\n", randhost, randhost, rand(24), rand(60), rand(1000); }
которая выдает соответствующие поля. Выравнивается ``999'' для
выравнивания размера вывода* (Эта программа использует синтаксис версии
5.004 с очень гибким расположением объявления my()
... если
при использовании более старых версий Perl 5 вы получаете сообщения об
ошибках, то удалите объявления my()
).
Так что у нас теперь есть относительно неинтересные данные. Давайте посмотрим как создается типичный отчет. Все отчеты будут требовать, чтобы данные были обработаны по колонка, что будет общей частью для каждой из программ обработки данные. Так что давайте решим эту проблему.
Основа программы обработки выглядит примерно так:
while (<>) { my ($from, $to, $hh, $mm, $bytes) = /^(\S+) (\S+) (\d+):(\d+) (\d+)$/ or (warn "bad format on line $.: $_"), next; # accumulate } # print the result here
Если в файле существует несколько форматов записей, то для регулярных выражений для которых произошел сбой, можно было бы попытаться применить другие сочетания. Это полезно в тех случаях, когда существуют разные варианты данных.
Программа лишь забирает данные, но не аккумулирует числа и не выдает результаты. Давайте начнем с чего-нибудь простого: подсчет общего количества переданных байтов и количества заданий передачи данных:
# for accumulate: $total_bytes += $bytes; $jobs++;
что выполняется внутри цикла, а затем вне цикла происходит выдача результатов:
# for print: print "total bytes = $total_bytes, jobs = $jobs\n";
Хорошо, это было не слишком трудно. Давайте попытаемся сделать нечто более интересное. Давайте посмотрим сколько байтов было передано с каждой машины. Мы сделаем это с помощью ассоциативного массива, в котором ключом будет являться имя машины, а соответствующим значением будет общее количество (а также сохраним количества заданий в отдельном ассоциативном массиве):
# for accumulate: $from_bytes{$from} += $bytes; $from_jobs{$from}++;
Теперь для выдачи результатов, нам необходимо пройти по ассоциативному массиву:
# for print: for my $from (sort keys %from_bytes) { my $bytes = $from_bytes{$from}; my $jobs = $from_jobs{$from}; print "$from sent $bytes bytes on $jobs jobs\n"; }
В этом коде получаются ключи %from_bytes
, потом они
сортируются и используются для получения соответствующих значений из
двух ассоциативных массивов.
Что делать, если мы хотим получить общее количество переданных байт и не хотим заботиться о том, в каком направлении они переданы. Для того, чтобы сделать это, я использую хитрый прием добавляя число в два разных места в ассоциативном массиве:
# for accumulate: $total_bytes{$from} += $bytes; $total_bytes{$to} += $bytes;
и теперь проход по ассоциативному массиву %total_bytes
аналогичен вышеприведенному коду:
# for print: for my $host (sort keys %total_bytes) { my $bytes = $total_bytes{$host}; print "$host did $bytes\n"; }
Заметьте, что если мы вычисляем ``общую сумму'' байтов, то значение должно дублироваться, так что в этом случае будьте осторожны. Если вы хотите иметь возможность вычисления ``общей суммы'' этих чисел, то одним из способов решения может быть выделение по половине трафика для каждой из машин:
# for accumulate: $bytes /= 2; # allocation correction $total_bytes{$from} += $bytes; $total_bytes{$to} += $bytes;
В этом случае суммарные данные этой таблицы будут показывать то же, что и суммарные данные других таблиц. ``Как можно обмануть с помощью статистики''.
До настоящего времени, мы выполняли накопление данных основанных на пустых объектах (суммарное количество) и основанных на одном объекте данных (таком как исходный сервер). Можем мы выполнять накопление данных, основываясь на двух или нескольких объектах данных? Конечно, благодаря имеющейся в Perl возможности использования вложенных ассоциативных массивов. Давайте взглянем на двумерную таблицу, показывающую все передачи данных между передающими и получающими данные машинами. Это будет выглядеть примерно так:
# for accumulate: $from_to_bytes{$from}{$to} += $bytes;
Теперь мы отслеживаем передающие и принимающие машины. Хотя выдача данных будет достаточно хитрой. Вот построчная выдача результатов для всех сочетаний передающих и принимающих машин:
# for print: for my $from (sort keys %from_to_bytes) { my $second = $from_to_bytes{$from}; for my $to (sort keys %$second) { my $bytes = $second->{$to}; print "$from to $to did $bytes\n"; } }
Это немного сложно, поскольку мы проходим по всей матрице. Внешний
цикл выполняет проход по всем передающим машинам, помещая ссылки на
внутренние ассоциативные массивы в переменную $second
. Затем с
помощью внутреннего цикла выполняется проход по внутреннему
ассоциативному массиву, выдавая хранящиеся в нем данные.
Вывод этой программы выглядит немного безобразно:
[...] barney to fred did 2792 barney to wilma did 4683 betty to barney did 2333 betty to betty did 2568 [...]
так, что давайте немного улучшим его, выдавая прямоугольную таблицу. Сначала нам необходимо вычислить все возможные названия принимающих машин, для того чтобы создать заголовки колонок:
my %to_hosts = (); for my $from (sort keys %from_to_bytes) { my $second = $from_to_bytes{$from}; my @keys = keys %$second; @to_hosts{@keys} = (); } my @to_hosts = sort keys %to_hosts;
Здесь я создаю временный ассоциативный массив с именем
%to_hosts
, который служит в качестве ``множества''. Для
каждой из передающих машин, я получаю список принимающих машин и
помещаю его в массив @keys
(внутри цикла), а затем добавляю
элементы этого массива в ``множество''. Последнее выражение выделяет
данные из множества и помещает их в массив @to_hosts
, который
я затем использую для выдачи заголовков колонок. Заголовки колонок
выдаются с помощью кода:
printf "%10s:", "bytes to"; for (@to_hosts) { printf " %10s", $_; } print "\n";
а затем выполняется проход по матрице, который немного проще чем в предыдущем примере:
for my $from (sort keys %from_to_bytes) { printf "%10s:", $from; for my $to (@to_hosts) { my $bytes = $from_to_bytes{$from}{$to} || "- none -"; printf " %10s", $bytes; } print "\n"; }
поскольку нам не нужно получать ключи второго ассоциативного
массива... они уже являются частью массива @to_hosts
. Вывод
программы выглядит примерно так:
bytes to: barney betty fred wilma barney: 3303 4429 2792 4683 betty: 2333 2568 3928 1813 fred: 2416 3542 226 5293 wilma: 5267 1196 2706 4580
Вот у вас и имеется программа сокращения некоторые простых данных. Развлекайтесь сокращением данных!