Home

Info

Artikel

Produkte

Stickers

UserGroups

Events

Bücher


Suchen:



Linux-Community
Jetzt bestellen!
> Kombiabo
> Jahres-CD 1999

Perl-Snapshot

24/7 Adressen-Butler

von Michael Schilli


Perls generische Datenbankschnittstelle DBI bietet einen Treiber an, der SQL-Abfragen auf Datenbestände ohne eine Datenbank zuläßt - ordinäre Dateien speichern die Tabellendaten in komma-separierten Einträgen. Ideal für ein kleines CGI-Adreßbuch!

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!

Datenbank ohne Datenbank

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.


Abb.1: Eingangsformular des Adreßbuches

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.


Abb. 2: Ein neuer Eintrag wandert ins Notizbuch

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.

Suchen und Finden

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.


Abb. 3: Auflistung der Suchergebnisse zum Buchstaben 'H'

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

Das Skript addr.pl im Detail

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             "&nbsp;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 }

Installation

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.

Epilog

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!

Nachtrag

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