XPath

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

Stellen Sie sich einmal vor, Sie würden über eine ganze Horde von getreuen Affen verfügen können, die auf’s Wort hören. Sie könnten dann die folgende Anweisung erteilen: »Bitte besorgt mir so einen Bananenmilchshake von der Eisdiele in der Lindenstraße, gleich auf der anderen Seite des Kudamm.« Da Ihre Affen nicht besonders intelligent sind (sonst würden sie nicht auf’s Wort hören ...), würden sie sich zwar auf den Weg machen, aber alles nur irgendwie erreich- und eßbare anschleppen. Ihnen bleibt nur übrig, so lange zu warten, bis Ihr Milchshake kommt, und bis dahin alles andere zurückgehen zu lassen. Oder Sie schicken die Affen auf eine Abendschule, damit sie nach einigen Monaten die menschliche Sprache verstehen. Dadurch wären sie dann in der Lage, Ihre Anweisungen genau zu befolgen, genau die gewünschten Dinge zu finden und Ihnen viel schneller zu bringen.

Wir haben damit genau die Art von Problem beschrieben, für die XPath geschaffen wurde. XPath ist eine der nützlichsten Technologien im Umgang mit XML. Die Sprache bietet eine Schnittstelle, mit der man Knoten auf rein beschreibende Art auswählen kann, ohne Schleifen oder if-Bedingungen zu programmieren. Sie geben einfach vor, welche Knoten Sie haben wollen, und der XPath-Parser übernimmt es, die passenden zu finden. Auf einmal wird der verwirrende Haufen von Knoten, den wir als XML-Dokument bezeichnen, zu einem wohlgeordneten und indizierten Aktenschrank.

Nehmen wir das XML-Dokument im folgenden Beispiel:

Beispiel: Eine Datei mit persönlichen Einstellungen (Preferences)

<preferences>
    <liste>
        <name>DefaultDirectory</name>
        <string>/usr/local/fooby</string>
        <name>RecentDocuments</name>
        <array>
            <string>/Users/bobo/docs/menu.pdf</string>
            <string>/Users/slappy/pagoda.pdf</string>
            <string>/Library/docs/Baby.pdf</string>
        </array>
        <name>BGColor</name>
        <string>sage</string>
    </liste>
</preferences>

So könnte eine typische Datei zur Speicherung persönlicher Einstellungen (Preferences) aussehen. Die Datei enthält einfach eine Liste von Schlüssel/Wert-Paaren, die sehr einfach zu interpretieren sind. Um zum Beispiel den Wert des Schlüssels BGColor zu bestimmen, suchen wir nach demjenigen Element <name>, das als Wert den String »BGColor« enthält. Dann suchen wir nach dem nächsten Element, das den Typ <string> hat. Abschließend lesen wir den Wert aus dem darin enthaltenen Textknoten. In DOM könnten wir das schon recht einfach lösen, wie das Beispiel unten zeigt.

Beispiel: Ein Programm zum Lesen der bevorzugten Hintergrundfarbe.

sub get_bgcolor {   
   my $doc = shift;
   my @keys = $doc->getElementsByTagName( 'name' );
   foreach my $key ( @keys ) {
      if( $key->getFirstChild->getData eq 'BGColor' ) {
         for (my $elem = $key->getNextSibling; $elem;
            $elem = $elem->getNextSibling) {
         if ($elem->nodeType == ELEMENT_NODE()) {
            return $elem->getFirstChild->getData;
         }
      }
   }
  }
  return;
}

Es hat etwas für sich, eine solche Funktion zu schreiben. Der Fall sieht anders aus, wenn wir Hunderte von der Sorte brauchen. Und dabei ging es hier noch um ein recht einfaches Dokument – vielleicht können Sie sich vorstellen, wie kompliziert und unübersichtlich das alles werden kann. Es wäre doch schön, wenn man das mit etwas weniger Aufwand bewältigen könnte, sagen wir zum Beispiel in einer einzigen Zeile Quelltext? Das wäre einfacher zu lesen und zu schreiben und weniger fehlerträchtig. Genau das leistet XPath.

XPath ist eine Sprache zur Beschreibung eines Knotens oder auch einer Menge von Knoten, die sich irgendwo im Dokument befinden. Die Sprache ist einfach und trotzdem recht mächtig und durch das W3C standardisiert. Auf XPath stützen sich inzwischen eine Reihe weiterer Technologien, vor allem auch XSLT. In XSLT wird mit Hilfe von XPath-Ausdrücken eine Regel ausgewählt, mit der ein Knoten transformiert wird. Ein anderes Beispiel ist XPointer, eine XML-Sprache, die sich mit dem Anhängen von XML-Dokumenten an andere Ressourcen beschäftigt. Wir werden schon bald sehen, daß man XPath inzwischen auch in vielen Perl-Modulen integriert findet.

Ein XPath-Ausdruck wird als Lokalisierungspfad bezeichnet und besteht aus einer Folge von einzelnen Schritten (Steps). Jeder einzelne Schritt soll den Anwender näher an sein Ziel – d. h. den oder die gesuchten Knoten – bringen. Man beginnt bei einer festgelegten und bekannten Stelle (zum Beispiel die Wurzel des Dokuments) und »durchläuft« mit jedem Schritt den Dokumentbaum, um am Schluß eine Menge von Ergebnisknoten zu erhalten. Die Syntax ähnelt ein wenig dem Pfad einer Datei im Dateisystem, denn die einzelnen Schritte werden durch den Slash (/) getrennt.

Formulieren wir das vorige Beispiel als Lokalisierungspfad:

 /preferences/liste/name[text()='BGColor']/following-sibling::*[1]/text( ) 

Die Auswertung des Lokalisierungspfades beginnt an einer festen Stelle des Dokuments, jeder Schritt kann zu einem (oder mehreren) neuen Knoten führen. Nach jedem durchgeführten Schritt wird der aktuelle Knoten zum Ausgangspunkt des nächsten Schritts. Falls in einem Schritt mehrere passende Knoten gefunden wurden, werden diese nacheinander zum jeweils aktuellen Knoten. Die so erhaltenen Ergebnismengen werden vereinigt. Schauen wir uns die einzelnen Schritte bei der Verarbeitung des obigen Lokalisierungspfades an:

  • Wir beginnen mit dem Wurzelknoten (das ist der eine Ebene über dem Dokumentelement stehende, unsichtbare Dokumentknoten).
  • Wir suchen ein Element <preferences>, das ein Kind des aktuellen Knotens sein muß.
  • Wir suchen ein Element <liste>, das ein Kind des aktuellen Knotens sein muß.
  • Wir suchen ein Element <name>, das ein Kind des aktuellen Knotens sein und zusätzlich den Wert BGColor haben muß.
  • Wir suchen das auf den aktuellen Knoten folgende Element.
  • Das Ergebnis besteht aus allen Textknoten, die das aktuelle Element enthält.

Es kann durchaus vorkommen, daß bei einem dieser Schritte mehrere Ergebnisknoten gefunden werden. In diesem Fall müssen wir die falschen durch Zusatzbedingungen ausschließen. Zum Beispiel müssen wir unter den Elementen vom Typ <name> alle diejenigen ausschließen, die nicht das Wort BGColor enthalten. Ohne diese Zusatzbedingung würde das Endergebnis aus allen Textknoten bestehen, die in irgendeinem Element stehen, das unmittelbar auf ein <name>-Element folgt.

Der folgende Lokalisierungspfad selektiert alle <name>-Elemente des Beispieldokuments:

 /preferences/liste/name 

Die verschiedenen Zusatzbedingungen werden alle als Boolesche Konstanten ausgewertet. Man kann zum Beispiel die Position eines Knotens (innerhalb einer Menge zuvor ausgewählter Knoten) prüfen, die Existenz von Kindelementen oder Attributen feststellen, Zahlenvergleiche anstellen und vor allem Boolesche Ausdrücke durch AND und OR kombinieren. In manchen Fällen besteht ein Test nur aus einer Zahl, in diesem Fall eine Abkürzung für die gewünschte Position innerhalb der Knotenliste. Das Symbol [1] im Beispiel bedeutet also: »Ignoriere alles außer dem ersten passenden Knoten.«

Wir erwähnten schon, daß man innerhalb der eckigen Klammern Boolesche Ausdrücke durch AND kombinieren kann. Eine alternative Schreibweise ist die Verwendung mehrerer aufeinanderfolgender Paare von eckigen Klammern. Die darin enthaltenen Booleschen Ausdrücke werden also durch ein unsichtbares AND verknüpft. Jeder Schritt auf dem Pfad enthält eine unsichtbare Bedingung, die Sackgassen ausschließt. Werden an irgendeiner Stelle keine passenden Knoten gefunden, dann war dieser ergebnislos und man kann die Suche vermutlich abbrechen (sofern nicht die Suche für in einem früheren Schritt gefundene Knoten noch aussteht).

