kreative Nutzung von Web-Diensten mittels HTML::TreeBuilder

Martin H. Sluka <martin@sluka.de>

2002-12-15

  1. Was ist HTML::TreeBuilder?
  2. HTML::TreeBuilder ist eine Sub-Klasse zu HTML::Parser (und HTML::Element), die es ermöglicht, ein HTML-Dokument in eine aus HTML::Element-Objekten und Strings bestehende Baum-Struktur zu zerlegen, die sodann mit verschiedensten Methoden bequem durchsucht, ggf. verändert und z. B. wieder als HTML-Dokument ausgegeben werden kann.

    Nehmen wir als Beispiel ein einfaches HTML-Dokument:

    <HTML>
      <HEAD>
        <TITLE>Beispielseite</TITLE>
        <META NAME="AUTHOR" CONTENT="fany">
      </HEAD>
      <BODY BGCOLOR="white">
        <H1>Beispiel</H1>
        <P>Das ist der Rand von Ostermundigen.</P>
        <P>Dieser Text ist <EM>wichtig</EM>.</P>
      </BODY>
    </HTML>
    Abb. 1: Beispiel-HTML-Dokument

    Dieses Dokument lässt sich folgendermaßen als Baum darstellen:

                           <HTML>
                          /      \
                         /        \
                        /          \
                  <HEAD>            <BODY BGCOLOR="white">
                 /  |                 |    |              \
                /   |                 |    |               \
               /    |                 |    |                \
        <TITLE>  <META              <H1>  <P>                <P>
           |      NAME="AUTHOR"    /       |                / | \
           |      CONTENT="fany"> /        |               /  |  \
           |                     /         |              /   |   \
    "Beispielseite"    "Beispiel"  "Das ist der     "Dieser  <EM>  "."
                                    Rand von         Text     |
                                    Ostermundigen."  ist"     |
                                                              |
                                                          "wichtig"
    Abb. 2: Beispiel-HTML-Dokument als Baum

    Alle Tags sind dabei HTML::Element-Objekte, alle Texte in Anführungszeichen einfache Strings.

    Liegt dieses Dokument in der Variable $html vor, lässt sich diese Baum-Struktur mit Hilfe von HTML::TreeBuilder sehr einfach erzeugen:

    	use HTML::TreeBuilder;
    	my $tree = new HTML::TreeBuilder $html;

    $tree enthält nun einen Verweis auf eine Struktur aus HTML::Element-Objekten; hier eine alternative Darstellung in Form einer vereinfachten bzw. verkürzten Data::Dumper-Ausgabe des Baums:

    bless( {
             '_tag'     => 'html',
             '_content' => [
                             bless( {
                                      '_tag'     => 'head',
                                      '_content' => [
                                                      bless( {
                                                               '_tag' => 'title',
                                                               '_content' => [
                                                                               'Beispielseite'
                                                                             ]
                                                             }, 'HTML::Element' ),
                                                      bless( {
                                                               '_tag'    => 'meta',
                                                               'name'    => 'AUTHOR',
                                                               'content' => 'fany'
                                                             }, 'HTML::Element' )
                                                    ]
                                    }, 'HTML::Element' ),
                             bless( {
                                      '_tag'     => 'body',
                                      '_content' => [
                                                      bless( {
                                                               '_tag' => 'h1',
                                                               '_content' => [
                                                                               'Beispiel'
                                                                             ]
                                                             }, 'HTML::Element' ),
                                                      bless( {
                                                               '_tag' => 'p',
                                                               '_content' => [
                                                                               'Das ist der Rand von Ostermundigen.'
                                                                             ]
                                                             }, 'HTML::Element' ),
                                                      bless( {
                                                               '_tag' => 'p',
                                                               '_content' => [
                                                                               'Dieser Text ist ',
                                                                               bless( {
                                                                                        '_tag' => 'em',
                                                                                        '_content' => [
                                                                                                        'wichtig'
                                                                                                      ]
                                                                                      }, 'HTML::Element' ),
                                                                               '.'
                                                                             ]
                                                             }, 'HTML::Element' )
                                                    ],
                                      'bgcolor' => 'white',
                                    }, 'HTML::Element' )
                           ],
           }, 'HTML::TreeBuilder' );
    Abb. 3: Objekt-Baum in Data::Dumper-ähnlicher Darstellung

    Eine weitere Alternative zur Darstellung des Baumes bietet die HTML::Element-Methode ->dump():

    <html> @0
      <head> @0.0
        <title> @0.0.0
          "Beispielseite"
        <meta content="fany" name="AUTHOR"> @0.0.1
      <body bgcolor="white"> @0.1
        <h1> @0.1.0
          "Beispiel"
        <p> @0.1.1
          "Das ist der Rand von Ostermundigen."
        <p> @0.1.2
          "Dieser Text ist "
          <em> @0.1.2.1
            "wichtig"
          "."
    Abb. 4: HTML-Baum des HTML-Dokuments aus Abb. 1

    Jedes HTML::Element steht also für ein HTML-Start-Tag inkl. des ggf. korrespondierenden -Ende-Tags; Code zwischen dem Start- und Ende-Tag wird unterhalb des HTML::Element-Objekts als dessen Content eingeordnet; dieser kann dabei aus mehreren Elementen bestehen.

    Anders gesagt: Jedes HTML::Element im Baum (mit Ausnahme des obersten) besitzt genau ein übergeordnetes Element ("parent") und beliebig viele (ggf. auch keine) untergeordneten ("content"). Der Content kann dabei auch einfache Strings enthalten, die normalen Text repräsentieren.

  3. Anwendungsbeispiel: HTML::PrettyPrinter
  4. Wie weiter unten noch klar werden wird, ist es bei der Arbeit mit HTML::TreeBuilder oft erforderlich, den Aufbau einer HTML-Seite im Quellcode zu analysieren. Meist sieht man sich dabei mit maschinell erzeugtem HTML-Code konfrontiert, der z. B. sehr lange Zeilen ohne Umbrüche und/oder keine sinnvollen Einrückungen enthält, so dass die Struktur mit bloßem Auge praktisch nicht erkennbar ist.

    Als Hilfswerkzeug basteln wir uns daher zunächst ein Script, das eine HTML-Seite in eine augenfreundlichere Form bringt. Dazu bietet sich das Modul HTML::PrettyPrinter an, das passenderweise auf HTML::TreeBuilder aufsetzt, indem es einen von diesem erzeugten HTML-Baum als Eingabe erwartet — was den netten Nebeneffekt hat, dass etwaige Restrukturierungen nicht standardkonformer HTML-Seiten, die HTML::TreeBuilder automatisch vornimmt, auch hier berücksichtigt werden, d. h. der ausgegebene HTML-Baum mag zwar nicht exakt der ursprünglichen Seite entsprechen, dafür aber dem HTML-Baum, mit dem wir auch später arbeiten werden:

    01| #!/usr/local/bin/perl -w
    02|
    03| use strict;
    04|
    05| use HTML::PrettyPrinter ();
    06| use HTML::TreeBuilder ();
    07|
    08| undef $/;
    09| my $tree = new_from_content HTML::TreeBuilder <>;
    10|
    11| my ($columns) = eval { require Term::ReadKey;
    12|                        Term::ReadKey::GetTerminalSize() };
    13|
    14| my $hpp  = new HTML::PrettyPrinter
    15|                allow_forced_nl => 1,
    16|                $columns ? ( linelength => $columns ) : ();
    17|
    18| $hpp->$_( 1, 'all!' ) for qw(set_nl_before set_force_nl);
    19|
    20| print @{ $hpp->format($tree) };
    21|
    22| $tree->delete;

    In Zeile 9 wird ein via ARGV zu übergebendes HTML-Dokument an HTML::TreeBuilder "verfüttert". (Details zur dabei verwendeten Methode ->new_from_content() sowie zu allen weiteren Methoden bitte in der Dokumentation zu HTML::TreeBuilder bzw. HTML::Element nachlesen!)

    Während in Zeilen 11 f. lediglich versucht wird, die Breite des Terminals zu ermitteln, wird in Zeilen 14 bis 16 ein HTML::PrettyPrinter-Objekt erzeugt; Zeile 18 nimmt daran noch einige Fein-Einstellungen vor, die dazu führen sollen, dass die spätere Ausgabe zumindest meinen Vorstellungen von lesbarem HTML-Code so nah wie möglich kommt.

    In Zeile 20 wird schließlich der weiter oben erzeugte HTML-Baum $tree an den HTML::PrettyPrinter $hpp übergeben, der daraufhin eine Referenz auf ein Array von Zeilen zurückliefert; das dereferenzierte Array wird sodann mittels print ausgegeben.

    Besondere Aufmerksamkeit verdient Zeile 22, in der der nun nicht mehr benötigte HTML-Baum explizit gelöscht wird. Dies ist grundsätzlich erforderlich, da er intern diverse zirkuläre Referenzen (Objekte verweisen auf untergeordnete Objekte und diese zurück auf die übergeordneten etc.) enthält, so dass die Perl-interne Garbage Collection nicht erkennen kann, wann ein Objekt nicht mehr benötigt wird, weil es eigentlich schon außer Reichweite (out of scope) ist. (Selbstverständlich hätte man im obigen Beispiel auf den Aufruf effektiv auch verzichten können, da der Speicher im Rahmen des auf dem Fuß folgenden Ende der Programmausführung sowieso wieder freigegeben worden wäre, doch erschien er mir hier aus didaktischen Gründen dennoch angebracht. :o) )

  5. Anwendungsbeispiel: Rechtswörterbuch
  6. Ein CGI-Script soll im Rechtswörterbuch des ARD-Ratgeber Recht, ausgehend von http://www.wdr.de/tv/recht/worte/rwindex1.html, alle Begriffe nachschlagen, die zum in der CGI-Variable Q übergebenen regulären Ausdruck passen und die zugehörigen Artikel anzeigen. Ebenfalls angezeigt werden sollen solche Artikel, die in diesen Artikeln referenziert werden, und dies rekursiv; optional soll dabei in der CGI-Variable R die maximale Rekursionstiefe angegeben werden können. Verweise zwischen den Artikeln sollen dabei in lokale Verweise umgeschrieben werden.

















    © 2002 WDR Köln
    16.12.2002


    Suche im ARD-Ratgeber Recht:

    Sie können ein oder mehrere Worte
    als Suchbegriff eingeben.

    [a-e]  [f-j]  [k-o]  [p-t]  [u-z]

    Rechtswörterbuch

    Abdingbar, Abfall, Abfindung, Abgabe, öffentliche, Abgabenordnung, Abgeordneter, Abhilfe im Widerspruchsverfahren, Abkömmlinge, Ablauforganisation, Ablösung des Erschliessungsbeitrags, Abordnung eines Beamten, Absatzgenossenschaft, Abschiebung, Abschlussfreiheit, Abschlussvollmacht, Abschlusszwang, [ weitere Begriffe aus Platzgründen gelöscht ], Europäische Atomgemeinschaft, Europäische Gemeinschaft für Kohle und Stahl, Europäische Gemeinschaften, Europäische Investitionsbank, Europäische Union, Europäische Wirtschaftsgemeinschaft, Europäischer Gerichtshof, Europäischer Rat, Europäischer Sozialfonds, Europäisches Gemeinschaftsrecht, Europäisches Parlament, Europäisches Währungssystem, Europarat, Evidenztheorie, Evokation, Exekutive

    [a-e]  [f-j]  [k-o]  [p-t]  [u-z]


     Impressum
    Abb. 5: <http://www.wdr.de/tv/recht/worte/rwindex1.html>

    Halten wir zunächst die wesentlichen zu implementierenden Punkte als Checkliste fest:

    1. Holen der o.g. URL
    2. Ermittlung der URLs der weiteren Index-Seiten ("[f-j]" etc.)
    3. Holen dieser Index-Seiten
    4. Durchsuchen aller Index-Seiten nach auf den regulären Ausdruck passenden Begriffen,
      d. h. Ermittlung der URLs der entspechenden Artikel-Seiten
    5. Holen der Artikel-Seiten
    6. Umschreiben der Verweise in den Artikel-Seiten zu lokalen Verweisen
    7. Ausgeben des bearbeiteten Artikel-Textes
    8. ggf. Verfolgung der Verweise bis zu einer angegebenen Rekursionstiefe,
      d. h. wiederum Ermittlung der URLs der referenzierten Artikel, Holen dieser Artikel etc.

    Zunächst etwas notwendiges Framework, das keiner weiteren Erläuterung bedürfen sollte:

    01| #!/usr/local/bin/perl -Tw
    02|
    03| use 5.006;
    04| use strict;
    05| use warnings;
    06|
    07| use CGI;
    08| use CGI::Carp 'fatalsToBrowser';
    09| use HTML::TreeBuilder ();
    10| use LWP::Simple 'get';
    11| use URI ();
    12|
    13| use constant STARTURL => 'http://www.wdr.de/tv/recht/worte/rwindex1.html';
    14|
    15| my $cgi = new CGI;
    16|
    17| defined( my $query = $cgi->param('Q') ) or die 'No query submitted.';
    18| my $maxdepth = $cgi->param('R');
    19|
    20| $| = 1;
    21|
    22| print $cgi->header,
    23|   $cgi->start_html(
    24|     -lang  => 'de-DE',
    25|     -title => "Rechtswörterbuch: Suchergebnis für /$query/"
    26|   );
    27| }

    Die Implementation von Schritt A ist recht einfach:

    	my $url = STARTURL;
    	my $page = get $url or die "Error GETting <$url>.\n";
    	my $tree = new_from_content HTML::TreeBuilder $page;

    Da sich alle für unsere Zwecke interessanten Informationen innerhalb des "weißen Kastens" auf o.g. Seite befinden, erscheint es zweckmäßig, zunächst diesen Teilbaum zu finden, um dann alle weiteren Aktionen darauf zu beschränken. Doch wie sagen wir das Perl? Werfen wir zunächst einen Blick auf einen Auschnitt des mit Hilfe des oben entwickelten PrettyPrinters aufbereiteten Quelltext der in Abb. 5 zu sehenden HTML-Seite:

    <td width=16 align=left bgcolor="#FFFFFF" valign=top>
      <img width=16 src="images/x.gif" border=0></td>
    <td width=313 bgcolor="#FFFFFF" align=left valign=top>
      <img width=313 src="images/x.gif" height=10 border=0>
      <br>
      <font face="Verdana, Arial, Helvetica, sans-serif"
        color="#000000" size=2>[a-e]&nbsp;&nbsp;[
        <a href=rwindex2.html>f-j</a>]&nbsp;&nbsp;[
        <a href=rwindex3.html>k-o</a>]&nbsp;&nbsp;[
        <a href=rwindex4.html>p-t</a>]&nbsp;&nbsp;[
        <a href=rwindex5.html>u-z</a>]
        <h2>Rechtswörterbuch</h2>
        <a href=rw00001.html>Abdingbar</a>,
        <a href=rw00002.html>Abfall</a>,
        <a href=rw00003.html>Abfindung</a>,
        <a href=rw00004.html>Abgabe, öffentliche</a>,
        <a href=rw00005.html>Abgabenordnung</a>,
    Abb. 6: Ausschnitt aus dem Quelltext zu <http://www.wdr.de/tv/recht/worte/rwindex1.html>

    (HTML-)Technisch gesehen ist der "weiße Kasten" also ein <TD>-Element. Doch enthält die Seite viele <TD>-Elemente. Wie also das richtige finden?

    Hierfür gibt es sicherlich keine Patentlösung, und alle potenziellen Ansätze können fehlschlagen, wenn sich der Quelltext der Seite einmal wesentlich ändern sollte.

    Ich habe mich hier dafür entschieden, zunächst von der <H2>-Überschrift "Rechtswörterbuch" auszugehen; das gesuchte Tabellenfeld ist dann das diese Überschrift unmittelbar umschließende:

    	(
    	    my ($h2) = $tree->look_down(
    	        _tag => 'h2',
    	        sub { shift->as_text eq 'Rechtswörterbuch' }
    	    )
    	  ) == 1
    	  or die "Wrong number of matching <H2> elements in <$url>.\n";
    
    	my $td = $h2->look_up( _tag => 'td' )
    	  or die "<TD> surrounding <H2> not found in <$url>.\n";

    Mittels ->look_down() wird hier nach einem <H2>-Tag-Paar gesucht, das den Text "Rechtswörterbuch" einschließt; die Methode gibt dann das entsprechende HTML::Element zurück. (Zu beachten ist, dass dieses weiterhin im Kontext des gesamten HTML-Baums steht; um es herauszulösen, müsste explizit $td->detach() aufgerufen werden.) Vorsichtshalber rufe ich die Methode hier in einem Listenkontext auf, um ggf. zu merken, falls die eingesetzten Selektionskriterien wider Erwarten nicht eindeutig sein sollten und in diesem Fall die Script-Ausführung gleich abzubrechen und so zu vermeiden, dass das Script mit möglicherweise falschen Daten weiterwerkelt und weiter unten zu seltsamen Ergebnissen kommt, deren Ursache aufgrund des verschleppten Fehlers dann wahrscheinlich schwer erkennbar wäre.

    Ausgehend vom gefundenen <H2>-Tag suche ich dann mittels ->look_up() wieder nach oben bis zum umschließenden <TD>-Element.

    Nun können wir uns an Schritt B wagen, dem Ermitteln der URLs der weiteren Index-Seiten.

    Dazu ist es zunächst erforderlich, die Verweise auf diese von Verweisen auf Artikel-Seiten zu unterscheiden. Ein hinreichend eindeutiges Selektions-Kriterium dürfte hierbei sein, dass die Verweise auf weitere Index-Seiten zwischen eckigen Klammern stehen. Sehen wir uns also alle Verweise innerhalb des Teilbaums $td an und schauen wir, welche auf obige Beschreibung passen, und merken uns die zugehörigen URLs in einem Array @todo:

    	for ( @{ $td->extract_links('a') } ) {
    
    	    my ( $linkurl, $element ) = @$_;
    
    	    $linkurl = new_abs URI $linkurl, $url;
    
    	    my $l = $element->left;
    	    my $r = $element->right;
    
    	    if (   defined $l && ref $l eq '' && $l =~ /\[$/
    	        && defined $r && ref $r eq '' && $r =~ /^\]/ )
    	    {
    	        push @todo, $linkurl;
    	    }

    Während das Extrahieren von Verweisen grundsätzlich freilich auch mit der bereits vorgestellten Methode ->look_down() möglich wäre, steht hierfür als spezialisierte Methode ->extract_links() zur Verfügung, die alle Hyperlink-Elemente (z. B. also auch <IMG>-Tags) ermittelt, wobei wir die Suche im vorliegenden Fall aber auf <A>-Elemente beschränken.

    Mit ->left() und ->right() werden diejenigen Elemente ermittelt, die unmittelbar vor bzw. nach dem gefundenen Hyperlink-Element stehen. Zu beachten ist hierbei, dass die Rückgabewerte dieser Methoden grundsätzlich in drei Kategorien fallen können:

    undef
    falls das Hyperlink-Element keinen linken bzw. rechten Nachbarn hat, weil es selbst das erste bzw. letzte Element im Content des übergeordneten Elements ist
    ein String
    falls das angrenzende Element aus reinem Text besteht
    ein HTML::Element
    falls das angrenzende Element selbst ein HTML-Tag ist

    Im vorliegenden Fall erwarte ich aufgrund der durch Blick in den Quelltext (vgl. oben Abb. 6) gewonnenen Erkenntnisse reinen Text und schließe die anderen Fälle daher explizit aus.

    Jetzt zu Punkt C: Zeigt der gefundene Verweis nicht auf eine weitere Index-Seite, gehen wir davon aus, dass er auf eine Artikel-Seite zeigt, und untersuchen in diesem Fall, ob das zugehörige Thema der in $query enthaltenen Suchanfrage entspricht:

    	    elsif ( ( my $topic = $element->as_trimmed_text ) =~ /$query/o )
    	    {
    	        push @todo_articles, @linkurl;
    	    }

    Hier nun ein weiterer Abschnitt des endgültigen Scripts, der die bisherigen Schritte zusammenfasst:

    28| my %depth = map +( $_ => 0 ), my @todo = STARTURL;
    29|
    30| while ( defined( my $url = shift @todo ) ) {
    31|
    32|     my $page = get $url or die "Error GETting <$url>.\n";
    33|     my $tree = HTML::TreeBuilder->new_from_content($page);
    34|
    35|     (
    36|         my ($h2) = $tree->look_down(
    37|             _tag => 'h2',
    38|             sub { shift->as_text eq 'Rechtswörterbuch' }
    39|         )
    40|       ) == 1
    41|       or die "Wrong number of matching <H2> elements in <$url>.\n";
    42|
    43|     my $td = $h2->look_up( _tag => 'td' )
    44|       or die "<TD> surrounding <H2> not found in <$url>.\n";
    45|
    46|     {
    47|         my $skip = defined $maxdepth && $depth{$url} == $maxdepth;
    48|
    49|         for ( @{ $td->extract_links('a') } ) {
    50|
    51|             my ( $linkurl, $element ) = @$_;
    52|
    53|             $linkurl = new_abs URI $linkurl, $url;
    54|
    55|             unless ( $skip || exists $depth{$linkurl} ) { # Verweise analysieren?
    56|
    57|                 my $l = $element->left;
    58|                 my $r = $element->right;
    59|
    60|                 if (   defined $l && ref $l eq '' && $l =~ /\[$/
    61|                     && defined $r && ref $r eq '' && $r =~ /^\]/ )
    62|                 {
    63|                     push @todo, $linkurl;
    64|                     $depth{$linkurl} = 0;
    65|                 }
    66|                 elsif ( ( my $topic = $element->as_trimmed_text ) =~ /$query/o
    67|                     || $depth{$url} )
    68|                 {
    69|                     push @todo, $linkurl;
    70|                     $depth{$linkurl} = $depth{$url} + 1;
    71|                 }
    72|             }

    Einige Details sind neu hinzugekommen:

    Damit sind nunmehr alle Punkte mit Ausnahmen von F ("Umschreiben der Verweise in den Artikel-Seiten...") und G ("Ausgeben des bearbeiteten Artikel-Textes") erledigt.

    Zunächst zu F; hier sind effektiv zwei Dinge zu erledigen:

    Einmal das eigentliche Umschreiben der Verweise in Artikel-Seiten:

    73|             $element->attr(
    74|                 href => ( exists $depth{$linkurl} && '#' ) . $linkurl )
    75|               if $depth{$url};
    76|
    76|         }	# Ende der foreach-Schleife aus Zeile 49
    77|     }	# Ende des Blocks aus Zeile 46

    Wir befinden uns hier immer noch in der in Zeile 49 beginnenden foreach-Schleife, die alle Verweise durchläuft. Falls gerade eine Artikel-Seite durchlaufen wird (Zeile 75), wird nun das HREF-Attribut des gefundenen Verweises umgeschrieben, und zwar zu einer absoluten URL, sofern das Verweisziel (aufgrund zu niedriger Rekursionstiefe) nicht Teil der Ausgabe des Scripts sein wird, andernfalls zu einem seiteninternen Verweis, dessen Ziel der Einfachheit halber ebenfalls nach der absoluten URL benannt ist.

    Zum Anderen müssen wir dafür sorgen, dass jeder Artikel über einen seiteninternen Verweis auch tatsächlich erreichbar ist, also einen entsprechenden <A NAME>-Anker setzen. Im Hinblick auf Punkt G bietet es sich außerdem an, schon einmal den eigentlichen Artikel, der später ausgegeben werden soll, zu extrahieren. Doch zuvor erscheint wieder etwas Quellcode-Studium angezeigt:

    <td width=16 align=left bgcolor="#FFFFFF" valign=top>
      <img width=16 src="../images/x.gif" border=0></td>
    <td width=313 bgcolor="#FFFFFF" align=left valign=top>
      <img width=313 src="../images/x.gif" height=10 border=0>
      <br>
      <font face="Verdana, Arial, Helvetica, sans-serif"
        color="#000000" size=2>[
        <a href=rwindex1.html>a-e</a>]&nbsp;&nbsp;[
        <a href=rwindex2.html>f-j</a>]&nbsp;&nbsp;[
        <a href=rwindex3.html>k-o</a>]&nbsp;&nbsp;[
        <a href=rwindex4.html>p-t</a>]&nbsp;&nbsp;[
        <a href=rwindex5.html>u-z</a>]
        <h2>Rechtswörterbuch</h2>
        <h3>Benutzer</h3>
        <b>Benutzer</b> ist im
        <a href=rw04142.html>Verwaltungsrecht</a> derjenige,
        der Leistungen einer öffentlichen Einrichtung (z.B. einer
        gemeindlichen Bibliothek) in Anspruch nimmt.</font>
      <img width=313 src="../images/x.gif" height=10 border=0>
      <br></td>
    Abb. 8: Ausschnitt aus dem (umformatierten) Quelltext der Beispiel-Artikel-Seite aus Abb. 7

    Wie oben schon angedeutet, ist die Struktur der Artikel-Seite der der Index-Seiten sehr ähnlich; die beiden unterscheiden sich effektiv erst ab der <H3>-Überschrift, die nur bei Artikel-Seiten existiert und das Thema des Artikels angibt. Da dieses aber sowieso gleich anschließend zu Beginn des eigentlichen Artikels noch einmal fettgedruckt wiederholt wird, wollen wir hier nur diesen ausgeben, und zwar als eigenen Absatz. Der zu setzende <A NAME>-Anker soll dabei den fettgedruckten Text umfassen. Das angestrebte Endergebnis sieht also so aus:
    <p><b><a name="http://www.wdr.de/tv/recht/worte/rw00232.html">Benutzer</a></b> ist im <a href="rw04142.html">Verwaltungsrecht</a> derjenige, der Leistungen einer &ouml;ffentlichen Einrichtung (z.B. einer gemeindlichen Bibliothek) in Anspruch nimmt.</p>
    Abb. 9: auszugebender HTML-Code für den Artikel aus Abb. 7 und 8

    Folgender Code — der noch innerhalb der in Zeile 30 beginnenden while-Schleife liegt, also pro HTML-Dokument einmal durchlaufen wird — führt uns dorthin:

    78|     if ( $depth{$url} ) {    # falls Artikel
    79|
    80|         ( my ($h3) = $td->look_down( _tag => 'h3' ) ) == 1
    81|           or die "Wrong number of matching <H3> elements in <$url>.\n";
    82|
    83|         my $article = new_from_lol HTML::Element [ p => $h3->right ];
    84|
    85|         my $b = $article->look_down( _tag => 'b' )
    86|           or die "Cannot find bold text in article <$url>.\n";
    87|
    88|         my $a_name = new HTML::Element 'a', name => $url;
    89|         $a_name->push_content( $b->detach_content );
    90|
    91|         $b->push_content($a_name);

    Zeile 78 stellt sicher, dass dieser Programmteil nur zum Einsatz kommt, wenn gerade ein Artikel (d. h. keine Index-Seite) bearbeitet wird.

    In Zeilen 80 f. wird das oben erwähnte <H3>-Element gesucht; wiederum wird vorsichtshalber überprüft, dass das Suchkriterium eindeutig war.

    Zeile 83 erzeugt in $article einen neuen HTML-Baum, der aus einem Absatz besteht, der als Inhalt alles bekommt, was "rechts" von der gefundenen Überschrift steht; ausgehend vom HTML-Quellcode aus Abb. 8 ist das alles bis zum und ausschließlich des schließenden </FONT>-Tags; da das korrespondierende Start-Tag bereits vor der fraglichen Überschrift steht, handelt es sich dabei um ein Parent-Element; gut zu sehen ist das auch in einem Ausschnitt des ge->dump()ten Baums dieser Seite (Abb. 10):

    AA| <td align="left" bgcolor="#FFFFFF" valign="top" width=16> @0.1.1.0.5
    AB|   <img border=0 src="../images/x.gif" width=16> @0.1.1.0.5.0
    AC| <td align="left" bgcolor="#FFFFFF" valign="top" width=313> @0.1.1.0.6
    AD|   <img border=0 height=10 src="../images/x.gif" width=313> @0.1.1.0.6.0
    AE|   <br> @0.1.1.0.6.1
    AF|   <font color="#000000" face="Verdana, Arial, Helvetica, sans-serif" size=2> @0.1.1.0.6.2
    AG|     "["
    AH|     <a href="rwindex1.html"> @0.1.1.0.6.2.1
    AI|       "a-e"
    AJ|     "]  ["
    AK|     <a href="rwindex2.html"> @0.1.1.0.6.2.3
    AL|       "f-j"
    AM|     "]  ["
    AN|     <a href="rwindex3.html"> @0.1.1.0.6.2.5
    AO|       "k-o"
    AP|     "]  ["
    AQ|     <a href="rwindex4.html"> @0.1.1.0.6.2.7
    AR|       "p-t"
    AS|     "]  ["
    AT|     <a href="rwindex5.html"> @0.1.1.0.6.2.9
    AU|       "u-z"
    AV|     "]"
    AW|     <h2> @0.1.1.0.6.2.11
    AX|       "Rechtswörterbuch"
    AY|     <h3> @0.1.1.0.6.2.12
    AZ|       "Benutzer"
    BA|     <b> @0.1.1.0.6.2.13
    BB|       "Benutzer"
    BC|     " ist im "
    BD|     <a href="rw04142.html"> @0.1.1.0.6.2.15
    BE|       "Verwaltungsrecht"
    BF|     " derjenige, der Leistungen einer öffentlichen Einrichtung (z.B. e..."
    BG|   "  "
    BH|   <img border=0 height=10 src="../images/x.gif" width=313> @0.1.1.0.6.4
    BI|   <br> @0.1.1.0.6.5
    Abb. 10: HTML-Code-Ausschnitt aus Abb.&nsbp;7 alterativ als ->dump()-Ausgabe

    Somit enthält $article nun exakt den Teilbaum, der später ausgegeben werden soll (Zeilen BA bis BF in Abb. 10); es fehlt lediglich noch der Anker, damit der Artikel durch einen seiteninternen Verweis anspringbar ist.

    Um diesen zu setzen, wird in Zeilen 85 f. zunächst das <B>-Element ermittelt, in das er eingebaut werden soll (vgl. Zeilen BA f. in Abb. 10); hier kann ich damit leben, dass es mehrere solcher Elemente geben könnte und rufe ->look_down() daher einfach in einem skalaren Kontext auf, so dass die Methode das erste passende Objekt zurückgibt.

    In Zeile 88 wird dann der <A NAME>-Anker-Element erzeugt, in das in der folgenden Zeile der dabei herausgelöste Inhalt des <B>-Elements eingesetzt wird, um schließlich in Zeile 91 das Anker-Objekt samt dem alten Inhalt wieder in den $article-Baum einzufügen.

    Damit ist Punkt F abgehakt, und der Artikel kann ausgegeben werden (Punkt G):

    92|         print $article-gt;as_HTML( undef, undef, {} );

    Die Argumente zur Methode ->as_HTML() dienen dabei nur zur Sicherstellung, dass z. B. auch bei Absätzen (<P>) explizite End-Tags mit ausgegeben werden, was HTML::Element standardmäßig nicht täte; für Details dazu möchte ich einmal mehr auf die Dokumentation des Moduls verweisen.

    Das war's; abschließend gilt es wieder, daran zu denken, die angelegten HTML-Bäume explizit zu demontieren, damit der dadurch belegte Speicher wieder freigegeben werden kann, und schließlich, die eigene HTML-Ausgabe korrekt abzuschließen:

    93|         $article->delete;
    94|     }
    95|     $tree->delete;
    96| }
    97|
    99| print $cgi->end_html;

    Benutzer ist im Verwaltungsrecht derjenige, der Leistungen einer öffentlichen Einrichtung (z.B. einer gemeindlichen Bibliothek) in Anspruch nimmt.

    Verwaltungsrecht umfaßt die Rechtsvorschriften, die vornehmlich für die öffentliche Verwaltung, ihre Organisation, ihr Verfahren, ihr Handeln, ihre Aufgaben und den Vollzug der Aufgaben gelten. Das Verwaltungsrecht ist Teil des öffentlichen Rechts. Zum Verwaltungsrecht gehören unter anderem das Baurecht, das Recht des öffentlichen Dienstes, Straßen- und Wegerecht, Naturschutzrecht, Immissionsschutzrecht, Sozialrecht, Schulrecht, Hochschulrecht, Kommunalrecht, Polizei- und Ordnungsrecht, Wasserrecht.

    Rechtsvorschrift bezeichnet einen Rechtssatz in einem Gesetz, einer Rechtsverordnung oder einer Satzung. Von der Verwaltungsvorschrift unterscheidet sich die Rechtsvorschrift dadurch, daß sie sich an die Allgemeinheit und nicht lediglich an Behörden oder Bedienstete wendet.

    Recht ist die Gesamtheit der geltenden Rechtsvorschriften eines Staates (= Recht im objektiven Sinne). Das subjektive Recht ist demgegenüber die einer Person verliehene Macht, ihre individuellen Befugnisse gegen andere Personen oder gegen Sachen geltend zu machen.

    Baurecht ist die Gesamtheit der rechtlichen Regelungen, die sich auf die Zulässigkeit und die Grenzen, die Ordnung und die Förderung der Errichtung von baulichen Anlagen sowie auf die bestimmungsgemäe Nutzung dieser Anlagen beziehen. Herkömmlich wird das Baurecht in die Bauleitplanung, die Bodenordnung und die Bauordnung eingeteilt.

    Wegerecht. Straßen- und Wegerecht ist für die Bundesfernstraßen im Bundesfernstraßengesetz und für die übrigen öffentlichen Straßen (Landes-, Kreis-, Gemeinde- und sonstige Straßen) in den Straßen- und Wegegesetze der Länder geregelt. Straßen und Wege werden durch öffentliche Widmung zu öffentlichen Straßen. Der Gebrauch der öffentlichen Straßen ist jedermann im Rahmen der Verkehrsvorschriften gestattet. Über diesen Gemeingebrauch hinaus bedarf die Nutzung der Erlaubnis des Trägers der Straßenbaulast.

    Kommunalrecht ist die Gesamtheit der Gesetze und Verordnungen, die die Organisation, Stellung und Wirtschaftsführung der Gemeinden, Landkreise und der anderen Gemeindeverbände regeln. Von besonderer Bedeutung sind die Gemeinde- und Kreisordnungen der einzelnen Bundesländer.

    Polizei. Der Begriff dient der Umschreibung der staatlichen Tätigkeit, die auf die Abwehr von Gefahren gerichtet ist. Diese Aufgaben werden von Verwaltungs- und Vollzugsorganen wahrgenommen.

    Ordnungsrecht bezeichnet die die öffentliche Sicherheit und Ordnung betreffenden Rechtsvorschriften.

    Wasserrecht ist die Bezeichnung für die gesetzlichen Bestimmungen über den Wasserschutz und die Wasserbenutzung sowie über die Be- und Entwässerung. Gesetzliche Regelungen enthalten das Wasserhaushaltsgesetz, das Bundeswasserstraßengesetz und die Wassergesetze der Länder.

    Abb. 11: Beispiel-Ausgabe des entwickelten Scripts für Q=^Benutzer$&R=3

  7. Nachteile von HTML::TreeBuilder gegenüber HTML::Parser
  8. Trotz aller Vorteile und Bequemlichkeiten, die das Modul bietet, sollte man vor dem Einsatz von HTML::TreeBuilder einige Punkte bedenken: