![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
||||||||
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
|||||||
![]() |
|||||||
![]() |
|
||||||
![]() |
Notizbücher und Palmtops haben den Nachteil, daß man sie immer dann einzustecken vergißt, wenn man sie am nötigsten braucht. Wer wie ich eh den ganzen Tag am Internet hängt, kommt da schon mal auf die Idee, ein Web-basiertes Adreßbuch anzulegen!
Nun hat nicht jeder eine Datenbank zur Verfügung - sei es, daß man aus purer Faulheit keine installieren will oder deshalb, weil der Internetprovider einfach keine anbietet. Andererseits sollte jede Applikation, die mit Daten jongliert, Datenbank-tauglich angelegt sein, man weiß schließlich nie, ob die Datenbestände nicht wider Erwarten doch plötzlich sprungartig wachsen und man schnellstens in die Arme von Mama Oracle oder einer Ihrer Kolleginnen springen möchte. Schön, wenn man dann nur eine Zeile im Skript ändern muß, die den richtigen Treiber installiert und die restliche Applikation mitsamt den SQL-Abfragen gleichbleibt.
Perls generische Datenbankschnittstelle DBI kann man seit neuestem auch mit einem Treiber für ordinäre Dateien ansteuern - statt mit einer Datenbank zu kommunizieren, verwaltet das DBD::File-Modul lesbare Dateien mit Komma-separierten Einträgen, die man mit SQL-Zugriffen traktieren darf. Hinweise zur Installation des Treibers und einiger abhängiger Module, sowie zur Aktivierung des vorgestellten Skripts addr.pl finden sich im Abschnitt "Installation" am Ende des Artikels.
Abbildung 1 zeigt das Eingangsformular, das das CGI-Skript addr.pl, wenn es einmal in cgi-bin installiert und initialisiert ist, beim ersten Aufruf in den Browser zaubert: Zunächst zeigt addr.pl keinerlei Daten an (auch wenn die Tabelle schon mit Einträgen gefüllt wäre), sondern nur eine Liste der Buchstaben des Alphabets, deren jeder mit einem Link verknüpft ist, der addr.pl nach einem Eintrag suchen läßt, dessen Vorname oder Nachname mit dem betreffenden Buchstaben anfängt. Ein Klick auf den Alle Einträge-Link zeigt das ganze Notizbuch an.
Weiter wird ein Suchfeld angezeigt, das Suchstrings entgegennimmt. Falls Suche starten gedrückt wird, sucht addr.pl ebenfalls in den Vorname/Nachname-Feldern der Tabelle nach Übereinstimmungen und zeigt die Ergebnisse in einer Liste an. Klickt der Benutzer auf den Neuer Eintrag-Knopf, wird ein Formular nach Abbildung 2 angezeigt.
Der nach dem Ausfüllen der Felder gedrückte Speichern-Knopf läßt die Daten in die Datenbank wandern. Die Einträge in der Adreßbuchdatei liegen bei dem im Skript verwendeten DBD::File-Treiber in der Datei addressbook/addressbook unterhalb des CGI-Verzeichnisses, der Inhalt sieht etwa folgendermaßen aus:
id,fname,lname,phone,email,addr,notes 9214031581423,Freddy,Holler,089/1234567,holler@aol.com, "Bon-Scott-Weg 3, 89834 Zuffenhausen", "Alte Email: fredy@aol.com" 9214038931493,Herbert,Rigatoni,08234/234435, herbertR@yahoo.com,"In der Grube 24, 82342 Kutzenbach", "Geburtstag: 1.3."
Die erste Zeile legt hierbei die Spaltennamen fest. Sie lauten genauso wie die später verwendeten CGI-Parameternamen, das erspart Kopfschmerzen bei der Programmierung. fname steht dabei für den Vornamen (First Name), lname für den Nachnamen (Last Name) usw. Einträge in den Datenzeilen werden durch Kommata getrennt. Einträge, die Leerzeichen enthalten, werden in doppelte Anführungszeichen eingeschlossen und eventuelle Sonderzeichen (doppelte Anführungszeichen und Kommata) entsprechend maskiert. Die erste Spalte (id) jeder Zeile weist dem Eintrag eine eindeutige ID zu. Sie setzt sich zusammen aus der gegenwärtigen Uhrzeit (Rückgabewert des time()-Kommandos) und der PID des aktuellen CGI-Prozesses beim Anlegen des Eintrags.
Eine gefüllte Datenbank liefert Ergebnisse auf Suchabfragen in Tabellenform nach Abbildung 3. Das Namensfeld jedes Eintrags ist mit einem Link unterlegt, klickt man darauf, springt der Browser auf die Editierseite und füllt die Felder dort gleich mit den Daten des ausgewählten Eintrags. Der Speichern-Knopf aktualisiert den Datenbank-Eintrag entsprechend den Formularfeldern, ein Druck auf den Delete-Knopf löscht den Eintrag.
Was addr.pl im einzelnen tut, ob es ein Eingabeformular darstellt oder einen neuen Eintrag anlegt oder das Ergebnis einer Suchanfrage anzeigt, bestimmen die CGI-Parameter mit denen es aufgerufen wird. Folgende Szenarien steuert addr.pl:
Datenbanktabelle initialisieren
addr.pl init=1
Dieser Aufruf erzeugt die Datenbank-Tabelle mit einem CREATE-Kommando aus dem SQL-Fundus.
Suchabfrage mit Ergebnisanzeige
addr.pl search=A
Die Such-Abfrage fördert Einträge hervor, deren Vorname oder Nachname mit A angehen und zeigt die Ergebnisse in einer Tabelle an. Wird der search-Parameter leergelassen (aber dennoch definiert mit search=), zeigt addr.pl eine vollständige Liste aller bestehenden Einträge in einer Tabelle an.
Formular zur Eingabe eines neuen Eintrags anzeigen
addr.pl edit=1
Formular zur Aktualisierung eines bestehenden Eintrags anzeigen
addr.pl edit=1 id=9214031581423
Jeder Tabelleneintrag enthält eine eindeutige ID, so daß addr.pl einmal gefundene Zeilen beim nächsten Aufruf schnell identifizieren und Manipulationen vornehmen kann (update, delete).
Neuen Eintrag aus den ausgefüllten Formularfeldern generieren
addr.pl insert=<gesetzt> fname=... lname=...
Eintrag mit den ausgefüllten Formularfeldern aktualisieren
addr.pl insert=<gesetzt> id=9214031581423 fname=... lname=...
Eintrag löschen
addr.pl delete=<gesetzt> id=9214031581423
Listing addr.pl zeigt die Implementierung des Web-Adreßbuches. Zeile 7 holt das CGI-Modul, die angegebenen Tags lassen es die Standard-HTML- und die Tabellen-Funktionen exportieren. Das CGI::Carp-Modul sorgt dafür, daß der Browser bei auftretenden Fehlern nicht das blöde Internal Server Error anzeigt, sondern einen aufschlußreiche Fehlermeldung. Die Zeilen 11 bis 14 spezifizieren die Parameter für den DBI-Flatfile-Treiber. $DB_DIR gibt das Verzeichnis unterhalb des cgi-bin-Verzeichnisses an, das die Tabellendaten als Datei enthält.
Zeile 20 nimmt die Verbindung mit der "virtuellen" Datenbank auf, die Zeilen 23 und 24 geben den CGI-Header und die Überschrift aus, die in jedem Fall im Dokument steht und färben den Hintergrund der Seite weiß ein. Dann scheiden sich die Wege: Der if-Block ab Zeile 26 wird angesprungen, falls ein Benutzer die Formularfelder für einen neuen Eintrag ausgefüllt und den Speichern-Knopf gedrückt hat. Der map-Befehl in Zeile 28 übergibt der insert_rec-Funktion, die die eigentliche Datenbank-Aktualisierung vornimmt, die Formulardaten, indem er für alle Elemente in @dbfields die param-Funktion des CGI-Moduls aufruft und so die entsprechenden CGI-Parameter entgegennimmt und weiterreicht.
insert_rec selbst steht ab Zeile 158 in addr.pl, nimmt das hereingereichete DB-Handle und die Formularparameter entgegen und setzt den SQL-Insert/Update-Befehl an die Datenbank ab. Ist der CGI-Parameter id gesetzt, handelt es sich um eine Aktualisierung eines bestehenden Records und Zeile 167 definiert einen SQL-Update-Befehl. Hier wie auch an anderen Stellen leistet der qq-Operator, der mehrzeilige Strings mit doppelten Anführungszeichen umschließt, nützliche Dienste. Fehlt andererseits id, handelt es sich um einen neuen Eintrag und Zeile 180 kreiert einen SQL-Insert-Befehl.
Zurück zur Hauptschleife: Die page_header-Funktion, die in Zeile 29 aufgerufen wird und ab Zeile 99 implementiert ist, klatscht das kleine Link-Alphabet, das in den Abbildungen 1 und 3 jeweils oben im Fenster zu sehen ist, dorthin und schreibt auch noch das Such-Feld samt den zwei Buttons auf die Seite. Die url()-Funktion aus dem CGI-Modul liefert hierzu den URL des gegenwärtig laufenden Skripts.
Ab Zeile 31 steht der Code zum Löschen eines Eintrags. Jede Zeile in der Datenbanktabelle enthält neben den Adreßbuchdaten auch noch eine eindeutige ID, die als verstecktes (hidden) Feld auf der Seite steht, die die Formularfelder zum Aktualisieren eines Eintrags darstellt. Drückt der Benutzer auf den Knopf Eintrag löschen, sendet der Browser neben den aktualisierten Feldern auch noch die ID mit und addr.pl kann einen DELETE-Befehl losschicken, der mit seinem Tintenkiller genau über die richtige Zeile der Tabelle fährt. Drückte der Benutzer entweder auf den Knopf Neuer Eintrag oder aber auf einen erleuchteten Namen der der Ergebnisliste, wird der Codeblock ab Zeile 40 angesprungen, da der Parameter edit in diesen Fällen gesetzt ist.
Diese zwei Fälle unterscheiden sich dahingehend, daß ein angeklickter Eintrag der Ergebnisliste den id-Parameter setzt. In diesem Fall muß addr.pl vor dem Darstellen der Felder die Werte aus der Datenbank übernehmen. Hierzu erzeugt es in Zeile 44 einen SQL-Select-Befehl, der die Daten aus der Datenbank holt. Die Zeilen 51 bis 56 holen das Ergebnis des Queries ab, wegen der eindeutigen ID im SELECT ist das Ergebnis stets eine einzelne Zeile. Die fetch-Methode im while-Kopf liefert eine Referenz auf einen Array zurück, dessen Elemente die Spaltenwerte des Tabelleneintrags beinhalten. Da die Tabellenzeile zusätzlich zu den in @dbfields aufgelisteten Spalten als erstes Element die id-Spalte führt, startet der Index $i in Zeile 52 mit dem Wert 1 statt des sonst üblichen 0 .
Der Aufruf der param-Methode in Zeile 54 manipuliert die CGI-Eingangsparameter und gaukelt den nachfolgenden Abfragen vor, der Benutzer hätte die Adreßdaten des selektierten Eintrags selbst eingetragen - derweil stammen sie aus der Datenbank. Die Zeilen 59 bis 80 geben eine zweispaltige HTML-Tabelle aus, die das Formular zum Anlegen/Editieren eines Adreßeintrags nach Abbildung 2 in den Browser zeichnet.
Für den Fall, daß der Benutzer eine Suchanfrage startete oder einen Buchstaben im Reiter-Alphabet des Seitenkopfes anklickte, ist der CGI-Parameter search gesetzt, entsprechend springt addr.pl den Block ab Zeile 82 an. Für den Buchstabenklick enthält search den entsprechenden Buchstaben, wurde etwas ins Suchfeld eingetragen und der Suche starten-Knopf gedrückt, steht in search der Suchbegriff. Die Funktion keyword_search übernimmt in beiden Fällen die Suche, sie ist ab Zeile 118 definiert. Dort holt eine SQL-Abfrage passende Records aus der Datenbank, indem sie mittels des CLIKE-Konstrukts überprüft, ob Vor- oder Nachname eines Eintrags mit dem gegebenen Suchausdruck beginnen, Groß- und Kleinschreibung werden ignoriert. Für einen leeren Suchstring liefert keyword_search großzügigerweise einfach alle Einträge der Tabelle.
Die while-Schleife ab Zeile 145 gibt die Treffer in einer HTML-Tabelle aus, indem sie Vor- und Nachnamen zu einer Tabellenspalte zusammenfaßt und einen HTML-Link drumherum baut, der die CGI-Parameter edit auf 1 und id auf die in der Datenbank gefundene ID des Eintrags setzt, so daß das Skript bei einem Klick auf den Eintrag sofort den Eintrag in der Datenbank referenzieren kann. Der Block ab Zeile 86 kommt nur bei der Installation des Skripts kurz zum Einsatz und ruft die Initialisierungsfunktion init_db auf, die ab Zeile 190 definiert ist, und das Unterverzeichnis der Pseudo-Datenbank erzeugt. Weiter setzt sie einen SQL-Create-Befehl ab, der die Pseudo-Tabelle anlegt. Ist überhaupt kein CGI-Parameter gesetzt (wie beim ersten Aufruf des Skripts), kommt Zeile 90 zum Einsatz und zeichnet lediglich den Seitenkopf mit dem Suchfeld und dem Reiteralphabet.
Listing 1: Listing_addr_pl |
1 #!/usr/bin/perl -w 2 ################################################## 3 # CGI Address Book 4 # 1999, mschilli@perlmeister.com 5 ################################################## 6 7 use CGI qw/:standard :html3/; 8 use CGI::Carp qw/fatalsToBrowser/; 9 use DBI; 10 11 my $DB_DIR = "./addressbook"; 12 my $DB_DSN = "DBI:CSV:f_dir=$DB_DIR"; 13 my $DB_USER = ""; 14 my $DB_PASSWD = ""; 15 16 my @dbfields = qw/fname lname phone email addr 17 notes/; 18 my $dbflist = join(', ', @dbfields); 19 20 my $dbh = DBI->connect($DB_DSN, $DB_USER, 21 $DB_PASSWD) or die "Cannot connect to DB"; 22 23 print header(), start_html(-BGCOLOR => 'white'), 24 h1("Adreßbuch"); 25 26 if(param('insert')) { 27 # Insert/Update record according to form fields 28 insert_rec($dbh, map { param($_) } @dbfields); 29 page_header(); 30 31 } elsif(param('delete')) { 32 # Delete a record according to ID field 33 my $id = param('id'); 34 $dbh->do(<<EOT) or die "Cannot delete data"; 35 DELETE FROM addressbook 36 WHERE id = '$id' 37 EOT 38 page_header(); 39 40 } elsif(param('edit')) { 41 # Display fields for inserting/updating a rec 42 if(my $id = param('id')) { 43 # ID exists - Get record and preset fields 44 my $sql = qq[SELECT id, $dbflist 45 FROM addressbook 46 WHERE id = '$id']; 47 my $cursor = $dbh->prepare($sql) or 48 die "Cannot select ($sql)"; 49 $cursor->execute() or die "SQL failed"; 50 51 while(defined($row = $cursor->fetch)) { 52 my $i = 1; 53 foreach $field (@dbfields) { 54 param($field, $row->[$i++]); 55 } 56 } 57 } 58 59 print start_form(), 60 hidden(-name => 'id'), 61 table({"border" => 1}, 62 TR(td("Vorname:"), 63 td(textfield(-name => 'fname'))), 64 TR(td("Nachname:"), 65 td(textfield(-name => 'lname'))), 66 TR(td("Telefon:"), 67 td(textfield(-name => 'phone'))), 68 TR(td("Email:"), 69 td(textfield(-name => 'email'))), 70 TR(td("Adresse:"), 71 td(textarea(-name => 'addr', -rows => 3))), 72 TR(td("Notizen:"), 73 td(textarea(-name => 'notes', -rows => 3))), 74 ); 75 76 print submit(-name => 'insert', 77 -value => 'Speichern'), 78 submit(-name => 'delete', 79 -value => 'Eintrag löschen'), 80 end_form(); 81 82 } elsif(defined param('search')) { 83 page_header(); 84 keyword_search($dbh, param('search')); 85 86 } elsif(param('init')) { 87 page_header(); 88 init_db($dbh); 89 90 } else { 91 page_header(); 92 } 93 94 print end_html(); 95 96 $dbh->disconnect(); # Datenbankverbindung lösen. 97 98 ################################################## 99 sub page_header { 100 ################################################## 101 print start_form(); 102 foreach $letter ('A'..'Z') { 103 print a({href => url() . 104 "?search=$letter"}, "$letter "); 105 } 106 print a({href => url() . "?search="}, 107 " Alle Einträge"), 108 p("Suchbegriff:", 109 textfield(-name => 'search'), 110 submit(-name => 'Search', 111 -value => 'Suche starten'), 112 submit(-name => 'edit', 113 -value => 'Neuer Eintrag')); 114 print end_form(); 115 } 116 117 ################################################## 118 sub keyword_search { 119 ################################################## 120 my ($dbh, $keyword) = @_; 121 my $cursor; 122 my $where_clause = ""; 123 124 if($keyword ne "") { 125 $where_clause = qq[ 126 WHERE fname CLIKE '$keyword' OR 127 lname CLIKE '$keyword']; 128 } 129 130 my $sql = qq[ SELECT id, $dbflist 131 FROM addressbook 132 $where_clause 133 ORDER BY lname]; 134 135 $cursor = $dbh->prepare($sql) or 136 die "Select failed: $sql"; 137 138 $cursor->execute() or 139 die "Can't execute ($sql)"; 140 141 print "<TABLE BORDER=1>\n"; 142 print TR(map { th($_) } 143 qw/Name Telefon Email Adresse Notizen/); 144 145 while(defined(my $row = $cursor->fetch)) { 146 print TR(td( 147 a({href => url() . "?id=$row->[0]&edit=1"}, 148 "$row->[2], $row->[1]"), 149 td("$row->[3]"), 150 td("$row->[4]"), td("$row->[5]"), 151 td("$row->[6]"), 152 )), "\n"; 153 } 154 print "</TABLE>\n"; 155 } 156 157 ################################################## 158 sub insert_rec { 159 ################################################## 160 my($dbh, $fname, $lname, $phone, 161 $email, $addr, $notes) = @_; 162 163 if(param('id')) { 164 # ID there, it's an update! 165 my $id = param('id'); 166 167 my $sql = qq[ 168 UPDATE addressbook 169 SET id='$id', fname='$fname', 170 lname='$lname', phone='$phone', 171 email='$email', notes='$notes' 172 WHERE id = '$id']; 173 174 $dbh->do($sql) or die "Update failed ($sql)"; 175 176 } else { 177 # ID not there, it's a new record! 178 my $id = time . $$; # Generate ID 179 180 my $sql = qq[ 181 INSERT INTO addressbook 182 (id, $dbflist) 183 VALUES ('$id', '$fname', '$lname', '$phone', 184 '$email', '$addr', '$notes')]; 185 $dbh->do($sql) or die "Insert failed ($sql)"; 186 } 187 } 188 189 ################################################## 190 sub init_db { 191 ################################################## 192 my $dbh = shift; 193 194 if(! -d $DB_DIR) { 195 mkdir($DB_DIR, 0755) || 196 die "Cannot create dir $DB_DIR"; 197 } 198 199 $dbh->do(<<'EOT') or die "Cannot create table"; 200 CREATE TABLE addressbook ( 201 id char(20), 202 fname char(40), lname char(40), 203 phone char(20), email char(40), 204 addr char(100), notes char(100) 205 ) 206 EOT 207 } |
Um die Komma-separierten Einträge in der Datenbank-Datei lesen zu können, braucht das DBD::File-Modul, das die Flatfile-Datenbank realisiert, zunächst das Modul Text::CSV_XS. Mit SQL::Statement kommt dann ein kleines SQL-Maschinchen hinzu und mit diesen beiden arbeitet dann DBD::FILE. Das DBI-Modul und das außerdem verwendete Modul CGI liegen standardmäßig Perl 5.005 bei. Alle Module stehen auf dem CPAN zur Verfügung, mit
perl -MCPAN -eshell > install Text::CSV_XS > install SQL::Statement > install DBD::File
kriegt Perl 5.005 den letzten Schliff und es kann losgehen. Anschließend muß das vorgestellte Skript addr.pl ausführbar ins cgi-bin-Verzeichnis des Webservers gestellt und http://localhost/cgi-bin/addr.pl?init=1 aufgerufen werden, das erzeugt ein Unterverzeichnis und die Datenbankdatei. Klappt das nicht, muß entweder das Unterverzeichnis geändert (Zeile 11) oder manuell angelegt und für den Benutzer, unter dem der Webserver läuft (meist nobody) ausführbar gemacht werden. Danach sollte ein http://localhost/cgi-bin/addr.pl die Eingangsseite hervorzaubern, ein Klick auf den "Insert new Record"-Button zeigt das Formular an, das zur Eingabe des ersten Eintrags einlädt. Da das Format der Datenbankdatei lesbar ist, können natürlich auch alte Datenbestände einfach importiert werden. Eine Einschränkung muß jedoch erwähnt werden: Noch sind keine SQL-Joins möglich, aber das sollte in naher Zukunft auch möglich sein.
Gerade habe ich im Duden nachgelesen, daß man Adreßbuch nach den neuen Rechtschreibregeln Adressbuch schreibt. Was für ein Elend, ich hoffe es gildet trotzdem!
Ein Leser hat mich darauf aufmerksam gemacht, daß das Adressbuch-Skript mit der neuen Version des SQL::Statement-Moduls nicht mehr voll funktioniert. Darum habe ich das Skript angepasst und auch noch einige Verbesserungen vorgenommen (die CGI-Eingaben werden ge-quoted und der CLIKE-Befehl in SQL braucht das %-Zeichen, damit es Records richtig selektiert). Es ist hier zu beziehen.
Infos |
[1] Official Guide to MiniSQL 2.0, Brian Jepson, David J. Hughes, John Wiley & Sons, Inc. 1998, ISBN 0-471-24535-6 [2] Gebunkert - Datenbankbedienung mit Perl und CGI, iX 08/97, Michael Schilli, http://www.heise.de/ix/artikel/1997/08/150/artikel.html |
Der Autor |
Michael Schilli arbeitet als Web-Engineer für AOL/Netscape in Mountain View, Kalifornien. Er ist Autor des 1998 bei Addison-Wesley erschienenen (und 1999 für den englischsprachigen Markt als "Perl Power" herausgekommenen) Buches "GoTo Perl 5" und unter michael@perlmeister.com oder http://perlmeister.com zu erreichen. |
Copyright © 1999 Linux-Magazin Verlag