Home

Info

Artikel

Produkte

Stickers

UserGroups

Events

Bücher


Suchen:



Addon
Jetzt bestellen!
> Kombiabo
> Jahres-CD 1999

Perl-Snapshot

Die Leiden eines Webmasters

von Michael Schilli


Mit dem Perl-Modul File::Find kann man bequem Dateibäume durchlaufen. In dieser Folge unserer Perl-Reihe werden damit Navigations-Balken und Fußzeilen einer ganzen Website auf einen Schlag geändert.

Obwohl meine neue Website http://perlmeister.com nur ein paar Seiten hat, zeigen sich schon typische Probleme: Jede Seite führt oben einen Navigations-Balken und unten eine Fußnote mit dem Hinweis, wohin man sich wenden kann, falls etwas nicht funktioniert. Ändert sich irgendetwas, ist der Teufel los: Soll nur ein neues Datum in die Fußzeile, muß man sämtliche Seiten editieren. Das treibt mich nicht nur zum Wahnsinn, sondern ist zudem auch sehr fehleranfällig. Hier kommt die Lösung: Da bestimmte HTML-Elemente auf vielen Seiten wiederkehren, liegt der Ansatz nahe, in den eigentlichen Seiten nur jeweils einen Tag (Täg!) im Format

<!-- include /german/foot.ger --> <!-- /include -->

abzulegen, den sich ein Spezialprogramm (bevor die Seiten "live" gehen) schnappt, die referenzierte Fußzeile, die angeblich in der Datei /german/foot.ger liegt, holt und sie, wie z.B. in

<!-- include /german/foot.ger --> Hier ist die
Fußzeile! <!-- /include -->

zwischen die Tags preßt. Der Browser zeigt die <!-- include ... --> Spezial-Tags nicht an, da es in einen HTML-Kommentar verpackt ist. Ändert sich die Fußzeile ein weiteres Mal, wiederholt der Tag-Ersetzer einfach seine Tätigkeit -- schließlich ist die Pfad-Information trotz ersetzten Inhalts immer noch da.

Der Includer richtet's

Das Skript aus Listing includer.pl durchstöbert ein Verzeichnis bis in beliebige Untiefen und ersetzt in allen gefundenen HTML-Dateien die include-Tags. Damit man die HTML-Stückchen schön hierarchisch abspeichern kann, liegt jedes von Ihnen in einer eigenen Datei in einem Verzeichnis unterhalb eines include-Verzeichnisses, in das der includer vor der Massen-Ersetzung eintaucht, alle Dateien ausliest und deren Inhalte in einem Hash %INCLUDE_MAP unter den Pfadnamen ablegt. Für meine Website sieht das include-Verzeichnis folgendermaßen aus:

include.production/
  english/  
    foot.eng  head.eng
  german/
    foot.ger  head.ger

Es gibt also Navigations-Balken (head) und Fußnoten (foot) für deutsche und englische Seiten. Steht in einer deutschen Seite im Verzeichnis HTML/index.html also

<!-- include /german/head.ger --> <!-- /include -->
<H1>Hier ist der Seitentext</H1>
<!-- include /german/foot.ger --> <!-- /include -->

stopft includer.pl mit dem Aufruf

includer.pl -i include.production HTML

die Navigationsbalken und Fußnoten in alle Seiten unterhalb des HTML-Verzeichnisses. Wandern die Seiten danach nicht auf den endgültigen Web-Server, sondern zunächst auf eine Testmaschine, sehen die Links im Navigationsbalken unter Umständen anders aus - kein Problem: Einfach ein zweites Include-Verzeichnis, beispielsweise include.test anlegen, die HTML-Stückchen darunter entsprechend modifizieren und

includer.pl -i include.test HTML

aufrufen, schon generiert includer.pl die Seiten für eine andere Konfiguration, denn die Dateien unterhalb von HTML referenzieren die HTML-Stückchen relativ zum include-Verzeichnis, so bezieht sich beispielsweise ein Tag, das /english/foot.eng enthält, auf include.test/english/foot.eng, falls die Option -i include.test des Includers gesetzt ist. Der Includer zeigt für jede Seite an, wieviele Ersetzungen er durchführen konnte:

HTML/index.html: 2 subs
HTML/resume.html: 2 subs
HTML/german/index.html: 2 subs
HTML/german/perl/index.html: 2 subs
HTML/german/perl/gotoperl/index.html: 2 subs

Vorsichtige Naturen starten den Includer zunächst mit der Option -r, die bewirkt, daß er zwar alle Dateien analysiert, bei eventuell nicht gefundenen Referenzen meckert, aber keine Ersetzungen durchführt. Der Includer arbeitet natürlich offline, entweder erzeugt man den HTML-Seiten-Baum auf einer anderen Maschine, um Ihn nach Vollendung auf den Webserver zu spielen, oder aber man installiert includer.pl und das include-Verzeichnis mit den HTML-Stückchen der Einfachheit halber auf dem Webserver selbst, in einem Verzeichnis oberhalb der Baumwurzel und läßt ihn nach jeder Änderung einmal durch die Original-Seiten rattern, die Ausfallzeit ist gering.

Wie funktioniert's?

Listing includer.pl zieht in Zeile 6 das Getopt::Std-Modul, dessen Funktion getopts in Zeile 14 die Kommandozeilen-Parameter -r und -i setzt und, falls vorhanden, die Einträge in $opt{r} und $opt_i entsprechend setzt. Bei fehlender -i-Option nutzt includer.pl das Verzeichis include im gegenwärtigen Verzeichnis. Um aus einer absoluten Angabe wie mydir/include eine relative zu formen, springt das Skript in den Zeilen 20-24 einfach schnell ins fragliche Verzeichnis, ermittelt mit cwd() aus dem Cwd-Modul den relativen Namen und springt wieder zurück.

In den Zeilen 31 und 32 folgen dann zwei Aufrufe der find-Funktion aus dem File::Find-Modul. Erst bekommt scan_include die Dateinamen aus dem Include-Verzeichnis zu fressen, wobei laut File::Find-Konvention der angesprungene Callback immer im gerade abgearbeiteten Verzeichnis steht, man also einfach mit $_ auf die aktuell angesprungene Datei zugreifen kann. Ändert man absichtlich oder unabsichtlich den Wert von $_ besteht File::Find ärgerlicherweise darauf, daß $_ seinen Wert am Ende des Callbacks wieder zurück erhält, sonst kracht's. scan_include liest also die einzelnen Dateien unterhalb des Include-Verzeichnisses aus und speichert deren Inhalt als Strings unter dem Pfadnamen im Hash %INCLUDE_MAP ab.

Substitutions-Monster

Schickt sich dann Zeile 32 an, die zu korrigierenden HTML-Seiten abzuklappern, öffnet der Callback process_file jeweils die Datei, liest sie in einen String $lines ein, führt in einer gewaltigen Anweisung zum Suchen und Ersetzen die ganze Transformation durch, und überschreibt, falls nicht gerade das Read-Only Flag -r gesetzt ist, die jeweilige Datei mit dem neuen Inhalt.

Die Anweisung aus den Zeilen 71-77 ersetzt alles zwischen den beiden gesuchten Spezialtags durch den Rückgabewert der Funktion include_replace() - der Modifikator e für evaluate macht's möglich. Die anderen Modifikatoren der Substitutionsanweisung (die statt "/" das Zeichen "@" als Trenner benutzt) sind g, i, x und s die für globale Bearbeitung (alle vorkommenden Tags werden ersetzt), ignore case (Groß-/Kleinschreibung ignorieren), eXtended (erlaubt Kommentare und Leerzeichen zur besseren Strukturierung) und single line (.* paßt über mehrere Zeilen hinweg) stehen.

include_replace kriegt für jeden Treffer den Namen der aktuell bearbeiteten HTML-Datei und den Namen der gesuchten Include-Datei mit - und prüft mit dem Hash %INCLUDE_MAP, ob diese vorher gefunden wurde. Falls nicht, bricht das Programm mit einer Fehlermeldung ab, falls ja, liefert include_replace einfach den im Hash ge-cache-ten Inhalt der Include-Datei zurück, mit dem die Substitutions-Anweisung in process_file dann endlich den Tag ersetzt. So einfach und doch so kompliziert!

Alltag

