Woche 3

# Tag 21

#
Ein paar längere Beispiele

Ebenso wie die ersten beiden Wochen dieses Buches werden wir auch die dritte Woche mit einem Beispielkapitel abschließen. Die beiden Beispielskripts, die ich in diesem Kapitel besprechen werde, decken so ziemlich alle Aspekte von Perl ab, die Sie in den letzten 20 Tagen kennengelernt haben. Nachdem Sie diese beiden Beispielskripts durchgearbeitet und verstanden haben, sollten Sie in der Lage sein, Ihren eigenen Weg mit Perl zu gehen.

Heute untersuchen wir zwei Beispiele, beides CGI-Skripts:

- Einen Homepage-Generator, der auf der Grundlage einer Konfigurationsdatei und des HTML-Codes einer Vielzahl anderer Sites eine Webseite mit den von Ihnen gewünschten Informationen erzeugt - wirklich nur mit den von Ihnen gewünschten Informationen
- Eine webbasierte Aufgabenliste, die es erlaubt, Aufgaben hinzuzufügen, zu entfernen, als fertig zu markieren, Prioritäten festzulegen und die Liste auf vielfältige Art und Weise zu sortieren

Beide Beispiele sind länger als die bisherigen, das letzte sogar mehr als doppelt so lang. Zur Erinnerung: Alle Beispiele dieses Buches sind auch auf der Buch-CD und online auf der »Sams Teach Yourself Perl«-Website unter `http://www.typerl.com/` verfügbar. Damit Sie nicht meinen, Sie müßten 350 Zeilen Code von Hand abtippen. Nutzen Sie dieses Kapitel, um den Code sorgfältig zu studieren, und wenn Sie dann noch weiter damit experimentieren wollen, kopieren Sie das Skript von der CD oder aus dem Internet.

Beide Beispiele werden als CGI-Skripts ausgeführt. Wie schon bei den CGI-Skripts, die wir in Kapitel 16, »Perl für CGI-Skripts«, untersucht haben, sind die Installation und die Anwendung dieser Skripts von Plattform zu Plattform und von Webserver zu Webserver unterschiedlich. Einige Server verlangen, dass die Skripts in einem speziellen Verzeichnis (cgi-bin) installiert werden, dass die Dateinamen der Skripts besondere Extensionen (`.cgi` statt `.pl`) haben oder dass spezielle Zugriffsrechte gesetzt werden. Unter Umständen müssen Sie auch die Zugriffsrechte für die Konfigurations- und Datendateien der Skripts setzen. Weitere Informationen zum Installieren und Verwenden von CGI-Skripts finden Sie in der Dokumentation zu Ihrem Webserver, oder wenden Sie sich an Ihren Systemverwalter.

## Ein Homepage-Generator (meinehomepage.pl)

Viele Websites verfügen heutzutage über Eintrittsseiten, deren Inhalt Sie anpassen und selbst zusammenstellen können (nach dem Motto »Mein Portal« oder ähnlich), so dass Sie beim Ansteuern dieser Websites schnell die Informationen finden, die Sie interessieren. Diese Seiten sind ein hervorragendes Mittel, um den Inhalt einer Website darzustellen. Ich dachte, es wäre ganz nett, wenn man jetzt noch einen Schritt weiter ginge und sich selbst eine eigene Seite anlegen würde, auf der man die verschiedensten Dinge von mehreren Websites kombinieren könnte - und damit meine ich nicht nur Links zu diesen Seiten, wie eine Textmarken- und Favoritenliste, sondern direkt den Inhalt der Seiten. Börsenkurse von einer Site, Wetterdaten oder Nachrichten von einer anderen, eine Sammlung von Informationen aus den verschiedensten Websites - und das alles auf einer Seite.

Und das ist genau das, was meinehomepage.pl macht. Das Skript nutzt das `LWP`- Modul, mit dem es Perl möglich ist, ausgewählte Webseiten von Websites über ein Netzwerk anzufordern (dies ist eines der Skriptbeispiele, in dem Sie eine ganze Reihe von Modulen einsetzen, um den Großteil der Arbeit zu erledigen, und ein wenig eigenen Perl-Code aufsetzen, um diese Module zusammenzubinden). Für jede Information, die auf der selbstgebauten Seite erscheinen soll, wird der HTML-Code der betreffenden Webseiten aus dem Internet herangezogen. Der relevante Code wird extrahiert und daraus die eigene Webseite mit den interessanten Teilen aufgebaut (plus einige weitere Links, die ich jedoch nicht jeden Tag besuchen möchte). Wie das Ergebnis von MeineHomepage aussehen kann, zeigt Ihnen Abbildung 21.1. Diese spezielle Version enthält die wichtigsten Börsendaten, das Wetter und den aktuellen Dilbert-Cartoon von United Media (Dilbert ist leider nicht zu sehen, da nicht genug Raum vorhanden ist und es außerdem Probleme mit dem Copyright gäbe; doch glauben Sie mir, er ist vorhanden.)

### Vorgehensweise

Ich hätte das Skript so schreiben können, dass ich alle Websites und deren Verarbeitung hart-kodiert hätte - man nehme eine Seite für Börsenkurse, extrahiere die Daten, schreibe den HTML-Code und wiederhole das gleich für das Wetter und den Comic-Strip. Und wenn ich mehr Daten aufnehmen oder weitere Links oder neue Inhalte einfügen wollte, müßte ich das Skript selbst modifizieren (siehe Abbildung 21.1).

Abbildung 21.1: Meine Homepage, Endergebnis

Auf den ersten Blick mag das zwar praktisch erscheinen, aber letztlich ist es sinnvoller, die Daten - das heißt, das was auf der Homepage erscheinen soll - von dem Skript, das alles herunterlädt und verarbeitet, zu trennen. Deshalb besteht dieses Skript auch aus zwei Teilen: einer Konfigurationsdatei und einem Perl-Skript, das die Daten aus der Konfigurationsdatei einliest und verarbeitet.

Die Konfigurationsdatei entspricht dem Adreßbuch aus Kapitel 14: Es ist eine Datei mit mehreren »Datensätzen«, die jeweils durch drei Bindestriche getrennt sind (`---`). Jeder Datensatz entspricht einem Abschnitt der Homepage und enthält vier Felder:

- Einen Titel für den im Abschnitt angezeigten Inhalt
- Einen URL, der auf den Inhalt weist, den wir aus dem Internet holen wollen
- Einen Code-Abschnitt, der den Code enthält, mit dem die wichtigen Teile der Webseite extrahiert werden (in der Regel eine Reihe regulärer Ausdrücke)
- Einen weiteren Abschnitt mit Code für einen Hash von weiteren Links, wobei die Schlüssel die Titel der Links sind und die Werte die URLs

Listing 21.1 zeigt einen Mustereintrag aus meiner config-Datei.

Listing 21.1: Meine Homepage-Konfigurationsdatei (Muster)

    title=Dilbert
    url=http://www.unitedmedia.com/comics/dilbert/
    code={
       if ($content =~ /src=\"(.*dilbert\d+.gif)\"/i) {
            print "<P><IMG SRC=\"";
            print url($1,$data{'url'})->abs->as_string, "\">\n";
        }
    }
    other=( 'United Media Comics' => 'http://www.unitedmedia.com/comics/',
           'San Jose Mercury news Comics' =>
               'http://comics.mercurycenter.com/comics/',
           'Tom Tommorow (mondays only)' =>
               'http://www.salonmagazine.com/comics/tomo/',
    )
    ---

Das Skript zu Meine Homepage ist ein CGI-Skript, das Sie wie jedes andere CGI- Skript installieren müssen. Im Gegensatz zu den in Kapitel 14 betrachteten CGI- Skripts gibt es zu diesem Skript kein Formular, durch das es aufgerufen wird, und es müssen auch keine Formularparameter verarbeitet werden. Das Skript wird direkt wie ein URL (`http://www.ihresite.com/cgi-bin/meinehomepage.pl` oder so ähnlich) aufgerufen und liest dann die Konfigurationsdatei ein und verarbeitet sie (installieren Sie die Konfigurationsdatei dort, wo das Skript sie finden kann). Der Rückgabewert ist der für die Homepage generierte HTML-Code.

### Das Skript

Kommen wir jetzt zum Perl-Teil. Gehen Sie den Code in Listing 21.2 einfach mal nur durch, und versuchen Sie einen Gesamteindruck von dem Ablauf des Skripts zu bekommen. Achten Sie dabei besonders auf die Subroutine `&procsection()`, denn hier fängt es an, interessant zu werden.

