Widmen wir den letzten Tag der Woche ein paar weiteren Beispielen. Sie werden in diesem Kapitel nicht viel Neues lernen, auch Übungen und Quiz gibt es heute nicht. Betrachten Sie es als kurze Verschnaufpause, in der Sie sich vollständige Perl-Skripts ganz in Ruhe ansehen können. Insgesamt hat das Buch drei dieser Beispiellektionen (immer am Ende der Wochen), die Ihr neues Wissen festigen.
Heute analysieren wir zwei längere Perl-Skripts:
Unser erstes Skript heute besteht aus zwei Teilen:
Dieses Skript nutzt so ziemlich alles, was Sie diese Woche gelernt haben: Skalar- und Hash-Daten, Bedingungen, Schleifen, Ein- und Ausgabe, Subroutinen, lokale Variablen und Mustervergleich. Es gibt sogar ab und zu einen Funktionsaufruf, um die Sache interessanter zu machen. Aber jetzt lassen Sie uns ohne weitere Umschweife beginnen.
Das Skript adressen.pl wird mit einem einzigen Argument aufgerufen: der Adreßdatei namens adressen.txt. Geben Sie den Aufruf in der Befehlszeile ein, wie Sie es bereits bei anderen Perl-Skripten gemacht haben:
% adressen.pl adressen.txt
Wenn Sie MacPerl verwenden, speichern Sie das Skript adressen.pl als Droplet und ziehen (Drag) Sie dann die Datei adressen.txt auf das Symbol adressen.pl, wo Sie diese ablegen (Drop).
Als erstes fragt das Adreßbuch-Skript Sie nach dem gesuchten Begriff:
Wonach soll gesucht werden? Johnson
Das Suchmuster, das Sie an adressen.pl
übergeben, kann verschiedene Formen
haben:
Johnson
im obigen Beispiel
John Maggie Alice
). Alle Adressen, die einem dieser Wörter
entsprechen, werden ausgegeben (gleicht einer ODER-Suche).
AND
oder OR
(in Groß- oder Kleinbuchstaben).
Boolesche Suchläufe verhalten sich wie logische Operatoren in Perl und werden
von links nach rechts getestet. (Denken Sie daran, dass ein AND-Suchlauf nur
sinnvoll ist, wenn der Vergleich innerhalb einer Adresse erfolgt; es erfolgt kein
Vergleich über mehrere Adressen, wie das bei OR-Suchläufen der Fall ist).
dies das
«) werden als ein einziges
Suchmuster betrachtet. In diesem Fall sind Leerzeichen von Bedeutung.
//
um die Muster fort).
So liefert zum Beispiel die Suche nach dem Namen Johnson
in meiner Beispieldatei
adressen.txt folgende Ausgabe zurück:
*********************
Paul Johnson
212 345 9492
234 33rd St Apt 12C, NY, NY 10023
http://www.foo.org/users/don/paul.html
*********************
Alice Johnson
(502) 348 2387
(502) 348 2341
*********************
Mary Johnson
(408) 342 0999
(408) 323 2342
mj@asd.net
http://www.mjproductions.com
*********************
Alle Namen, Adressen, Telefonnummern und Webseiten dieser Beispiel-Adreßdatei sind natürlich frei erfunden. Irgendwelche Übereinstimmungen dieser Daten mit lebenden oder toten Personen sind reiner Zufall.
Das Herzstück eines Adreßbuches (das Sie selbst erstellen müssen, wenn Sie dieses Skript nutzen wollen) ist eine Datei von Adressen in einem speziellen Format, das von Perl verstanden wird. Sie können es auch als einfache textbasierte Datenbank betrachten und dann Perl-Skripts schreiben, die diese Datenbank um Datensätze (Adressen) ergänzen oder auch Datensätze löschen.
Das Format der Adreßbuch-Datei in seiner generischen Form sieht folgendermaßen aus:
Name: Name
Telefon: Nummer
Fax: Nummer
Adresse: Adresse
EMail: email-Adresse
URL: Web URL
---
Name: Paul Johnson
Telefon: 212 345 9492
Adresse: 234 33rd St Apt 12C, NY, NY 10023
URL: http://www.foo.org/users/don/paul.html
---
Jeder Datensatz besteht aus einer Reihe von Feldern (Name, Telefon, Fax, Adresse,
E-Mail und URL, die jedoch nicht alle erforderlich sind) und endet mit drei
Gedankenstrichen. Die Feldnamen (Name
, Telefon
, URL
usw.) sind von ihren Werten
durch einen Doppelpunkt und ein Leerzeichen getrennt. Die Werte müssen kein
spezielles Format aufweisen. Sie können weitere Feldnamen in die Datenbank
aufnehmen, und die Suchschlüssel werden diese zusätzlichen Felder auch
durchsuchen, aber in der Ausgabe werden diese Felder übergangen. (Wenn Sie
wirklich ein zusätzliches Feld benötigen, zum Beispiel für eine Handy-Nummer,
können Sie das Skript jederzeit ändern. Perl macht es Ihnen in dieser Hinsicht leicht.)
Sie können beliebig viele Adressen in die Datei adressen.txt aufnehmen. Doch je größer das Adreßbuch, um so länger die Zeitdauer, bis übereinstimmende Datensätze gefunden werden, da für jede Adresse die Datei von vorn bis hinten durchgegangen wird. Solange Sie jedoch keine vier bis fünf Millionen Freunde haben, werden Sie von Perls Anstrengungen nichts mitbekommen.
Das Skript adressen.pl liest die Datei adressen.txt Adresse für Adresse ein und
gleicht dann das Suchmuster mit jeder Adresse ab. Der oberste Teil des Skripts ist
eine while
-Schleife, die diese Aufgabe übernimmt, wobei wiederum fünf weitere
Subroutinen abgearbeitet werden, um die komplexeren Teile des Skripts zu
bewältigen.
Lassen Sie uns ganz oben im Skript beginnen. Auf oberster Ebene definieren wir drei globale Variablen:
%rec
- enthält den aktuellen Adreßdatensatz, der nach Feldnamen indiziert ist.
$search
- nimmt das Suchmuster auf, das Sie bei der Eingabeaufforderung
eingeben.
$bigmatch
- gibt an, ob in der Adreßdatei ein Datensatz gefunden wurde, der dem
Suchmuster entspricht (es gibt auch lokale Variablen für den Fall, dass der aktuelle
Datensatz übereinstimmt, doch dazu gleich mehr).
Der erste Schritt in dem äußeren Teil des Skripts besteht darin, zur Eingabe des
Suchbegriffs aufzufordern und diesen dann in $search
abzulegen:
$search = &getpattern(); # fragt nach dem Suchmuster
Die Subroutine &getpattern()
besteht aus den grundlegenden Schritten »Eingabe
lesen / zurechtschneiden / Ergebnis zurückliefern«, die Ihnen schon viel zu oft in
diesem Buch begegnet sind:
sub getpattern {
my $in = ''; # Eingabe
print 'Wonach soll gesucht werden? ';
chomp($in = <STDIN>);
return $in;
}
Schritt zwei im äußeren Teil des Skripts ist eine endlose while
-Schleife, die einen
Datensatz einliest, das Suchmuster verarbeitet und bei Übereinstimmung den
Datensatz ausgibt:
while () { # durchsucht die Adressdatei
%rec = &read_addr();
if (%rec) { # Datensatz gefunden
&perform_search($search, %rec);
} else { # Ende der Adressdatei, Aufräumarbeiten
if (!$bigmatch) {
print "Nichts gefunden.\n";
} else { print "*********************\n"; }
last; # verlassen, wir sind fertig
}
}
Innerhalb der while
-Schleife rufen wir &read_addr()
auf, um einen Datensatz
einzulesen, und wenn ein Datensatz gefunden wurde, durchsuchen wir ihn mit Hilfe
der Subroutine &perform_search()
. Sind wir am Ende der Adreßdatei angekommen
und die Variable $bigmatch
ist gleich 0
, heißt das, dass keine Übereinstimmungen
gefunden wurden, und wir informieren den Anwender darüber. Am Ende der
Adreßdatei rufen wir jedoch auf alle Fälle last
auf, um aus der Schleife auszusteigen
und das Skript zu beenden.
Mit der Subroutine &read_addr()
wird ein Adreßdatensatz eingelesen. In Listing 14.1
sehen Sie den Inhalt von &read_addr()
.
Listing 14.1: Die Subroutine &read_addr()
1: sub read_addr {
2: my %curr = (); # aktueller Datensatz
3: my $key = ''; # temp. Schlüssel
4: my $val = ''; # temp. Wert
5:
6: while (<>) {
7: chomp;
8: if ($_ ne '---') { # Datensatz-Trennzeichen
9: ($key, $val) = split(/: /,$_,2);
10: $curr{$key} = $val;
11: }
12: else { last; }
13: }
14: return %curr;
15: }
In früheren Beispielen mit while
-Schleife, die <>
verwendeten, haben wir die gesamte
Datei auf einmal eingelesen und verarbeitet. Bei dieser while
-Schleife ist das etwas
anders; sie liest nur Teile der Datei ein und hört auf, wenn Sie auf ein Datensatz-
Trennzeichen stößt (in diesem Fall der String '---
'). Das nächste Mal, wenn die
Subroutine &read_addr()
aufgerufen wird, fährt die while
-Schleife dort fort, wo sie in
der Adreßdatei gestoppt hat. Perl hat keine Schwierigkeiten mit diesem Stop-and-Go
der Eingabe und ist damit besonders geeignet zum Einlesen und Verarbeiten von
Teilabschnitten einer Datei, wie sie hier vorliegen.
Konkret ausgedrückt, liest diese Subroutine eine Zeile ein. Lautet die Zeile nicht '---
',
so befindet sich die Zeile mitten in einem Datensatz und besteht aus dem Feldnamen
(Name:
, Telefon:
, usw.) und dem Wert. Der Aufruf der split
-Funktion erfolgt in Zeile
9. Beachten Sie das zweite Argument am Ende von split
; damit wird angezeigt, dass
es in jeder Datensatzzeile nur zwei Teile gibt. Mit dem Feldnamen ($key
) und dem
Wert ($val
) können Sie beginnen, den Hash für diese Adresse einzurichten.
Handelt es sich bei der eingelesenen Zeile um die Datensatzende-Marke, springt die
if
-Anweisung in Zeile 8 direkt zum else
-Teil in Zeile 12, wo der last
-Befehl die
Schleife verläßt. Das Ergebnis dieser Subroutine ist ein Hash, der alle Zeilen der
Adresse nach Feldnamen indiziert enthält.
Inzwischen sind Sie in der Ausführung des Skripts soweit fortgeschritten, dass Sie
einen Suchbegriff in der Variablen $search
und eine Adresse in der Variablen $rec
gespeichert haben. Der nächste Schritt besteht jetzt darin, zum nächsten Teil unser
großen while
-Schleife zu Beginn des Skripts überzugehen. Das heißt, wenn %rec
definiert ist (also eine Adresse existiert), rufen wir die Subroutine &perform_search()
auf, um konkret festzustellen, ob der Suchbegriff in $search
auch mit der Adresse in
&rec
übereinstimmt.
Die Subroutine &perform_search()
wird in Listing 14.2 gegeben:
Listing 14.2: Die Subroutine &perform_search()
1: sub perform_search {
2: my ($str, %rec) = @_;
3: my $matched = 0; # Übereinstimmung
4: my $i = 0; # Position innerhalb des Suchmusters
5: my $thing = ''; # temporäres Wort
6:
7: my @things = $str =~ /("[^"]+"|\S+)/g; # in Suchelemente aufspalten
8:
9: while ($i <= $#things) {
10: $thing = $things[$i]; # Suchelement, UND oder ODER
11: if ($thing eq 'ODER' || $thing eq 'oder') { # OR Fall
12: if (!$matched) { # noch keine Übereinstimmung,
# nächstes Element
13: $matched = &isitthere($things[$i+1], %rec);
14: }
15: $i += 2; # ODER überspringen und nächstes Element
16: }
17: elsif ($thing eq 'UND' || $thing eq 'und') { # UND-Fall
18: if ($matched) {
# Übereinstimmung gefunden, andere Seite prüfen
19: $matched = &isitthere($things[$i+1], %rec);
20: }
21: $i += 2; # UND überspringen und nächstes Element
22: }
23: elsif (!$matched) { # noch keine Übereinstimmung
24: $matched = &isitthere($thing, %rec);
25: $i++; # weiter!
26: }
27: else { $i++; } # $match wurde gefunden, weiter zum
# nächsten Element
28: }
29:
30: if ($matched) { # alle Schlüssel durchgearbeitet, gab es
# eine Übereinstimmung?
31: $bigmatch = 1; # Ja, wir haben etwas gefunden
32: print_addr(%rec); # Datensatz ausgeben
33: }
34: }
Diese Subroutine ist sehr lang und etwas komplexer. Aber keine Bange, sie ist nicht so kompliziert, wie es den Anschein hat. Fangen wir oben an; dort übernimmt die Subroutine zwei Argumente: den Suchbegriff und den Adressen-Hash: Beachten Sie, dass diese Werte in globalen Variablen gespeichert sind. Deshalb gibt es an sich auch keinen Grund, sie als Argumente an die Subroutine zu übergeben. Wir hätten auf diese globalen Variablen einfach im Rumpf der Subroutine zugreifen können. Unsere Strategie, die Daten als Argumente zu übergeben, führt allerdings dazu, dass die Subroutine in sich abgeschlossener ist, da nur die Daten bearbeitet werden, die explizit übergeben werden. Sie könnten zum Beispiel diese Subroutine kopieren und in ein anderes Suchskript einfügen, ohne einen Gedanken an die Umbenennung von Variablen verschwenden zu müssen.
Die erste richtige Operation in dieser Subroutine erfolgt in Zeile 7, in der wir den
Suchbegriff in seine Bestandteile aufsplitten. Denken Sie daran, dass der Suchbegriff
in vielen Formen auftreten kann, einschließlich verschachtelter Strings in
Anführungszeichen, UNDs und ODERs oder als eine Liste von Schlüsselwörtern. In
Zeile 7 werden die einzelnen Elemente aus dem Suchbegriff extrahiert, und
anschließend werden diese »Suchelemente« gemeinsam in dem Array @things
gespeichert. Dabei sollten Sie beachten, dass der reguläre Ausdruck mit der Option g
endet und in einem Listenkontext ausgewertet wird - das soll heißen, dass die
@things
-Liste alle möglichen von den Klammern eingefangenen Übereinstimmungen
enthält. Womit stimmt dieses besondere Muster überein? Es gibt zwei Gruppen von
Mustern, getrennt durch das Alternationszeichen (|
). Die erste Gruppe lautet:
"[^"]+"
Dabei handelt es sich, wenn Sie an Ihre Muster zurückdenken, um ein doppeltes
Anführungszeichen gefolgt von einem oder mehreren Zeichen, die kein doppeltes
Anführungszeichen sind, und einem abschließenden Anführungszeichen. Dieses
Muster vergleicht mehrteilige Strings im Suchbegriff (in Anführungszeichen), wie zum
Beispiel »John Smith
« oder »San Francisco
«, und behandelt sie als einen Suchbegriff.
Der zweite Teil des Musters besteht lediglich aus einem oder mehreren Zeichen, die
keine Whitespace-Zeichen sind (\S
). Dieser Teil des Musters vergleicht alle einfachen
Wörter, wie zum Beispiel UND oder ODER, oder einzelne Schlüsselwörter. Von
diesen beiden Mustern wird ein langer, komplexer Suchbegriff wie »San Jose
« ODER
»San Francisco
« UND
John
in folgende Liste aufgesplittet (»San Jose
«, ODER
, »San
Francisco
«, UND
, John
).
Nachdem nun alle unsere Suchteile in einer Liste stehen, besteht die eigentliche
Arbeit darin, diese Liste durchzugehen, wenn nötig, nach der Adresse zu suchen und
die logischen Ausdrücke abzuarbeiten. All dies wird in der großen while
-Schleife
ausgeführt, die in Zeile 9 beginnt. Die Schleife verwendet eine Platzhaltervariable $i
zum Festhalten der aktuellen Position in dem Muster und geht das Muster bis zum
Ende durch. Innerhalb der while
-Schleife prüft die Variable $matched
ständig, ob ein
bestimmter Teil des Musters mit dem Datensatz übereinstimmt. Begonnen wird mit
einer 0
, falsch, für keine Übereinstimmung.
Innerhalb der while
-Schleife starten wir in Zeile 10, wo wir der Variablen $things
auf
den aktuellen Teil des zu untersuchenden Musters zuweisen, einfach um uns bei
weiteren Zugriffen Tipparbeit zu ersparen. Anschließend folgen vier größere Tests:
$matched
) zurückgeliefert hat. Ist $matched
wahr, wurde für die linke
Seite eine Übereinstimmung gefunden, und es gibt keinen Grund, die
Untersuchung auf die rechte Seite auszudehnen (genau, es handelt sich um ein
Ausschluß-ODER). Gibt es für die linke Seite keine Übereinstimmung, ist der Wert
der $matched
-Variablen 0, und wir müssen die rechte Seite ebenfalls überprüfen.
Dieser Schritt erfolgt in Zeile 13. Dort wird die Subroutine &isitthere()
aufgerufen, die die eigentliche Suche nach dem Suchmuster durchführt und der
als Argumente die rechte Seite des ODER (der nächste Teil in dem @things
-Array)
und der Datensatz selbst (%rec
) übergeben werden. @things
-Array zwei Elemente vorrücken. Diese Aufgabe übernimmt
Zeile 15, die den Zähler $i
um zwei inkrementiert.
x
UND
y
.
Wenn x
falsch ist, ist der gesamte Ausdruck falsch. Wenn aber x
wahr ist, muss
immer noch y
überprüft werden, um zu sehen, ob diese Variable ebenfalls wahr
ist. Und so funktioniert auch dieser Test. Wenn die Variable $matched
wahr
ergibt, dann ist damit die linke Seite des UND-Ausdrucks wahr, und wir rufen
&isitthere()
auf, um die rechte Seite zu testen. Andernfalls machen wir einfach
gar nichts und überspringen die nächsten zwei Elemente, sowohl das UND als
auch die rechte Seite des UND ($i+=2
, Zeile 21), um danach fortzufahren.
&isitthere()
daher nur dann auf, wenn wir bisher keine
Übereinstimmung gefunden haben.
$thing
ein richtiger Suchschlüssel steht und
$matched
wahr ist. Wir müssen hier nichts machen, da bereits eine
Übereinstimmung gefunden wurde. Deshalb muss nur die Position in dem Muster
um eins inkrementiert und die Schleife erneut gestartet werden.
Wenn Sie mir soweit folgen konnten, haben Sie den schwierigsten Teil des Skripts
bereits hinter sich. Sollten Sie noch Schwierigkeiten haben, versuchen Sie es einfach
mal selbst mit verschiedenen Suchmustern: mit einzelnen Suchelementen, Elementen,
die durch UND oder ODER getrennt sind, und Mustern mit mehreren Suchschlüsseln.
Kontrollieren Sie die Werte von $i
und $matched
beim Schleifendurchlauf (wenn Sie
mit dem Perl-Debugger bereits umgehen können, ist dies recht einfach, aber Sie
können es auch auf Papier von Hand machen).
Was also passiert in der mysteriösen &isitthere()
-Subroutine, die in der großen
while
-Schleife immer wieder aufgerufen wird? Nachdem Suchbegriff und Datensatz
gegeben sind, findet hier die eigentliche Suche statt. Ich werde darauf verzichten,
Ihnen hier den Inhalt von &isitthere()
vorzustellen, Sie finden diese Subroutine in
voller Länge als Teil des Gesamtcodes in Listing 14.3. Ich möchte Sie jedoch darauf
hinweisen, dass die Subroutine lediglich den Inhalt des Adressen-Hash durchläuft und
mit Hilfe eines regulären Ausdrucks den Suchbegriff mit jeder Zeile vergleicht. Bei
einer Übereinstimmung liefert die Subroutine 1 zurück, bei keiner Übereinstimmung
0.
Im letzten Teil der Subroutine sind alle Teile des Suchbegriffs verarbeitet, einige
Suchläufe wurden durchgeführt, und wir wissen, ob der Suchbegriff mit dem
Datensatz übereinstimmt oder nicht. Die Zeilen 30 bis 33 testen, ob eine
Übereinstimmung gefunden wurde. Wenn ja, setzen wir die Variable $bigmatch
(mindestens eine Adresse wurde als Übereinstimmung gefunden) und rufen
&print_addr()
auf, um diese Adresse auszudrucken.
Von hier an wird es einfach. Die letzte Subroutine in der Datei wird nur aufgerufen,
wenn eine Übereinstimmung gefunden wurde. Die Subroutine &print_addr()
durchläuft einfach den Datensatz-Hash und gibt die Werte aus, um den
Adreßdatensatz anzuzeigen.
sub print_addr {
my %record = @_;
print "*********************\n";
foreach my $key (qw(Name Telefon Fax Adresse EMail URL)) {
if (defined($record{$key})) {
print "$record{$key}\n";
}
}
}
Interessant an dieser Subroutine ist allein die Liste der Schlüssel in der foreach
-
Schleife. Ich habe die Schlüssel in dieser Reihenfolge aufgeführt (und mit der Funktion
qw
in Anführungszeichen gesetzt), so dass die Ausgabe in einer bestimmten
Reihenfolge erfolgt. Da Hashes in einer intern festgelegten Reihenfolge gespeichert
werden, können wir nur so dafür sorgen, dass die Daten in der korrekten Reihenfolge
ausgegeben werden. Nebenbei wird dadurch auch erreicht, dass nur die Zeilen
ausgegeben werden, die tatsächlich verfügbar waren - der Aufruf von defined
innerhalb der foreach
-Schleife stellt sicher, dass nur die Felder ausgegeben werden,
die auch tatsächlich im Datensatz existieren.
Alles OK? Nein? Manchmal ist es eine Hilfe, wenn man den ganzen Code auf einmal
sieht. Listing 14.3 enthält den vollständigen Code für adressen.pl. Wenn Sie den
Quelltext von der Website zu diesem Buch (unter http://www.typerl.com
)
heruntergeladen haben, werden Sie feststellen, dass der Code wesentlich mehr
Kommentare enthält, um deutlich zu machen, was gerade passiert.
Wie ich bereits gestern in dem Abschnitt zu
my
-Variablen angedeutet habe, können einige Perl-Versionen Schwierigkeiten mit der Verwendung dermy
-Variablen im Skript und denforeach
-Schleifen haben. Sie können das Problem umgehen, indem Sie dieforeach
-Variable einfach wie folgt vor ihrem Gebrauch deklarieren:
Listing 14.3: Der Code für adressen.pl
1: #!/usr/bin/perl -w
2: use strict;
3:
4: my $bigmatch = 0; # wurde etwas gefunden?
5: my %rec = (); # zu durchsuchender Datensatz
6: my $search = ''; # Suchmuster
7:
8: $search = &getpattern(); # Eingabeaufforderung für das Muster
9:
10: while () { # durchsucht die Adressdatei
11: %rec = &read_addr();
12: if (%rec) { # Datensatz gefunden
13: &perform_search($search, %rec);
14: } else { # Ende der Adressdatei, Aufräumarbeiten
15: if (!$bigmatch) {
16: print "Nichts gefunden.\n";
17: } else { print "*********************\n"; }
18: last; # Verlassen, wir sind fertig
19: }
20: }
21:
22: sub getpattern {
23: my $in = ''; # Eingabe
24: print 'Wonach soll gesucht werden? ';
25: chomp($in = <STDIN>);
26: return $in;
27: }
28:
29: sub read_addr {
30: my %curr = (); # aktueller Datensatz
31: my $key = ''; # temp. Schlüssel
32: my $val = ''; # temp. Wert
33:
34: while (<>) { # bricht bei EOF ab
35: chomp;
36: if ($_ ne '---') { # Datensatz-Trennzeichen
37: ($key, $val) = split(/: /,$_,2);
38: $curr{$key} = $val;
39: }
40: else { last; }
41: }
42: return %curr;
43: }
44:
45: sub perform_search {
46: my ($str, %rec) = @_;
47: my $matched = 0; # gesamte Übereinstimmung
48: my $i = 0; # Position innerhalb des Suchmusters
49: my $thing = ''; # temporäres Wort
50:
51: my @things = $str =~ /("[^"]+"|\S+)/g; # in Suchelemente aufspalten
52:
53: while ($i <= $#things) {
54: $thing = $things[$i]; # Suchelement, UND oder ODER
55: if ($thing eq 'ODER' || $thing eq 'oder') { # ODER-Fall
56: if (!$matched) { # noch keine Übereinstimmung, nächstes
# Element
57: $matched = &isitthere($things[$i+1], %rec);
58: }
59: $i += 2; # ODER überspringen und nächstes Element
60: }
61: elsif ($thing eq 'UND' || $thing eq 'und') { # UND-Fall
62: if ($matched) { # Übereinstimmung gefunden, andere Seite
# prüfen
63: $matched = &isitthere($things[$i+1], %rec);
64: }
65: $i += 2; # UND überspringen und nächstes Element
66: }
67: elsif (!$matched) { # noch keine Übereinstimmung
68: $matched = &isitthere($thing, %rec);
69: $i++; # weiter!
70: }
71: else { $i++; } # $match wurde gefunden, weiter zum
# nächsten Element
72: }
73:
74: if ($matched) { # alle Schlüssel durchgearbeitet, gab es
# eine Übereinstimmung?
75: $bigmatch = 1; # Ja, wir haben etwas gefunden
76: print_addr(%rec); # Datensatz ausgeben
77: }
78: }
79:
80: sub isitthere { # einfacher Test
81: my ($pat, %rec) = @_;
82: foreach my $line (values %rec) {
83: if ($line =~ /$pat/) {
84: return 1;
85: }
86: }
87: return 0;
88: }
89:
90: sub print_addr {
91: my %record = @_;
92: print "*********************\n";
93: foreach my $key (qw(Name Telefon Fax Adresse EMail URL)) {
94: if (defined($record{$key})) {
95: print "$record{$key}\n";
96: }
97: }
98: }
Das zweite Beispielskript übernimmt eine Protokolldatei, wie Sie von Webservern erzeugt wird, und erstellt aus den darin enthaltenen Daten eine Statistik. Die meisten Webserver legen Dateien dieser Art an, mit denen sie unter anderem verfolgen, wie viele Besucher (»Hits«) es für eine Website gab, welche Dateien angefordert wurden und von welchen Sites die Anfragen kamen.
Im Web gibt es bereits viele Programme zur Analyse von Protokolldateien (einschließlich der Programme, die üblicherweise für Ihren Webserver zur Verfügung stehen). Deshalb ist dieses Beispiel auch nicht gerade neu. Die Statistiken, die damit erstellt werden, sind relativ einfach. Sie können dieses Skript jedoch ohne weiteres umschreiben, so dass es jede beliebige von Ihnen gewünschte Information mit aufnimmt. Es ist eine gute Ausgangsbasis zum Verarbeiten von Webprotokollen beziehungsweise ein gutes Muster, wenn Protokolldateien anderer Programme zu verarbeiten sind.
Das Skript weblog.pl wird mit einem Argument, der Protokolldatei, aufgerufen. Auf
vielen Webservern werden diese Dateien access_log
genannt und folgen dem
sogenannten common log-Format. Das Skript arbeitet erst einmal eine Weile vor sich
hin. Dabei gibt es die Datumsstempel der bearbeiteten Protokolle aus, damit Sie
erkennen, dass das Skript noch arbeitet. Anschließend werden einige Ergebnisse
ausgegeben. Sehen Sie im folgenden, wie eine Ausgabe aussehen kann (das
untenstehende Beispiel ist von den Protokollen meines eigenen Webservers
www.lne.com
):
% weblog.pl access_log
Log-Dateien verarbeiten....
Verarbeite 09/Apr/1998
Verarbeite 10/Apr/1998
Verarbeite 11/Apr/1998
Verarbeite 12/Apr/1998
Auswertung der Log-Datei:
Gesamtzahl der Treffer: 55789
Gesamtzahl der fehlgeschlagenen Treffer: 1803 (3.23%)
(erfolgreiche) HTML-Dateien: 18264 (33.83%)
Anzahl der Hosts: 5911
Anzahl der Domänen: 2121
Die beliebtesten Dateien:
/Web/index.html (2456 Treffer)
/lemay/index.html (1711 Treffer)
/Web/Title.gif (1685 Treffer)
/Web/HTML3.2/3.2thm.gif (1669 Treffer)
/Web/JavaProf/javaprof_thm.gif (1662 Treffer)
Die beliebtesten Hosts:
202.185.174.4 (487 Treffer)
vader.integrinautics.com (440 Treffer)
linea15.secsa.podernet.com.mx (437 Treffer)
lobby.itmin.com (284 Treffer)
pyx.net (256 Treffer)
Die beliebtesten Domänen:
mindspring.com (3160 Treffer)
aol.com (1808 Treffer)
uu.net (792 Treffer)
grid.net (684 Treffer)
compuserve.com (565 Treffer)
Die Ausgabe hier zeigt, um Platz zu sparen, nur die ersten fünf Dateien, Hosts und Domänen. Sie können das Skript allerdings so konfigurieren, dass es eine beliebige Anzahl dieser Statistiken ausgibt.
Der Unterschied zwischen einem Host und einer Domäne mag vielleicht nicht direkt
ersichtlich sein. Ein Host ist der vollständige Hostname des Systems, das auf den
Webserver zugegriffen hat, und kann dynamisch zugewiesene Adressen und Proxy-
Server mit umfassen. So unterscheidet sich der Host dialup124.servers.foo.com
vom Host dialup567.servers.foo.com
. Die Domäne andererseits bezeichnet eine
größere Gruppe von mehreren Hosts und besteht in der Regel aus zwei oder drei
Teilen. So ist foo.com
ein Domäne, ebenso wie aol.com
oder demon.co.uk
. Die
Domänen-Listings neigen dazu, einzelne Host-Einträge in größere Gruppen
zusammenzufassen - alle Hosts im Wirkungsbereich von aol.com
erscheinen in der
Domänenliste als Hit von aol.com
.
Beachten Sie, dass es sich bei einem einfachen Hit um eine HTML-Seite, ein Bild, ein eingereichtes Formular oder eine andere beliebige Datei handeln kann. Es gibt normalerweise wesentlich mehr Hits als tatsächliche Zugriffe auf eine Seite (page views). Dieses Skript macht den Unterschied deutlich, indem HTML-Anfragen getrennt von der Gesamtzahl der Hits gezählt werden.
Da das Skript weblog.pl Webprotokolle verarbeitet, ist es von Vorteil, wenn man weiß, wie diese Protokolldateien aussehen. Webprotokolle speichern einen Treffer pro Zeile und jede Zeile in dem sogenannten allgemeinen Protokollformat (allgemein, da das Format mehreren Webservern gemeinsam ist). Die meisten Webserver erzeugen ihre Protokolldateien in diesem Format beziehungsweise können entsprechend konfiguriert werden (viele Server verwenden eine erweiterte Form des allgemeinen Protokollformats, das mehr Informationen enthält). Eine Zeile einer Protokolldatei im allgemeinen Protokollformat kann folgendermaßen aussehen (diese Zeile steht aus drucktechnischen Gründen in zwei Zeilen, in Wirklichkeit steht sie jedoch nur in einer Zeile):
proxy2bh.powerup.com.au - - [03/Apr/1998:00:09:02 -0800]
"GET /lemay/ HTTP/1.0" 200 4621
Die einzelnen Elemente jeder Zeile der Protokolldatei sind:
proxy2bh.powerup.com.au
).
ident
(ein Unix-Programm zum Identifizieren von Benutzern) oder durch den Benutzer
Ihrer Site selbst bekanntgemacht. Diese zwei Teile erscheinen normalerweise als
zwei Bindestriche (- -
), wenn der Benutzername nicht ermittelt werden kann.
GET
steht für das Anfordern einer Datei oder das
Abrufen eines Formulars, POST
für das Einreichen eines Formulars auf anderem
Wege und HEAD
für das Auslesen von Header-Informationen zu einer Datei.
/lemay/
.
HTTP/1.0
.
Natürlich sind nicht all diese Elemente einer Protokolldatei von Interesse für ein Skript zum Aufbau einer Statistik, und viele werden Sie erst verstehen, wenn Sie wissen, wie Webserver funktionieren. Ein paar jedoch, wie der Host, das Datum, der Dateiname und der Rückgabecode können extrahiert und für jede Zeile der Datei verarbeitet werden.
Der logische Ablauf dieses Skripts ist leichter zu verfolgen als für das Skript adressen.pl. Es gibt eigentlich nur zwei größere Schritte - die Verarbeitung des Protokolls und die Erzeugung der Statistik. Dabei werden wir von einer Reihe von Subroutinen unterstützt.
Um genau zu sein, der gesamte Code für dieses Skript ist in Subroutinen
untergebracht. Der Rumpf des Codes besteht aus einer Reihe von globalen Variablen
und dem Aufruf von zwei Subroutinen: &process_log()
und &print_results()
.
Die globalen Variablen dienen dazu, die verschiedenen statistischen Daten und sonstigen Informationen über Teile der Protokolldatei zu speichern. Da viele dieser statistischen Daten Hashes sind, wäre die Verwendung von lokalen Variablen und das Weiterreichen der Daten zu kompliziert. In diesem Fall ist die Verwaltung der Variablen einfacher, wenn sie globaler Art sind. Zu den globalen Daten, die wir verfolgen, gehören:
Darüber hinaus gibt es noch zwei weitere globale Variablen:
$topthings
speichert eine Zahl, die angibt, wie viele Einträge Sie für
die »beliebtesten« Teile der Statistik ausgeben wollen. In meinem Beispiel habe ich
den Wert für $topthings
auf 5 gesetzt und damit eine nette kurze Ausgabe
erzeugt. Wenn Sie den Wert auf 20 setzen, werden die ersten 20 Dateien, Hosts
und Domänen ausgegeben.
$default
sollte auf die Standard-HTML-Seite Ihres Webservers
gesetzt werden. Meistens heißt diese index.html
oder Home.html
. Dies ist die
Datei, die als Hauptdatei für ein Verzeichnis dient, wenn der Benutzer keine
bestimmte Datei anfordert. Normalerweise lautet der Name index.html
.
Diese zwei Variablen legen fest, wie sich das Skript selbst verhält. Wir hätten diese Variablen auch tief im Innern des Programms verbergen können. Doch dadurch, dass wir sie hier direkt am Anfang aufgeführt haben, können Sie oder alle anderen, die das Skript nutzen, das Gesamtverhalten des Skripts ändern, ohne nach den zu ändernden Variablen lange suchen zu müssen. Diese Verfahrensweise gehört zum »guten Programmierstil«, unabhängig davon, welche Programmiersprache Sie verwenden.
Der erste Teil des weblog.pl-Skripts besteht aus der Subroutine &process_log()
, die
die einzelnen Zeilen der Protokolldatei durchläuft und die statistischen Daten aus der
Zeile speichert. Ich werde Ihnen nicht jede Zeile dieser Subroutine erläutern, aber ich
zeige Ihnen die wichtigsten Teile. Den vollständigen Code können Sie in Listing 14.7
am Ende dieses Kapitels einsehen.
Das Kernstück der Subroutine &process_log()
ist eine weitere while (<>)
-Schleife,
um die Zeilen der Eingabe einzeln einzulesen. Im Gegensatz zu adressen.pl liest dieses
Skript die Datei von Anfang bis Ende ein.
Für die Verarbeitung der Zeile splitten wir sie zuerst in ihre Bestandteile und speichern
diese Teile in einem Hash, dessen Schlüssel der Teilename ist ('site'
, 'file'
und so
weiter). Für das Aufsplitten gibt es eine separate Subroutine namens &splitline()
,
die in Listing 14.4 zu sehen ist.
Listing 14.4: Die Subroutine &splitline()
1: sub splitline {
2: my $in = $_[0];
3: my %line = ();
4: if ($in =~ /^([^\s]+)\s # Site
5: ([\w-]+\s[\w-]+)\s # Benutzer
6: \[([^\]]+)\]\s # Datum
7: \"(\w+)\s # Protokoll
8: (\/[^\s]*)\s # Datei
9: ([^"]+)\"\s # HTTP-Version
10: (\d{3})\s # Rückgabe-Code
11: ([\d-]+) # übertragene Bytes
12: /x) {
13: $line{'site'} = $1;
14: $line{'date'} = $3;
15: $line{'file'} = $5;
16: $line{'code'} = $7;
17: return %line;
18: } else { return (); }
19: }
Das erste, was Ihnen bei dieser Subroutine wahrscheinlich ins Auge fällt, ist der nicht enden wollende reguläre Ausdruck in der Mitte von Zeile 4 bis Zeile 11. Er ist so häßlich, dass er sechs Zeilen belegt! Und Kommentare erforderlich macht! Dieser reguläre Ausdruck hat die Form erweiterter regulärer Ausdrücke. Ich habe diese Ausdrücke bereits in dem Abschnitt »Vertiefung« in Kapitel 5, »Mit Hashes arbeiten«, eingehend beschrieben. Hier eine kurze Zusammenfassung: Angenommen Sie haben einen besonders ekligen regulären Ausdruck wie den in diesem Beispiel (aus drucktechnischen Gründen steht er auf zwei Zeilen, da er nicht in eine Zeile paßt!):
if ($in =~ /^([^\s]+)\s([\w-]+\s[\w-]+)\s\[([^\]]+)\]\s\"(\w+)
\(\/[^\s]*)\s([^"]+)\"\s(\d{3})\s([\d-]+)/)
Sehr wahrscheinlich brauchen Sie entweder eine unendliche Geduld oder sehr starke
Beruhigungsmittel, um diesen Ausdruck aufzuschlüsseln und zu verstehen. Und das
Debuggen dieses Ausdrucks ist auch nicht sehr lustig. Wenn Sie jedoch an das Ende
des Ausdrucks die Option /x
setzen (wie hier in Zeile 12), können Sie den regulären
Ausdruck aufsplitten und auf verschiedene Zeilen verteilen, die Sie außerdem noch mit
Kommentaren versehen können. Alle Leerzeichen darin werden ignoriert. Wenn Sie
im Text eine Übereinstimmung auf Leerzeichen suchen wollen, müssen Sie \s
verwenden. Die Option /x
erleichtert lediglich das Lesen und Debuggen des regulären
Ausdrucks.
Unser regulärer Ausdruck geht von dem allgemeinen Protokollformat (common log format) aus, das ich bereits oben beschrieben habe:
\s
, damit sich das erweiterte Muster
anwenden läßt).
\w
-Klasse sind sie nicht mit eingeschlossen.
[]
).
GET
, HEAD
usw.). Der String beginnt mit einem
Anführungszeichen gefolgt von einem oder mehreren Zeichen (das schließende
Anführungszeichen steht nach der HTTP-Version in Zeile 9).
/
)
gefolgt von einer 0 oder mehreren anderen Zeichen und endet mit einem
Whitespace-Zeichen.
\d+
zu verwenden,
aber so habe ich eine Gelegenheit, Ihnen die Verwendung des Musters {3}
zu
zeigen).
Jedes Element dieses regulären Ausdrucks wird in einem geklammerten Ausdruck (und einer Übereinstimmungsvariablen) gespeichert, wobei die zusätzlichen Klammern oder Anführungszeichen entfernt werden. Sobald das Pattern Matching beendet ist, können wir die verschiedenen übereinstimmenden Teile in einem Hash ablegen. Beachten Sie, dass wir nur die Hälfte der Übereinstimmungen in dem Hash ablegen. Wir müssen nur das abspeichern, was wir am Ende auch nutzen wollen. Wenn Sie aber dieses Beispiel erweitern wollen, um Statistiken über weitere Teile der Treffer zu erstellen, müssen Sie lediglich Zeilen einfügen, die diese Übereinstimmungen dem Hash hinzufügen. Sie brauchen den regulären Ausdruck nicht zu verändern, um mehr Informationen zu erhalten.
Nachdem die Zeile jetzt in ihre einzelnen Elemente zerlegt ist, kehren wir von der
Subroutine &splitline()
zurück zu der Hauptroutine &process_log()
. Diese Routine
überprüft als nächstes alle fehlgeschlagenen Treffer. Wenn eine Zeile im Webprotokoll
nicht dem Muster entspricht - was bei einigen der Fall ist -, liefert die Subroutine
&splitline()
Null zurück. Dieses Ergebnis wird als fehlgeschlagener Treffer
interpretiert, der dann zu der Zahl der fehlgeschlagenen Treffer addiert wird.
Anschließend wird der Rest der Schleife übersprungen, um mit der nächsten Zeile
fortzufahren:
if (!%hit) { # mißgestaltete Zeile im Webprotokoll
$failhits++;
next;
}
Der nächste Schritt im Skript ist ein Entgegenkommen an all diejenigen, die das Skript ausführen. Die Verarbeitung einer beliebig großen Protokolldatei kann lange dauern, und manchmal ist es schwer zu sagen, ob Perl noch die Protokolldatei bearbeitet oder ob sich das System aufgehängt hat und keinen Wert mehr liefern wird. Dieser Teil des Skripts gibt eine Nachricht mit dem Datum der Zeilen aus, die gerade verarbeitet werden. Jedesmal, wenn die Treffer eines Tages vollständig bearbeitet worden sind, erscheint eine neue Nachricht, die das Fortschreiten von Perl in der Datei anzeigt:
$dateshort = &getday($hit{'date'});
if ($currdate ne $dateshort) {
print "Verarbeite $dateshort\n";
$currdate = $dateshort;
}
In diesem Fragment ist &getday()
eine kurze Subroutine, die den Monat und den Tag
aus dem Datumsfeld ausliest. Dabei wird ein Muster verwendet, so dass Monat und
Tag mit dem gerade verarbeiteten Datum verglichen werden können (auf den
Ausdruck des Codes für &getday()
verzichte ich, da Sie ihn in dem vollständigen
Listing am Ende des Kapitels finden). Sind sie unterschiedlich, wird eine Nachricht
ausgegeben und die Variable $currdate
aktualisiert.
Zusätzlich zu den Zeilen in der Protokolldatei, die nicht dem Protokollformat entsprechen, werden auch jene Zeilen als fehlgeschlagene Treffer bezeichnet, die zwar dem Muster entsprechen, aber nicht dazu führten, dass wirklich eine Datei zurückgeliefert wurde (falsche URL-Angaben oder Dateien, die verschoben wurden, lösen diese Art von Treffer aus). Diese Treffer werden in der Protokolldatei mit einem Fehlercode aufgezeichnet, der mit 4 beginnt (vielleicht ist Ihnen bereits der Error 404 im Web aufgefallen). Der Rückgabecode gehört mit zu den Elementen der Zeile, die wir gespeichert hatten. Deshalb ist die Überprüfung ein einfacher Musterabgleich:
if ($hit{'code'} =~ /^4/) { # 404, 403, etc. (Fehler)
$failhits++;
Der else
-Teil dieser if
-Anweisung betrifft alle anderen Treffer - gemeint sind damit
alle erfolgreichen Treffer, die tatsächlich HTML-Dateien oder Grafiken zurückgeliefert
haben. Diese Treffer haben einen Rückgabecode von 200 oder 304.
} elsif ($hit{'code'} =~ /200|304/) { # behandelt nur erfolgreiche Treffer
Webserver sind so eingerichtet, dass sie eine Standarddatei, in der Regel index.html
,
zurückliefern, wenn eine URL angefordert wird, die einem Verzeichnisnamen
entspricht. Das bedeutet, dass eine Anfrage nach /web/
und eine Anfrage nach /web/
index.html
sich auf die gleiche Datei beziehen, jedoch in der Protokolldatei als
unterschiedliche Einträge erscheinen. Um Verzeichnisse und Standarddateien als
einen Eintrag zu behandeln, gibt es einige Zeilen, die prüfen, ob die angeforderte
Datei mit einem Slash endet, und wenn ja, dafür sorgen, dass der Standarddateiname
hinten angehängt wird. Die Standarddatei, wie ich bereits oben erwähnt habe, wird
durch die Variable $default
definiert:
if ($hit{'file'} =~ /\/$/) { # slashes werden zu $default
$hit{'file'} .= $default;
}
Nachdem wir dies erledigt haben, können wir die Verarbeitung damit abschließen,
dass wir die Variable $htmlhits
inkrementieren, wenn es sich bei der Datei um eine
HTML-Datei handelt, und die Hashes für die Site und für die Datei aktualisieren:
if ($hit{'file'} =~ /\.html?$/) { # .htm oder .html
$htmlhits++;
}
$hosts{ $hit{'site'} }++;
$files{ $hit{'file'} }++;
Damit sind wir ans Ende der while
-Schleife angelangt, die mit der nächsten Zeile in
der Datei von vorne beginnt. Die Schleife wird so lange durchlaufen, bis alle Zeilen
verarbeitet sind. Danach gehen wir zum Ausgeben der Ergebnisse dieses Skripts über.
Die Subroutine &process_log()
verarbeitet die Protokolldatei zeilenweise und ruft zu
ihrer Unterstützung die Subroutinen &splitline()
und &getday()
auf. Der zweite Teil
unseres weblog.pl-Skripts besteht aus der Subroutine &print_results()
, die ebenfalls
auf einige weitere Subroutinen zur Unterstützung zurückgreift. Der größte Teil der
Subroutine besteht jedoch aus einer Reihe von print
-Anweisungen, um die
verschiedenen Statistiken auszugeben.
Die ersten Zeilen geben die Gesamtzahl der Treffer, Gesamtzahl der fehlgeschlagenen
Treffer und Gesamtzahl der HTML-Treffer aus. Die letzteren werden auch als Prozent
der gesamten Treffer aufgeschlüsselt, wobei die HTML-Treffer sich nur auf die
Gesamtsumme der erfolgreichen Treffer beziehen. Wir erhalten diese Werte mit ein
wenig Mathematik und der Anweisung printf
:
print "Auswertung der Log-Datei:\n";
print "Gesamtzahl der Treffer: $totalhits\n";
print "Gesamtzahl der fehlgeschlagenen Treffer: $failhits (";
printf('%.2f', $failhits / $totalhits * 100);
print "%)\n";
print "(erfolgreiche) HTML-Dateien: $htmlhits (";
printf('%.2f', $htmlhits / ($totalhits - $failhits) * 100);
print "%)\n";
Als nächstes kommt die Gesamtzahl der Hosts. Diesen Wert erhalten wir, indem wir
die Schlüssel aus dem Hash %hosts
herausziehen und in einer Liste ablegen.
Anschließend werten wir diese Liste in einem skalaren Kontext aus (mit Hilfe der
Funktion scalar
).
print 'Anzahl der Hosts: ';
print scalar(keys %hosts);
print "\n";
Um die Anzahl der Domänen zu ermitteln, müssen wir den Hash %hosts
verarbeiten,
um die Hosts ihren Domänen zuzuordnen, und einen neuen Hash (%domains
)
einrichten, der die Anzahl der Treffer für die Domänen aufnimmt. Dazu verwenden
wir eine Subroutine namens &getdomains()
, die ich im nächsten Abschnitt
besprechen werde. Gehen wir einfach davon aus, dass wir unseren Hash %domains
bereits haben. Wir können auf die Schlüssel dieses Hash den gleichen Trick mit
scalar
anwenden, um die Anzahl der Domänen zu ermitteln:
my %domains = &getdomains(keys %hosts);
print 'Anzahl der Domänen: ';
print scalar(keys %domains);
print "\n";
Als letztes sind die beliebtesten Dateien, Hosts und Domänen auszudrucken. Für die
Ermittlung dieser Werte gibt es die Subroutine &gettop()
, die jeden Hash nach seinen
Werten sortiert (die Häufigkeit, mit der jede Datei, Host oder Domäne in einem
Treffer vorkam) und dann einen Array deskriptiver Strings einrichtet mit den
Schlüsseln und Werten im Hash. Das Array enthält nur die 5 oder 10 (oder was auch
immer Sie als Wert in $topthings
ablegen) beliebtesten Dateien, Hosts oder
Domänen. Doch gleich mehr zu der Subroutine &gettops()
.
Jedes dieser Arrays wird zum Schluß ausgegeben. Hier sehen Sie den Code für die Ausgabe der Dateien:
print "Die beliebtesten Dateien: \n";
foreach my $file (&gettop(%files)) {
print " $file\n";
}
Noch sind wir nicht fertig. Es fehlen uns noch die Hilfsroutinen zur Ausgabe der
Statistiken: &getdomains()
, um die Domänen aus dem %hosts
-Hash zu extrahieren
und die Statistik neu zu berechnen, und &gettop()
, um einen Hash von Schlüsseln
und Frequenzwerten zu übernehmen und die beliebtesten Elemente zurückzuliefern.
Die Subroutine &getdomains()
finden Sie in Listing 14.5.
Listing 14.5: Die Subroutine &getdomains()
1: sub getdomains {
2: my %domains = ();
3: my ($sd,$d,$tld); # sekundäre Domäne, Domäne, oberste Domäne
4: foreach my $host (@_) {
5: my $dom = '';
6: if($host =~ /(([^.]+)\.)?([^.]+)\.([^.]+)$/ ) {
7: if (!defined($1)) { # nur zwei Domänen (i.e. aol.com)
8: ($d,$tld) = ($3, $4);
9: } else { # eine gewöhnliche Domäne x.y.com etc
10: ($sd, $d, $tld) = ($2, $3, $4);
11: }
12: if ($tld =~ /\D+/) { # ignoriert reine IP-Zahlen
13: if ($tld =~ /com|edu|net|gov|mil|org$/i) { # US TLDs
14: $dom = "$d.$tld";
15: } else { $dom = "$sd.$d.$tld"; }
16: $domains{$dom} += $hosts{$host};
17: }
18: } else { print "Fehlerhaft: $host\n"; }
19: }
20: return %domains;
21: }
Diese Subroutine ist nicht so kompliziert, wie sie aussieht. Ich gehe dabei von ein paar Grundvoraussetzungen für den Hostnamen aus: dass zum Beispiel jeder Hostname aus mehreren Teilen besteht, die durch Punkte getrennt sind, und dass die Domäne abhängig von ihrem Namen entweder aus den zwei oder drei ganz rechts stehenden Teilen besteht. In dieser Subroutine werden wir dann jeden Host auf seine eigentliche Domäne reduzieren und dann diesen Domänennamen als Index für einen neuen Hash nutzen, wobei wir alle ursprünglichen Treffer für den Hostnamen in dem neuen domänenbasierten Hash speichern.
Die Hauptarbeit dieser Subroutine wird in der foreach
-Schleife, die in Zeile 4 startet,
geleistet. Das Argument, das dieser Subroutine übergeben wird, ist ein Array mit den
Hostnamen aus dem %hosts
-Array. Dabei durchläuft die Schleife alle Hostnamen, um
sicherzustellen, dass sie alle berücksichtigt werden.
Der erste Teil der foreach
-Schleife ist der lange und abschreckende reguläre Ausdruck
in Zeile 6. Dieser Ausdruck greift sich die letzten zwei Teile des Hostnamens und,
wenn es kann, auch die letzten drei (einige Hostnamen bestehen nur aus zwei Teilen,
die jedoch auch vom regulären Ausdruck erfaßt werden). Von Zeile 7 bis 11 wird
geprüft, wie viele Teile wir haben (2 oder 3). Diese Teile werden den Variablen $sd
,
$d
und $tld
zugewiesen ($sd
steht für sekundäre Domäne, $d
für Domäne und $tld
für Top-Level-Domäne, falls Sie sie auseinanderhalten wollen).
Der zweite Teil der Schleife legt fest, ob wir zwei oder drei Teile des Hostnamens als
eigentliche Domäne verwenden wollen, und ignoriert in Zeile 12 alle Hosts, die aus
IP-Nummern anstelle von eigentlichen Domänennamen bestehen. Die rein
willkürliche Regel, nach der ich entschieden habe, ob eine Domäne aus zwei oder drei
Teilen besteht, lautet: Handelt es sich bei der Top-Level-Domäne (den am weitesten
rechts gelegenen Teil des Hostnamens) um eine US-Domäne wie .com
, .edu
etc. (die
vollständige Liste sehen Sie in Zeile 13), dann hat die Domäne nur zwei Teile. Dazu
gehören aol.com
, mit.edu
, whitehouse.gov
etc. Lautet die Top-Level-Domäne
anders, ist es mit großer Wahrscheinlichkeit eine landesspezifische Domäne wie .us
,
.au
, .mx
etc. Diese Domänen verwenden in der Regel drei Teile, um auf eine Site
Bezug zu nehmen (zum Beispiel citygate.co.uk
oder monash.edu.au
). Zwei Teile
wären in diesem Falle nicht genau genug (edu.au
bezieht sich auf alle Universitäten in
Australien und nicht auf eine spezielle namens edu
).
Das ist also die Aufgabe der Zeilen 13 bis 15: einen Domänennamen aus zwei oder
drei Teilen zusammenzusetzen und in dem String $dom
zu speichern. Wenn wir den
Domänennamen zusammengesetzt haben, können wir ihn als Schlüssel für den neuen
Hash verwenden und die Treffer, die wir für den ursprünglichen Host ermittelt haben,
übertragen (Zeile 16). Nachdem der Domänen-Hash eingerichtet ist, sollten alle
Treffer in dem Host-Hash auch in dem Domänen-Hash berücksichtigt sein, so dass
wir diesen Hash an die Subroutine &print_results
zurückgeben können.
Noch eine Sache: In Zeile 18 prüft die Subroutine auf Fehler im Hostnamen. Wenn der Ausdruck des Mustervergleichs in Zeile 6 zu keiner Übereinstimmung führt, muss in der Tat ein sehr seltsamer Hostname vorliegen, und wir geben eine entsprechende Nachricht aus. Im allgemeinen sollte eine solche Nachricht allerdings nicht erscheinen, da ein abartiger Hostname in der Protokolldatei normalerweise bedeutet, dass ein abartiger Hostname auf dem Host selbst vorliegt, was eigentlich über das Internet nur schwer möglich sein dürfte.
Noch eine Subroutine, und dann können wir den Lehrstoff dieser Woche zur Seite
legen, ein Bierchen trinken und feiern, dass wir zwei Drittel dieses Buches bereits
bewältigt haben. Die letzte Subroutine &gettop()
übernimmt einen Hash, sortiert ihn
nach Werten und schneidet dann die obersten X Elemente ab, wobei X durch die
Variable &topthings
gegeben ist. Die Subroutine liefert ein Array von Strings zurück,
wobei jeder String den Schlüssel und den Wert für die obersten X Elemente in einer
Form enthält, die leicht durch die Subroutine &print_results()
, von der aus diese
Subroutine aufgerufen wurde, ausgegeben werden kann. Sehen Sie dazu das Listing
14.6.
Listing 14.6: Die Subroutine &gettop()
1: sub gettop {
2: my %hash = @_;
3: my $i = 1;
4: my @topkeys = ();
5: foreach my $key (sort { $hash{$b} <=> $hash{$a} } keys %hash) {
6: if ($i <= $topthings) {
7: push @topkeys, "$key ($hash{$key} hits)";
8: $i++;
9: }
10: }
11: return @topkeys;
12: }
Listing 14.7 enthält den vollständigen Code für das Skript weblog.pl.
Je nach Perl-Version sollten Sie auch hier an die
my
-Variablen innerhalb derforeach
-Schleifen denken. Details finden Sie in dem Hinweis direkt vor dem Listing 14.3.
Listing 14.7: Der Code für weblog.pl
1: #!/usr/bin/perl -w
2: use strict;
3:
4: my $default = 'index.html'; # Angabe Ihrer Standard-HTML-Datei
5: my $topthings = 30; # Anzahl der zu protokollierenden
# Dateien, Sites etc.
6: my $totalhits = 0;
7: my $failhits = 0;
8: my $htmlhits = 0;
9: my %hosts= ();
10: my %files = ();
11:
12: &process_log();
13: &print_results();
14:
15: sub process_log {
16: my %hit = ();
17: my $currdate = '';
18: my $dateshort = '';
19: print "Log-Dateien verarbeiten....\n";
20: while (<>) {
21: chomp;
22: %hit = splitline($_);
23: $totalhits++;
24:
25: # Prüfen auf fehlerhafte Zeilen
26: if (!%hit) { # fehlerhafte Zeilen im Webprotokoll
27: $failhits++;
28: next;
29: }
30:
31: $dateshort = &getday($hit{'date'});
32: if ($currdate ne $dateshort) {
33: print "Verarbeite $dateshort\n";
34: $currdate = $dateshort;
35: }
36:
37: # nach 404ern suchen
38: if ($hit{'code'} =~ /^4/) { # 404, 403, etc. (Fehler)
39: $failhits++;
40: # andere Dateien
41: } elsif ($hit{'code'} =~ /200|304/) {
# nur im Erfolgsfall bearbeiten
42: if ($hit{'file'} =~ /\/$/) { # slashes werden zu $default
43: $hit{'file'} .= $default;
44: }
45:
46: if ($hit{'file'} =~ /\.html?$/) { # .htm oder .html
47: $htmlhits++;
48: }
49:
50: $hosts{ $hit{'site'} }++;
51: $files{ $hit{'file'} }++;
52: }
53: }
54: }
55:
56: sub splitline {
57: my $in = $_[0];
58: my %line = ();
59: if ($in =~ /^([^\s]+)\s # Site
60: ([\w-]+\s[\w-]+)\s # Benutzer
61: \[([^\]]+)\]\s # Datum
62: \"(\w+)\s # Protokoll
63: (\/[^\s]*)\s # Datei
64: ([^"]+)\"\s # HTTP-Version
65: (\d{3})\s # Rückgabe-Code
66: ([\d-]+) # übertragene Bytes
67: /x) {
68: # wir sind nur an bestimmten Daten interessiert
69: # (zufällig jede 2. Information)
70: $line{'site'} = $1;
71: $line{'date'} = $3;
72: $line{'file'} = $5;
73: $line{'code'} = $7;
74: return %line;
75: } else { return (); }
76: }
77:
78: sub getday {
79: my $date;
80: if ($_[0] =~ /([^:]+):/) {
81: $date = $1;
82: return $date;
83: } else {
84: return $_[0];
85: }
86: }
87:
88: sub print_results {
89: print "Auswertung der Log-Datei:\n";
90: print "Gesamtzahl der Treffer: $totalhits\n";
91: print "Gesamtzahl der fehlgeschlagenen Treffer: $failhits (";
92: printf('%.2f', $failhits / $totalhits * 100);
93: print "%)\n";
94:
95: print "(erfolgreiche) HTML-Dateien: $htmlhits (";
96: printf('%.2f', $htmlhits / ($totalhits - $failhits) * 100);
97: print "%)\n";
98:
99: print 'Anzahl der Hosts: ';
101: print scalar(keys %hosts);
102: print "\n";
103:
104: my %domains = &getdomains(keys %hosts);
105: print 'Anzahl der Domänen: ';
106: print scalar(keys %domains);
107: print "\n";
108:
109: print "Die beliebtesten Dateien: \n";
110: foreach my $file (&gettop(%files)) {
111: print " $file\n";
112: }
113: print "Die beliebtesten Hosts: \n";
114: foreach my $host (&gettop(%hosts)) {
115: print " $host\n";
116: }
117:
118: print "Die beliebtesten Domänen: \n";
119: foreach my $dom (&gettop(%domains)) {
120: print " $dom\n";
121: }
122: }
123:
124: sub getdomains {
125: my %domains = ();
126: my ($sd,$d,$tld); # sekundäre Domäne, Domäne, oberste Domäne
127: foreach my $host (@_) {
128: my $dom = '';
129: if($host =~ /(([^.]+)\.)?([^.]+)\.([^.]+)$/ ) {
130: if (!defined($1)) { # nur zwei Domänen (i.e. aol.com)
131: ($d,$tld) = ($3, $4);
132: } else { # eine normale Domäne x.y.com etc
133: ($sd, $d, $tld) = ($2, $3, $4);
134: }
135: if ($tld =~ /\D+/) { # ignoriert reine IP-Zahlen
136: if ($tld =~ /com|edu|net|gov|mil|org$/i) { # US TLDs
137: $dom = "$d.$tld";
138: } else { $dom = "$sd.$d.$tld"; }
139: $domains{$dom} += $hosts{$host};
140: }
141: } else { print "Fehlerhaft: $host\n"; }
142: }
143: return %domains;
144: }
145:
146: sub gettop {
147: my %hash = @_;
148: my $i = 1;
149: my @topkeys = ();
150: foreach my $key (sort { $hash{$b} <=> $hash{$a} } keys %hash) {
151: if ($i <= $topthings) {
152: push @topkeys, "$key ($hash{$key} hits)";
153: $i++;
154: }
155: }
156: return @topkeys;
157: }
Meistens wird Ihnen das Wissen in Programmierbüchern mit vielen Worten, aber zu wenigen praktischen Codebeispielen vermittelt, so dass Sie oft Schwierigkeiten haben, das Erlernte umzusetzen. Ich möchte zwar nicht behaupten, dass dieses Buch zu den wortkargen gehört, mit diesen Beispielkapiteln möchte ich Ihnen jedoch etwas längere Programme präsentieren, die richtige Probleme lösen und demonstrieren, wie ein reales Skript zusammengesetzt wird.
In der heutigen Lektion haben wir zwei längere Skripts unter die Lupe genommen: eine einfache Adreßdatei mit Suchfunktionen, die eine textbasierte Datenbank mit Namen und Adressen verwendet. Das Skript zur Verarbeitung dieser Datei ermöglicht es Ihnen, ein relativ komplexes Suchmuster zu verarbeiten, einschließlich dem Verschachteln logischer Ausdrücke, und das Zusammenstellen von Wörtern und Phrasen mit Hilfe von Anführungszeichen. Sie könnten dieses Beispiel so erweitern, dass Sie damit so ziemlich jede Situation meistern, in der eine komplexe Suche über Teile einer Datendatei ausgeführt werden soll: zum Beispiel um Mail-Nachrichten anhand von bestimmten Kriterien aus einem Mail-Ordner zu filtern oder nach besonderen Comic-Büchern aus einer Sammlung von Comics zu suchen. Jede Textdatei kann als einfache Datenbank dienen, und dieses Skript kann sie durchsuchen, solange es dahingehend modifiziert wurde, die Daten dieser Datenbank zu verarbeiten.
Das zweite Beispiel war ein Skript zur Auswertung von Log-Dateien, das Protokolle von Web-Servern verarbeitet und Statistiken ausgibt. Reine Protokolle sind häufig vom Äußeren ziemlich abschreckend. Dieses Skript liefert Ihnen einige grundlegende Informationen über das, was auf einer Website so alles abläuft. Dabei bediente es sich einiger komplexer regulärer Ausdrücke und einer Menge von Hashes, um die Rohdaten zu speichern. Sie könnten dieses Beispiel dahingehend erweitern, dass es auch andere Statistiken erstellt (zum Beispiel um Histogramme über die Anzahl der Treffer pro Tag oder pro Stunde anzulegen oder außer HTML-Dateien auch Bild- oder andere Dateien zu verfolgen). Oder Sie könnten Änderungen vornehmen, so dass andere Protokollarten (Mail-Protokolle, FTP-Protokolle oder was gerade anfällt) statistisch erschlossen werden.
Meinen Glückwunsch zum erfolgreichen Abschluß der zweiten Woche dieses dreiwöchigen Exkurses. Nach dieser Woche haben Sie bereits ein Großteil der Skriptsprache aufgenommen, so dass Sie jetzt in der Lage sein sollten, bereits einige Aufgaben in Perl zu lösen. Ab jetzt werden wir auf das Erlernte aufbauen. Also auf zu Woche 3!