Apache Logfile Analyse mit PHP 5 zur Erstellung von Web-Statistiken
Es gibt einen Haufen von fertigen Möglichkeiten Statistiken für die eigene Webseite zu generieren. Die wohl bekanntesten Vertreter der Statistik-Generatoren, die auf dem Server laufen und die Apache Logfiles (oder anderer Webserver-Dienste) auswerten, dürften AWStats [1] und Webalizer [2] sein.
Wer nicht so versiert ist diese Programme einzurichten oder nicht die Möglichkeit dazu hat, z. B. bei Webhosting Diensten die keine (oder unzureichende) Statistiken anbieten, greift des öfteren auf Online-Dienste wie Google Analytics oder ähnlichen zurück.
Die Nutzung von externen Diensten/Dienstleistern zur Auswertung von Webseitenzugriffen birgt jedoch einige Risiken, die immer noch nicht allen bekannt sind. Ein großer Punkt hierbei ist sicherlich (insbesondere in Deutschland) die Datenschutz-Problematik. Auf diese Problematik will ich nicht näher eingehen, das überlasse ich lieber in diesem Thema vermutlich versierteren Autoren [3].
Für mich persönlich besteht ein weiteres Manko der Online-Dienste darin, dass sie darauf angewiesen sind, dass jede Seite der Web-Präsenz, die untersucht werden soll auch den entsprechenden und korrekten Code enthält und dass dieser auch aufgerufen und ausgeführt wird. In der Regel funktioniert dies nicht, wenn JavaScript im Browser deaktiviert wurde – und auch manche Werbe/Tracking-Blocker-Erweiterungen für den Browser verhindern die korrekte Auswertung [4]. Weiterhin hat man als Anwender auch keine (oder zumindest wenig) Kontrolle darüber, was wie ausgewertet wird.
Der Exkurs ist nun doch etwas länger geworden, als gedacht – denn eigentlich soll es bei diesem Artikel darum gehen, wie man sich sein eigenes kleines Analyseprogramm für Apache (und kompatible) Logdateien in PHP5 bastelt.
Natürlich gibt es bereits viele Programme, die mindestens den gleichen Umfang bieten wie das hier entwickelte Skript, jedoch dürfte es für einige – wie auch für mich – interessant sein selbst etwas in diesem Bereich – für die eigenen Bedürfnisse angepasst – zu entwickeln, ohne sich in andere Programmiersprachen oder komplexe Programme einzuarbeiten.
Das Ergebnis der Entwicklung aus diesem Beitrag bzw. dieser Beitragsreihe bietet schon einige ganz nette Funktionen.
Hier wurden aus den Logfiles für einige Tage die Statistiken für den Blog von Sören generiert. Am Ende des letzten Teils dieses Artikels werde ich natürlich auch den kompletten Source-Code des Skripts veröffentlichen, zuerst tasten wir uns aber langsam an die Erstellung heran.
Ich werde in diesem Artikel lediglich die Skript-Variante behandeln, die von der Kommandozeile gestartet wird. Bei entsprechendem Interesse werde ich gegebenenfalls später die notwendigen Anpassungen für einen Aufruf im Browser behandeln. Etwas versiertere PHP Entwickler, können dies natürlich auch ganz einfach selbst machen.
Fangen wir mit den Grundlagen an. Zuerst müssen wir uns ansehen wie die Apache Logdatei (im Standard „combined“ Format) aussieht:
[pastacode lang=“bash“ message=“Logdatei“ highlight=““ provider=“manual“]
aaa.bbb.ccc.ddd - - [10/Apr/2012:06:56:48 +0200] "GET /mozilla/firefox/2012/04/04/mozilla-blockt-altere-java-versionen-in-firefox/ HTTP/1.1" 200 14759 "http://www.google.at/url?sa=t&rct=j&q=firefox%20java%20plugin&source=web&cd=5&ved=0CFgQFjAE&url=http%3A%2F%2Fwww.soeren-hentzschel.at%2Fmozilla%2Ffirefox%2F2012%2F04%2F04%2Fmozilla-blockt-altere-java-versionen-in-firefox%2F&ei=hL2DT9OxN4ap4gTx4ZDUBw&usg=AFQjCNHN8mJQ50bjEgOqo9GXOq_xTfz5jQ&sig2=QIGNicLoQqy32xHOmaM74w&cad=rjt" "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko/20100101 Firefox/11.0"
[/pastacode]
Hier haben wir direkt ein schönes Beispiel für einen Aufruf. Der erste Eintrag enthält die (in diesem Fall anonymisierte) IP Adresse des Besuchers. Sie wird gefolgt von einem Leerzeichen.
Danach folgen zwei weitere Felder mit Leerzeichen getrennt, deren Inhalt uns aber hier nicht weiter interessiert.
Der vierte Abschnitt enthält das Datum und die Uhrzeit, wann der Aufruf stattgefunden hat in eckigen Klammern.
Im nächsten Abschnitt findet man den eigentlichen Aufruf, also die aufgerufene URL sowie die Methode (unter anderem GET für normale Anfragen, POST für Formulare) und die HTTP Version (z. B. HTTP/1.0 oder HTTP/1.1).
Das sechste Feld enthält den Status Code [5], den der Server zurückgegeben hat.
Das siebte Feld enthält die Anzahl von Bytes, die übertragen wurden.
Im vorletzten Feld ist der Referrer, also die Seite enthalten, die durch einen Link oder die Einbettung eines Bildes auf die aufgerufene URL verwiesen hat. In unserem Beispiel ist dies die Google Suche.
Das letzte Feld enthält die Kennung, die der Browser (oder sonstige Web-Clients) an den Server gesendet haben.
Um die Log-Zeile in die entsprechenden Felder aufzuteilen, verwenden wir eine Regular Expression (Regulären Ausdruck) zur späteren Verwendung in der PHP Funktion preg_match(). Eine gewisse Grundkenntnis an Regulären Ausdrücken setze ich hier voraus, da das Thema zu umfangreich für die Behandlung in diesem Artikel ist. Google spuckt jede Menge Suchergebnisse zu „php regular expression“ aus.
1. Zeilenbeginn -> ^
2. IP (alles bis zum Leerzeichen) -> ([^ ]+)
3. Feld2 und Feld3 (jeweils alles bis zum nächsten Leerzeichen) -> ([^ ]+)
4. Datum/Uhrzeit in eckigen Klammern -> \[([^\]]+)\]
wir setzen die Klammern hier nicht um das ganze Feld, da wir nur die eigentliche Datums- und Zeitangabe möchten.
5. Methode, Url und Version getrennt mit Leerzeichen -> „(.*?) (.*?) (.*?)“
6. Status Code (nur Ziffern oder -) -> ([0-9\-]+)
7. Byteanzahl (nur Ziffern oder -) -> ([0-9\-]+)
8. Referrer (alles zwischen zwei „) -> „(.*)“
9. User Agent (Browserkennung – alles zwischen zwei „) -> „(.*)“
10. Zeilenende $
Wie gesagt behandele ich Reguläre Ausdrücke hier nicht im Detail, wer also die Zusammensetzung dieses Ausdrucks nicht nachvollziehen kann, muss ihn als funktionierend voraussetzen oder sich in die Materie einlesen.
Unser fertiger Regulärer Ausdruck sieht nun so aus:
[pastacode lang=“bash“ message=“Regulärer Ausdruck“ highlight=““ provider=“manual“]
^([^ ]+) ([^ ]+) ([^ ]+) \[([^\]]+)\] "(.*?) (.*?) (.*?)" ([0-9\-]+) ([0-9\-]+) "(.*)" "(.*)"$
[/pastacode]
Da ich das Skript gerne ein wenig objektorientiert aufbauen möchte legen wir uns nun eine Klasse für die Verarbeitung der einzelnen Zeilen in der Logdatei an.
[pastacode lang=“php“ message=“PHP“ highlight=““ provider=“manual“]
class pxALogLine {
private $_is_valid = false;
private $_info = array('ip' => '',
'time' => '',
'timestamp' => '',
'method' => '',
'url' => '',
'orig_url' => '',
'http_version' => '',
'code' => '',
'size' => '',
'referrer' => '',
'user_agent' => '',
'browser' => '',
'browser_version' => '',
'browser_comment' => '',
'browser_os' => '',
'crawler_hit' => false);
public function __construct($log_line = '') {
// Regulären Ausdruck definieren
$pattern = '/^([^ ]+) ([^ ]+) ([^ ]+) \[([^\]]+)\] "(.*?) (.*?) (.*?)" ([0-9\-]+) ([0-9\-]+) "(.*)" "(.*)"$/';
// Übergebenen String durchsuchen und bei nicht zutreffendem Ausdruck abbrechen
if(!preg_match($pattern, $log_line, $matches)) return;
// Felder füllen
$this->_info['ip'] = $matches[1];
$this->_info['time'] = $matches[4];
$this->_info['timestamp'] = strtotime($matches[4]);
$this->_info['method'] = $matches[5];
$this->_info['orig_url'] = $matches[6];
$this->_info['url'] = $matches[6];
$this->_info['http_version'] = $matches[7];
$this->_info['code'] = $matches[8];
$this->_info['size'] = $matches[9];
$this->_info['referrer'] = $matches[10];
$this->_info['user_agent'] = $matches[11];
$this->_info['referrer_domain'] = '';
$this->_info['file_ext'] = '';
$this->_info['page_title'] = '';
$this->_info['hits_only'] = false;
$this->_info['crawler_hit'] = false;
$this->_is_valid = true;
}
public function isValid() {
return $this->_is_valid;
}
}
[/pastacode]
Wie ihr seht enthält das „info“ Array der Klasse schon ein paar mehr Felder als in der Logdatei vorhanden sind. Diese sind bereits für spätere Funktionen vorgesehen.
Dem Konstruktor der PHP Klasse wird eine einzelne(!) Logzeile übergeben. Diese wird dann anhand des vorher zusammengestellten Ausdrucks geprüft und wenn dieser zutrifft werden die Felder des „info“ Arrays der Klasse mit den korrekten Werten gefüllt.
Eine kleine Besonderheit ist die Zeile, welche den „timestamp“ Wert des Arrays setzt. Hier bedienen wir uns der PHP Funktion strtotime(), die verschieden Formatierte Datums- und Zeitangaben in einen Unix Zeitstempel (timestamp) umwandeln kann, den wir später noch brauchen werden.
Ein Beispiel-Aufruf:
[pastacode lang=“php“ message=“PHP“ highlight=““ provider=“manual“]
// hier Einbinden der Klasse von oben
$logzeile = 'aaa.bbb.ccc.ddd - - [10/Apr/2012:06:56:48 +0200] "GET /mozilla/firefox/2012/04/04/mozilla-blockt-altere-java-versionen-in-firefox/ HTTP/1.1" 200 14759 "http://www.google.at/url?sa=t&rct=j&q=firefox%20java%20plugin&source=web&cd=5&ved=0CFgQFjAE&url=http%3A%2F%2Fwww.soeren-hentzschel.at%2Fmozilla%2Ffirefox%2F2012%2F04%2F04%2Fmozilla-blockt-altere-java-versionen-in-firefox%2F&ei=hL2DT9OxN4ap4gTx4ZDUBw&usg=AFQjCNHN8mJQ50bjEgOqo9GXOq_xTfz5jQ&sig2=QIGNicLoQqy32xHOmaM74w&cad=rjt" "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko/20100101 Firefox/11.0"';
$parser = new pxALogLine($logzeile);
if($parser->isValid()) {
print "Gültige Logzeile erkannt.\n";
var_dump($parser);
} else {
print "Scheint keine gültige Logzeile zu sein.\n";
}
[/pastacode]
Dieser Aufruf erzeugt mit unserer Testlogzeile folgende Ausgabe:
[pastacode lang=“bash“ message=“Ausgabe“ highlight=““ provider=“manual“]
Gültige Logzeile erkannt.
object(pxALogLine)#1 (2) {
["_is_valid":"pxALogLine":private]=>
bool(true)
["_info":"pxALogLine":private]=>
array(20) {
["ip"]=>
string(15) "aaa.bbb.ccc.ddd"
["time"]=>
string(26) "10/Apr/2012:06:56:48 +0200"
["timestamp"]=>
int(1334033808)
["method"]=>
string(3) "GET"
["url"]=>
string(76) "/mozilla/firefox/2012/04/04/mozilla-blockt-altere-java-versionen-in-firefox/"
["orig_url"]=>
string(76) "/mozilla/firefox/2012/04/04/mozilla-blockt-altere-java-versionen-in-firefox/"
["http_version"]=>
string(8) "HTTP/1.1"
["code"]=>
string(3) "200"
["size"]=>
string(5) "14759"
["referrer"]=>
string(324) "http://www.google.at/url?sa=t&rct=j&q=firefox%20java%20plugin&source=web&cd=5&ved=0CFgQFjAE&url=http%3A%2F%2Fwww.soeren-hentzschel.at%2Fmozilla%2Ffirefox%2F2012%2F04%2F04%2Fmozilla-blockt-altere-java-versionen-in-firefox%2F&ei=hL2DT9OxN4ap4gTx4ZDUBw&usg=AFQjCNHN8mJQ50bjEgOqo9GXOq_xTfz5jQ&sig2=QIGNicLoQqy32xHOmaM74w&cad=rjt"
["user_agent"]=>
string(65) "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko/20100101 Firefox/11.0"
["browser"]=>
string(0) ""
["browser_version"]=>
string(0) ""
["browser_comment"]=>
string(0) ""
["browser_os"]=>
string(0) ""
["crawler_hit"]=>
bool(false)
["referrer_domain"]=>
string(0) ""
["file_ext"]=>
string(0) ""
["page_title"]=>
string(0) ""
["hits_only"]=>
bool(false)
}
}
[/pastacode]
Die Felder des info Arrays sind mit den korrekten Werten aus der Logzeile gefüllt.
Dies ist das Ende des ersten Teils – noch nicht mit tollen Ergebnissen, da die Einführung etwas länger sein musste.
Im zweiten Teil wird es dann mehr Ergebnisse in Richtung des fertigen Skriptes geben.
Verweise:
[1] http://awstats.sourceforge.net/
[2] http://www.webalizer.org/
[3] z. B. http://www.datenschutzbeauftragter-info.de/fachbeitraege/google-analytics-datenschutzkonform-einsetzen/, http://www.zdnet.de/magazin/41556436/google-analytics-datenschutzkonform-einsetzen.htm uvm.
[4] z. B. Do Not Track Header, Do Not Track+, Ghostery, u.a.
[5] z. B. Liste von Status Codes
Hey,
das ist ein echt guter Artikel. Ich hoffe es geht bald weiter damit, bin schon sehr gespannt 😉
Gruß,
Sebastian
Danke, im Moment muss ich mich etwas intensiver um Kunden kümmern, daher lässt das noch etwas auf sich warten.
Würde mich auch freuen, wenn das Projekt weiter geht – sieht bisher sehr vielversprechend aus!
Teil 2 ist nun endlich da
https://www.soeren-hentzschel.at/technik/programmierung/2012/06/19/apache-logfile-analyse-mit-php-5-zur-erstellung-von-web-statistiken-teil-2/