Listing 21.2: Der Code für meinehomepage.pl

    1:  #!/usr/bin/perl -w
    2:  use strict;
    3:  use LWP::Simple;
    4:  use URI::URL;
    5:  use CGI qw(header start_html end_html);
    6:
    7:  my $config = 'config';
    8:
    9:  print header;
    10: print start_html('Meine Homepage');
    11: print "<H1 ALIGN=CENTER><FONT FACE=\"Helvetica,Arial\" SIZE+2>";
    12: print "Meine Homepage</FONT></H1>\n";
    13: print "<HR>\n";
    14:
    15: open(CONFIG, $config) or &quitnow();
    16:
    17: while () {
    18:    my %rec = &readrec();
    19:
    20:     if (%rec) {
    21:         &procsection(%rec);
    22:     } else { last; }
    23: }
    24:
    25: print end_html;
    26:
    27: sub readrec {
    28:    my %curr = ();
    29:     my $key = '';
    30:     my $val = '';
    31:
    32:     while (<CONFIG>) {
    33:        chomp;
    34:         if ($_ ne '---' and $_ ne '') { # Trennsymbol für Datensätze
    35:            if ($_ =~ /^\#/) { next;} # Kommentare
    36:
    37:             ($key, $val) = split(/=/,$_,2);
    38:
    39:             # mehrzeilige Einträge verarbeiten
    40:             if ($key eq 'code' or $key eq 'other' ) {
    41:                 my $in = '';
    42:                 while () {
    43:                     ($in = <CONFIG>);
    44:                     $val .= $in;
    45:                    if ($in =~ /^[})]/) { last; }
    46:                 }
    47:             }
    48:
    49:             $curr{$key} = $val;
    50:         }
    51:         else { last; }
    52:    }
    53:     return %curr;
    54: }
    55:
    56: sub procsection {
    57:     my %data = @_;
    58:
    59:     print "<H2><FONT FACE=\"Helvetica,Arial\">";
    60:     print "$data{'title'}</FONT></H2>\n";
    61:
    62:     my $content = get($data{'url'});
    63:     if (defined $content) {
    64:         eval $data{'code'};
    65:     } else {
    66:        print "<P><B>Hoppla!</B> Kann nicht auf die Daten zugreifen\n ";
    67:         exit;
    68:     }
    69:
    70:     if (defined $data{'other'}) {
    71:         my %others = eval $data{'other'};
    72:
    73:         print "<P><B>Weitere Links:</B> <BR>\n";
    74:         foreach (keys %others) {
    75:             print "<A HREF=\"$others{$_}\">$_</A><BR> ";
    76:         }
    77:        print "\n";
    78:     }
    79:
    80: }
    81:
    82: sub quitnow {
    83:     print "<H2>Hoppla! Fehler:</H2><P><TT> Konfigurationsdatei konnte nicht geöffnet werden: $!</TT>\n";
    84:     print "<P>Überprüfen Sie Ihre Installation oder kontaktieren ";
    85:     print "Sie Ihren Systemadministrator\n";
    86:     print end_html;
    87:     die "Konfigurationsdatei konnte nicht geöffnet werden: $!\n";
    88: }

Das Skript meinehomepage.pl geht in vier Schritten vor:

- Es öffnet das Konfigurationsskript und liest einen Datensatz ein, wobei es einen Hash für die Felder in diesem Datensatz anlegt.
- Es besorgt sich den HTML-Quelltext für die Seite mit dem angegebenen URL.
- Es wendet den Code aus der Konfigurationsdatei auf den HTML-Quelltext an.
- Es gibt den resultierenden HTML-Code aus.

Das Skript wiederholt diese Schritte für jeden Eintrag in der Konfigurationsdatei.

Das meiste in diesem Skript sollte Ihnen bekannt vorkommen. Um ehrlich zu sein, ich habe das Skript adressen.pl aus Kapitel 14 als Grundlage für dieses Skript genommen, da sich die Konfigurationsdateien ähneln. Was Ihnen fremd erscheinen dürfte, ist die Verwendung der zwei Module `LWP::Simple` und `URI::URL` (und die damit verbundenen Subroutinen: `get`, `url`, `abs` und `as_string`) sowie die Verwendung der Funktion `eval`, um den Code der Konfigurationsdatei in den Rumpf des Skripts zu holen.

### Die Module LWP::Simple und URI::URL

Wenn man sich nicht sicher ist, wie eine bestimmte Aufgabe zu lösen ist, verwendet man einfach den Code von anderen Programmierern. Oder noch besser: Anstatt viel Zeit damit zu verschwenden, Skripts für komplizierte Aufgaben zu schreiben (zum Beispiel Netzwerkcode zu schreiben, um eine Datei von einer Website anzufordern), schauen Sie erst einmal im CPAN nach. In diesem Fall waren die `LWP`-Module (Bibliothek für WWW-Zugriff in Perl) genau das, was ich benötigte. Da ich gerade im CPAN war, habe ich gleich das gesamte `libwww`-Paket heruntergeladen, das alle Arten von Modulen für die Handhabung von Webseiten und Webdaten enthält.

Um dieses Skript auszuführen, müssen Sie diese Dateien vom CPAN herunterladen (siehe `www.perl.com` für weitere Details) und dort auf ihrem System installieren, wo Sie das Skript ausführen. Lesen Sie im Kapitel 13, »Gültigkeitsbereiche, Module und das Installieren von Code«, nach, wie man Module in Perl installiert und verwendet, wenn Sie sich nicht mehr genau erinnern können, wie dies geht.

> Die Bibliothek `libwww` allein wird Ihnen nicht ausreichen. Darüber hinaus gibt es noch eine ganze Reihe von weiteren Modulen, die Sie installieren müssen, einschließlich `libnet`, `HTML-Parser` und einige andere. All die Module, die wiederum von anderen Modulen benötigt werden, zu installieren, kann einem manchmal endlos vorkommen (Modul A benötigt Modul B, das wiederum Modul C benötigt und so weiter). Sie sollten sich aber die Zeit dafür nehmen, denn diese Module sind wichtig und sehr nützlich. Die README-Datei zu der Bibliothek `libwww` teilt Ihnen genau mit, welche Module Sie herunterladen und installieren müssen.

Das `LWP`-Paket enthält eine Reihe von Modulen für den Umgang mit dem Web und seinen Webseiten. Die meisten dieser Module sind objektorientiert und bieten eine große Bandbreite an Möglichkeiten. Wenn Sie interessiert sind, empfehle ich Ihnen, die POD-Dokumentation zu diesen Modulen zu lesen. Für unser spezielles Skript benötigen wir allerdings nur eine einfache Lösung, um eine Webseite von einem Webserver in das Skript zu holen, und dafür reicht das Modul `LWP::Simple` fraglos aus.

`LWP::Simple` ist, wie der Name schon verrät, ein simpler Satz an Subroutinen zur Bewältigung einfacher Aufgaben. Aus diesem Satz haben wir die Subroutine `get` gewählt, die bei Übergabe eines URL als Argument die zugehörige Website über das Internet ansteuert, die Datei hinter dem URL abruft und als einen großen String zurückliefert. In Zeile 62 des Skripts meinehomepage.pl wird die Funktion wie folgt aufgerufen:

    my $content = get($data{'url'});

Diese Zeile ist schnell erklärt: Der Hash `%data` enthält den Datensatz für diesen Abschnitt der Homepage, wobei der URL in dem URL-Schlüssel gespeichert ist. Den URL übergeben wir als Argument an die Subroutine `get` und speichern das Ergebnis in der Skalarvariable $`content`. Weiter hinten, im Abschnitt über `eval`, zeige ich Ihnen, wie Sie die wichtigen Teile aus dem Inhalt von `$content` herausziehen.

Das andere Modul, das wir in diesem Skript verwenden, lautet `URI::URL`. URI leitet sich von Uniform Resource Identifier ab und ist eine Art generische Version von URL (Uniform Resource Locator). Das URL-Modul ist ebenfalls objektorientiert und bietet verschiedene Möglichkeiten, um URLs aller Art zu erzeugen, zu ändern oder zu analysieren. Wir benötigen es in den Code-Abschnitten der `config`-Datei. Lassen Sie uns eine Zeile aus der `config`-Datei betrachten, die ich Ihnen oben bereits vorgestellt habe:

    print url($1,$data{'url'})->abs->as_string, "\">\n";

