Konvertierung von XML in HTML mit XSLT

(Auszug aus "Perl & XML" von Erik T. Ray & Jason McIntosh)

Wenn Sie sich schon einmal mit Web-Programmierung in Perl beschäftigt haben, dann haben Sie in einem gewissen Sinne auch schon mit XML gearbeitet: HTML ist gar nicht so weit entfernt von den Wohlgeformtheitsprinzipien von XML. Zumindest gilt das in der Theorie. In der Praxis wird HTML als Kombination aus Markup, eingebetteten Skripten, Metadaten und einem Dutzend anderer Dinge eingesetzt. Diese Vielfalt macht es so schwer, mit modernen Webseiten verläßliche Ergebnisse zu erzielen. Durch die übergroße Toleranz der Browser in Bezug auf fehlerhafte Konstrukte und Faulheit des HTML-Programmierers wird diese Entwicklung leider noch gefördert.

Zur Zeit ist HTML die Sprache des Web schlechthin. Vermutlich bleibt das auch noch eine ganze Weile so. Man kann aber auch bereits XML zur Erstellung von Webseiten einsetzen, indem man dem W3C-Standard XHTML vertraut.

XHTML gibt es in zwei Varianten. Wir bevorzugen den weniger strengen Stil »transitional« (eine Art Übergangsmodus zu HTML). Dieser übersieht viele der altvertrauten Sünden großzügig, zum Beispiel die Verwendung von <font>-Tags statt der empfohlenen Verwendung kaskadierender Stylesheets.

Möchte man aber Probleme mit Benutzern älterer Browser oder dergleichen vermeiden, ist es relativ wahrscheinlich, daß man die dafür erstellten Seiten in HTML konvertieren muß.

Das kann auf vielfältige Weise geschehen. Die Hammermethode ist das Parsen eines Dokuments und die Ausgabe in einem CGI-Skript. Das folgende Beispiel liest eine Datei in MonkeyML mit den Namen meiner Hausaffen und gibt diese in einer Standardwebseite aus. (Dabei wird Lincoln Steins einzigartiges CGI-Modul verwendet, um ein gewisses Maß an syntaktischem Zucker zu gewähren.)

#!/usr/bin/perl

use warnings;
use strict;
use CGI qw(:standard);
use XML::LibXML;

my $parser = XML::XPath;
my $doc = $parser->parse_file('monkeys.xml');

print header;
print start_html("Meine Hausaffen");
print h1("Meine Hausaffen");
print p("Zu Hause habe ich die folgenden Affen:");
print "<ul>\n";
foreach my $name_node ($doc->documentElement->findnodes("//mm:name")) {
    print "<li>" . $name_node->firstChild->getData ."</li>\n";
}

print end_html;

Eine andere Möglichkeit ist der Einsatz von XSLT.

