Geschrieben von alopex am 23.03.2005, 16:34

================================================
Eine Anmerkung vorweg:
Ich habe sämtliche Quelltextbereiche nur deshalb in "quotes" gepackt,
weil die Darstellung in den "code"-Bereichen fehlerhaft ist
und sehr schlecht lesbar wäre.
================================================

Vom Hitcounter zum Besucherzähler
Hitcounter mit einfacher IP-Adress-Sperre ("IP-Sperre"/"Reload-Sperre")

Wer mit sich mit PHP beschäftigt, stolpert früher oder später über so genannte Hitcounter:
Fast jede private Website hat einen. Irgendwo auf der Homepage befindet sich ein Text in der Art:
"Hallo, sie sind Besucher Nummer 08154711 auf dieser Website."

Ein simpler Hitcounter ist schnell gebastelt:
(1) Eine Textdatei (Hitcounter-Datei) mit einem Anfangswert anlegen.
(2) Den Anfangswert aus der Datei einlesen.
(3) Den Wert um Eins erhöhen und wieder in die Datei schreiben.

Blöderweise reagiert so ein Zähler jedesmal, wenn jemand die Seite aufruft.
Wer zwanzigmal die Seite lädt, erzeugt auch 20 Hits, und der Counter zählt munter mit, obwohl doch derselbe Besucher nur einmal gezählt werden sollte.

Um aus so einem Hitcounter einen echten Besucherzähler zu machen ist etwas mehr Aufwand nötig:
Der Zähler muss sich die IP-Adressen von gezählten Besuchern für einen gewissen Zeitraum merken.
Normalerweise genügt dafür ein Tag. Wer also 24 Stunden später wieder auf die Website kommt, darf wieder als neuer Besucher gezählt werden -- wir wollen schließlich nicht pingelig sein.

Das folgende Skript tut genau dies. Weiterhin speichert es alles, was es zum Zählen von Besuchern braucht,
in einer einzigen Datei. Diese hat folgenden prinzipiellen Aufbau:

1111568968,127.0.0.3 Unix-Datestamp, IP-Adresse
1111568968,127.0.0.3 dito
1111568968,127.0.0.3 dito
... dito
47110815 Der aktuelle (letzte) Zählerstand

Es werden nur die IP-Adressen der Besucher während der letzten 24 Stunden erfasst. Ältere Einträge werden bei einem neuen "Hit" automatisch entfernt.

Um das Skript universell verwendungsfähig zu machen, habe ich es in eine Unterfunktin gepackt.
Sie heißt "count_hits()" und erwartet einen optionalen Parameter: den Pfad zur Hitcounter-Datei (in der die IP-Adressen und der Zählerstand gespeichert werden).
Phne Parameter aufgerufen, versucht die Funktion die Hitcounter-Datei im aktuellen Verzeichnis unter dem Namen "counter_data.txt" zu speichern.
Ging alles glatt, enthält der Rückgabewert der Funktion den aktuellen Zählerstand.
Im Fehlerfall wird die Zahl Null zurückgegeben.