Dieses kleine Codefragment verwendet drei Subroutinen (eigentlich Methoden, wenn wir uns in Erinnerung zurückrufen, dass dies ein objektorientiertes Modul ist): `url`, `abs` und `as_string`. Durch die Zusammenarbeit dieser drei Methoden, kann man einen relativen URL - wie Sie ihn in Webseiten vorfinden, die Sie gerade aus ihrem Home- Verzeichnis ihres Webservers herausgelöst haben - in einen vollständigen, absoluten URL konvertieren. Mit diesem Code stellen wir sicher, dass alle Links in dem Code unserer Homepage auch tatsächlich auf die korrekten Websites verweisen. Wenn Sie an weiteren Informationen zu diesem Thema interessiert sind, lesen Sie die POD- Dokumentation zu der `URI::URL`-Manpage.

> Um ehrlich zu sein: Ich habe diesen Code einem großartigen Dokument, dem `LWP` `cookbook`, entnommen, das mit dem `LWP`-Modul erhältlich ist. Es enthält eine Fülle von Beispielen für die Lösung von häufigen Aufgaben mit `LWP`. Wenn Sie dieses Skript ausbauen oder einfach nur mehr mit dem Web und Perl arbeiten wollen, konsultieren Sie erst einmal dieses Kochbuch. Vielleicht finden Sie dort ja eine Lösung zu Ihrem Problem.

### Die Funktion eval

Hier kommt die Quizfrage: Wie kann man generischen Code aufsetzen, der wichtige Daten aus einer HTML-Datei extrahiert, wenn sich die wichtigen Teile von Datei zu Datei unterscheiden. Die Antwort lautet: Es geht nicht generisch, aber halten Sie den Code vom Hauptskript getrennt. In meinehomepage.pl finden Sie keinen Code zum Extrahieren der uns interessierenden Teile einer HTML-Datei. Dieser Code - normalerweise eine ganze Reihe von regulären Ausdrücken - befindet sich in der Konfigurationsdatei, zusammen mit den Titeln und den URLs, und unterscheidet sich je nach Seite.

Und nun die nächste Frage: Wie bekommen Sie den Code in Ihr Skript? Der erste Schritt besteht einfach darin, den Code aus der Konfigurationsdatei auszulesen. Der Teil des Skripts, der die Konfigurationsdatei einliest, legt den ganzen Codeabschnitt als ein einziges Element in einem Hash mit dem Schlüssel `'code'` ab.

Das letzte Teil in unserem Puzzle ist die `eval`-Funktion. Diese Funktion übernimmt, wie ich gestern erklärt habe, einen String und führt diesen String aus, als handele es sich um Perl-Code. In diesem Fall besteht unser String aus Perl-Code, so dass `eval` gleichbedeutend mit Kopieren und Einfügen an dieser Stelle ist. Der Code, der mit `eval` ausgeführt wird, hat Zugriff auf alle Variablen, die im Vorfeld definiert wurden, so dass wir den Hash `%data` und den String `$content` dazu nutzen können, den HTML-Code zu verarbeiten und auszugeben.

Sehen Sie hier noch einmal das Beispiel aus der `config`-Datei. Es extrahiert den URL der Comic-Strip-Grafik (bestehend aus dem Namen `dilbert`, einer Reihe von Zahlen und der Extension `.gif`) und erzeugt ein neues `IMG`-Tag in unserer Homepage (wobei der URL zu seinem absoluten Namen expandiert wird).

    code={
       if ($content =~ /src=\"(.*dilbert\d+.gif)\"/i) {
            print "<P><IMG SRC=\"";
            print url($1,$data{'url'})->abs->as_string, "\">\n";
        }
    }

Der HTML-Code, der durch dieses bißchen Code erzeugt wird, sieht ungefähr folgendermaßen aus:

    <P><IMG SRC="http://www.unitedmedia.com/comics/dilbert/archive/images/
    dilbert973012490104.gif">

Die `eval`-Funktion wird auch für den `'other'`-Teil der `config`-Datei benötigt, der einen Hash mit den zusätzlichen Links enthält. Ich hätte diese Schlüssel und URLs auch anders formatieren und in einem Hash des Skripts verarbeiten können, aber so war es einfacher. Die Zeilen 70 bis 78 in dem Skript meinehomepage.pl zeigen, wie die anderen Links ausgewertet und auf der HTML-Seite angezeigt werden.

### Noch eine Konfiguration

Die Musterkonfigurationsdatei war ziemlich einfach. Deshalb möchte ich noch eine weitere, etwas komplexere besprechen, allerdings vom Standpunkt des Skript meinehomepage.pl aus. Konzentrieren wir uns auf die Subroutine `procsection` aus meinehomepage.pl, wie sie in Listing 21.3 wiedergegeben wird:

Listing 21.3: Die Subroutine procsection

    1:  sub procsection {
    2:      my %data = @_;
    3:
    4:      print "<H2><FONT FACE=\"Helvetica,Arial\">";
    5:      print "$data{'title'}</FONT></H2>\n";
    6:
    7:      my $content = get($data{'url'});
    8:      if (defined $content) {
    9:         eval $data{'code'};
    10:     } else {
    11:        print "<P><B> Hoppla!</B> Kann nicht auf die Daten zugreifen \n";
    12:        exit;
    13:     }
    14:
    15:     if (defined $data{'other'}) {
    16:     my %others = eval $data{'other'};
    17:
    18:     print "<P><B>Weitere Links:</B> <BR>\n";
    19:     foreach (keys %others) {
    20:         print "<A HREF=\"$others{$_}\">$_</A><BR> ";
    21:     }
    22:     print "\n";
    23:     }
    24: }

Beginnen wir oben, wo wir das Datensatz-Hash-Argument in einer lokalen Variablen speichern und den obersten Teil des Abschnitts ausgeben (indem wir von dem Titel- Teil aus der Konfigurationsdatei in Zeile 5 Gebrauch machen).

In Zeile 7 wird mit Hilfe der `LWP`-Subroutine `get` die HTML-Datei angefordert. Im Erfolgsfall (Zeile 9) werten wir den Codeabschnitt der Konfigurationsdatei aus. Im anderen Fall geben wir einen Fehler aus und gehen zum nächsten Abschnitt über.

Lassen Sie uns einen Blick auf den Code aus der Konfigurationsdatei werfen (Listing 21.4). Dieser Code beschreibt den Wetterteil der Seite und zeichnet eine ganze Reihe von Tabellenzellen in die Mitte der Seite.

Listing 21.4: Konfigurationscode für den Wetterabschnitt

    1:  {
    2:      my $table = "<table>\n";
    3:      while ($content =~ /(<tr.*?<\/tr>)/gis) {
    4:          my $row = $1;
    5:          if ($row =~ /FOUR-DAY/is) { # Überschriftenzeile
    6:              $table .= $row;
    7:          }
    8:          if ($row =~ /<strong>(Fri|Sat|Sun|Mon|Tue|Wed|Thu)\./is) {
    9:              $table .= $row;
    10:         }
    11:     }
    12:     $table =~ s/src="([^\"]+)"/'src="' .
    13:          url($1,$data{'url'})->abs->as_string . '"'/eisg;
    14:     $table =~ s/"#FF[9C]+66"/"#8FCDFF"/g; # #FF9966 oder #FFCC66
    15:
    16:     $table .= "</table>";
    17:     print "$table\n";
    18: }

Der größte Teil dieses Codes besteht aus regulären Ausdrücken, doch wenn Sie eine Vorstellung davon haben, wonach wir suchen, ist das Entziffern des Codes einfacher. In Abbildung 21.2 sehen Sie das, was wir als Ergebnis anstreben: Dazu gehören eine oberste Zeile (»Four-Day Forecast«) und vier lange Zellen mit den Wettergrafiken und den Temperaturangaben.

Abbildung 21.2: Der Wetterteil meiner Homepage

Jedes dieser Elemente wird in der Webseite durch Tabellenreihen repräsentiert. Deshalb sucht der erste reguläre Ausdruck in Zeile 3 nach Code für Tabellenreihen - das heißt Text, der von den Tags `<TR>` und `</TR>` umgeben ist. In einer Hinsicht ist dieser reguläre Ausdruck allerdings etwas tükkisch - er verwendet ein Muster, das Sie in den Kapiteln 9 und 10 nicht direkt gelernt haben (es wurde zwar im Vertiefungsabschnitt angesprochen, aber fand im Hauptteil keine Erwähnung). Vielleicht erinnern Sie sich, dass `.*` ein gieriger Quantifizierer ist, der mit Freude alles bis ans Ende der Reihe »aufsaugt«. Das ist zwar nicht gerade eine glückliche Lösung, aber leider hier nicht zu vermeiden, da wir eine Übereinstimmung bis `</TR>` und nicht bloß für ein einziges Zeichen benötigen. Aus diesem Grunde verwenden wir `.*?`, eine spezielle nichtgierige Version des Musters `.*` - es führt den Abgleich bis zum ersten Vorkommen von `</TR>` durch und hält dann an, anstatt alles abzugleichen und danach von hinten zu prüfen.

Das einzige, was an dem Ausdruck in Zeile 3 noch bemerkenswert ist, sind die Optionen: `g` für global, um der `while`-Schleife die Gelegenheit zu geben alles abzugleichen, `i` für eine Suche, die nicht zwischen Groß- und Kleinschreibung unterscheidet, und `s`, damit der Punkt (`.`) zeilenübergreifend abgleicht (zur Erinnerung: die gesamte HTML-Datei ist mitsamt seinen Neue-Zeile-Zeichen als ein String in `$content` gespeichert).

Sind wir auf Tabellenreihen gestoßen, besteht der nächste Schritt darin, nach den gewünschten Tabellenreihen Ausschau zu halten. Die erste ist die dunkle oberste Reihe in Zeile 5, die einfach nur Textzeichen enthält. Darauf folgen die eigentlichen Wetterreihen, die durch Abgleich mit den Tagesnamen (Zeile 8) ermittelt werden, wobei das Muster mit einem `<strong>`-Tag beginnt und mit einem Punkt endet. Bei jedem Abgleich hängen wir den übereinstimmenden Code an den String `$table` an und bauen so die HTML-Tabelle für unsere Webseite auf.

In den Zeilen 12 bis 14 polieren wir die so aufgebaute Tabelle noch etwas auf. Zeile 12 bedient sich des URL-Codes, den Sie im vorigen Abschnitt kennengelernt haben, und expandiert die URLs der Grafikdateien, so dass sie auch wirklich auf die zugehörigen Websites zeigen. Zeile 14 ist eher persönlicher Art: Hier habe ich die Hintergrundfarben der Wetterreihen, die verschiedene Orangetöne aufweisen, in Blau geändert (ich mag einfach kein Orange).

In den Zeilen 16 und 17 wird die Tabelle nur noch mit einem schließenden `</table>`- Tag abgeschlossen; anschließend wird das Ergebnis ausgegeben. Von hier aus springen wir zurück zu unserem Skript meinehomepage.pl, geben die verschiedenen Links in dem `other`-Hash aus und erhalten als Ergebnis eine individuelle Webseite, die nur das enthält, was Sie sehen wollen.

Dieses System ist natürlich nicht unfehlbar. Werden die Quellseiten überarbeitet oder zu anderen URLs verschoben, müssen Sie Ihre Konfigurationen ändern. Wenn Sie Glück haben und Ihren Code so allgemein wie möglich halten, müssen Sie nicht allzu oft Änderungen vornehmen - dies ist einer der Nachteile, wenn man sich auf die Webseiten dritter verlassen muss.

## Ein webbasierter Aufgabenlisten-Manager

Unser zweites - und letztes - Beispiel ist eine einfache Aufgabenlisten-Anwendung namens Aufgabenliste.pl, die im Web ausgeführt werden kann. Sie können Listeneinträge hinzufügen, löschen und ändern, nach Datum, Priorität oder Beschreibungen sortieren und Einträge als fertig markieren. In Abbildung 21.3 sehen Sie ein Beispiel für eine Aufgabenliste.

Die Aufgabenlisten-Anwendung besteht aus einer großen Tabelle, in der die einzelnen Aufgaben eingetragen sind. Jeder Eintrag besteht aus einem Markierungskästchen, das anzeigt, ob die Aufgabe erledigt ist oder nicht, sowie Feldern für die Priorität, die Fälligkeit und die Beschreibung. Die Daten dieser Tabelle können über die Formularelemente, aus denen die Tabelle aufgebaut ist, bearbeitet werden. Die Änderungen werden gültig, wenn der Benutzer den Schalter Aktualisieren drückt. Auch das Entfernen eines Elements (die Markierungskästchen in der ganz rechten Spalte der Tabelle), das Sortieren nach einem Feld (Auswahlfeld Sortieren nach unter der Tabelle) und die Entscheidung, ob erledigte Aufgaben angezeigt werden sollen oder nicht (das Markierungskästchen Erledigte Einträge anzeigen ebenfalls unter der Tabelle) werden erst nach Betätigen von Aktualisieren gültig.

Außer der Tabelle und den Anzeigeoptionen gibt es noch einen Bereich zum Einfügen neuer Einträge in die Liste. Nachdem Sie die Felder in diesem Teil der Anwendung ausgefüllt und Neuer Eintrag ausgewählt haben, werden diese Eingaben als neuer Eintrag mit aufgenommen (und alle anderen Änderungen werden ebenfalls gültig).

Wie schon bei Meine Homepage ist das der Aufgabenliste zugrundeliegende Skript ein CGI-Skript, das direkt als URL ausgeführt wird und kein Formular benötigt. Das Skript erzeugt seinen eigenen Inhalt, einschließlich der Formulare, mit denen Sie die Elemente in der Liste oder deren Anzeige ändern können. Alles geschieht in einem Skript. Der einzige zusätzliche Teil ist eine Datendatei - listdaten.txt -, die die Daten der Aufgaben speichert und von dem Aufgabenskript gelesen und beschrieben werden kann.

Abbildung 21.3: Die Web-Anwendung einer Aufgabenliste

Dieses Skript ist doppelt so lang wie alles, was wir bisher in diesem Buch behandelt haben. Deshalb werde ich, wie schon im Beispiel zuvor, nicht jede Zeile einzeln besprechen. Das vollständige Skript befindet sich am Ende dieses Abschnitts in Listing 21.5 und ist darüber hinaus auf der CD und der Website zu diesem Buch erhältlich. In diesem Abschnitt beschreibe ich den Fluß und die allgemeine Struktur dieses Skripts. Sollten Sie über das Besprochene hinaus noch Fragen dazu haben, gehen Sie den Code ruhig einmal selbst durch.

### Die Datendatei

Wie die meisten Beispiele, die wir diese Woche betrachtet haben, hat auch dieses Skript eine Datendatei, aus der es liest und in die es schreibt, um die Daten auf dem aktuellen Stand zu halten. Die Datendatei zu diesem Skript, genannt listdata.txt, speichert die Daten der einzelnen Aufgaben. Das Skript Aufgabenliste.pl liest diese Datei bei jeder Iteration und schreibt bei jeder Änderung die neuen Daten zurück. Diese Datei ähnelt stark den Datendateien, die Sie bereits in anderen Beispielen kennengelernt haben:

    desc=Kapitel 21 beenden
    date=23.10.1998
    prior=1
    done=0
    ---
    desc=Punkt 3
    date=05.12.1998
    prior=3
    done=1
    ---
    desc=Pyramide bauen
    date=10.01.1999
    prior=5
    done=0
    ---
    desc=Kapitel 20 beenden, überarbeiten
    date=31.10.1998
    prior=1
    done=0
    ---
    desc=Treffen mit Fred zum Essen
    date=05.01.1999
    prior=2
    done=0
    ---

Wie schon in der Datendatei für das Meine Homepage-Skript werden die einzelnen Datensätze in der Datei durch drei Bindestriche (`---`) getrennt. Jedes Feld in dem Datensatz hat einen Schlüssel und einen Wert, die durch ein Gleichheitszeichen voneinander getrennt sind. Im Gegensatz zu den Daten für Meine Homepage gibt es hier keine mehrzeiligen Datensätze, sondern nur Schlüssel und Werte.

Wenn das CGI-Skript für die Aufgabenliste installiert wird, muss auch eine Datendatei angelegt werden, die der Webserver lesen und beschreiben kann. Diese Datendatei kann leer sein - das Skript wird dann eine Webseite ohne Einträge erzeugen -, muss aber vorhanden sein, damit das Skript funktioniert.

### Wie das Skript funktioniert

Das Skript Aufgabenliste.pl ist zwar groß, aber nicht sonderlich kompliziert. Es gibt nur wenige verwirrende reguläre Ausdrücke, und der Fluß von Funktion zu Funktion ist auch ziemlich überschaubar. Genau genommen besteht ein Großteil des Skripts aus `print`-Anweisungen, um den HTML-Code für die Aufgabenliste und ihre verschiedenen Formularelemente zu erzeugen - und diese Elemente so anzupassen, dass sie sich in verschiedenen Situationen unterschiedlich verhalten.

Die ersten Subroutinen für das Skript Aufgabenliste.pl lauten `&init()` und `&process()`. `&init()` legt das aktuelle Datum fest, ruft die Subroutine `&read_data()` auf und gibt den obersten Teil der HTML-Datei aus, die von dem Skript erzeugt werden soll. An diesem Punkt wollen wir mit der Besprechung beginnen und uns von dort nach unten vorarbeiten.

#### Dateninitialisierung mit &init()

Die Hauptaufgabe der Initialisierungs-Subroutine besteht darin, dass `&read_data()` aufgerufen wird, so dass die Datendatei geöffnet und die einzelnen Einträge in eine Datenstruktur eingelesen werden. Bei dieser Datenstruktur handelt es sich um ein Array von Hashes, wobei die einzelnen Hashes die Daten für die verschiedenen Aufgaben der Liste enthalten. Die Schlüssel in dem Hash lauten:

- `desc` - die Beschreibung des Eintrags
- `date` - das Fälligkeitsdatum für das Element. Das Datum hat das Format `MM/TT/ JJJJ` (dieses Format wird vom Skript verlangt)
- `prior` - die Priorität des Elements von 1 (höchste Priorität) bis 5
- `done` - zeigt an, ob die Aufgabe erledigt ist oder nicht

Zusätzlich zu diesen Schlüsseln, die aus der Datendatei kommen, bekommt jeder Eintrag in der Liste noch eine ID-Nummer. Dieser ID werden während des Einlesens Werte von 0 bis »Anzahl der Einträge« zugewiesen. Die ID wird später benötigt, um die verschiedenen Formularelemente für die Listenelemente auseinanderzuhalten.

#### Das Formular und die Daten verarbeiten - &process()

Die Routine `&process()` erledigt den wesentlichen Teil der Arbeit in dem Skript. In dieser Subroutine gibt es zwei Hauptzweige, die auf der `param()`-Funktion aus dem Modul `CGI.pl` basieren. Am Tag 16 habe ich Ihnen `param()` vorgestellt, und Sie haben gelernt, wie man diese Funktion dazu nutzt, um Werte aus Formularelementen auszulesen. Die Funktion `param()` läßt sich allerdings auch ohne Argumente anwenden, in welchem Falle sie die Namen aller vorhandenen Formularelemente oder - falls das Skript nicht für ein Formular aufgerufen wurde - den undefinierten Wert (`undef`) zurückliefert. Dieses Verhalten machen wir uns in der Subroutine `&process()` zunutze, um zwei verschiedene Ergebnisse zu erhalten:

- Beim ersten Aufruf des Skripts gibt es keine Parameter. Deshalb wird lediglich die aktuelle Aufgabenliste (mit der Subroutine `&display_all()`) angezeigt.
- Wenn das Skript danach erneut aufgerufen wird, gilt es auf mögliche Änderungen zu reagieren. Das können entweder Änderungen an bestehenden Listeneinträgen oder neu hinzugefügte Einträge sein. Wird der Schalter Aktualisieren betätigt, werden alle zu löschenden Elemente gelöscht (mit der Subroutine `&remove_selected()`), alle Daten aktualisiert (mit der Subroutine `&update_data()`), die Daten zurück in die Datei geschrieben (mit der Subroutine `&write_data()`) und diese dann wieder angezeigt (mit der Subroutine `&display_all()`).
- Wurde der Schalter Neuer Eintrag gedrückt, gehen wir analog vor, außer dass zwischen dem Aktualisieren der Daten und dem Zurückschreiben in die Datei ein Zwischenschritt dafür Sorge trägt, dass mit dem Aufruf an `&add_item()` eine weitere Aufgabe der Aufgabenliste als neuer Eintrag hinzugefügt wird.

Während der ganzen Aktualisierungs- und Hinzufügeoperationen prüfen wir gleichzeitig auf Formatierungsfehler in den Daten. Doch mehr dazu später, wenn ich auf die Aktualisierung der Daten und das Hinzufügen von neuen Listenelementen zu sprechen komme.

#### Die Daten mit &display_all() und &display_data() anzeigen

Der größte Teil des Aufgabenlisten-Skripts steckt allerdings in den Subroutinen `&display_all()` und `&display_data()`. Diese Subroutinen erzeugen nicht nur den HTML-Code für die Daten; die Datentabelle ist auch ein Formular, und alle Elemente in diesem Formular müssen automatisch erzeugt werden. Außerdem ist ein Großteil des HTML-Codes einer Reihe von Bedingungen unterworfen, die sich aus den Zuständen der Tabelle ergeben. So werden zum Beispiel Aufgaben mit der Priorität 1 in Rot ausgegeben, und die Auswahlfelder für die Prioritäten müssen so initialisiert werden, dass sie die aktuellen Werte der Datensätze widerspiegeln. Anstatt also für diesen Teil des Skripts ein riesiges Hier-Dokument zu verwenden, müssen wir Zeile für Zeile durchgehen, um es zu erzeugen.

Die Subroutine `&display_all()` ist die Hauptroutine, die als erstes aufgerufen wird. Sie startet die Tabelle, gibt die Header aus, sortiert das Hauptarray nach der aktuell vorgegebenen Sortierreihenfolge und ruft dann innerhalb einer `for`-Schleife `&display_data()` auf, um die einzelnen Einträge in der Aufgabenliste auszugeben. Außerdem erzeugt es die Elemente unter den Daten: das Auswahlfeld Sortieren nach, das Markierungskästchen Erledigte Einträge anzeigen, die Formularelemente zum Hinzufügen eines neues Eintrags und die beiden Schalter zum Abschicken des Formulars. Nebenbei kontrolliert die Subroutine, ob ein Fehler bei der Verarbeitung der Datumsangaben aufgetreten ist, und gibt entsprechende Warnungen aus. Für all dieses benötigen Sie eine Menge von bedingten Befehlen und `print`- Anweisungen sowie eine Menge Zeilen HTML-Code.

Der Subroutine `&display_data()` obliegt die Aufgabe, die einzelnen Einträge in der Aufgabenliste zu verarbeiten. Jede Reihe der Tabelle besteht aus fünf Spalten, die jeweils ein Formularelement enthalten. Jedes Element bedarf eines eindeutigen Namens, und viele Formularelemente ändern ihre Erscheinung, je nachdem welche Daten sie widerspiegeln (so muss zum Beispiel das Markierungskästchen am Beginn der Reihe markiert sein, wenn eine Aufgabe erledigt ist). Die Subroutine `&display_data()` sorgt auch dafür, dass Einträge NICHT angezeigt werden - wenn das Markierungskästchen Erledigte Einträge anzeigen nicht markiert ist, werden die Aufgaben, die als erledigt markiert wurden, nicht angezeigt. Es wird aber ein verborgenes Formularelement erzeugt, das einige der Daten der Aufgabe enthält, so dass die Aktualisierungen ordnungsgemäß durchgeführt werden können.

Wie schon bei `&display_all()` bedeutet auch dies eine Menge von `if`-Anweisungen und viel HTML-Code. Das Ergebnis ist ein gigantisches Formular, bei dem jedes Formularelement mit einer Aufgabe verbunden ist; es zeigt stets die aktuellen Daten der Aufgabenliste an. Wenn Sie irgend etwas an diesen Daten ändern, werden die Änderungen beim Abschicken des Formulars in die Originaldatensammlung übernommen.

#### Änderungen mit &update_data() übernehmen

Da wir inzwischen beim Eintragen der Änderungen angelangt sind, möchte ich auf die Subroutine `&update_data()` zu sprechen kommen. Diese Subroutine wird sowohl für den Schalter Aktualisieren als auch für den Schalter Neuer Eintrag aufgerufen, denn es soll sichergestellt werden, dass alle Änderungen am Skript in beiden Fällen übernommen werden. Die Aufgabe von `&update_data()` besteht darin, alle Formularelemente der Seite zu durchlaufen - die Element für die Listeneinträge und die Formularelemente Sortieren nach und Erledigte Einträge anzeigen - und die Daten oder globalen Einstellungen entsprechend den Änderungen, die auf einer Webseite vorgenommen wurden, zu aktualisieren.

