Der letzten Monat vorgestellte Cookie-Tracker ([1]) hat einen Monat lang fleißig Daten produziert -- Zeit für die graphische Aufbereitung!
Nochmal schnell zur Wiederholung: Der Cookie-Tracker schreibt jeden Request in der Form
1999/05/31 14:42:47 F 129.13.216.41 92818696712249 928186967 100 \ http://perlmeister.com/ Mozilla/4.5 [en] (X11; I; Linux 2.0.36 i586)
in die Logdatei. Steht dort nach dem Datum und der Uhrzeit
ein ``F'' für ``First'', sandte
also der Browser kein Cookie an den Server, war er entweder noch nie
auf perlmeister.com
zu Besuch oder unterstüzt das Cookie-Protokoll
nicht, sei es, daß der Benutzer es manuell deaktivierte -- oder mit einem
Steinzeit-Browser surft.
Für die heutige Analyse der Daten sollen nur die Einträge mit dem ``R'' zählen, denn nur die kommen sicher von Benutzern, die sich identifizieren ließen.
Die rohen Benutzerzahlen, die mir mein Webseitenbetreiber in Form von aufbereiteten Logfiles liefert, kenne ich schon lange -- aber heute wird tiefer gebohrt:
Abbildung 1 zeigt die Lösung: Täglich kommen zwischen 0
und 8
Stammkunden wieder, die Balkengraphik oben zeigt die
Verteilung über die letzten vier Wochen. Die X-Achse zeigt
die Wochentage, über die Y-Achse sind die Benutzerzahlen
aufgetragen. Die Werte fallen natürlich viel niedriger aus
als die Hit-Zahlen
aus der Server-Logdatei glauben machten -- aber immerhin!
Die fliegende Untertasse direkt darunter stellt eine dreidimensionale
Tortengrafik dar, die anzeigt, wieviele URLs der durchschnittliche
Benutzer sich so zu Gemüte führt. Etwa 25
% lassen's mit einer
gut sein, weitere 25
% klicken noch eins weiter, 15%
schaffen drei
und so fort bis es schließlich einen kleinen
Prozentsatz Unersättlicher gibt, die es auf 10 URLs brachten.
Unten in Abbildung 1 kann man ablesen, wieviel Benutzer einmal im
Monat vorbeischauen: Ungefähr 300
. Auf zweimal im Monat bringen's
ungefähr 25
, selbst drei- und viermal kommen manche angedackelt --
obwohl ich meine Seiten im Schnitt nur einmal im
Monat auf den neuesten Stand bringe -- hätt' ich nur mehr Zeit, eieiei!
Wie ist es möglich, staubtrockene Logfiles in aussagekräftige Statistiken zu verwandeln? Na, kommt mal her, und laßt's Euch erzählen, von Eurem Perl-Onkel ...
Das in [2] vorgestellte Chart-Paket von David Bonner könnte freilich
die graphische Aufbereitung der Daten mühelos übernehmen, rein der
Abwechslung halber kommt heute jedoch GIFgraph
von
Martien Verbruggen dran, das sich ebenfalls auf Lincoln Steins
GD
-Bibliothek abstützt. Wie's installiert wird, zeigt der Abschnitt
``Installation'' am Ende.
Zunächst geht's ans Einlesen der Logdatei: Listing Logdata.pm
zeigt
eine Perl-Klasse, die mittels der
Methode new
eine Logdatei öffnet, zeilenweise durch sie durchgeht,
jeden Eintrag in seine Einzelfelder zerlegt, und ihn nur dann
indiziert in den Objekt-Speicher übernimmt, falls er nicht länger
als eine eingestellte Zeitspanne $ndays
zurückliegt.
use Logdata; $log = Logdata->new("track.dat", 30);
im Hauptprogramm liest also alle Records aus der Logdatei track.dat
ein, die nicht älter als 30
Tage sind. Hierzu ruft new
jedesmal die add
-Methode mit den Einzelfeldern als Argumenten
auf. add
erzeugt aus den Feld-Daten, falls es sich um einen
R
-Eintrag handelt, eine Unterstruktur in Form eines anonymen
Hashs, der die Feldnamen auf die Feldwerte abbildet
("day" => "1999/05/31", "time" => "14:42:47", ...
)
und hängt eine Referenz darauf in zwei Listen ein, die ganz speziell
aufgehängt werden, damit man sie später leicht wiederfindet:
Einmal unter die Instanzvariable $self->{ids}
, die auf einen
Hash verweist, der als Keys IDs (Cookie-Werte) von Benutzern und als
Values jeweils eine Liste mit deren Log-Ereignissen enthält.
Der Hash ermöglicht es also, daß new()
Schritt für Schritt durch
die Einträge
der Logdatei marschieren kann, um sie jeweils so abzuspeichern,
daß man sie später
einfach unter der richtigen ID findet. Es reicht, im Hash den zur
angegebenen ID passenden Eintrag zu finden, die gefundene Listenreferenz
zu dereferenzieren -- schon stehen alle Zugriffe dieses Benutzers
zur Verfügung.
Die zweite Liste, in die der Hit-Eintrag wandert,
hängt unter einem Hash, der für jeden Tag einen Datumseintrag enthält,
an dem ein Benutzer, der vorher schon mal da war, zurückgekehrt ist.
Die add
-Methode nutzt dazu in den Zeilen 47-50 zwei Hilfs-Hashs,
seen_first
und seen_today
.
seen_first
enthält für jeden Benutzer den Tag, an dem dieser mit
seiner ID zuerst im perlmeister.com
-Universum
auftauchte. seen_today
reiht als Keys jeweils
Datum und Benutzer-ID hintereinander, und stellt mittels eines Zählers
fest, ob der gleiche Benutzer heute schonmal da war.
Hängt also ein Eintrag unterhalb $self->{odays}->{$day}
, gehört er
einem alten Bekannten, der an diesem Tag zurückkehrte -- wenn
wir später über die Einträge dieses Hashs iterieren, kommt
für jeden Tag eine Liste mit Hits früherer Besucher heraus, die den Weg
zurückgefunden haben, alle doppelten Einträge wurden eliminiert.
Diese Umstrukturierungen machen den Weg frei für die graphische Aufbereitung.
Das CGI-Skript stats.pl
nutzt die Dienste von Logdata.pm
, aber statt
sauber über Zugriffsfunktionen auf die Instanzvariablen des Logdata
-Objekts
zuzugreifen, stochert es direkt darin herum. Schließlich befinden
wir uns in Perl-Land -- wo alles erlaubt ist, was Spaß macht.
Wird stat.pl
, das CGI-Skript, das die Graphen erzeugt,
ohne CGI-Parameter aufgerufen, wie dies üblicherweise
mit einem Web-Browser geschieht, springt es in den ersten if
-Block
ab Zeile 15, gibt einen HTTP-Header für ein HTML-Dokument aus
und nutzt die praktischen Funktionen von Lincoln Steins
Modul CGI.pm
, um drei Überschriften mit den zugehörigen Bildern
auszugeben. Die URLs der Bilder verweisen wiederum auf stat.pl
, diesmal
mit gesetztem CGI-Parameter graph
, der die Werte returns
, urls
und customers
annimmt. Ein Aufruf von stat.pl
gibt also das
HTML-Dokument aus und generiert drei weitere Aufrufe von stat.pl
, die
jeweils die entsprechenden Graphiken dynamisch erzeugen.
Die Zeilen 11 und 12 in stat.pl
definieren zwei globale Variablen --
die Anzahl der zu untersuchenden Tage und den Namen der auszulesenden
Logdatei.
Falls der CGI-Parameter graph
gesetzt wurde, kommt der else
-Zweig
ab Zeile 26 zum Zug, der einen HTTP-Header für ein GIF-Bild ausgibt,
die Logdatei einliest und die oben erklärte Datenstruktur im Objekt
$log
erzeugt.
Für den returns
-Graphen muß stats.pl
zunächst eine Liste
mit Checkpoints generieren, für jeden dargestellten Tag eine
kleine Zweier-Liste, die als erstes Element das Datum im Format
YYYY/MM/DD
und als zweites den Wochentag als zweibuchstabige
deutsche Abkürzung (Mo, Di, Mi ...) enthalten. Statt dicke
Keulen wie die Datumsmanipulationswerkzeuge Date::Calc
oder Date::Manip
zu schwingen, springt stats.pl
einfach tageweise in die Vergangenheit und
nutzt Perls localtime
-Funktion, die die Unix-übliche
Sekunden-seit-1970-Zeit mühelos in ein von Menschen lesbares
Format umrechnet. Der map
-Befehl in Zeile 43 liefert aus allen
Unterlisten jeweils das zweite Element, weist also @days
eine Liste
mit deutschen Wochentagsabkürzungen zu. Der in Zeile 45 nachfolgende
map
-Befehl hingegen holt die jeweils ersten Elemente
(im Format YYYY/MM/DD
), greift damit in den von Logdata.pm
erzeugten
Hash mit Datumsangaben (Instanzvariable odays
) von Zugriffen wiederkehrender
Benutzer und ermittelt die Anzahl der Benutzer mit dem scalar
-Befehl,
der eine Liste in den skalaren Kontext stellt und deswegen ihre Länge
zurückliefert.
Zeile 48 erzeugt eine neue Säulengraphik, Zeile 50 setzt die Beschriftung der X- und Y-Achsen und Zeile 53 gibt ein aus den beiden übergebenen Listen generiertes GIF-Bild aus.
Der ab Zeile 56 erzeugte urls
-Graph zeigt an, wieviele Benutzer
wieviele URLs anforderten. Hierzu klappert stats.pl
die nach Benutzer-ID sortierten
Log-Einträge in $log->{ids}
ab, und nutzt den alten Bauerntrick aus der
Perl-FAQ, mittels eines Hashs %urls
doppelte Einträge aus einer Liste
zu eliminieren, denn schließlich zählt jeder URL nur einmal
pro Benutzer. grep
liefert eine Liste zurück, deren Länge (wegen des skalaren Kontexts)
$nofurls
zugewiesen wird -- die Anzahl unterschiedlicher URLs für einen
bestimmten Benutzer. Der Hash %users_with_n_urls
ordnet dann User-IDs den
URL-Zahlen zu, die Schleife ab Zeile 64 modelt diese Daten in die
beiden Listen um, die die Daten für die Kuchengraphik
enthalten.
Der customers
-Graph schließlich gibt an, wieviele Benutzer an
wievielen Tagen im Monat aktiv sind -- wieder halten die nach der
Benutzer-ID sortierten Log-Daten her, diesmal fliegen doppelte
Datumseinträge für einen Benutzer über das grep
-Konstrukt raus.
$nofudates
gibt jeweils an, an wievielen unterschiedlichen Tagen
der jeweilige Benutzer zu Besuch da war, der Hash %ids_per_udates
ordnet Tageszahlen Benutzerzahlen zu, gibt also an, wieviel Benutzer
es jeweils gibt, die einmal, zweimal, n-mal da waren. Da die Graphenfunktion
wieder zwei Listen für die X- und Y-Achsen erwartet, dröselt auch hier
wieder eine foreach
-Schleife den Hash in zwei Listen @days
und @customers
auf, und wie immer schreibt die plot
-Methode
das Ergebnis als rohe GIF-Daten.
Abb.1: Der Browser zeigt die Statistik an. |
GIFgraph
gibt's auf dem CPAN, die praktische CPAN-Shell lädt's herunter
und installiert's:
perl -MCPAN -eshell > install GIFgraph
In report.pl
muß noch der Pfad zur Logdatei angepaßt werden, dann
muß es ins CGI-Verzeichnis des Webservers wandern und Ausführungsrechte
erhalten. Bleibt die Variable $LOG_FILE
wie im Listing angegeben,
dann muß track.dat
, die Logdatei des letztens vorgestellten Cookie-Trackers,
mit nach cgi-bin
, damit stats.pl
sie einlesen kann.
Ich hoffe, Ihr seid von der vielen Häscher- und Listerei jetzt nicht vollständig plemplem im Kopf -- denn das Jonglieren mit Logdaten kann ganz schön ans Eingemachte gehen, wenn man Schlüsse jenseits des alltäglichen Logfile-Einerleis ziehen will. Bis zum nächstenmal -- habt Spaß und erholt Euch gut!
01 ################################################## 02 # 99/06/14 mschilli@perlmeister.com 03 ################################################## 04 package Logdata; 05 06 sub new { 07 ################################################## 08 my ($class, $filename, $ndays) = @_; 09 10 my $startpoint = time() - $ndays * 24 * 3600; 11 12 my $self = { ids => {}, odays => {} }; 13 14 bless $self, $class; 15 16 open DATA, "<$filename" or return undef; 17 while(<DATA>) { 18 chop; 19 @fields = split(' ', $_); 20 21 use Time::Local; 22 my($y, $mo, $d, $h, $m, $s) = (split(m#[/ :]#, $_, 6)); 23 my $time = timelocal($s,$m,$h,$d,$mo-1,$y); 24 25 if($time > $startpoint) { 26 $self->add(@fields); 27 } 28 } 29 close(DATA); 30 31 $self; 32 } 33 34 ################################################## 35 sub add { 36 ################################################## 37 my ($self, $day, $time, $stat, $ip, $id, $secs, 38 $ver, $url, $browser) = @_; 39 40 my $hit = { day => $day, 'time' => $time, 41 stat => $stat, ip => $ip, 42 id => $id, secs => $secs, 43 ver => $ver, url => $url, 44 ua => $ua }; 45 46 if($stat eq 'R') { 47 push(@{$self->{ids}->{$id}}, $hit); 48 49 $self->{seen_first}->{$id} ||= $day; 50 51 if( $self->{seen_first}->{$id} ne $day && 52 ! $self->{seen_today}->{"$day$id"}++) { 53 push(@{$self->{odays}->{$day}}, $hit); 54 } 55 } 56 } 57 58 1;
01 #!/usr/bin/perl -w 02 ################################################## 03 # mschilli@perlmeister.com, 1999 04 ################################################## 05 06 use Logdata; 07 use CGI qw/:standard/; 08 use GIFgraph::pie; 09 use GIFgraph::bars; 10 11 my $NOF_DAYS = 30; 12 my $LOG_FILE = "track.dat"; 13 14 if(!param('graph')) { 15 print header, 16 start_html('-title' => "Cookie Statistik"), 17 p("Die letzten $NOF_DAYS Tage ..."), 18 h1("Wiederkehrende Kunden"), 19 img({src => self_url . "?graph=returns"}), 20 h1("URLs pro Kunde"), 21 img({src => self_url . "?graph=urls"}), 22 h1("Benutzerverteilung über aktive Tage"), 23 img({src => self_url . "?graph=customers"}), 24 ; 25 } else { 26 print header(-type => "image/gif"); 27 my $log = Logdata->new($LOG_FILE, $NOF_DAYS) or 28 die "Cannot read logfile"; 29 30 if(param('graph') eq "returns") { 31 my $TODAY = time(); 32 my @wdays = qw/So Mo Di Mi Do Fr Sa/; 33 my @chkpts = (); 34 my $offset; 35 36 foreach $offset (0..$NOF_DAYS-1) { 37 my @L = localtime $TODAY-$offset*24*3600; 38 my $date = sprintf "%d/%02d/%02d", 39 1900+$L[5], $L[4]+1, $L[3]; 40 unshift @chkpts, [$date, $wdays[$L[6]]]; 41 } 42 43 my @days = map { $_->[1] } @chkpts; 44 my @returners = 45 map { scalar @{ $log->{odays}->{$_->[0]}} } 46 @chkpts; 47 48 my $graph = GIFgraph::bars->new(); 49 50 $graph->set(x_label => 'Wochentage', 51 y_label => 'Benutzer', 52 'title' => ''); 53 print $graph->plot([\@days, \@returners] ); 54 55 } elsif(param('graph') eq "urls") { 56 foreach $id (keys %{$log->{ids}}) { 57 58 my %urls = (); 59 $nofurls = grep { ! $urls{$_->{url}}++ } 60 @{$log->{ids}->{$id}}; 61 62 $users_with_n_urls{$nofurls}++; 63 } 64 foreach $n (keys %users_with_n_urls) { 65 push(@xvals, $n); 66 push(@yvals, $users_with_n_urls{$n}); 67 } 68 69 my $graph = GIFgraph::pie->new(); 70 71 print $graph->plot([\@xvals, \@yvals] ); 72 73 } elsif(param('graph') eq "customers") { 74 foreach my $id (keys %{$log->{ids}}) { 75 my %uniq = (); 76 my $nofudates = 77 scalar grep { ! $uniq{$_->{day}}++ } 78 @{$log->{ids}->{$id}}; 79 $ids_per_udates{$nofudates}++; 80 } 81 82 foreach $udates (sort {$a <=> $b} 83 keys %ids_per_udates) { 84 push(@days, $udates); 85 push(@customers, $ids_per_udates{$udates}); 86 } 87 88 my $graph = GIFgraph::bars->new(); 89 90 $graph->set(x_label => 'Aktive Tage', 91 y_label => 'Benutzer'); 92 93 print $graph->plot([\@days, \@customers] ); 94 } 95 }
Chart
-Paket), Michael Schilli -- Linux-Magazin 12/97,
http://www.linux-magazin.de/ausgabe.1997.12/Chart/chart.html
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |