Next Previous Contents

Unix Review Column 15 -- Анализ файлов протокола

Randal Schwartz

Июль 1997

Перевод 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

Вот у вас и имеется программа сокращения некоторые простых данных. Развлекайтесь сокращением данных!


Next Previous Contents