Doch konzentrieren wir uns auf die Daten selbst. Jedes Element des HTML- Formulars, das durch `&display_data()` erzeugt wird, trägt einen eindeutigen Namen, der aus dem Namen des Feldes (Beschreibung, Priorität und so weiter) und der ID- Nummer dieses Elements erzeugt wird. Durch Teilen der Liste der Formularelementnamen, die zurückgeliefert werden, wenn das Formular abgeschickt wurde, können wir feststellen, welcher Name zu welchem Teil eines Datensatzes gehört, und die Werte vergleichen. Sollten diese nicht identisch sein, aktualisieren wir den Datensatz mit dem neuen Wert. Jedesmal, wenn das Formular abgeschickt wird, werden die Elemente einzeln geprüft. Das ist zwar nicht unbedingt der effizienteste Weg, Änderungen in den Daten zu verfolgen, aber wir können dadurch alles auf einer Seite belassen.

Eine andere Aufgabe, die die Subroutine `&update_data()` noch erfüllt, ist die Prüfung auf fehlerhafte Datumsangaben in den bestehenden Daten. Wenn Sie versuchen, ein Datum hinsichtlich seines normalen Formats zu ändern (`10/9/1998` oder so ähnlich), wird dies von `&update_data()` aufgefangen und ein Fehler generiert, der dann beim nächsten Aufruf von `&display_all()` für den Datensatz angezeigt wird.

#### Elemente mit &add_item() hinzufügen und mit &remove_selected() entfernen

Um Einträge aus der Liste zu entfernen, markieren Sie die Markierungskästchen in der Entfernen-Spalte für die betreffenden Daten und klicken auf Aktualisieren. Um ein Element der Liste hinzuzufügen, geben Sie diese Daten am Ende der Seite in das Formular ein und klicken auf Neuer Eintrag. In beiden Fällen wird `&remove_selected()` aufgerufen, im letzteren Fall darüber hinaus `&add_item().`

Die Subroutine `&remove_selected()`aktualisiert die Daten, indem die vom Benutzer zum Löschen vorgesehenen Datensätze entfernt werden. In diesem Fall ist das Entfernen der Elemente einfach, da unsere ganzen Daten in einem Array von Referenzen gespeichert sind. Wir richten einfach ein weiteres Array von Referenzen ein, abzüglich der, die wir löschen wollen, und legen dieses neue Array in der Variablen des alten Arrays ab. Da es sich hierbei um ein Array von Referenzen handelt, bleiben alle Daten, auf die mit den Referenzen verwiesen wird, an ihrer Position und müssen nicht umkopiert werden.

Die Subroutine `&add_item()` ist ebenfalls einfach. Wir müssen die Daten aus den Formularelementen nur in einem Hash ablegen und in dem Datenarray eine Referenz auf diesen Hash einrichten. Wir weisen dem neuen Eintrag außerdem eine neue ID zu, die um eins höher liegt als die momentan größte ID. (Wenn dazwischenliegende Einträge gelöscht wurden, versuchen wir nicht, deren IDs neu zu vergeben - die Einträge erhalten ehedem neue IDs, wenn die Datei bei der nächsten Iteration des Skripts wieder eingelesen wird).

#### Andere Subroutinen: Daten fortschreiben und auf Fehler prüfen

Übriggeblieben sind jetzt nur noch ein paar kleine, aber wichtige Subroutinen: `&write_data()`, um die Daten zurück in die Datei listdaten.txt zu schreiben, und zwei Subroutinen für die Datenformate und die Vergleiche.

Die Subroutine `&write_data` ist einfach: Ihre einzige Aufgabe besteht darin, die Datei listdaten.txt zum Schreiben zu öffnen und dann die Datensätze durchzugehen und in die Datei zu schreiben. Da diese Subroutine jedesmal, wenn das Skript ausgeführt wird und nachdem irgendwelche Änderungen an den Daten vorgenommen wurden, aufgerufen wird, können wir fast absolut sicher sein, dass die Daten nie korrumpiert werden oder Einträge verlorengehen. Beachten Sie, dass die IDs der Einträge nicht mit dem Rest der Daten in die Datendatei geschrieben werden. Die IDs werden erzeugt, wenn die Daten anfangs eingelesen werden, und nur dazu benötigt, um die Formularelemente zuordnen zu können. Darum müssen Sie nicht zwischen den Skriptaufrufen gespeichert werden.

Die letzten zwei Subroutinesätze beziehen sich auf die Datumsangaben. Datumsangaben haben, wie ich bereits erwähnt habe, das Format `MM/TT/JJJJ`. Es ist wichtig, ein konsistentes Format zu verwenden, da es nur dadurch möglich ist, die Liste der Elemente nach dem Datum zu sortieren - eine Art von numerischer Sortierung. Um das Datumsformat in eine Zahl zu verwandeln, die dann mit einer anderen Zahl verglichen werden kann, muss die Formatierung stimmen. Aus diesem Grund wird jede geänderte oder mit einem neuen Eintrag hinzukommende Datumsangabe mit der Subroutine `&check_date()` auf ihr Format hin geprüft. Liegen Abweichungen im Format vor, wird ein Fehler ausgegeben (eine große rote Meldung oben auf der Webseite und Sternchen in dem falschen Datum).

Die Sortierung der Liste nach dem Datum erfolgt in der Subroutine `&display_all()`. Dazu muss der Wert im Menü Sortieren nach auf Datum eingestellt worden sein. Um die Datumsangaben in etwas zu konvertieren, das mit etwas anderem verglichen werden kann, verwenden wir das Modul `Time::Local`. Dabei handelt es sich um ein vorgegebenes Modul, das dazu verwendet werden kann, verschiedene Teile einer Datums- oder Zeitangabe in ein `time`-Format zu konvertieren - das ist die Anzahl der Sekunden seit 1900 (das ist der Wert, der von der `time`-Funktion zurückgeliefert wird). Diesem Zwecke dient die Subroutine `&date2time()`, die die korrekt formatierte Datumsangabe in ihre Elemente aufsplittet und den `time`-Wert zurückliefert. Diese Routine hält gleichzeitig Ausschau nach falsch formatierten Datumsangaben - mit führenden Sternchen - und sortiert diese Werte ganz nach oben.

Beachten Sie, dass wir für die Jahresangabe in allen Daten unseres Skripts eine vierstellige Zahl verwenden. Damit stellt der Jahreswechsel 2000 kein Problem für uns dar. `Time::Local` ist eigentlich recht gut darin, festzustellen, ob eine zweiziffrige Jahresangabe in diese Epoche fällt oder in die nächste. Aber ich fand, dass es besser sei, sicherzugehen. Denn es kann durchaus sein, dass Sie dieses Buch im Jahr 2000 lesen.

### Der Code

Listing 21.5 enthält den (ziemlich) vollständigen Code für das Skript Aufgabenliste.pl. Gehen Sie es von vorn nach hinten durch. Kritisch sind nur die Teile, die mit der Vergabe der IDs an die Formularelemente und mit der Behandlung von Fehlern in den Datumsangaben zu tun haben (achten Sie auf die Subroutine `&check_date()`). Und wie bei allen CGI-Skripts ist es von Vorteil, wenn Sie HTML- Kenntnisse haben und wissen, wie Formulare und `CGI.pm` miteinander agieren.