Zurück zum Alltag: Ändert sich nun ein Objekt, das in mehreren HTML-Seiten vertreten ist (z.B. Navigationsbalken), wird es einfach im Include-Verzeichnis einmal geändert und includer.pl aufgerufen - ratz-fatz erscheint die ganze Website in neuem Gewand. Die Webseiten selbst dürfen nach Herzenslust editiert werden, nur die Bereiche zwischen <!-- include ... --> und <!-- /include --> werden bekanntlich automatisch ersetzt.

Danke, danke!

Über die zahlreichen Zuschriften wegen meines September-Aufrufs zur Beifallsbekundung habe ich mich sehr gefreut, meine lieben Leser und Leserinnen, vielen Dank dafür! Deswegen lass' ich mich auch nicht lange bitten und mache weiter ... see ya in Perl land!

Listing: includer.pl

 1 #!/usr/bin/perl -w
 2 ##################################################
 3 # Syntax: includer [-i includedir] directory
 4 ##################################################
 5 
 6 use Getopt::Std;
 7 use File::Find;
 8 use Cwd;
 9 use strict;
10                          
11 my (%INCLUDE_MAP, $INCLUDE_ROOT);        # Globals
12 
13 my %opt;
14 getopts('ri:', \%opt) || usage("Argument Error");
15 
16 print "READONLY MODE\n" if $opt{r};
17 
18 my $include_dir = $opt{i} || "include";
19 
20 my $now = cwd();                # Get absolute path
21 chdir($include_dir) || 
22     usage("Cannot include from $include_dir");
23 $INCLUDE_ROOT = cwd();
24 chdir($now);
25 
26 usage("No start directory given") if $#ARGV < 0;
27 
28 usage("Start directory doesn't exist: $ARGV[0]")
29     unless -d $ARGV[0];
30 
31 File::Find::find(\&scan_include, $INCLUDE_ROOT);
32 File::Find::find(\&process_file, $ARGV[0]);
33 
34 ##################################################
35 sub scan_include {            # Scan include files
36 ##################################################
37     my $file = $_;            # Save $_ 
38 
39     return unless -f $_;      # No directories
40 
41     open(FILE, "<$file") || 
42         die "Cannot open $file (read)";
43 
44                             # relative path name
45     (my $rel = $File::Find::name) =~
46         s#^$INCLUDE_ROOT/*##g;
47 
48                             # read and store
49     my $data = join('', <FILE>);
50     chomp($data);
51     $INCLUDE_MAP{"/$rel"} = $data;
52 
53     close(FILE);
54 
55     $_ = $file;             # reset $_
56 }
57 
58 ##################################################
59 sub process_file {
60 ##################################################
61     my $file = $_;
62 
63     return if -d $file;
64     return unless $file =~ /\.html$/;
65 
66     open(FILE, "<$file") ||            # Read file
67         die "Cannot open $file (read)";
68     my $lines = join('', <FILE>);
69     close(FILE);
70 
71     my $subs = ($lines =~        # Replace includes
72         s@<!-- \s* include       # Intro tag
73           \s+                    # Whitespace
74           ([^\s]+)               # include file
75           .*?-->                 # end of tag
76           .*?<!--\s*/include\s*-->
77          @include_replace($file, $1)@gsexi);
78                                  # replace function
79 
80     if($subs) {
81         print "$File::Find::name: $subs subs\n";
82         
83         if(!$opt{r}) {
84             open(FILE, ">$file") || 
85                 die "Cannot open $file (write)";
86             print FILE $lines; 
87             close(FILE);
88         }
89     }
90 
91     $_ = $file;
92 }
93 
94 ##################################################
95 sub include_replace {
96 ##################################################
97     my ($file, $tag) = @_;
98 
99                      # Check if tag defined
100     if(exists $INCLUDE_MAP{$tag}) {
101                      # ... and return replacement
102         return "<!-- include $tag -->" .
103                "$INCLUDE_MAP{$tag}" .
104                "<!-- /include -->";
105     } else {
106         die "Cannot resolve include '$tag' " .
107             "in file ", cwd(), "/$file";
108     }
109 }
110     
111 ##################################################
112 sub usage {
113 ##################################################
114     $0 =~ s#.*/##g;
115     print "$0: @_.\n";
116     print "usage: $0 " . 
117           "[-r] [-i includedir] directory\n" .
118           "-r: read only\n" .
119           "-i: include file directory\n";
120     exit 1;
121 }

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 © 1998 Linux-Magazin Verlag