Die Aufgabe von XSLT ist in diesem Fall die Transformation eines XML-Dokuments in ein anderes. XSLT paßt hier relativ gut ins Bild, weil das auszugebende HTML-Dokument ja ohnehin häufig nichts anderes ist als die Darstellung der in einem vorhandenen XML-Dokument enthaltenen Informationen. Eine sehr umfassende und speziell für diesen Zweck entwickelte Anwendung ist Matt Sergeants AxKit (http://www.axkit.org). Dabei handelt es sich um ein Serverframework, bei dem man eine Website als eine Menge von XML-Dateien entwickelt, die dann durch AxKit dynamisch in HTML konvertiert und an den Browser ausgegeben werden.

Beispiel: Apache::DocBook

Schreiben wir ein kleines Modul, das DocBook-Dateien liest und in HTML konvertiert. Unser Ziel ist weniger ambitioniert als das von AxKit. Trotzdem übernehmen wir einige der Ideen dieses Programms. Zum Beispiel werden auch wir uns auf mod_perl stützen. Dieses Modul erweitert den Webserver Apache um einen Perl-Interpreter. Dies ist die schnellste Möglichkeit, HTML-Seiten mit Perl dynamisch zu erzeugen.

Wir benutzen einige der wichtigsten Features von mod_perl. Das beginnt damit, daß wir ein eigenes Perl-Modul mit einer Subroutine handler schreiben. Dies ist ein von mod_perl vorgegebener Standardname. Die Funktion wird als Callback von mod_perl aufgerufen und erhält dabei ein Objekt übergeben, das den von Apache empfangenen HTTP-Request simuliert.

Achtung! Wenn man innerhalb einer Apache-Umgebung mit Perl und XML entwickelt, dann ist der Apache-Server selbst oft eine Quelle der Frustration. Genauer gesagt sind es die Optionen, die bei der Kompilierung des Servers gesetzt waren oder eben nicht. Die Apache-Standarddistribution enthält eine Version der C-Bibliothek Expat. Sofern man dies nicht ausdrücklich untersagt, ist dieser Parser fest in den Apache integriert. Unglücklicherweise steht dieser Parser in Konflikt mit dem Expat-Parser von XML::Parser. Unter Unix kann das zum Beispiel leicht zu einem Segmentation-Fault oder ähnlichen Störungen führen.
Unter Apache-Entwicklern gibt es die Absicht, diesen Parser in zukünftigen Versionen zu entfernen oder wenigstens zu verstecken. Zur Zeit ist es aber durchaus möglich, daß man zunächst einen Apache ohne Expat kompilieren muß, bevor man XML::Parser einsetzen kann. Zu diesem Zweck muß man die configure-Option EXPAT auf »no« setzen.
Einfacher kann es sein, XML::LibXML oder einen anderen Parser aus der XML::SAX -Familie einzusetzen, da diese nicht auf Expat aufbauen.

Wir beginnen unseren Film »Der mit dem Modul tanzt« mit einer Reise nach Callback-City:

package Apache::DocBook;   

use warnings;
use strict;

use Apache::Constants qw(:common);

use XML::LibXML;
use XML::LibXSLT;

our $xml_path; # Verzeichnis mit zu lesenden Dokumenten
our $base_path; # Verzeichnis für HTML-Ausgabe
our $xslt_file; # Pfad für DocBook-nach-HTML-Stylesheet
our $icon_dir; # Verzeichnis mit Icons für Indexseite

sub handler {
   my $r = shift; # Requestobjekt, von Apache übergeben
   # Konfiguration von Apache lesen
   $xml_path = $r->dir_config('doc_dir')
   or die "Variable doc_dir ist nicht gesetzt.\n";
   $base_path = $r->dir_config('html_dir')
   or die "Variable html_dir ist nicht gesetzt.\n";
   $icon_dir = $r->dir_config('icon_dir')
   or die "Variable icon_dir ist nicht gesetzt.\n";
   unless (-d $xml_path) {
   $r->log_reason("Das Verzeichnis $xml_path existiert nicht: $!", $r->filename);
   die;
   }
   my $filename = $r->filename;
   
   $filename =~ s/$base_path\/?//;
   # Pfad der zu erzeugenden Datei
      $filename .= $r->path_info;

      $xslt_file = $r->dir_config('xslt_file')
      or die "Variable xslt_file ist nicht gesetzt.\n";
     
   # Die folgenden Subroutinen besorgen die Ausgabe von HTML an den Client.
     
   # Soll ein Index ausgegeben werden?
      if ( (-d "$xml_path/$filename") or ($filename =~ /index.html?$/) ) {
   # Keine Indexseite vorhanden! Na und, machen wir eben eine.
      my ($dir) = $filename =~ /^(.*)(\/index.html?)?$/;
   # Der URI sollte einen abschließenden Slash (/) enthalten.
      if (not($2) and $r->uri !~ /\/$/) {
      $r->uri($r->uri . '/');
   }
      make_index_page($r, $dir);
      return $r->status;
   } else {
   # Nein, hier soll tatsächlich eine normale Seite ausgegeben werden.
      make_doc_page($r, $filename);
      return $r->status;
      }
      return $r->status;
}

Die folgende Subroutine erledigt die eigentliche XSLT-Transformation. Sie erhält als Eingabe den Namen der originalen XML-Datei sowie den Namen einer Datei, in die das transformierte Ergebnis in Form von HTML zu schreiben ist:

sub transform {    
    my ($filename, $html_filename) = @_;
      
    # Sicherstellen, daß das Zielverzeichnis existiert
    maybe_mkdir($filename);
      
    my $parser = XML::LibXML->new;
    my $xslt = XML::LibXSLT->new;
      
    # libxslt scheint einen Bug zu haben, der ein "include" anderer Dateien
    # nur dann funktionieren läßt, wenn man sich im betreffenden Verzeichnis
    # befindet.
    use Cwd; # Um das aktuelle Verzeichnis zu erhalten
    my $original_dir = cwd;
    my $xslt_dir = $xslt_file;
    $xslt_dir =~ s/^(.*)\/.*$/$1/;
    chdir($xslt_dir)
        or die "Kann nicht ins Verzeichnis $xslt_dir wechseln: $!";
    
    my $source = $parser->parse_file("$xml_path/$filename");
    my $style_doc = $parser->parse_file($xslt_file);
      
    my $stylesheet = $xslt->parse_stylesheet($style_doc);
      
    my $results = $stylesheet->transform($source);
      
    open (HTML_OUT, ">$base_path/$html_filename");
    print HTML_OUT $stylesheet->output_string($results);
    close (HTML_OUT);
      
    # Ins ursprüngliche Verzeichnis zurückkehren
    chdir($original_dir)
        or die "Kann nicht ins Verzeichnis $original_dir wechseln: $!";
}

Als nächstes folgen zwei Subroutinen zur Erzeugung einer Indexseite. Im Gegensatz zu den aus Docbook generierten Detailseiten ist die Indexseite nicht das Ergebnis einer XSLT-Transformation. Wir füllen eine Tabelle mit Informationen, die wir aus den Docbook-Dokumenten per XPath lesen. Diese Tabelle geben wir dann als HTML aus.

 sub make_index_page {   
      my ($r, $dir) = @_;
     
      # Wenn es kein entsprechendes Verzeichnis gibt, brechen wir ab.
     
      my $xml_dir = "$xml_path/$dir";
      unless (-r $xml_dir) {
      unless (-d $xml_dir) {
      # Das ist kein Verzeichnis!
   $r->status( NOT_FOUND );
   return;
   }
   # Das ist ein Verzeichnis, aber wir dürfen es nicht lesen.
   $r->status( FORBIDDEN );
   return;
   }
     
   # Lies die Modifikationszeiten des Verzeichnisses und der darin enthaltenen Dateien
      my $index_file = "$base_path/$dir/index.html";
     
      my $xml_mtime = (stat($xml_dir))[9];
      my $html_mtime = (stat($index_file))[9];
     
   # Wenn die Indexseite älter ist als das XML-Verzeichnis oder wenn sie nicht
   # existiert, erzeugen wir eine neue Indexseite.
   if ((not($html_mtime)) or ($html_mtime <= $xml_mtime)) {
      generate_index($xml_dir, "$base_path/$dir", $r->uri);
      $r->filename($index_file);
      send_page($r, $index_file);
      return;
   } else {
   # Die Indexseite ist immer noch aktuell. Der Apache-Server darf sie ausgeben.
   $r->filename($index_file);
   $r->path_info('');
   send_page($r, $index_file);
      return;
   }
   }
   
sub generate_index {
   my ($xml_dir, $html_dir, $base_dir) = @_;
  
   # Evtl. abschließendes / aus base_dir entfernen.
      $base_dir =~ s|/$||;
     
      my $index_file = "$html_dir/index.html";
     
      my $local_dir;
      if ($html_dir =~ /^$base_path\/*(.*)/) {
         $local_dir = $1;
      }

   # Falls erforderlich, Zielverzeichnis erstellen
   maybe_mkdir($local_dir);
   open (INDEX, ">$index_file")
   or die "Kann die Datei $index_file nicht erzeugen: $!";
   
   opendir(DIR, $xml_dir) or die "Kann das Verzeichnis $xml_dir nicht öffnen: $!";
   chdir($xml_dir) or die "Kann nicht in das Verzeichnis $xml_dir wechseln: $!";
   
   # Icondateien setzen
   my $doc_icon = "$icon_dir/generic.gif";
   my $dir_icon = "$icon_dir/folder.gif";
   
   # Erzeuge einen sprechenden Namen für $local_dir (möglicherweise einfach
   # den Verzeichnisnamen)
   my $local_dir_label = $local_dir || '/';
   
   # Start der Seite ausgeben
   print INDEX <<END;
   <html>
   <head><title>Inhaltsverzeichnis von $local_dir_label</title></head>
   <body>
   <h1>Inhaltsverzeichnis von $local_dir_label</h1>
   table width="100%">
   END
   
   # Pro Datei eine Zeile mit Link ausgeben
   
   while (my $file = readdir(DIR)) {
   # Ignoriere .sowieso-Dateien und Verzeichnisse
   if (-f $file && $file !~ /^\./) {
      # Parser erzeugen
      my $parser = XML::LibXML->new;
     
      # Datei lesen und überspringen, falls nicht wohlgeformt:
      my $doc;
      eval {$doc = $parser->parse_file($file);};
   if ($@) {
      warn "Die Datei $file scheint kein XML-Dokument zu sein.";
      warn $@;
      next;
      }
     
   my %info;               # Wird mit Informationen gefüllt
     
    Dokumenttyp bestimmen
   my $root = $doc->documentElement;
   my $root_type = $root->getName;
     
   # Information wird aus einem speziellen Infoknoten gelesen, der den Namen
   # $FOOinfo trägt.
   my ($info) = $root->findnodes("${root_type}info");
   if ($info) {
     
      # Gefunden, fülle %info damit.
   if (my ($abstract) = $info->findnodes('abstract')) {
      $info{abstract} = $abstract->string_value;
      } elsif ($root_type eq 'reference') {
     
   if ( ($abstract) =
      $root->findnodes('/reference/refentry/refnamediv/refpurpose') ) {
      $info{abstract} = $abstract->string_value;
     }
   }
   if (my ($date) = $info->findnodes('date')) {
      $info{date} = $date->string_value;
      }
   }
   if (my ($title) = $root->findnodes('title')) {

      $info{title} = $title->string_value;
   }

   # Für einige Informationen brauchen wir kein XML ...
   unless ($info{date}) {
   my $mtime = (stat($file))[9];
   $info{date} = localtime($mtime);
   }
   $info{title} ||= $file;
   
   # Das genügt. Wir können jetzt eine Zeile HTML ausgeben.
   print INDEX "<tr>\n";
   
   # Wir simulieren einen Dateinamen für einen Link -- foo.html
   my $html_file = $file;
   $html_file =~ s/^(.*)\..*$/$1.html/;
   print INDEX "<td>";
   print INDEX "<img src=\"$doc_icon\">" if $doc_icon;
   print INDEX "<a href=\"$base_dir/$html_file\">$info{title}</a></td> ";
   foreach (qw(abstract date)) {
      print INDEX "<td>$info{$_}</td> " if $info{$_};
   }
      print INDEX "\n</tr>\n";
   } elsif (-d $file) {
     
   # Eventuell Link auf ein Verzeichnis erzeugen
   # ... sofern das Verzeichnis nicht besser ignoriert wird.
   next if grep (/^$file$/, qw(RCS CVS .)) or ($file eq '..' and not $local_dir);
   print INDEX "<tr>\n<td>";
   print INDEX "<a href=\"$base_dir/$file\"><img src=\"$dir_icon\">" if $dir_icon;
   print INDEX "$file</a></td>\n</tr>\n";
   }
   }


   # Tabelle und Seite beenden
   print INDEX <<END;
 </table>
 </body>
 </html>
 END
   
 close(INDEX) or die "Konnte $index_file nicht schließen: $!";
 closedir(DIR) or die "Konnte $xml_dir nicht schließen: $!";
 }

Die folgenden Subroutinen bauen auf den obigen Transformationen auf und vervollständigen die HTML-Seiten. Beachten Sie, daß die Modifikationszeiten von DocBook- und HTML-Datei zum Aufbau eines Cache verglichen werden. Die HTML-Datei wird nur dann neu erzeugt, wenn sie älter als die DocBook-Datei ist. (Wenn es noch überhaupt keine HTML-Datei gibt, wird selbstverständlich immer eine neue HTML-Datei erzeugt.)

 sub make_doc_page {    
     my ($r, $html_filename) = @_;
      
    # Generiere den Namen der Eingabedatei, indem die Endung durch .xml
    # ersetzt wird.
    my $xml_filename = $html_filename;
    $xml_filename =~ s/^(.*)(?:\..*)$/$1.xml/;
      
    # Falls es ein Problem beim Lesen der XML-Eingabedatei gibt, wird eine
    # Fehlermeldung ausgegeben.
    unless (-r "$xml_path/$xml_filename") {
        unless (-e "$xml_path/$xml_filename") {
        $r->status( NOT_FOUND );
        return;
    } else {
        # Existiert, aber keine Berechtigung zum Lesen
        $r->status( FORBIDDEN );
        return;
        }
    }
      
    # Lies die Modifikationszeiten der beiden Dateien zum Vergleich
    my $xml_mtime = (stat("$xml_path/$xml_filename"))[9];
    my $html_mtime = (stat("$base_path/$html_filename"))[9];
    # Eine HTML-Datei wird erzeugt, wenn sie noch nicht existiert oder wenn
    # die XML-Datei neueren Datums ist.
      
    if ((not($html_mtime)) or ($html_mtime <= $xml_mtime)) {
        transform($xml_filename, $html_filename);
        $r->filename("$base_path/$html_filename");
        $r->status( DECLINED );
        return;
    } else {
        # Treffer im Cache. Laß Apache die existierende Datei ausgeben.
        $r->status( DECLINED );
        }
    }
    
 sub send_page {
    my ($r, $html_filename) = @_;
    # Wenn die Zieldatei gerade eben erst erzeugt wird, können wir anschließend
    # nicht mit einem 'DECLINED' die Ausgabe an den Apache übergeben, da dieser
    # dann ein File-not-found melden würde.
    $r->status( OK );
    $r->send_http_header('text/html');
    open(HTML, "$html_filename")
        or die "Konnte Datei $base_path/$html_filename nicht lesen: $!";
    while (<HTML>) {
        $r->print($_);
    }
    close(HTML) or die "Konnte die Datei $html_filename nicht schließen: $!";
    return;
}

Abschließend haben wir noch eine Funktion, die dafür sorgt, daß alle nötigen Verzeichnisse existieren. Dabei spiegelt die Struktur des Verzeichnisses mit HTML-Dateien die Struktur des Originalverzeichnisses mit den XML-Dateien:

sub maybe_mkdir {    
    # Erzeuge zu einem gegebenen vollständigen Pfad alle noch nicht vorhandenen
    # Teilverzeichnisse
    my ($filename) = @_;
    my @path_parts = split(/\//, $filename);
    # Entferne eventuellen Dateinamen
    pop(@path_parts) if -f $filename;
    my $traversed_path = $base_path;
    foreach (@path_parts) {
        $traversed_path .= "/$_";
        unless (-d $traversed_path) {
            mkdir ($traversed_path)
              or die "Konnte das Verzeichnis $traversed_path nicht erzeugen: $!";
      }
    }
    return 1;
}

  

  

<< zurück vor >>

 

 

 

Tipp der data2type-Redaktion:
Zum Thema Perl & XML bieten wir auch folgende Schulungen zur Vertiefung und professionellen Fortbildung an:

Copyright © 2003 O'Reilly Verlag GmbH & Co. KG
Für Ihren privaten Gebrauch dürfen Sie die Online-Version ausdrucken.
Ansonsten unterliegt dieses Kapitel aus dem Buch "Perl & XML" denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

O’Reilly Verlag GmbH & Co. KG, Balthasarstraße 81, 50670 Köln, kommentar(at)oreilly.de