Listing 21.5: Der Code für Aufgabenliste.pl

    1:   #!/usr/bin/perl -w
    2:   use strict;
    3:   use CGI qw(:standard);
    4:   use CGI::Carp qw(fatalsToBrowser);
    5:   use Time::Local;
    6:
    7:   my $listdata = 'listdaten.txt';  # Datendatei
    8:   my @data = ();                  # Array von Hashes
    9:   my $id = 0;                     # oberste ID
    10:
    11:  # global Standardeinstellungen
    12:  my $sortby = 'prior';       # Sortierreihenfolge
    13:  my $showdone = 1;           # Bearbeitete Einträge anzeigen?  (1 == ja)
    14:
    15:  &init();
    16:  &process();
    17:
    18:  sub init {
    19:      # aktuelles Datum ermitteln und im Format MM/TT/YY ausgeben
    20:      my ($day,$month,$year) = 0;
    21:      (undef,undef,undef,$day,$month,$year) = localtime(time);
    22:      $month++;             # Monate starten bei 0
    23:      $year += 1900;        # Perl-Jahre sind die Jahre seit 1900;
    24:
    25:      my $date = "$month/$day/$year";
    26:
    27:      # Datendatei öffnen und lesen
    28:      &read_data();
    29:
    30:      # HTML-Code
    31:      print header;
    32:      print start_html('Meine Aufgabenliste');
    33:      print "<H1 ALIGN=CENTER><FONT FACE=\"Helvetica,Arial\" SIZE+2>";
    34:      print "Aufgabenliste</FONT></H1>\n";
    35:      print "<H2 ALIGN=CENTER><FONT FACE=\"Helvetica,Arial\" SIZE+2>";
    36:      print "$date</FONT></H2>\n";
    37:      print "<HR>\n";
    38:      print "<FORM ACTION=\"/cgi-bin/Aufgabenliste.pl\" METHOD=POST>\n";
    39:  }
    40:
    41:  sub process {
    42:      my $dateerror = 0;      # Fehler im Datumsformat der alten Liste
    43:      my $newerror = 0;       # Fehler im Datumsformat des neuen Eintrags
    44:
    45:      # Hauptverzweigung.  Es gibt zwei Möglichkeiten:
    46:      # keine Parameter, alles anzeigen
    47:      # irgendwelche Parameter: aktualisieren, Eintrag hinzufügen,
             # schreiben und anzeigen
    48:      if (!param()) {                 # nur beim ersten Mal
    49:          &display_all();
    50:      } else { # Schalter behandeln
    51:          &remove_selected();
    52:          $dateerror = &update_data(); # vorhandene Änderungen
                                              # aktualisieren
    53:
    54:        # Elemente hinzufügen
    55:          if (defined param('additems')) {
    56:              $newerror = &check_date(param('newdate'));
    57:              if (!$newerror) {
    58:                  &add_item();
    59:              }
    60:          }
    61:
    62:        &write_data();
    63:          &display_all($dateerror,$newerror);
    64:      }
    65:
    66:    print end_html;
    67:  }
    68:
    69:  # Datendatei in ein Array von Hashes einlesen
    70:  sub read_data {
    71:      open(DATA, $listdata) or
                        die " Datendatei kann nicht geöffnet werden: $!";
    72:      my %rec = ();
    73:      while (<DATA>) {
    74:          chomp;
    75:          if ($_ =~ /^\#/) { next;} # Kommentare
    76:
    77:          if ($_ ne '---' and $_ ne '') { # Datensatz erstellen
    78:              my ($key, $val) = split(/=/,$_,2);
    79:              $rec{$key} = $val;
    80:          } else {                # Ende des Datensatzes
    81:              $rec{'id'} = $id++;
    82:              push @data, { %rec };
    83:              %rec = ();
    84:          }
    85:      }
    86:      close(DATA);
    87:  }
    88:
    89:  # HTML-Ausgabe
    90:  sub display_all {
    91:      my $olderror = shift;               # ist ein Fehler aufgetreten?
    92:      my $newerror = shift;
    93:
    94:
    95:      print "<TABLE BORDER WIDTH=75% ALIGN=CENTER>\n";
    96:
    97:      if ($olderror or $newerror) {
    98:          print "<P><FONT COLOR=\"red\">
                        <B>Fehler:  Daten, die mit *** markiert sind";
    99:          print "haben falsches Format (verwende MM/TT/JJJJ)</B>
                        </FONT><P>\n";
    100:     }
    101:
    102:     print "<TR BGCOLOR=\"silver\"><TH>Fertig?<TH>Priorität";
    103:     print "<TH>Fälligkeitsdatum
                    <TH ALIGN=LEFT>Beschreibung<TH>Entfernen?</TR>\n";
    104:
    105:     # Sortierreihenfolge festlegen (numerisch oder alphabetisch)
    106:     my @sdata = ();
    107:
    108:     # Array von Hashes gemäß Wert von $sortby sortieren
    109:     if ($sortby eq 'date') {   # nach Datum sortieren
    110:         @sdata = sort {&date2time($a->{'date'}) <=>
    111:                            &date2time($b->{'date'})} @data;
    112:     } else { # Text/Priorität-Sortierung
    113:         @sdata = sort {$a->{$sortby} cmp $b->{$sortby}} @data;
    114:     }
    115:
    116:     # Einträge der Reihe nach ausgeben
    117:     foreach (@sdata) {
    118:         &display_data(%$_);     # Datensatz verarbeiten
    119:     }
    120:
    121:     print "</TABLE>\n";
    122:
    123:     # Tabelle für Einstellungen
    124:     print "<P><TABLE BORDER WIDTH=75% ALIGN=CENTER>\n";
    125:     print "<TR><TD ALIGN=CENTER><B>Sortieren nach:</B> <SELECT
             NAME=\"sortby\">\n";
    126:
    127:     # Wert aus sortby abfragen, Auswahlfeld anzeigen
    128:     print "<OPTION VALUE=\"prior\"  ";
    129:     if ($sortby eq 'prior') { print "SELECTED>"}
    130:     else { print ">"; }
    131:     print "Priorität\n";
    132:     print "<OPTION VALUE=\"date\" ";
    133:     if ($sortby eq 'date') { print "SELECTED>"}
    134:     else { print ">"; }
    135:     print "Datum\n";
    136:     print "<OPTION VALUE=\"desc\" ";
    137:     if ($sortby eq 'desc') { print "SELECTED>"}
    138:     else { print ">"; }
    139:     print "Beschreibung\n";
    140:     print "</SELECTED></TD>\n";
    141:
    142:
    143:     # Wert von showdone abfragen, Markierungskästchen anzeigen
    144:     print "<TD ALIGN=CENTER WIDTH=50%><B>
                   Erledigte Einträge anzeigen?<B>\n";
    145:     my $checked = '';
    146:     if ($showdone == 1) { $checked = 'CHECKED'}
    147:     print "<INPUT TYPE=\"checkbox\" NAME=\"showdone\"
              VALUE=\"showdone\"";
    148:     print " $checked> </TD>\n";
    149:
    150:     # Aktualisieren-Schalter und Tabelle zum Hinzufügen von Einträgen
    151:     print <<EOF;
    152:     </TR></TABLE>
    153:     <P><TABLE ALIGN=CENTER>
    154:     <TR><TD ALIGN=CENTER VALIGN=CENTER>
    155:     <INPUT TYPE="submit" VALUE="   Aktualisieren   "
              NAME="update"></TD></TR>
    156:     </TABLE><HR>
    157:     <TABLE ALIGN=CENTER>
    158:     <TR><TH>Priorität<TH>Datum<TH ALIGN=LEFT>Beschreibung
    159: EOF
    160:     # Prioritäten-Auswahlfeld;
    161:     print "<TR><TD><SELECT NAME=\"newprior\">\n";
    162:     my $i;
    163:     foreach $i (1..5) {         # Prioritäten von 1 bis 5
    164:         if ($newerror and param('newprior') == $i) {
    165:             $checked = 'SELECTED';
    166:         }
    167:         print "<OPTION $checked>$i\n";
    168:     }
    169:     print "</SELECT></TD>\n";
    170:
    171:     # Datum und Beschreibung ausgeben;
    172:     # Fehlerfälle beachten
    173:     my $newdate = '';
    174:     my $newdesc = '';
    175:     print "<TD ALIGN=CENTER><INPUT TYPE=\"text\" NAME=\"newdate\"";
    176:     if ($newerror) {            # Fehler aufgetreten?
    177:         $newdate = "***" . param('newdate');
    178:        $newdesc = param('newdesc');
    179:     }
    180:     print "VALUE=\"$newdate\" SIZE=10></TD> \n";
    181:
    182:     # Beschreibung; im Fehlerfalle alten Wert beibehalten
    183:     print "<TD><INPUT TYPE=\"text\" NAME=\"newdesc\"
              VALUE=\"$newdesc\"";
    184:     print "SIZE=50></TD></TR></TABLE><TABLE ALIGN=CENTER>\n";
    185:
    186:    # fertigstellen
    187:     print <<EOF;
    188:     <TR><TD ALIGN=CENTER VALIGN=CENTER>
    189:     <INPUT TYPE="submit" VALUE="Neuer Eintrag"
              NAME="additems"></TD></TR>
    190:     </TABLE></FORM>
    191: EOF
    192: }
    193:
    194: # Daten zeilenweise ausgeben.  Daten sind bereits sortiert;
    195: # einzelnen Datensatz ausgeben
    196: sub display_data {
    197:     my %rec = @_;               # auszugebender Datensatz
    198:
    199:     # Keine erledigten Einträge anzeigen, wenn die Option
    200:     # Erledigte Einträge anzeigen nicht markiert ist.
    201:     # Der Einfachheit halber die Einträge aber mit
    202:     # verarbeiten
    203:     if ($showdone == 0 and $rec{'done'}) {
    204:         print "<INPUT TYPE=\"hidden\" NAME=\"Erledigt", $rec{'id'};
    205:         print "\">\n";
    206:         next;
    207:     }
    208:
    209:     # Einträge mit Priorität 1 in Rot ausgeben
    210:     my $bgcolor = '';
    211:     if ($rec{'prior'} == 1) {
    212:         $bgcolor = "BGCOLOR=\"red\"";
    213:     }
    214:
    215:     # Erledigt oder nicht erledigt?
    216:     my $checked = '';           # Erledigte Einträge
    217:     if ($rec{'done'}) {
    218:         $checked = 'CHECKED';
    219:     }
    220:
    221:     print "<TR>\n";             # mit Reihe beginnen
    222:
    223:     # Erledigt-Kästchen
    224:     print "<TD WIDTH=10% ALIGN=CENTER $bgcolor>";
    225:     print "<INPUT TYPE=\"checkbox\" NAME=\"done", $rec{'id'};
    226:     print "\" $checked></TD>\n";
    227:
    228:     # Priorität
    229:     print "<TD WIDTH=10% ALIGN=CENTER $bgcolor>";
    230:     print "<SELECT NAME=\"prior", $rec{'id'}, "\">\n";
    231:     my $selected = '';
    232:     my $i;
    233:     foreach $i (1..5) {         # Prioritäten von 1 bis 5
    234:        if ($rec{'prior'} == $i) {
    235:             $selected = 'SELECTED';
    236:         }
    237:         print "<OPTION $selected>$i\n";
    238:         $selected = '';
    239:     }
    240:     print "</SELECT></TD>\n";
    241:
    242:     # Datum
    243:     print "<TD $bgcolor WIDTH=10% ALIGN=CENTER>";
    244:     print "<INPUT TYPE=\"text\" SIZE=10 NAME=\"date", $rec{'id'}, "\" ";
    245:     print "VALUE=\"", $rec{'date'}, "\"></TD>\n";
    246:
    247:     # Beschreibung
    248:     print "<TD $bgcolor><INPUT TYPE=\"text\" NAME=\"desc", $rec{'id'};
    249:     print "\" SIZE =50 VALUE=\"", $rec{'desc'}, "\"></TD>\n";
    250:
    251:     # Entfernen-Kästchen
    252:     print "<TD $bgcolor ALIGN=CENTER>";
    253:     print "<INPUT TYPE=\"checkbox\" NAME=r", $rec{'id'}, "></TD>";
    254:
    255:     # Ende der Reihe
    256:     print "</TR>\n";
    257: }
    258:
    259: # Alle Werte aktualisieren
    260: sub update_data {
    261:     my $error = 0;              # Fehlerbehandlung
    262:
    263:     # wurde showdone ausgewählt?
    264:     if (defined param('showdone')) {
    265:         $showdone = 1;
    266:     } else { $showdone = 0; }
    267:
    268:     # Wert von sortby abfragen
    269:     $sortby = param('sortby');
    270:
    271:     foreach (@data) {
    272:         my $id = $_->{'id'};    # nicht das globale $id
    273:
    274:         # Einträge , die als erledigt markiert sind, können nicht
    275:         # geändert werden. In so einem Fall können wir die
    276:         # Überprüfung der weiteren Daten dieses Eintrags getrost
    277:         # übergehen.
    278:         if ($_->{'done'} == 1 && defined param('done' . $id)) { next; }
    279:
    280:         # Einträge, die als erledigt markiert wurden.
    281:         if (defined param('done' . $id)) {
    282:             $_->{'done'} = 1;
    283:         } else { $_->{'done'} = 0; }
    284:
    285:         # Datum.  Auf fehlerhafte Datumsangaben prüfen
    286:         if (param('date' . $id) ne $_->{'date'}) {
    287:             $error = check_date(param('date' . $id));
    288:             if ($error) {
    289:                 $_->{'date'} = "***" . param('date' . $id);
    290:             } else {
    291:                 $_->{'date'} = param('date' . $id);
    292:            }
    293:         }
    294:
    295:         # Priorität, Beschreibung
    296:         my $thing;
    297:         foreach $thing ('prior', 'desc') {
    298:             if (param($thing . $id) ne $_->{$thing}) {
    299:                $_->{$thing} = param($thing . $id);
    300:             }
    301:         }
    302:     }
    303:     return $error;
    304: }
    305:
    306: # Einträge löschen
    307: sub remove_selected {
    308:     my @newdata = ();
    309:
    310:     foreach (@data) {
    311:         my $id = $_->{'id'};    # nicht das globale id
    312:
    313:         if (!defined param('r' . $id)) {
    314:             push @newdata, $_;  # $_ ist die Referenz
    315:        }
    316:     }
    317:     @data = @newdata;           # Eintrag löschen
    318: }
    319:
    320: # neues Element hinzufügen. Wird nur aufgerufen, wenn check_date bereits OK ist
    321: sub add_item {
    322:     my %newrec = ();
    323:
    324:     $newrec{'desc'} = param('newdesc');
    325:     $newrec{'date'} = param('newdate');
    326:     $newrec{'prior'} = param('newprior');
    327:     $newrec{'done'} = 0;
    328:     $newrec{'id'} = $id++;      # global ID + 1
    329:
    330:     push @data, { %newrec };
    331: }
    332:
    333: # Datumsangaben müssen das Format XX/XX/XX haben
    334: sub check_date {
    335:     my $date = shift;
    336:
    337:     # MM/TT/JJJJ, MM und TT können 0 oder 1 Zeichen haben, JJJJ müssen
    338:     # 4 sein. Abschließende Leerzeichen sind okay
    339:     if ($date !~ /^\d{1,2}\/\d{1,2}\/\d{4}\s*$/) {
    340:         return 1;               # Fehler!
    341:     }
    342:    return 0;                   # OK
    343: }
    344:
    345: # Datendatei überschreiben
    346: sub write_data {
    347:     open(DATA, ">$listdata") or
                          die "Listendatei konnte nicht geöffnet werden: $!.";
    348:     foreach (@data) {
    349:         my %rec = %$_;
    350:
    351:         foreach ('desc', 'date','prior','done') { # keine ids
    352:             print DATA "$_=$rec{$_}\n";
    353:         }
    354:         print DATA "---\n";
    355:     }
    356:     close(DATA);
    357: }
    358:
    359: # Ich verwende das Datumsformat MM/TT/JJ.  Um nach Datum zu sortieren,
    360: # müssen Sie dieses Format in das Perl-Format (Sekunden-seit 1900)
    361: # konvertieren. (Modul Time::Local, Funktion timelocal.)
    362: sub date2time {
    363:     my $date = shift;
    364:     if ($date =~ /^\*\*\*/) {   # Formatierungsfehler
    365:         return 0;
    366:     } else {
    367:         my ($m,$d,$y) = split(/\//,$date);
    368:         $m--;           # Monate beginnen in Perls Zeitformat mit 0
    369:         return timelocal(0,0,0,$d,$m,$y);
    370:     }
    371: }

## Zusammenfassung

Wenn jemand, der über keinerlei Perl-Kenntnisse verfügt, dieses Buch aufschlägt und das Skript in Listing 21.5 betrachtet, stünden die Chancen nicht schlecht, dass er ziemliche Schwierigkeiten mit dem Entziffern des Quelltextes hätte - auch wenn er schon einiges über Programmiersprachen wissen sollte (Perl ist in dieser Hinsicht etwas sonderbar). Nachdem Sie jetzt aber 21 Tage tief in die Sprache und ihre Idiosynkrasien eingetaucht sind, sollte Ihnen das Lesen dieses Quelltextes leichtfallen - oder zumindest nicht ganz ratlos zurücklassen.

Heute haben wir die Woche und das Buch mit den üblichen längeren Beispielen (einige länger als andere) beendet. Die beiden Skripts, die wir in diesem Kapitel untersucht haben, den Homepage-Generator und die Aufgabenliste, sind beides CGI- Skripts, die Daten von verschiedenen Quellen (oder mehreren Quellen) verarbeiten und HTML-Code als Ausgabe erzeugen. Das zweite Skript, die Aufgabenliste, erzeugte darüber hinaus sein eigenes Formular, mit dem es seine eigenen Daten verarbeiten kann. Das erste Skript ist ein gutes Beispiel dafür, wie man Code von verschiedenen Modulen aus dem CPAN mit eigenem Code zusammenklebt und allgemein gehaltene Anwendungen den eigenen Bedürfnissen anpaßt. Das letztere Skript demonstrierte den Aufbau eines HTML-Formulars und verwendete eine verschachtelte Datenstruktur zur Aufbewahrung seiner Daten. Mit diesen Skripts und der in den letzten 20 Kapiteln geleisteten Arbeit entlasse ich Sie jetzt, was Perl angeht, in die Welt ...

Nun gehen Sie schon!

© Markt&Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH