Анализ файлов регистрации событий (log-файлов)

Введение

Зароком динамичного развития и процветания узла является систематический анализ статистики, полученной путем правильной обработки файлов регистрации событий. Во многих случаях генерацию файлов статистики можно доверить существующим бесплатным или коммерческим анализаторам . Некоторые из них имеют "безграничные" возможности для настройки, другие просты и быстры.

Но со временем все чаще возникает желание что-нибудь изменить в используемом анализаторе, добавить некий специфичный для вашего узла отчет. Хорошо, если используемый вами анализатор написан на Перле или к нему прилагался исходный код, но даже в этом случае разобраться в чужом коде, да еще если отсутствуют комментарии, достаточно сложно. Поэтому самое хорошее, это написать свой анализатор.

Конечно писать анализатор с нуля дело неблагодарное, да и ненужное. В свое время я, выбрав самый подходящий, написанный на Перле анализатор (FTPWebLog), стал потихоньку добавлять в него нужные мне функции. Спустя год я наконец созрел для того, чтобы, используя его каркас, переписать его заново так, как мне нужно. А чтобы мои усилия не пропали зря, я решил рассказать об этом и вам. Хочу заранее предупредить, что для адекватного восприятия статьи необходимо иметь как минимум средние знание Перла и хорошее знание принципов функционирования http-сервера.

Что будет уметь анализатор?

Заранее определимся, какие отчеты будет генерировать наш анализатор, т.к. возможное их количество почти бесконечно. Мне (помимо узко специализированных) достаточно следующих отчетов:

Как видите количество реализуемых отчетов велико, а некоторые из них уникальны, во всяком случае я таких не встречал.

Взятый мной за основу анализатор имел некоторые функции, которые я не буду реализовывать, а именно:

С другой стороны я считаю очень полезной функцию сопоставления (aliasing) хостов. Т.е. функцию, благодаря которой в отчетах www.server.ru, server.ru, www.server.mirror1.ru, www.server.mirror2.com будут считаться одним хостом. Также мной будет добавлена возможность выбора критерия сортировки некоторых отчетов.

Принцип построения анализатора

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

Независимость этих частей означает, что при изменении формата файла отчетов нам понадобится изменить только вторую часть. Чтобы изменить способ отображения отчета, достаточно внести изменения в четвертую часть. Правда, чтобы добавить новый отчет, придется вносить добавления в первую, третью и четвертую части, но это не затронет уже существующий код.

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

Чтение файла регистрации событий

Для того, чтобы прочитать файл регистрации событий, необходимо прежде всего определить его формат и местоположение. Создадим константы:

$LogFormat = '%h %l %u %t \"%r\" %s %b \"%{referer}i\"
 \"%{user-agent}i\"';
$LogFile='/usr/local/www/logs/httpd.access';

Описание параметров строки формата файла регистрации содержится в конечном файле, содержащем полный текст анализатора. Для тех, кто не может указать формат вашего файла регистрации, определим переменную

$LogType='common';

которая может принимать два значения: common и combined. С ее помощью можно быстро указать формат вашего файла, не заботясь о установке $LogFormat.

Теперь определим функцию проверки формата и сопоставления номеров полей с их именами и обратно. Она нам необходима для того, чтобы в функции чтения файла не связываться с номерами полей. Это сделано для вышеупомянутой независимости модулей:

