Wohin laufen die Benutzer auf einer Website? Cookies helfen, die Gewohnheiten der Fans zu erforschen und ihre Wege durch den HTML-Dschungel nachzuvollziehen.
Die Log-Datei eines Webservers spricht schon Bände. Geeignete Auswertungsprogramme zeigen nicht nur die Benutzerzahlen an, sondern geben, wenn sie die REFERER-Logs mit einbeziehen, auch Antwort auf detailliertere Fragen wie ``Kommt der typische Besucher durch den Haupteingang oder führen hauptsächlich Browser-Bookmarks zu bestimmten Seiten?''
Aber letztendlich kann kein Server-Log der Welt den Benutzern wirklich auf die Finger schauen, denn falls ein Request eingeht, identifiziert der Server nicht die Person, die hinter einem Browser sitzt, sondern meldet die IP-Adresse des anfragenden Rechners -- und die kann im Falle von Online-Riesen wie AOL Hunderttausenden von Benutzern gehören. Fragen wie ``Wieviele Besucher entdeckten die Website letzten Monat das erste Mal?'' ``Wieviele Stammkunden aus dem vorletzten Monat kamen wieder?'' ``Wieviel Zeit verbringt der durchschnittliche Besucher auf jeder Seite auf dem Weg in die Tiefe?'' erfordern eine personalisierte Überwachung. Cookies, die der Server dem jeweiligen Browser heimlich unterjubelt, welcher sie wiederum beim nächsten Ankoppeln unaufgefordert mitsendet, erlauben es dem Server, wiederkehrende Browser zu erkennen. Ein Browser wird zwar nicht in allen Fällen von nur einer Person bedient (z.B. auf Messeständen), doch für den Löwenanteil der Benutzer einer Website gilt der ``Ein Mann/Frau, ein BrowserInnen''-Grundsatz.
Verlangt ein Browser nach der Eingangsseite
index.html
, sendet der Server kein
Cookie, da es sich um eine statische Datei handelt, die er
ohne nachzudenken einfach rauspustet.
Ein CGI-Skript könnte ein Cookie einschmuggeln, doch es würde wohl
einige Telefonanrufe beim Internet-Provider kosten, die Eingangsseite
einer Website zu dynamisieren und auf ein CGI-Skript umzuleiten
oder einen Server-Filter zu aktivieren.
Es geht auch einfacher: Ein in die Homepage integriertes
unscheinbares Ein-Pixel-GIF-Bild trägt zwar nichts zur graphischen
Gestaltung der Seite bei, übernimmt aber die Cookie-Bombardierung
und schreibt den Vorgang in eine Logdatei, denn der Image-Tag verweist
auf ein CGI-Skript track.pl
, das die Drecksarbeit erledigt und anschließend,
um den Schein zu wahren, ein Pseudo-Bild zurückschickt:
<IMG SRC=/cgi-bin/track.pl>
Mit Lincoln Steins Modul GD
läßt sich das notwendige Dummy-GIF
leicht erzeugen:
#!/usr/bin/perl -w use GD; my $i = new GD::Image(1,1); my $bg = $i->colorAllocate(0xff,0xdc,0xba); print pack('u', $i->gif);
Dieses kurze Skript legt ein neues GIF-Bild in der Farbe
#ffdcba
(die Hintergrundfarbe von perlmeister.com
)
im Speicher an und gibt die
Daten uuencoded aus:
C1TE&.#=A`0`!`(```/_<N@```"P``````0`!```"`D0!`#L`
Um Zeit zu sparen, nimmt das CGI-Skript track.pl
einfach diesen
Buchstabensalat her, und, statt jedesmal GD
einzubinden und
ein Bild zu erzeugen, legt es die Rohdaten mit einem uudecode
nach folgendem Muster frei:
print unpack('u', $data);
Zeile 49 in unserem Cookie-Tracker track.pl
tut genau dies.
Doch von vorne: track.pl
bindet in Zeile 6 Lincoln Steins praktisches CGI-Modul CGI.pm
ein, das die HTML-Ausgaben und die Cookiejongliererei elegant übernimmt.
Die Konstanten in den Zeilen 8 bis 10 definieren die verwendete Cookie-Version (falls sich das Format mal ändern sollte), den Namen des Cookies und den Pfad zur Logdatei.
Zeile 12 testet, ob der Browser dem Request ein Cookie mit dem eingestellten Namen mitgegeben hat. Falls ja, war er offensichtlich schon vorher einmal da und Zeile 14 schreibt den zum nachfolgenden GIF-Bild gehörenden Header, damit der Browser glaubt, es folge lediglich eine belanglose Web-Graphik. Doch in Wirklichkeit analysiert das Skript das ankommende Cookie, das etwa als
VE100 IP205.134.227.213 CT925961787 ID92596178720836
vorliegt und anzeigt, daß es mit Version 1.00 der Tracker-Software
erzeugt wurde, und zwar bei einer Anfrage eines Rechners mit der
IP-Adresse 205.134.227.213
, zu einem Zeitpunkt, der 925961787
Sekunden
nach dem 01.01.1970 lag -- also am 05.05.1999 um 20:36:27
Pacific Standard Time.
Die eindeutige ID des Cookies, mittels derer der Server einen
einmal angedockten Browser beim nächsten Mal wiedererkennt,
ist 92596178720836
.
Zeile 20 prüft rudimentär, ob das Format auch ungefähr stimmt
und schreibt die Daten im Erfolgsfall in die Logdatei weg, wobei
es noch den HTTP_REFERER
, also die Seite, auf der das Pseudo-Bild
liegt, und den HTTP_USER_AGENT
, also den Browsertyp, hinzufügt.
Kommt kein Cookie an, weil der Browser noch nie vorher zu Besuch da
war (oder ein Spielverderber das Cookie abblockt), kommt der
else
-Zweig ab Zeile 26 zum Zug. Dort entsteht das Cookie Schritt
für Schritt. Die eindeutige ID besteht aus der aktuellen
Sekunden-Uhrzeit mit der angehängten Nummer des gerade laufenden
Prozesses ($$).
Zeile 34 sichert den Vorgang in die Logdatei, als erstes Feld
steht diesmal ein F
für first anstatt wie in Zeile 21 ein R
für recur. Geloggt wird in folgendem Format:
1999/05/05 20:36:27 F 205.134.227.213 92596178720836 \ 925961787 100 http://perlmeister.com/index.html \ Mozilla/4.07 [en] (X11; I; Linux 2.0.36 i686)
Der cookie
-Befehl in den Zeilen 38 bis 42 baut ein standardgemäßes
Cookie zusammen und übernimmt praktischerweise die notwendige
URL-Maskierung des Cookie-Wertes. Das Verfallsdatum liegt ein
Jahr in der Zukunft, sodaß der Browser es in seiner Cookie-Truhe
aufbewahrt, falls er ausgeschaltet wird. Abbildung 1 zeigt, wie
es ankommt.
Die header
-Funktion aus dem CGI
-Modul sendet den
Header, der ein nachfolgendes GIF ankündigt und bläst das Cookie
zum Browser.
Damit der Browser das kleine Kontroll-Bildchen nicht etwa im Cache abspeichert
und so beim nächsten Zugriff keinen Track-Impuls mehr gibt, schreibt Zeile
46 (wie auch schon im anderen Fall Zeile 15)
einen Expire-Header, der ein Jahr in der Vergangenheit liegt (-1y
), also
glaubt der Cache stets, daß eine veraltete Ausgabe des Bildes vorliegt
und wird nicht müde, es weiter fleißig anzufordern.
Zeile 49 schließlich sendet den Inhalt des Ein-Pixel-Bildes -- wie oben besprochen --, indem es die vorher ausbaldowerten uuencode-Daten entpackt.
Ab Zeile 53 ist noch die oben schon genutzte
Log-Funktion definiert, die einfach alle ihre Argumente zu einem
String zusammenschweißt,
das aktuelle Datum und die Uhrzeit davorsetzt und das ganze an die Logdatei
anhängt. Wegen der quasi-parallel abgearbeiteten Requests
auf einem Webserver synchronisiert logmsg
den Zugriff auf die Logdatei
mit flock
. Um nicht noch 6 Monate vor dem Jahr 2000 noch einen
Y2K-Fehler zu machen, zählt Zeile 65 zum Jahr, das localtime
zurückliefert, noch 1900 dazu, schon stimmt's. Wer's nicht glaubt,
liest's unter [2] nach und setzt sich eine braune Papiertüte auf den Kopf.
track.pl
muß ins cgi-bin
-Verzeichnis des Webservers, die eingestellte
Logdatei muß für das Skript beschreibbar sein.
Im HTML-Code jeder Webseite, die den Cookie-Tracker aktivieren soll,
fügt man anschließend ein Image-Tag vom Format
<IMG SRC=/cgi-bin/track.pl>
ein. Wer den in [1] beschriebenen Includer verwendet, packt das Tag einfach in die eh schon definierte Kopf/Fußzeile und läßt das Skript nochmal über alle Seiten laufen.
In der nächsten Folge werden wir die gesammelten Daten auswerten
und einige unerwartete Schlüsse ziehen. Als kleinen Vorgeschmack
gibt's heute schon das Analyseprogramm des kleinen Mannes
report.pl
, das die Logdatei track.dat
einliest und feststellt,
wieviele alte Freunde von perlmeister.com
denn täglich so wiederkehren.
Die Ausgabe
1999/05/04> 0 1999/05/05> 1 1999/05/06> 3 1999/05/07> 5
zeigt die ersten 4 Tage, in denen das Track-Skript in Betrieb war. Nachdem
vor dem Tage Null noch keine Besucher registriert wurden, ist das Ergebnis
am ersten Tag gleich 0
-- am nächsten Tag kam einer wieder, der schon
am Vortag rumschnüffelte. Am dritten Tag waren es drei von den zwei
vorhergehenden Tagen und so weiter.
Wie funktioniert's? Zeile
13 in report.pl
extrahiert aus jeder Zeile der Log-Datei das erste,
das dritte
und das fünfte Feld: das Datum im Format YYYY/MM/DD
, den Status
(R
oder F
) und die eindeutige
Cookie-ID. Der Hash %dates
enthält für jeden analysierten Tag einen
Schlüssel im Format YYYY/MM/DD
, speichert also die Datumsangaben
aller untersuchten Tage.
Zwei weitere Hashes sind im Gebrauch:
%id_seen_today
enthält als Keys eine Kombination aus Datum und
Cookie ID und kann so für jedes eintrudelnde Cookie sofort
feststellen, ob es am selben Tag schon mal da war.
Falls der Status des Cookies in der Logdatei "R"
ist,
hat es der Browser wiedergeschickt.
Kann %id_seen_today
das Cookie nicht
finden, handelt es sich um einen früheren Besucher, der
wiedergekehrt ist, der Datumseintrag in %returns_per_date
wird um eins hochgezählt.
Ich bin ja schon so gespannt darauf, was wir nächstesmal mit den bis dahin gesammelten Daten anstellen werden -- und hoffe, Ihr schaltet wieder ein: Zum nächsten Perl-Snapshot!
Abb.1: Das Cookie kommt an. |
01 #!/usr/bin/perl -w 02 ################################################## 03 # mschilli@perlmeister.com, 1999 04 # One-Pixel Image Cookie-Tracker 05 ################################################## 06 use CGI qw(:standard); 07 08 $VERSION = "100"; 09 $COOKIE_NAME = "id"; 10 $LOG_FILE = "/DATA/track.dat"; 11 12 if(defined ($v=cookie(-name => $COOKIE_NAME))) { 13 14 print header('-type' => "image/gif", 15 '-expires' => '-1y'); 16 17 my ($ve, $ip, $ct, $id) = 18 ($v =~ /VE(\S+) IP(\S+) CT(\S+) ID(\S+)/); 19 20 if(defined $id) { 21 logmsg("R $ip $id $ct $ve", 22 $ENV{HTTP_REFERER} || '-', 23 $ENV{HTTP_USER_AGENT} || '-'); 24 } 25 26 } else { 27 28 my $ve = $VERSION; 29 my $ip = $ENV{'REMOTE_ADDR'}; 30 my $ct = time(); 31 my $id = time() . $$; 32 my $cookie = "VE$ve IP$ip CT$ct ID$id"; 33 34 logmsg("F $ip $id $ct $ve", 35 $ENV{HTTP_REFERER} || '-', 36 $ENV{HTTP_USER_AGENT} || '-'); 37 38 $cookie = cookie( 39 '-name' => $COOKIE_NAME, 40 '-value' => $cookie, 41 '-expires' => '+1y', -path => '/' 42 ); 43 44 print header('-type' => 'image/gif', 45 '-cookie' => $cookie, 46 '-expires' => '-1y'); 47 } 48 49 print unpack('u', 'C1TE&.#=A`0`!`(```/_<N@```"P`'. 50 '`````0`!```"`D0!`#L`'); 51 52 ################################################## 53 sub logmsg { 54 ################################################## 55 my $msg = join(' ', @_); 56 57 my ($sec,$min,$hour,$mday,$mon,$year) = 58 localtime(time); 59 open(LOG, ">>$LOG_FILE") || 60 die "Cannot open $LOG_FILE"; 61 62 flock(LOG, 2); 63 64 printf LOG "%d/%02d/%02d %02d:%02d:%02d ", 65 $year+1900, $mon+1, $mday, 66 $hour, $min, $sec; 67 68 print LOG "$msg\n"; 69 70 close(LOG); 71 }
01 #!/usr/bin/perl -w 02 ################################################## 03 # mschilli@perlmeister.com, 1999 04 # Run report on cookie tracker log file. 05 ################################################## 06 07 open(DATA, "<track.dat") || die "Cannot open"; 08 09 while(<DATA>) { 10 11 # Format: date time stat ip id secs ver url ua 12 my @fields = split(' ', $_); 13 my ($date, $stat, $id, $url) = @fields[0,2,4,7]; 14 15 $dates{$date} = 1; 16 17 if($stat eq "R" && 18 ! exists($id_seen_today{"$date$id"})) { 19 $returns_per_date{$date}++; 20 $returns_per_url{$url}++; 21 } 22 $id_seen_today{"$date$id"} = 1; 23 } 24 25 foreach $date (sort keys %dates) { 26 printf "$date> %4d\n", 27 $returns_per_date{$date} || 0; 28 } 29 30 foreach $url (sort keys %returns_per_url) { 31 printf "$url> $returns_per_url{$url}\n"; 32 } 33 34 close(DATA);
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. |