Neben Booleschen Suchkriterien kann man einen Lokalisierungspfad auch mit einer sogenannten Achse als Direktive versehen. Eine Achse ähnelt einer Kompaßnadel, die dem Prozessor den Weg weist. Standardmäßig erfolgt die Suche vom aktuellen Knoten abwärts zu dessen Kindknoten. Mit Hilfe einer Achse kann man die Suche aber auch aufwärts zum Vaterknoten und dessen Vorfahren oder seitwärts zu dessen Geschwistern verlaufen lassen. Die Achse wird als Präfix des Schrittes angegeben, für den sie gelten soll, getrennt durch einen doppelten Doppelpunkt (::). In unserem vorigen Beispiel haben wir die Achse following-sibling benutzt, um vom aktuellen Knoten ausgehend seinen nächsten Nachbarn zu suchen.

Es ist keineswegs gesagt, daß ein bestimmter Schritt immer nur Elemente finden muß. Im Gegenteil, man kann beliebige Arten von Knoten auswählen, einschließlich Attributen, Text, Verarbeitungsanweisungen und Kommentaren. Ganz nach Wunsch kann man den Knotentyp auch unspezifisch lassen, indem man »alle Knotentypen« verlangt. Die verschiedenen Möglichkeiten der Auswahl finden Sie in der folgenden Liste:

Symbol Getroffene Auswahl
node( ) Alle Knoten
text( ) Textknoten
element::foo Ein Element namens foo
foo Ein Element namens foo
attribute::foo Ein Attribut namens foo
@foo Ein Attribut namens foo
@* Beliebige Attribute
* Beliebige Attribute
. Dieses Element
.. Der Vaterknoten
/ Der Wurzelknoten
/* Das Wurzelelement
//foo Ein Element namens foo an einer beliebigen Stelle

Elemente sind der Standardsuchtyp, einfach weil sie letzten Endes doch am häufigsten gesucht werden. Aber natürlich gibt es gute Gründe, gelegentlich auch andere Typen zu suchen. In unserem Beispiel eines Lokalisierungspfades verwenden wir text( ), um den Inhalt des <value>-Elements als Ergebnis zurückzugeben.

Die meisten Schritte sind relative Lokatoren, weil sie relativ zum Ausgangsknoten wirken. Lokalisierungspfade bestehen meistens aus relativen Lokatoren, aber es gibt auch absolute Lokatoren , die auf einen festen Punkt des Dokuments verweisen. Die beiden wichtigsten sind id( ), also das Element mit der angegebenen ID, und root( ), der Wurzelknoten des Elements (der Vaterknoten des Wurzelelements, ein abstrakter Knoten). Man sieht häufig das Symbol /, das am Anfang des Lokalisierungspfades als Abkürzung für root( ) steht.

Da unsere Affen inzwischen XPath verstehen, wollen wir davon in Perl Gebrauch machen. Das Modul XML::XPath von Matt Sergeant ist eine solide XPath-Implementierung. Für das nächste Beispiel haben wir ein Programm geschrieben, das zwei Kommandozeilenargumente enthält: einen Dateinamen und einen XPath-Lokalisierungspfad. Als Ergebnis wird der textuelle Wert der durch den Pfad gefundenen Knoten ausgegeben.

Beispiel: XPath im Einsatz

use XML::XPath;
use XML::XPath::XMLParser;

# Erzeuge ein Objekt, das die Datei liest und die Ausführung von XPath-Queries erlaubt
my $xpath = XML::XPath->new( filename => shift @ARGV );

# Lies den Pfad von der Kommandozeile und erzeuge eine Liste passender Knoten
my $nodeset = $xpath->find( shift @ARGV );

# Gib jeden Knoten in der Liste aus
foreach my $node ( $nodeset->get_nodelist ) {
  print XML::XPath::XMLParser::as_string( $node ) . "\n";
}

Das Beispiel ist recht einfach gehalten. Als nächstes benötigen wir eine Beispieldatei für den Test. Dazu dient das Beispiel unten.

Beispiel: Eine XML-Datei als Test für XPath

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE inventar [
<!ENTITY gift "<hinweis>Vorsicht: Gift!</hinweis>">
<!ENTITY gefaehrdet "<hinweis>Bedrohte Art</hinweis>">
]>
<!-- Inventar der Kräutersammlung von Rivenwood -->
<inventar datum="2001.9.4">
    <kategorie typ="Baum">
        <eintrag id="284">
            <name stil="lateinisch">Carya glabra</name>
            <name stil="deutsch">Ferkelnuß</name>
            <aufbewahrungsort>Ostflügel</aufbewahrungsort>
            &gefaehrdet;
        </eintrag>
        <eintrag id="222">
            <name stil="lateinisch">Toxicodendron vernix</name>
            <name stil="deutsch">Giftsumach</name>
            <aufbewahrungsort>Westliche Ballustrade</aufbewahrungsort>
            &gift;
        </eintrag>
    </kategorie>
    <kategorie typ="Hecke">
        <eintrag id="210">
            <name stil="lateinisch">Cornus racemosa</name>
            <name stil="deutsch">Rispenhartriegel</name>
            <aufbewahrungsort>Südlicher Anbau</aufbewahrungsort>
        </eintrag>
        <eintrag id="104">
            <name stil="lateinisch">Alnus rugosa</name>
            <name stil="deutsch">Runzelerle</name>
            <aufbewahrungsort>Ostflügel</aufbewahrungsort>
            &gefaehrdet;
        </eintrag>
    </kategorie>
</inventar>

Der erste Test benutzt den Pfad /inventar/kategorie/eintrag/name:

> grabber.pl data.xml "/inventar/kategorie/eintrag/name"
<name stil="lateinisch">Carya glabra</name>
<name stil="deutsch">Ferkelnuß</name>
<name stil="lateinisch">Toxicodendron vernix</name>
<name stil="deutsch">Giftsumach</name>
<name stil="lateinisch">Cornus racemosa</name>
<name stil="deutsch">Rispenhartriegel</name>
<name stil="lateinisch">Alnus rugosa</name>
<name stil="deutsch">Runzelerle</name>

Alle <name>-Elemente wurden gefunden und ausgegeben. Probieren wir als nächstes den etwas spezifischeren Pfad /inventar/kategorie/eintrag/name[@stil='lateinisch']:

> grabber.pl data.xml "/inventar/kategorie/eintrag/name[@stil='lateinisch']"
<name stil="lateinisch">Carya glabra</name>
<name stil="lateinisch">Toxicodendron vernix</name>
<name stil="lateinisch">Cornus racemosa</name>
<name stil="lateinisch">Alnus rugosa</name>

Versuchen wir nun einmal, mit dem Pfad //eintrag[@id='222']/hinweis als Startpunkt das ID-Attribut zu verwenden. (Wenn das Attribut in einer DTD als ID deklariert wäre, könnten wir auch kurz id('222')/hinweis schreiben. Das haben wir nicht getan, aber die erste Methode tut’s auch.)

> grabber.pl data.xml "//eintrag[@id='222']/hinweis"
<hinweis>Vorsicht: Gift!</hinweis>

Sie finden die »hinweis«-Tags störend? Kein Problem:

> grabber.pl data.xml "//eintrag[@id='222']/hinweis/text( )"
Vorsicht: Gift!

Wann war die letzte Inventur?

> grabber.pl data.xml "/inventar/@datum"
  datum="2001.9.4"

Auch XPath kann irgendwann einmal unübersichtlich werden. Ein etwas verwirrter Affe könnte zum Beispiel dem folgenden Pfad folgen:

> grabber.pl data.xml "//*[@id='104']/parent::*/preceding-sibling::*/child::*[2]/
name[not(@stil='lateinisch')]/node( )"
Giftsumach

Der Affe beginnt damit, das Element mit dem Wert '104' im Attribut id zu finden. Anschließend steigt er eine Ebene hoch, springt zum vorigen Element zurück, klettert zum zweiten Kindelement, sucht nach einem <name>, dessen Attribut stil den Wert lateinisch enthält, und springt anschließend zu demjenigen Kind dieses Elements, das den Textknoten »Giftsumach« enthält.

Wir haben gerade gesehen, wie XPath-Ausdrücke zum Suchen und Sammeln von Knoten eingesetzt werden. Als nächstes wollen wir eine noch raffiniertere Implementierung einsetzen. XML::Twig, ein geniales Modul von Michel Rodriguez, paßt besser zu Perl. Sein Trick ist die Abbildung von bestimmten Ausdrücken auf Subroutinen. Man kann zum Beispiel eigene Funktionen für bestimmte Knotentypen aufrufen lassen.

Das Programm im Beispiel unten zeigt, wie das funktioniert. Bei der Initialisierung des XML::Twig-Objekts wird eine Reihe von Handlern in einem Hash übergeben. Die Schlüssel des Hashs sind XPath-Ausdrücke. Der Parser baut einen Baum auf und ruft dabei die betreffenden Handler auf, wenn ein passender Knoten gefunden wurde.

Vielleicht fällt Ihnen bei diesem Beispiel auf, daß der Klammeraffe (@) als Sonderzeichen behandelt wird. Das geschieht, weil der Klammeraffe im Kontext von Perl eine besondere Bedeutung hat. In XPath dagegen steht @foo für das Attribut foo und nicht für das Perl-Array foo. Diese Unterscheidung sollten Sie bei allen XPath-Beispielen dieses Buchs im Hinterkopf behalten. Das gilt auch für die Zukunft, wenn Sie Ihre eigenen Perl-Programme schreiben, die XPath einsetzen. Der Klammeraffe muß stets mit einem Backslash versehen werden, um die Interpretation als Array zu verhindern.

Wenn Sie in Ihrem Code mehr mit Perl-Arrays und weniger mit XPath-Attributreferenzen arbeiten, wäre vielleicht die Verwendung der langen Schreibweise »attribute« für die Wahl der XPath-Achse sinnvoll: attribute::foo. Natürlich hat auch der doppelte Doppelpunkt in Perl und XPath verschiedene Bedeutungen. Allerdings hat XPath nur einige wenige hart codierte Achsennamen, die noch dazu zwingend in Kleinbuchstaben geschrieben werden, so daß sich Unterscheidungsschwierigkeiten im Rahmen halten dürften.

Beispiel: Wie man mit Twig-Handlern arbeitet

use XML::Twig;

# Buffer zum Aufnehmen von Text
my $cat_buf = '';
my $eintrag_buf = '';

# Initialisierung des Parsers mit Handlern zur Verarbeitung von Knoten
my $twig = new XML::Twig( TwigHandlers => {                              
                              "/inventar/kategorie" => \&kategorie,
                              "name[\@stil='lateinisch']" => \&name_lateinisch,
                              "name[\@stil='deutsch']" => \&name_deutsch,
                              "kategorie/eintrag" => \&eintrag,
                                 });

# Parsen der Datei, mit Aufruf der Handler
$twig->parsefile( shift @ARGV );    

# Ein Element "kategorie" wurde gefunden
sub kategorie {
    my( $tree, $elem ) = @_;
    print "Kategorie: ", $elem->att( 'typ' ), "\n\n", $cat_buf;
    $cat_buf = '';
}

# Ein Element "eintrag" wurde gefunden
sub eintrag {
    my( $tree, $elem ) = @_;
    $cat_buf .= "Eintrag: " . $elem->att( 'id' ) . "\n" . $eintrag_buf . "\n";
    $eintrag_buf = '';
}

# Behandlung eines lateinischen Namens
sub name_lateinisch {
    my( $tree, $elem ) = @_;
    $eintrag_buf .= "Lateinischer Name: " . $elem->text . "\n";
}

# Behandlung eines deutschen Namens
sub name_deutsch {
    my( $tree, $elem ) = @_;
    $eintrag_buf .= "Deutscher Name: " . $elem->text . "\n";
}

Unser Programm arbeitet mit einer Eingabedatei wie der von Beispiel 8-7 und gibt eine Zusammenfassung aus. Beachten Sie, daß ein Handler erst aufgerufen wird, wenn das betreffende Element komplett aufgebaut wurde. Aus diesem Grund werden die Handler nicht unbedingt in der Reihenfolge aufgerufen, die man als »natürlich« erwarten würde: Die Handler der Kinder werden vor denen der Eltern aufgerufen. Aus diesem Grund müssen wir die Ausgabe puffern und für einen späteren Zeitpunkt aufbewahren.

So sieht das Ergebnis aus:

Kategorie: Baum

Eintrag: 284
Lateinischer Name: Carya glabra
Deutscher Name: Ferkelnuß

Eintrag: 222
Lateinischer Name: Toxicodendron vernix
Deutscher Name: Giftsumach
Kategorie: Hecke

Eintrag: 210
Lateinischer Name: Cornus racemosa
Deutscher Name: Rispenhartriegel

Eintrag: 104
Lateinischer Name: Alnus rugosa
Deutscher Name: Runzelerle

XPath vereinfacht die Suche nach Knoten im Dokument und die Beschreibung zu verarbeitender Knoten drastisch. Die Menge an Quelltext für die Navigation innerhalb des Dokuments schwindet auf ein Minimum und wird erheblich lesbarer. Wir sind damit außerordentlich zufrieden. Obendrein ist XPath ein Standard, mit dessen Einsatz deshalb in vielen anderen Modulen zu rechnen ist.

  

  

<< 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