sub ParseLogFormat {
  local ($matchitem,$matchpattern,@logelements,$count,
  $identitem,
      %LogFormatRegexPatterns,%LogFormatIdentifiers);

  %LogFormatRegexPatterns = (
    '%b' => '\d+|-',   # Number of bytes sent
    '%h' => '[a-zA-Z0-9-.]+',   # Remote host
    '%f' => '\S+',   # Filename
    '%{referer}i' => '\S*', # Referrer
    '%{user-agent}i' => '[^"]*', # User Agent
    '%i' => '[^"]*', # Header junk
    '%l' => '\S+',   # remote logname (identd 
	if provided)
    '%p' => '\d+',   # Port the request was server to
    '%P' =>  '\d+',   # Process ID of the child that 
	serviced the request
    '%r' => '[^"]*', # First line of request
    '%s' => '\d\d\d', # Status Code
    '%t' => '\[\d\d/\w\w\w/\d\d\d\d:\d\d:\d\d\:\d\d\s
	[+-]?\d\d\d\d]',
    '%T' => '\d', # Time taken to serve the request 
	in seconds
    '%u' => '\S+', # Remote user (authentication)
    '%U' => '\S+', # URL path requested
    '%v' => '[a-zA-Z0-9.-]+', # Server name
  );

  %LogFormatIdentifiers = (
    '%b' => 'bytes',
    '%h' => 'remotehost',   # Remote host
    '%f' => 'filename',   # Filename
    '%{referer}i' => 'referrer', # Referrer
    '%{user-agent}i' => 'useragent', # User Agent
    '%i' => '[^"]*', # Header junk
    '%l' => 'rfc931',   # remote logname 
	(identd if provided)
    '%p' => 'port',   # Port the request was server to
    '%P' =>  'process',   # Process ID 
	of the child that serviced the request
    '%r' => 'request', # First line of request
    '%s' => 'status', # Status Code
    '%t' => 'timedate', # Date/Time stamp in CLF format
    '%T' => 'servicetime', # Time taken to serve 
	the request in seconds
    '%u' => 'authuser', # Remote user (authentication)
    '%U' => 'url', # URL path requested
    '%v' => 'server', # Server name
  );

  # [10/Aug/1996:09:55:25 -0700] - CLF format time

  if ($LogFormat) {
    
  } elsif ($LogType =~ m#^common$#oi) {
    $LogFormat = '%h %l %u %t \"%r\" %s %b';
  } elsif ($LogType =~ m#^combined$#oi) {
    $LogFormat = '%h %l %u %t \"%r\" %s %b \"?%
	{referer}i\"? \"%{user-agent}i\"';
  }
  $MatchFormat="\^$LogFormat";
  # We are going to assume that everything is
   seperated by white space
  (@logelements) = split(/\s+/,$LogFormat);
  for ($count=0;$count <= $#logelements;$count++) {
    $identitem = $logelements[$count];
    $identitem =~ s#[^%}{a-z-]##ogi;
    if (defined($LogFormatRegexPatterns{$identitem})) {
      $matchitem=quotemeta($identitem);
      $matchpattern=$LogFormatRegexPatterns
	  {$identitem};
      $MatchFormat =~ s/$matchitem/\($matchpattern\)/;
      $FieldToName{$count}=$LogFormatIdentifiers
	  {$identitem};
      $NameToField{$LogFormatIdentifiers
	  {$identitem}}=$count;
    }
  }
}

Теперь перейдем к чтению самого файла. Первым делом проверим, указано ли местоположение файла и, если надо, распакуем его.

return if (($LogFile eq '') ||
			 ($LogFile eq '/dev/null'));

local (@LogFields,$Ndomain,$Nrfc931,$Nauthusers,
$Ntimedate,
  $Nrequest,$Nstatus,$Nserver,$Nreferrer,
  $Nuseragent);

$LogFile="$Zcat $LogFile |" if ($LogFile=~m/
(\.gz|\.Z)/o);
die ("Unable to open $LogFile\n$!") if 
(!(open(LOGFILE,$LogFile)));

Далее в цикле будем читать файл. Записи, в которых не определены поля домена, rfc931, имени авторизации, времени, запроса и кода ответа, считаются некорректными:

while(<LOGFILE>) {
  chop;
  (@LogFields) = m/$MatchFormat/o;

  if (!($Domain && $rfc931 && $authuser && $TimeDate 
&& $Request &&  $Status)) {
    $InvalidLogLines++;
    next;
  }

После этого необходимо произвести некоторые действия над полученными полями, например выделить имя файла из запроса, отрезать параметры и якоря, удалить добавку прокси из UserAgent, выполнить запрос к DNS для определения имени хоста и т.п.

Наконец необходимо вызвать функцию обработки данных, чтобы она учла все полученные при чтении поля.

Обработка данных - генерация отчетов

Функция &RecordData, вызываемая после прочтения каждой строчки файла регистрации событий, призвана накапливать данные, используемые при составлении различных отчетов. Именно в ней определяется, что и как мы хотим подсчитывать. Далее мы увидим, как накапливать данные для создания различных отчетов.

Сводный отчет

В сводном отчете необходимо отобразить общий объем трафика, количество запрошенных документов, количество уникальных документов, количество уникальных хостов, количество ненайденных документов, количество некорректных записей.

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

$Today="$Year $MonthToNumber{$Month} $Day";
$Now="$Today $Hour $Minute $Second";
$StartDate=$Now if ($Now lt $StartDate);
$EndDate=$Now if ($Now gt $EndDate);

Подсчитаем общее количество переданных байт и файлов. При этом будем учитывать, что при ответе, со статусом не 200, также передается некое количество байт:

$Bytes+=$RespEstimates{$Status};
$TotalBytesCounter+=$Bytes;
$TotalFilesCounter++;

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

Отчет по дням

В отчет по дням входит общее количество переданных за каждый день файлов и их суммарный объем.

$DayFilesCounter{$Today}++;
$BytesDay{$Today}+=$Bytes;

Отчет по часам

В этот отчет также входит количество файлов переданное за час и их суммарный объем. Этот отчет нас интересует "в среднем за час", а "не всего за весь период".

$HourFilesCounter{$Hour}++;
$BytesHour{$Hour}+=$Bytes;
$HourCounter{"Today $Hour"}++;

Отчет по доменам верхнего уровня

Для формирования этого отчета необходимо выделить верхний домен из полного имени хоста. Остальное не представляет никакой сложности. Данные в них учитываются только, если документ был в действительности возвращен запрашивающему хосту.

($TopDomain)=($Domain=~m#\.(\w+)$#o);
$TopDomain=~tr/A-Z/a-z/;
$TopDomain="Unresolved" if ($TopDomain > 0 ||
 $TopDomain eq '');
if ($Status==200) {
  $TopDomainFilesCounter{$TopDomain}++;
  $TopDomainBytesCounter{$TopDomain}+=$Bytes;
}

Отчеты по хостам

В эту группу входят отчет по наиболее активным хостам, отчет по количеству хостов в день и отчет по всем хостам. Данные в них учитываются только, если документ был в действительности возвращен запрашивающему хосту.

if ($Status==200) {
  $DomainsFilesCounter{$Domain}++;
  $DomainsBytesCounter{$Domain}+=$Bytes;
  $UniqueDomains++ if ($DomainsFilesCounter
  {$Domain}==1);
  if ($LastAccessDomain{$Domain} lt $Now) {
    $LastAccessDomain{$Domain}=$Now;
  }
  $DayDomainsCounter{$Today}{$Domain}++;
}

Отчет по файлам

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

if ($Status==200) {
  $HitsFileCounter{$FileName}++;
  $BytesFileCounter{$FileName}+=$Bytes;
  if ($LastAccessFile{$FileName} lt $Now) {
    $LastAccessFile{$FileName}=$Now;
  }
}

Отчет по рубрикам

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

sub ProcessArchiveSections {
  foreach $file (keys (%HitsFileCounter)) {
    foreach $pattern (keys (%ArchiveSectionPatterns)) {
      $match='';
      ($match) = $file =~ m:$pattern:;
      next if (! $match); # Skip stuff that does not 
	  match an archive section
      $archiveSection = $file;
      if ($ArchiveSectionPatterns{$pattern} 
	  eq '--matched pattern--') {
        $substitute=$match;
      } else {
        $substitute=$ArchiveSectionPatterns{$pattern};
      }
      $archiveSection =~ s:$pattern:$substitute:;
      $ArchiveSectionHitsCounter{$archiveSection} 
	  += $HitsFileCounter{$file};
      $ArchiveSectionBytesCounter{$archiveSection}
	   += $BytesFileCounter{$file};
      if ((! $ArchiveSectionLastAccess
	  {$archiveSection}) || 
          ($ArchiveSectionLastAccess{$archiveSection} 
		  lt $LastAccessFile{$file})) {
        $ArchiveSectionLastAccess{$archiveSection} =
		 $LastAccessFile{$file};
      }
    }
  }
}

Для работы этой функции необходимо определить массив, содержащий шаблоны совпадения, например такой:

%ArchiveSectionPatterns=(
 '^(/[^/]*/).*$',              '--matched pattern--',
 '^.*(\.[Hh][Tt][Mm][Ll]?)$',  ' html files',
 '^.*(\.[Tt][Xx][Tt])$',       ' text files',
 '^.*(\.[Gg][Ii][Ff])$',       ' gif graphic files',
 '^.*(\.[Jj][Pp][Gg])$',       ' jpg graphic files',
 '^(\/)$',                     ' server home page',
);

Здесь код '--matched pattern--' обозначает, что в качестве описания нужно использовать попадающую в (...) подстроку. Именно этой строкой мы добиваемся включения в рубрики каталогов первого уровня. В принципе в качестве рубрики можно определить все, что угодно.

Отчет по всем ненайденным файлам

Для создания отчета по ненайденным файлам необходимо выделить запросы, окончившиеся ошибкой 404:

if ($Status==404) {
  $ErrorHitsFileCounter{$FileName}++;
  if ($LastTryErrorFile{"$FileName $Referrer"} 
  lt $Now) {
    $LastTryErrorFile{"$FileName $Referrer"}=$Now;
  }
}

Отчет по авторизованным пользователям

Пользователь авторизован тогда, когда соответствующее поле строки файла не равно дефису:

if ($Status==200) {
  if ($authuser ne '-') {
    $UserCounter{$authuser}++;
    if ($LastAccessUser{$authuser} lt $Now) {
      $LastAccessUser{$authuser}=$Now;
    }
  }
}

Отчет по ссылкам на узел

В эту группу входят отчет по ссылающимся доменам и отчет по всем ссылкам на узел. Для того, чтобы выделить домен из ссылки мы воспользуемся командой split. Нам также необходимо отсеять все внутренние ссылки из других документов узла. Для этого нам нужно определить константу с именем узла. Нам также неинтересны ссылки с сохраненных страниц.

$SystemName = "webclub.ru";

$Home = $SystemName;
$Home =~s#\.#\\\.#goi;

($Protocol,$Thrash,$TopReferrer,$File)=split
(/\//,$Referrer,4);

if (!($Referrer=~m#$Home#oi) && !($Referrer eq '-')
    && !($Referrer eq '') && !
	($Referrer=~m#^file:#oi)) {
  $ReferrerCounter{$Referrer}++;
  $TopReferrerCounter{$TopReferrer}++;
}

Отчет по запросам в поисковых системах

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

%SearchPatterns=(
  '^http://[\w\.]*?yahoo\.com/bin/query.*?\?p=
  ([%\w\+]*).*$',      'Yahoo',
  '^http://[\w\.]*?yandex\.ru/yandsearch\
  ?.*?&text=([%\w\+]*).*$', 'Yandex',
  '^http://www\.ru/cgi/find_[re]\.cgi\
  ?Str_Find=([%\w\+]*).*$',    'www.ru',
);

Как и в случае с рубриками функция обсчета вызывается один раз после полного прочтения файла регистрации событий для убыстрения выполнения программы:

sub ProcessSearchEngines {
  foreach $referrer (keys (%ReferrerCounter)) {
    foreach $pattern (keys (%SearchPatterns)) {
      $match='';
      ($match) = $referrer =~ m:$pattern:i;
      next if (! $match); # Skip stuff that
	   does not match an archive section
      $MatchReferrer = $referrer;
      $substitute=$match;
      $engine=$SearchPatterns{$pattern};
      $MatchReferrer =~ s:$pattern:$substitute:i;
      $EngineCounter{$engine}{$MatchReferrer} +=
	   $ReferrerCounter{$referrer};
    }
  }
}

Отчет по браузерам

В эту группу входят отчет по популярным браузерам, отчет по версиям браузеров, отчет по операционным системам и отчет по всем браузерам.

Отчет по всем браузерам обсчитывается чрезвычайно просто:

$UserAgentCounter{$UserAgent}++;

Перед тем, как подсчитывать популярные браузеры, необходимо определить шаблоны их аутентификации. Делать это нужно аккуратно, так как очень многие браузеры прикидываются Навигатором. В принципе вы сюда можете добавить любой интересующий вас браузер.

($Browser,$rest)=split(/\//,$UserAgent,2);
$TopBrowser = '';
$TopBrowser = 'Navigator' if ($Browser=~m#Mozilla#o);
$TopBrowser = 'Opera' if ($rest=~m#Opera#o);
$TopBrowser = 'Explorer' if ($rest=~m#MSIE#o);
if ($TopBrowser) {
  $BrowserCounter{$TopBrowser}++;
}

Версии браузеров подсчитываются только для Навигатора и Эксплорера. Именно они интересуют разработчика Веб-страниц.

$ver='';
if ($TopBrowser eq 'Navigator') {
  ($ver,$rest)=split(/ /,$rest,2);
  ($ver)=$ver=~m#^(\d\.)\d.*#o;
}
if ($TopBrowser eq 'Explorer') {
  ($ver)=$rest=~m#^.*;\sMSIE\s(.*)(;.*)+#o;
  ($ver)=$ver=~m#^(\d\.)\d.*#o;
}
if ($ver) {
  $BrowserVerCounter{$ver}++;
}

Отчет по платформам опять же выгоднее подсчитывать в самом конце. Здесь опять используется массив шаблонов для определения операционной системы:

%PlatformPatterns=(
  '(Win)',                                'Windows',
  '(X11|Linux|BSD|SunOS|AIX|IRIX|OSF1)',  'Unix',
  '(Mac)',                                'Macintosh',
  '(OS/2)',                               'OS/2',
  '(Amiga)',                              'Amiga',
);

Функция обработки этих шаблонов выглядит следующим образом:

sub ProcessPlatforms {
  foreach $ua (keys (%UserAgentCounter)) {
    foreach $pattern (keys (%PlatformPatterns)) {
      $match='';
      ($match) = $ua =~ m:$pattern:;
      next if (! $match); # Skip stuff that
	   does not match a platform
      $Platform = $ua;
      $substitute=$PlatformPatterns{$pattern};
      $PlatformCounter{$substitute} += 
	  $UserAgentCounter{$ua};
    }
  }
}

Обратите внимание, что все три приведенные функции обработки массивов шаблонов отличаются друг от друга, т.к. преследуют разные цели.

Вывод результатов

Заключительным этапом работы анализатора является визуализация результатов. Удобнее всего, если анализатор будет генерировать HTML документ, выводя его на стандартный вывод согласно соглашениям Unix. Примечание: конечная версия будет позволять указывать в качестве получателя HTML кода дисковый файл.

Для того, чтобы облагообразить документ, целесообразно ввести три специальных константы:

$BodyTag=<<"EOF";
<body bgcolor="#ffffff" text="#000000">
EOF

# Any extra stuff you want to put in
 the head of the generated HTML
# goes here
$SpecialHead=<<"EOF";
EOF

$SpecialFoorer=<<"EOF";
lt;a href=/index.html>Server Root</a>;
EOF

Теперь можно определить саму функцию вывода скелета документа. Для экономии места здесь приведен пример, выводящий только один отчет, не считая сводного:

sub PrintReport {
  print <<"EOF";
<html>
<head>
<title>Analysis for $SystemName</title>
${SpecialHead}
</head>
${BodyTag}
<a name=index></a>
<h2>Оглавление</h2>
<ul>
EOF

  print "<li><a href=#summary
  >Сводный отчет</a>\n";
  print "<li><a href=#daily
  >Отчет по дням</a>\n";
  print "</ul>\n";

  &PrintSummaryReport;
  &PrintDailyReport;
  
  print <<"EOF";
<br><br>Создано с помощью
<a href=http://www.webclub.ru/repository/scripts/
>WebAnalyzer</a>.
<br>
</body></html>
EOF
}

Вывод отчетов является достаточно скучным занятием, поэтому я приведу примеры только наиболее интересных отчетов, а остальные можно будет посмотреть в листинге.

В большинстве отчетов используется специальная функция, расставляющая запятые в числах для удобства их восприятия:

sub commas { # Insert commas into an integer
  local($_)=@_;
  1 while s/(.*\d)(\d\d\d)/$1,$2/;
  $_;
}

Рассмотрим отчет, выводящий запросы в поисковых системах. Он интересен тем, что использует трехмерный ассоциативный массив и некоторые преобразования данных. Прежде всего мы выводим заголовок. Затем во внешнем цикле "проходимся" по всем добавленным поисковым системам. Обратите внимание, что здесь в массиве присутствуют только те системы, которые были найдены в файле регистрации, а не все те, которые мы описали в массиве шаблонов. Для каждой системы мы выводим список ключевых слов. При этом мы заменяем плюсы на пробелы и "распаковываем" русские буквы:

sub PrintSearchEngineReport {
  print "<a name=searchengine></a><h2>
  Отчет по запросам в поисковых системах</h2>\n";
  foreach $engine (sort keys(%EngineCounter)) {
    $DateHash=\$EngineCounter{$engine};
    print "<h3>$engine</h3>\n";
    print "<pre>\n",
      "Количество  ",
      "Запрос\n\n";
    foreach $words (sort keys(%{$$DateHash})) {
      $value = $EngineCounter{$engine}{$words};
      $pwords=$words;
      $pwords=~s/\b\+/ /g;
      $pwords=~s/%([a-fA-F0-9][a-fA-F0-9])/pack("C",
	   hex($1))/eg;
      printf "%10s  %-s\n",
        $value,
        $pwords;
    }
    print "</pre>\n";
  }
  print "<a href=#index>К оглавлению</a>\n\n";
}

Функция вывода отчета по наиболее популярным браузерам использует функцию расстановки запятых и внешнюю функцию сортировки:

sub ByBrowser {
  $BrowserCounter{$b}<=>$BrowserCounter{$a};
}

sub PrintPopularBrowserReport {
  print "<a name=popularbrowser></a><h2>
  Отчет по популярным браузерам</h2>\n";
  print "<pre>\n",
    "Запросов  ",
    "Процент  ",
    "Браузер\n\n";
  foreach $key (sort ByBrowser keys(%BrowserCounter)) {
    printf "%8s  %7.1f  %-s\n",
      &commas($BrowserCounter{$key}),
      $BrowserPCCounter{$key},
      $key;
  }
  print "</pre>\n";
  print "<a href=#index>К оглавлению</a>\n\n";
}

Все остальные функции вывода отчетов выглядят почти идентично этой, за исключением функции вывода отчета по доменам верхнего уровня. Но она слишком громоздка и, поэтому, остается вам на "факультативное" изучение.

Инициализация

Одним из важных свойств нашего анализатора является возможность гибкой настройки. К основным категориям настройки относятся:

Что же подразумевается под этими грозными фразами?

Управление выводом отчетов

Не всегда и не всем нужны все отчеты, предусмотренные в программе. Для того, чтобы можно было включать или выключать каждый отчет заводятся соответствующие константы:

$PrintSummary = 1;
$PrintDaily = 1;
...
$PrintPlatform = 1;
$PrintFullUserAgent = 1;

Если константа равна единице, соответствующий отчет выводится. Если нет, то целесообразно не только не выводить данный отчет, но и запретить обсчет данных, необходимых только этому отчету, для увеличения производительности. Но при этом нельзя забывать про другие отчеты, использующие эти же данные:

if ($PrintPopularBrowser == 1 &&
 $PrintBrowserVersion == 1) {
  $TopBrowser = '';
  $TopBrowser = 'Navigator' if ($Browser=~m#Mozilla#o);
  $TopBrowser = 'Opera' if ($rest=~m#Opera#o);
  $TopBrowser = 'Explorer' if ($rest=~m#MSIE#o);
  if ($TopBrowser) {
    $BrowserCounter{$TopBrowser}++;
  }
}

Управление подробностью некоторых отчетов

Во многих отчетах необходимо регламентировать, в каком объеме выводить данные. Для этого введем несколько параметров ограничителей:

$TopNDomains=10;
$TopNSections=15;
$TopNFiles=20;
$MinRequestsPerHost=100;
$MinErrorsPerDocument=5;
$MinRequestsPerUser=3;
$MinRefsPerDomain=10;
$MinRefsPerReferrer=5;

Мне таких ограничителей достаточно, хотя можно определять ограничители, которые будут регламентировать количество байт или некое процентное отношение к максимуму или минимуму.

Управление сортировкой строк некоторых отчетов

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

$SortDomains='bytes';
$SortFiles='req';
$SortSections='req';

Теперь в самой функции сортировки проверим у словие и зададим различные критерии:

sub ByFiles {
  if ($SortFiles eq 'req') {
    $HitsFileCounter{$b}<=>$HitsFileCounter{$a};
  } elsif ($SortDomains eq 'bytes') {
    $BytesFileCounter{$b}<=>$BytesFileCounter{$a};
  } else {
    $a<=>$b;
  }
}

Как видно, отключение параметра сортировки приводит к сортировке по имени.

Управление включением данных в отчеты

Данная задача является самой главной, но при этом и самой сложной. Нам необходимо иметь возможность включать/исключать некоторые файлы или группы файлов из рассмотрения. Необходимо также иметь возможность включать/исключать хиты с определенных доменов, например ваши собственные. Для этого задаются константы, содержащие определенные шаблоны:

$TopFileListFilter='(\.xbm$|\.gif$|\.png$|
\.jpg$|^Code \d\d\d)'; 
$ExcludeDomain='(194\.85\.135\.|\.citmgu\.ru|
\.citforum\.ru|\.webclub\.ru)';
$ExcludeRefsTo='(\.gif$|\.jpg$|\.css$|\.class$|
chat\.pl$|/images/)';
#$IncludeOnlyRefsTo='^/~novikov/';
#$IncludeOnlyDomain='(\.webclub\.ru)';

Понятно, что вторая и третья константы и четвертая и пятая константы являются взаимоисключающими.

Эти константы используются в цикле чтения файла:

while() {
  ...
  next if (($IncludeOnlyDomain) &&
   !($Domain=~m#$IncludeOnlyDomain#o));
  next if (($IncludeOnlyRefsTo) &&
   !($FileName=~m#$IncludeOnlyRefsTo#o));
  next if (($ExcludeDomain) &&
  ($Domain=~m#$ExcludeDomain#o));
  next if (($ExcludeRefsTo) && (
  $FileName=~m#$ExcludeRefsTo#o));
  ...
}
Сопоставление имен доменов

Для того, чтобы реализовать сопоставление имен прежде всего необходимо объявить массив, хранящий регулярные выражения, выделяющие имена доменов, которые необходимо заменить. В круглые скобки заключается все то, что при совпадении будет заменено на строку, идущую за регулярным выражением. Например все зеркала AltaVista мы заменяем на имя главного сервера, а ip адрес счетчика Рамблера - на его имя:

%HostAliases=(
  '^http://([\w\.]*?citforum[\.\w*]*\.\w\w\w?)/.*$',  
   'citforum.ru',
  '^http://([\w\.]*?altavista[\.\w*]*\.\w\w\w?)/.*$', 
   'altavista.digital.com',
  '^http://(194\.87\.13\.2)/.*$',            
            'counter.rambler.ru',
  '^http://([\w\.]*?webclub\.ru)/.*$',               
    'www.webclub.ru',
);

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

while() {
  if ($UseHostAliases==1) {
    foreach $pattern (keys (%HostAliases)) {
      $match='';
      ($match) = $Referrer =~ m:$pattern:i;
      next if (! $match);
      $substitute=$match;
      $host=$HostAliases{$pattern};
      $Referrer =~ s:$substitute:$host:i;
    }
  }
}
Полный текст анализатора

Как и положено в заключение хочу привести полный текст анализатора. В нем обобщено все то, что мы только что обсудили и добавлены комментарии к параметрам настройки для удобства дальнейшего использования. Он полностью идентичен варианту, хранящемуся в Кладовой (по состоянию на 7 июля 1998 года), хотя в будущем я могу внести в тот вариант изменения и дополнения. Здесь, также приведен примет создаваемого отчета. Сразу предупрежу, что он сильно сокращен и поэтому не соответствует действительности.

Заключение

Напоследок хочу высказать некоторые общие соображения по поводу описанной здесь программы.

1. Кому-то может показаться, что она бесподобна. Другие решат, что она не идет ни в какое сравнение с той, которую они используют. Моей задачей не было создать уникальный универсальный анализатор, наоборот я постарался показать общие принципы. Даже если вы и не собираетесь писать свой анализатор или использовать этот, всегда полезно знать, как они в принципе обрабатывают данные файлов регистрации. Хочу также заметить, что мне в большинстве случаев (см. пункт 2) хватает всего того, что здесь описано.

2. Как я сказал в самом начале, главной целью наших усилий является создание анализатора, приспособленного к вашему конкретному узлу. Поэтому даже готовый анализатор нужно использовать, как основу для дальнейшего развития. Например я сразу же добавлю в него статистику по запрашиваемым словам в локальной поисковой машине. Ведь понятно, что они у всех разные. Мне также интересна статистика по Конференц-залу: кто какими тематиками интересуется, насколько он активен. Для каждого конкретного узла можно придумать множество таких специфических отчетов. И не обязательно при этом ограничиваться исключительно чтением log-файла, можно совместить это с обращениями к базе данных, для дополнения или облагораживания отчетов, например для вывода реальных имен вместо логинов в предыдущем примере.

3. Текущий пример реализации отображения отчетов годится скорее для средних и малых серверов, т.к. вывод осуществляется на одну страницу, и просматривать такую простыню не совсем удобно. Наибольшие проблемы при этом могут вызывать полные отчеты. Рекомендую их отключать при более чем 200 документах на узле или log-файле объемом более 50Mb. Думаю первым изменением в распространяемой версии будет генерация нескольких страниц с фреймовой структурой, когда в одном маленьком фрейме будет отображаться список отчетов, а в другом фрейме сам выбранный отчет.

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