function count_hits(
$counter_file = 'counter_data.txt'
) {
$hit_time = time(); // time(); unix-sekunden-Datestamp

Ein Tag hat 24 x 60 x 60 Sekunden. Wer beim Testen nicht so lange warten will, setzt einen Inline-Kommentar "//" vor die nächste Zeile.
Dann erlaubt der Counter schon nach ca. 10 Sekunden eine Erhöhung des Zählerstandes.


$cycle = 24 * 60 * 60; // in sekunden == eine Erdrotation ;-)
$cycle = 10; // nur zum Testen, so vergehen die Tage schneller ;-)
$past_time = $hit_time - $cycle; // die heutige Zeit vor einer Erdrotation

Nun erfolgen einige "Einstellarbeiten". Wer das Format der Hitcounter-Datei nicht mag, kann hier beispielsweise das Komma gegen ein Semikolon austauschen.
Die Variable $webroot bekommt den Basispfad der aktuellen Website. Im Gegensatz zu $_SERVER['DOCUMENT_ROOT'] enthält sie auch schon erweiterte Pfade.
Beispiel: Bei Lima-City-Webspace ist $webroot = $_SERVER['DOCUMENT_ROOT'].'/username/html'.


$komma = ',';
$nl = "\n";
$webroot = preg_replace("|".$_SERVER['PHP_SELF']."\Z|", '', $_SERVER['SCRIPT_FILENAME']); // ohne abschließenden Slash!

Jetzt müssen wir uns ein paar Gedanken über die Ausgabe von Fehlermeldungen machen.
Fehlermeldungen sollten nicht im Browserfenster auftauchen, es sei denn es ist wirklich etwas ganz Schlimmes passiert.
Ansonsten ist es besser, unser Skript schreibt Meldungen in eine separate Datei -- ein so genanntes Logfile.
Der folgende Code sorgt dafür, dass die Fehlermeldung ins Standard-Error-Logfile des Webservers oder in die in $elog_file angegebene Datei geschrieben wird.


$elog_file = 'count_hits_error.log'; // auskommentieren, dann werden Fehler im Server-Error-Log ausgegeben
if( isset($elog_file) ) {
$emsg_type = 3;
$elog_nl = $nl; // Im User-Error-Log muessen wir selbst fuer Zeilenumbrueche sorgen
}
else{
$emsg_type = 0;
$elog_file = '';
}

Die IP-Adresse des Besuchers holen wir uns aus den Server-Umgebungsvariablen.
Dabei sollten wir auch die Besucher nicht vergessen, die über einen Proxy ins Internet gehen.


$remote_ip = $_SERVER['REMOTE_ADDR']; // IP-Adresse des Besuchers holen
if( isset($_SERVER['X-FORWARDED-FOR']) ) $remote_ip = $_SERVER['X-FORWARDED-FOR']; // Besucher mit Proxy unterwegs?

Wo kommen die Hits hin? (Und her?) Den Pfad zur Hitcounter-Datei basteln wir uns jetzt zusammen:
Fängt der (unserer Funktion übergebene) Pfad mit einem "/" (Slash) an, nehmen wir an, dass damit ein Unterverzeichnis von $webroot gemeint ist.
Ansonsten wird einfach vom aktuellen Pfad ausgegangen.


if( preg_match("|\A\/|", $counter_file) ) {
// fängt $counter_pfad mit einem "/" an, kleben wir ihn an den $webroot
// ansonsten vermuten wir $counter_file im selben Pfad wie unser PHP-Skript
$counter_file = $webroot.$counter_file;
}

Rufen wir das Skript das erste Mal auf, ist die Hitcounter-Datei noch nicht vorhanden.
Der folgende Codeabschnitt erzeugt in diesem Fall eine leere Datei.


$fh = FALSE; // ein File-Handle
if(!file_exists($counter_file) ) {
$fh = @fopen($counter_file, 'w');
if($fh === FALSE ) {
error_log('Fehler: Konnte Datei "'.$counter_file.'" nicht anlegen!'.$elog_nl, $emsg_type, $elog_file);
return(0);
}
fclose($fh);
}

Die Hitcounter-Datei wird zum gleichzeitigen Lesen und Schreiben geöffnet.
Wir wollen alle Manipulationen an der Datei in einem Aufwasch durchführen.
Sonst könnte in der Zwischenzeit ein anderes Skript dazwischenfunken und unsere Datei unbrauchbar machen.


$fh = @fopen($counter_file, 'r+'); // wir wollen lesen und schreiben!
if($fh === FALSE) {
error_log('Fehler: Konnte Datei "'.$counter_file.'" nicht oeffnen!'.$elog_nl, $emsg_type, $elog_file);
return(0);
}

fseek($fh, 0); // Filepointer auf Dateianfang setzen

Die Hitcounter-Datei wird Zeile für Zeile eingelesen.
Jede Zeile wird mit explode() in die Bestandteile Timestamp und IP-Adresse zerlegt.
Diese Bestandteile werden in ein assoziatives Array gepackt.
Den Schlüssel (Key) stellt dabei der Timestamp und den Wert (Value) die IP-Adresse dar.
Die Anweisung if($past_time > $xpl[0]) continue; sorgt dafür, dass Einträge, die älter als 24 Stunden sind, übersprungen werden.

Hat die Zeile nur einen Eintrag (also kein Komma vorhanden), so handelt es sich um die Anzahl der Hits. Der Einlesevorgang kann dann beendet werden.


$list = array(); // ist notwendig, denn die Counter-Datei kann auch leer sein!
$xpl = array();
// $xpl[0] == timestamp; $xpl[1] == ip-adresse
while($buffer = fgets($fh) ) {
$xpl = explode($komma, preg_replace( "/(\x0a|\x0d|\x0a\x0d)$/", '', $buffer) ); // chomp($buffer)
if( count($xpl) == 1 ) {
$last_counter = $xpl[0];
break;
}
if($past_time > $xpl[0]) continue;
$list[$xpl[0]] = $xpl[1];
}

Eventuell auftretende Fehler sollten wir besser abfangen.
Schließlich könnte die Hitcounter-Datei ja auch fehlerhafte Einträge enthalten.


if( !isset($last_counter) ) $last_counter = 0;
if( !is_numeric($last_counter) ) {
error_log('Fehler: $last_counter ('.$last_counter.') ist keine Ganzzahl!'.$elog_nl, $emsg_type, $elog_file);
return(0);
}

Die im Array $list vorhandenen Einträge müssen nun einer nach dem anderen auf Übereinstimmung mit der IP-Adresse des aktuellen Besuchers geprüft werden.
Gleichzeitig übernehmen wir die Timestamp-IP-Kombinationen in unseren Ausgabe-Puffer-String $buffer.


$buffer = '';
$same_user = FALSE;
if( count($list) > 0 ) {
foreach($list as $key => $val) {
$buffer .= $key.$komma.$val.$nl;
if($remote_ip == $val) $same_user = TRUE; // Benutzer hat schon mal geklickt
}
}
$list[$hit_time] = $remote_ip;

Der Hitcounter soll selbstverständlich nur dann um Eins erhöht werden, wenn der Zugriff auf die Seite "berechtigt" erfolgte.
In diesem Fall müssen wir auch einen neuen Eintrag in die Hitcounter-Datei hinzufügen, damit der selbe Besucher für die nächsten 24 Stunden gesperrt wird.


if($same_user === FALSE) {
$last_counter++;
$buffer .= $hit_time.$komma.$remote_ip.$nl;
}
$buffer .= $last_counter.$nl;

Die gesamte neue Liste von IP-Adressen und Timestamps wird nun vom Ausgabe-Puffer-String ins Logfile geschrieben.


fseek($fh, 0);
fputs($fh, $buffer);

Die neue Dateilänge ist möglicherweise geringer als die alte. Daher muss die Datei noch auf die neue Länge gekürzt werden -- wenn nötig.
Danach können wir die Datei wieder freigeben.


ftruncate($fh, strlen($buffer) ); // Datei auf neue Länge kürzen
fclose($fh);

Der neue Counter-Wert wird an das aufrufende Hauptprogramm übermittelt.


return($last_counter);
}

Beispiel eines Aufrufes im Hauptprogramm


<?php
error_reporting(E_ERROR | E_WARNING | E_PARSE | E_NOTICE);
require_once('count_hits.php');

$meldung = 'Ich glaub, der Counter ist kaputt! :(';
$hits = count_hits('test.txt');
if($hits > 0) {
$meldung = sprintf("Hallo, Besucher Nummer %s!", $hits);
}
?>
<html>
<head>
<title><?php print($meldung) ?></title>
</head>
<body>
<?php
print($meldung."<br />\n");
?>
</body>
</html>
<?php
exit();
?>

Andere Anwendung

Da das Skript als Unterfunktion auch mit verschiedenen Dateinamen aufgerufen werden kann, sind auch Anwendungen denkbar, die auf dem ersten Blick nichts mit Besucherzählern zu tun haben.
So ist eine einfache Ja-/Nein-Abstimmung mit zwei verschiedenen Dateien durchaus realisierbar.

Erweiterte Anwendung

Besser formatierte (und eventuell fehlerbereinigte) Quelltexte für count_hits.php und das Aufruf-Beispiel befinden sich auf der Seite:
http://alopex.pyrokar.lima-city.de/index.php/PHP/Hitcounter.html

Dort kann der Counter auch in Aktion erlebt werden.

Fanden Sie diesen Text nützlich?
Ja: http://alopex.pyrokar.lima-city.de/xlink.php/tut_ja
Nö: http://alopex.pyrokar.lima-city.de/xlink.php/tut_nein

Bewertung Anzahl
6
100,0 %
9 Bewertungen