Aus juristischen Gründen sollte niemand mehr GIF-Bilder auf seiner
Website verwenden. Ein Skript durchwandert den Dokumentenbaum, wandelt
GIFs in freie PNG-Dateien um und zieht die <IMG> und
<A>-Tags im HTML aller sie referenzierenden Webseiten nach.
Wie in [3] nachzulesen, spielte sich vor einiger Zeit wegen eines Patents auf den Komprimierungsalgorithmus in GIF-Bildern ein bösartiges Gerangel zwischen der Firma Unisys und erschrockenen Webseitenbetreibern ab, denen plötzlich Briefe von Rechtsanwälten mit Geldforderungen ins Haus flatterten. Die Open-Source-Gemeinde reagierte prompt und schuf einen GIF-Erstatz: Das PNG-(Portable Network Graphics)-Format, das GIF überlegen ist -- Grund genug, alle alten GIFs zu verbrennen ([4]).
Enthält eine Website aber tausende von HTML-Seiten, die die GIF-Bilder referenzieren, kann das schnell in Arbeit ausarten. Deswegen wühlt sich das heute vorgestellte Perlskript durch alle Seiten, analysiert das HTML und findet Referenzen auf lokale GIF-Dateien. Findet es zum Beispiel irgendwo den HTML-Code
<IMG SRC="/images/buidl.gif">
weiß es, wo die zugehörige Datei buildl.gif liegt, wirft
das Programm convert aus der ImageMagick-Sammlung auf sie an und
kreiert buildl.png. Anschließend korrigiert es auch noch den Link
in der HTML-Seite, die dann mit
<IMG SRC="/images/buidl.png">
auf das neue Bild zeigt. Wie das geht? Führt man sich zu Gemüte, dass
unter Umständen im Originaltext
(wie zum Beispiel in diesem Artikel)
der String "/images/buidl.gif" außerhalb des HTML-Markups vorkommt,
den das Skript dann bitte nicht ersetzen soll,
wird schnell klar, dass
diese Aufgabe nicht mit einfachen regulären Ausdrücken zu erschlagen ist.
Einfaches Parsen tut's auch nicht, denn
die HTML-verarbeitenden Browser achten (im Gegensatz etwa zu XML-Parsern)
nicht streng auf die Syntaxregeln und murren
selbst manchmal bei haarsträubenden Fehlern nicht. Zum Glück gibt es
das Modul HTML::Parser von Gisle Aas und Michael Chase, mit
dem man HTML einfach und schlampig wie ein Browser durchforsten und, hier
kommt's, nebenbei manipulieren kann.
Das in Listing burngifs.pl gezeigte Skript verlangt in
den Zeilen 6 bis 8 nach perl in mindestens der Version 5.6.0. Weiter
besteht es mit use strict
und use warnings auf sauberem Code und nörgelt bei zwielichtigen
Vorgängen.
An Modulen braucht es mindestens die 3.0er Version von HTML::Parser,
URI::URL zum Herumspielen mit URLs, File::Find zum Durchstöbern
von Dateibäumen, und einige Funktionen aus File::Basename und
File::Spec zum handlichen Manipulieren von Dateipfaden.
Die Konfigurationssektion ab Zeile 18 stellt das Skript auf den
aktuellen Einsatzort ein. Da burngifs.pl sowohl absolute als auch
relative Image-Links unterstützt, muss es wissen, dass die Linkangaben
http://perlmeister.com/i.gif
http://www.perlmeister.com/i.gif
/i.gif
i.gif
unter Umständen auf dasselbe Bild verweisen. Hierzu braucht es
in @SITES die Top-URLs der Website und in $BASE_DIR das
zugehörige Basisverzeichnis auf der Festplatte.
$CONVERT gibt den Pfad zum Konvertierungsprogramm convert aus
der Sammlung ImageMagick an, die praktisch jeder Linux-Distribution
beiliegt, falls nicht, gibt es sie unter [2]. Ein aus der Shell
aufgerufenes which convert zeigt, ob und wo convert installiert ist.
Der reguläre Ausdruck
in $PAGEMATCH legt fest, dass das Skript nur .html und .htm-Dateien
anfasst -- wer zum Beispiel noch .asp und .php braucht,
schreibt statt dessen einfach qr#\.html?$|\.asp$|\.php$#.
Zeile 25 definiert mit our drei globale Variablen, über die die
verschiedenen Funktionen im Skript miteinander kommunizieren:
Nach $OUTDATA schreibt der Parser seine manipulierten Daten,
$REPS ist die Anzahl von Ersetzungen pro Datei für informative
Zwecke und im Hash %BURNED schmoren die Pfade aller bislang
konvertierten GIFs.
Zeile 28 macht aus dem Array von URLs in @SITES einen Array von
URI::URL-Objekten, damit wir sie später leichter manipulieren können.
HTML::ParserZeile 31 definiert den HTML::Parser, der mit Version 3.0 ein neues
API erhielt: Man gibt einfach an, welche benutzerdefinierten Funktionen
der Parser bei welchen Ereignissen anspringen soll, und welche Argumente
jene erwarten. Der Eintrag unter start_h gibt an, dass der
Parser im Falle eines sich öffnenden Tags (z.B.
<IMG SRC="i.gif" WIDTH=65>)
der weiter unten definierten Funktion burn_gif folgendes übergibt:
tagname
einen Skalar mit dem in Kleinbuchstaben umgewandelten Namen des Tags
(z.B. img)
mit attrseq eine Referenz auf einen Array, der als Elemente alle
in Kleinbuchstaben umgewandelten
Attributnamen des Tags
in der ursprünglichen Reihenfolge
Tags enthält: ("src", "width")
mit attr eine Referenz auf einen Hash, der den Attributnamen von
attrseq entsprechende Werte zuordnet:
("src" => "i.gif", "width" => "65")
mit text den gesamten ursprünglichen Text des Tags:
<IMG SRC="i.gif" WIDTH=65>)
Für alle anderen Ereignisse (z.B. Kommentare, Text oder
schließende Tags) springt der Default-Handler default_h
ein, den Zeile 32 so
definiert, dass er den ursprünglich gefundenen Text zur unmodifizierten
Ausgabe an die weiter unten definierte Funktion print_out()
weiterleitet.
Die aus dem Modul File::Find exportierte Funktion find() startet
in Zeile 38 den Ersetzungsreigen und ruft hierzu für jeden Eintrag,
den sie rekursiv unter dem Dokumentenpfad des Webservers $BASE_DIR
findet, die Funktion warp_file() mit der HTML::Parser-Objektreferenz als
Argument auf.
Am Ende stehen in %BURNED alle ersetzten GIF-Dateien, die
Zeile 43 dann für immer von der Platte löscht.
Da warp_file() auch für Verzeichnisse und Binärdateien aufgerufen
wird, muss sie in Zeile 52 prüfen,
ob denn überhaupt eine ersetzungswürdige Datei vorliegt -- falls nicht,
kehrt sie sofort mit return zurück und
lässt die Datei unangetastet.
-T lässt nur Textdateien durch und der in der
Konfigurationssektion definierte reguläre Ausdruck $PAGEMATCH schränkt
die Auswahl weiter ein.
Die Zeilen 58 bis 61 lesen den HTML-Code aus der Datei
in den Skalar $data ein. Zeile 65 gibt sie dem Parser mit der
data()-Methode zu fressen und signalisiert mit der anschließend
abgesetzten eof()-Methode, dass die aktuelle
Datei zuende ist, andernfalls interpretierte der persistente Parser
alle Dateien als kontinuierlichen Datenstrom und käme eventuell
ins Schleudern.
Die manipulierten Daten schreibt er in den vorher auf den Leerstring
initialisierten globalen Skalar $OUTDATA. Liegen anschließend
tatsächlich Veränderungen gegenüber der Originalversion vor,
überschreiben die Zeilen 70 bis 73
die Originaldatei mit dem korrigierten HTML.
Die ab Zeile 79 definierte Funktion print_out hängt Ausgabedaten
einfach an den globalen String $OUTDATA an. Wir wollen ganz sicher
gehen, dass alles, einschließlich der Bildformatkonvertierungen glatt
geht, bevor die Originaldatei letztendlich überschrieben wird.
burn_gif() ab Zeile 87 wird vom Parser für jedes sich öffnende
Tag aufgerufen. Da wir uns nur für das
src-Attribut von <img> und das href-Attribut
von <a> interessieren, muss der else-Zweig ab Zeile 100
den Orginaltext herausschreiben und burn_gif beenden, da der Parser
für andere Tags keine Veränderungen am HTML-Code vornehmen darf.
Die Zeilen 104 bis 106 prüfen, ob eines unserer gesuchten Attribute
vorliegt, sieht nach, ob dessen Stringwert auf .gif oder .GIF
endet und findet mit der weiter unten definierten Funktion
url2file heraus, ob es sich um einen Link auf eine tatsächlich
existierende Datei auf dem lokalen Webserver handelt.
Falls ja, macht warp_name() aus dem GIF-Namen den PNG-Namen,
falls nein, gibt's nichts zu verändern und Zeile 114 gibt den
Originaltext aus bevor Zeile 115 burn_gif() abbricht.
Da Zeile 110 bereits den Hash-Eintrag für den Image-Link
in $attr->{$key} verändert hat, geben die Zeilen 119 bis 123 den
modifizierten Tag einfach dadurch aus, dass sie mit
mit map durch alle Attributname und -einträge laufen, sie jeweils
in Textstrings im Format KEY="VALUE" umwandeln und diese
dann mit join durch Leerzeichen getrennt hintereinander ausdrucken.
Existiert zu einer gefundenen GIF-Datei noch keine PNG-Datei oder
ist die PNG-Datei älteren Datums als das Original-GIF, wirft
Zeile 135 den Formatierer convert aus dem ImageMagick-Paket an,
der aus GIF-Dateien ohne Firlefanz PNG-Bilder macht.
Zeile 137 merkt sich im Hash %BURNED, dass die alte GIF-Datei später
gelöscht werden kann.
Die vorher erwähnte Funktion url2file() ab Zeile 142 versucht,
einem Link eine lokale Datei zuzuordnen. Drei Möglichkeiten gibt's:
http://perlmeister.com/img/i.gif. Die zugehörige Datei befindet sich
im Unterverzeichnis img von $BASE_DIR.
Der Link ist eine absolute Pfadangabe, wie z.B. /img/2/i.gif. Die
Datei befindet sich in img/2 unterhalb von $BASE_DIR.
Der Link ist eine relative Pfadangabe, wie z.B. ../img/i.gif. Die
Datei befindet sich in ../img, ausgehend vom aktuellen Verzeichnis
der entsprechenden Webseite.
Zeile 146 macht aus dem URL-String ein URI::URL-Objekt, über dessen
Methoden wir anschließend einfach auf die verschiedenen Teile
des URLs zugreifen können. $uri->scheme() liefert den
Protokollteil des URLs zurück (also etwa http) und undef,
falls nur ein lokaler Pfad vorliegt. Bei vollständigen URLs untersucht
die for-Schleife ab Zeile 150, ob der URL mit einem der
in @SITES in der Konfigurationssektion angegebenen Alias-URLs
übereinstimmt. Die Methode netloc() der Klasse URI::URL
liefert hierzu
die ersten drei Teile der URL, die das Protokoll, den Host und
den Port festlegen. Liegt der Link irgendwo unterhalb des aktuell
untersuchten Alias-Namens $s, transformiert die rel()-Methode mit
$s als Argument in Zeile 152 den aktuellen Link in eine
relative Pfadangabe zur Basis $s.
Aus http://perlmeister.com/img/i.gif wird so /img/i.gif.
Für den Fall, dass der Link bereits als Pfadangabe vorliegt, kommt
der else-Zweig ab Zeile 157 zum Einsatz. Absolute Pfadangaben
können wir belassen, relative hingegen müssen wir in ``absolute''
Angaben relativ zur Dokumentenwurzel des Webservers umrechnen.
file_name_is_absolute() aus File::Spec::Functions sieht nur
nach, ob ein / vorne dran steht, und falls dem nicht so ist, rechnet
Zeile 159 zunächst mit rel2abs (auch aus File::Spec::Functions)
den zugehörigen absoluten Pfad aus. rel2abs bezieht hierzu
den Linknamen auf das aktuelle Verzeichnis (in das File::Find während
der rekursiven Suche praktischerweise wechselt) und ermittelt daraus
den Pfad, ausgehend vom / des Unix-Systems. Um diese absolute
Angabe dann wieder auf die Dokumentenwurzel des Webservers zu
relativieren, wandelt abs2rel (auch aus File::Spec::Functions)
$rel wieder zur Basis $BASE_DIR um. Uff!
catfile und canonpath aus dem gleichen Modul machen daraus
in Zeile 164
wieder eine absolute Pfadangabe auf der aktuellen Festplatte, in
dem sie $BASE_DIR und $rel zusammenhängen und den entstehenden
Pfad minimalisieren.
Falls es zu einem GIF-Bild-Link keine zugehörige lokale Datei gibt,
meldet dies Zeile 166 dem Benutzer und Zeile 169 lässt
url2file den Wert undef zurückgeben. Andernfalls kommt der
absolute Pfad in $p als String zurück.
Die Funktion warp_name ab Zeile 173 führt nur eine einfache
Textersetzung durch, und liefert den zu einem GIF-Namen
zugehörigen PNG-Namen zurück.
Vor dem Laufenlassen des Skripts müssen noch die Konfigurationsvariablen
in den Zeilen 18 bis 22 an die lokalen Verhältnisse angepasst werden:
Die URLs, unter denen die Website erreichbar ist in @SITES,
in $BASE_DIR die Dokumentenwurzel des Webservers, und in $CONVERT
der Pfad zum convert-Programm.
Die verwendeten Perl-Module liegen jeder neuen
Perl-Distribution bei, falls nicht, kann man sie entweder vom CPAN abholen
oder, noch besser, die neueste Perl-Version auf www.perl.com abholen.
Dann schnell aus Sicherheitsgründen ein Backup gemacht -- und los geht's,
putzt die obsoleten GIFs weg!
001 #!/usr/bin/perl
002 ##################################################
003 # burngifs.pl -- Mike Schilli, 2001
004 # (m@perlmeister.com)
005 ##################################################
006 use 5.6.0;
007 use warnings;
008 use strict;
009
010 use HTML::Parser 3.0;
011 use URI::URL;
012 use File::Spec::Functions qw(catfile canonpath
013 rel2abs abs2rel file_name_is_absolute);
014 use File::Find;
015 use File::Basename;
016
017 # Namen, unter denen die Website bekannt ist
018 my @SITES = qw( http://perlmeister.com
019 http://www.perlmeister.com );
020 my $BASE_DIR = "/web/HTML";
021 my $CONVERT = "/usr/local/bin/convert";
022 my $PAGEMATCH = qr#\.html?$#;
023
024 # Globale Variablen
025 our ($OUTDATA, $REPS, %BURNED);
026
027 # Alias-Namen als URL::URI-Objekte speichern
028 @SITES = map { URI::URL->new($_) } @SITES;
029
030 # Parser aufsetzen
031 my $parser = HTML::Parser->new(
032 default_h => [ \&print_out, 'text' ],
033 start_h => [ \&burn_gif,
034 'tagname,attrseq,attr,text']);
035
036 # Rekursiv suchen, manipulieren und GIFs
037 # konvertieren
038 find(sub {warp_file($parser)}, $BASE_DIR);
039
040 # Ersetzte GIF-Dateien löschen
041 for my $gif (keys %BURNED) {
042 print "Deleting $gif\n";
043 unlink $gif or warn "Cannot unlink $gif ($!)";
044 }
045
046 ##################################################
047 sub warp_file { # Eine Datei konvertieren
048 ##################################################
049 my $parser = shift;
050 my $file = $_;
051
052 return unless -T $file and
053 $file =~ $PAGEMATCH;
054
055 $REPS = 0;
056
057 # Daten aus Datei holen
058 open FILE, "<$file" or
059 die "Cannot open $file ($!)";
060 my $data = join '', <FILE>;
061 close FILE;
062
063 $OUTDATA = "";
064
065 $parser->parse($data) || die $!;
066 $parser->eof;
067
068 if($data ne $OUTDATA) {
069 # Zurückschreiben
070 open FILE, ">$file" or
071 die "Cannot open $file ($!)";
072 print FILE $OUTDATA;
073 close FILE;
074 print " $REPS replacements\n" if $REPS;
075 }
076 }
077
078 ##################################################
079 sub print_out {
080 ##################################################
081 my ($text) = shift;
082
083 $OUTDATA .= $text;
084 }
085
086 ##################################################
087 sub burn_gif {
088 ##################################################
089 my($tagname, $attrseq, $attr, $text) = @_;
090 my($path, $key);
091
092 if($tagname eq "img") {
093 # <IMG SRC=...> Tag gefunden
094 $key = "src";
095 } elsif($tagname eq "a") {
096 # <A HREF=...> Tag gefunden
097 $key = "href";
098 } else {
099 # Anderes Tag => unverändert ausgeben
100 print_out $text;
101 return;
102 }
103
104 if(exists $attr->{$key} and
105 $attr->{$key} =~ /\.gif$|\.GIF$/ and
106 defined ($path = url2file($attr->{$key}))
107 ) {
108 # Tag referenziert eine existierende
109 # GIF-Datei auf Website.
110 $attr->{$key} = warp_name($attr->{$key});
111 } else {
112 # Keine lokale GIF-Datei existiert
113 # => Unverändert ausgeben
114 print_out $text;
115 return;
116 }
117
118 # Tag mit veränderten Attributen ausgeben
119 $OUTDATA .= "<" . uc($tagname) . " " .
120 join(" ", map { uc($_) . '="' .
121 $attr->{$_} . '"'
122 } @$attrseq ) .
123 ">";
124
125 print "$File::Find::name\n" if $REPS++ < 1;
126
127 my $new = warp_name($path);
128
129 # GIF->PNG-Konvertierer aufrufen, falls
130 # PNG-Datei noch nicht existiert oder
131 # älter als GIF-Datei ist.
132 if(! -f $new or -M $new > -M $path) {
133 print " Converting ", basename($path),
134 " -> ", basename($new), "\n";
135 system($CONVERT, $path, $new) and
136 die "Converting failed";
137 $BURNED{$path} = 1;
138 }
139 }
140
141 ##################################################
142 sub url2file {
143 ##################################################
144 my($link) = @_;
145
146 my $uri = URI::URL->new($link);
147 my $rel = "";
148
149 if($uri->scheme) {
150 for my $s (@SITES) {
151 if($uri->netloc() eq $s->netloc()) {
152 $rel = $uri->rel($s);
153 last;
154 }
155 }
156 } else {
157 $rel = $link;
158 if(!file_name_is_absolute($rel)) {
159 $rel = rel2abs($rel);
160 $rel = abs2rel($rel, $BASE_DIR);
161 }
162 }
163
164 my $p = canonpath(catfile($BASE_DIR, $rel));
165
166 print " $File::Find::name: No local GIF ",
167 "for '$link'\n" unless -f $p;
168
169 return -f _ ? $p : undef;
170 }
171
172 ##################################################
173 sub warp_name {
174 ##################################################
175 my $link = shift;
176
177 (my $new = $link) =~ s/\.gif$|\.GIF$/.png/;
178
179 return $new;
180 }
![]() |
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |