Suchen
Inside Wiki
Nützliche Links




 
phpforum.de bei Facebook
 
phpforum.de bei Twitter
 

Zurück   PHP Forum: phpforum.de > phpforum.de Wiki > phpforum.de Wiki

PHP Wiki Dieses Wiki sammelt Lösungen, zu Problemen, welche immer wieder im Forum auftauchen.

 
 
Artikel-Optionen Ansicht
  #1  

Standard XPath

 

Inhalte

XPath


Einleitung


Zitat:
It is recommended that anyone serious about processing XML should learn to use the Xpath language, which is as important to XML as Regular Expressions are to plain text.
Sterling Hughes (Autor der SimpleXML Extension)

Was ist XPath?


XPath (kurz für: "XML Path Language") ist eine vom WWW-Konsortium entwickelte Hilfstechnologie zum Durchsuchen von XML- und (X)HTML-Dokumenten nach beliebig komplexen Kriterien. In PHP ist XPath in das DOM und SimpleXML integriert.

Informationen in Dokumenten suchen? Das kennen wir doch schon. Vielfach kommen für solche Anwendungsfälle reguläre Ausdrücke oder sogar die gewöhnlichen Stringfunktionen zum Einsatz.

Wozu also jetzt XPath?

PHP verfügt mit der Perl-Compatible Regular Expression Engine (PCRE) über ein mächtiges Suchwerkzeug für gewöhnlichen Text. Es ist also durchaus naheliegend sie auch für XML und X?HTML zu verwenden.

Der große Vorteil, den die PCRE gegenüber den Methoden von SimpleXML und DOM bietet, ist der geringere Overhead. Sowohl DOM, als auch SimpleXML müssen das komplette Dokument in Objektform im Speicher vorhalten, um darin zu navigieren. Da XPath nur bei DOM und SimpleXML zur Verfügung steht, teilt es diesen Nachteil. Für die PCRE ist XML einfach nur ein String wie jeder andere.

Das ist aber auch gleichzeitg der Nachteil der PCRE. Es ist dort kein "Verständnis" für Tags, Attribute und dergleichen eingebaut. Deshalb sind Regexe für XML auch schwer zu formulieren. Die Regex für den Titel einer X?HTML-Datei könnte z.B. so aussehen: /<title>([\<]+)<\/title>/.

Doch was, wenn das title-Element noch Attribute hat? Ein xml:lang-Attribut z. B.?

Auch das ist noch kein Problem: /<title[\>]*>([\<]+)<\/title>/. Die Regex ist jetzt zwar schon ziemlich unlesbar geworden, aber immerhin funktioniert sie. Oder etwa doch nicht? Nun, es könnte ja sein, daß der Titel Sonderzeichen enthält - ein Ampersand z.B. So lange dafür eine Entity benützt wird, ist das kein Problem: <title>Q&amp;A</title> . Das schafft unsere Regex. Nur was, wenn statt der Entity eine CDATA-Sektion (z.B. <title><![CDATA[Q&A]]></title>) benutzt wurde?

Auch das wäre allerdings durchaus noch lösbar. Es ist grundsätzlich noch relativ einfach Regexe für Dokumente zu schreiben, deren Struktur und Inhalt man genau kennt, aber spätestens wenn man sich um Allgemeingültigkeit bemüht, artet es in Schwerstarbeit aus, denn schon kleine - für den XML-Prozessor völlig irrelevante - Veränderungen können eine Regex wertlos machen.

Der XPath für das title-Element aus dem obigen Beispiel ist dagegen vergleichsweise schlicht: /html/head/title.

Der XPath-Engine ist es dabei egal, ob Attribute vorkommen oder nicht... ob es da eine CDATA-Sektion gibt oder nicht... ob da vielleicht noch ein Kommentar im title ist oder nicht... Der XPfad bleibt jeweils derselbe. XPath "kennt" die Struktur von XML und X?HTML und kann Tags von Inhalten unterscheiden.

Das Dokument ist auch schon vorprozessiert, wenn die XPath-Engine ihre Arbeit beginnt. Character Entities sind aufgelöst. CDATA-Sektionen sind in gewöhnlichen Text umgesetzt worden. Whitespace-Knoten - also Knoten, die keine Bedeutung haben und nur der Formattierung dienen - sind herausgefiltert. Und Mixed Content - also Vermischungen von Text und anderen Knotentypen - sind für XPath auch kein Problem.

Da es möglich ist, bei XPath PHP-Funktionen zu registrieren, muss man auf die PCRE übrigens nicht völlig verzichten. Man sollte sie halt nur da einsetzen, wo sie sich zu Hause fühlt: bei den Inhalten von Text- und Attributknoten.

XPath 1.0 und XPath 2.0



2007 wurde der Standard XPath 2.0 verabschiedet. Die SimpleXML und DOM zugrunde liegende libxml2 versteht allerdings nach wie vor nur XPath 1.0. Diese erste Version von XPath wurde zunächst nur als Hilfstechnologie für XSLT 1.0 (siehe auch diese allgemeine Einführung zu XSLT) entwickelt. Die zweite Version wurde um zusätzliche Funktionen erweitert, um die Ansprüche der neuen Standards XSLT 2.0 und XQuery 1.0 zu befriedigen.

Der Schritt von XPath 1.0 nach 2.0 mag klein erscheinen, aber der Schritt von XSLT 1.0 nach 2.0 ist es nicht.

Hinzu kommt die neue - und noch teilweise unfertige - Sprache XQuery. Die Macher der libxml2 (und die Macher von PHP) haben sich noch nicht dazu durchgerungen XSLT 2.0 und XQuery 1.0 zu unterstützen. Deshalb liegt auch XPath 2.0 vorläufig auf Eis. Unglücklicherweise beziehen sich fast alle XPath-Referenzen im Netz schon auf Version 2.0 und sind deshalb für PHPler nur bedingt nutzbar.

Deshalb enthält dieser Artikel neben einer Einführung auch eine komplette Referenz von XPath 1.0.

Syntax von XPath


Location Steps


Einleitung



Location Steps sind die Grundbausteine von XPath und im Aufbau an Dateipfade unter *nix Systemen angelehnt.

Ein einfacher / am Anfang eine XPfades bezeichnet das Dokument. Ein darauffolgender Name bezeichnet das Dokumentelement. /html wäre z.B. das Dokumentelement einer X?HTML Datei.

Weitere Elemente werden durch weitere Slashes voneinander getrennt. /html/head/title wären z.B. die title-Elemente einer X?HTML Datei. Man beachte dabei den Plural, denn anders als im Dateisystem ist es in XML erlaubt (und üblich), gleichnamige Knoten auf der selben Ebene der Hierarchie zu verwenden. Deshalb evaluieren XPfade niemals zu einzelnen Knoten, sondern grundsätzlich zu Knotenmengen (engl. "nodesets").

Dabei muss man sich unbedingt vor Augen halten, daß auf jeder Ebene der Hierarchie Elemente gleichen Namens erlaubt sind. Der XPfad //table/tr/td findet also nicht nur alle Zellen einer bestimmten Zeile einer bestimmten Tabelle, sondern alle Zellen aller Zeilen aller Tabellen.

Der Doppelslash // im letzten Beispiel ist der Suchoperator. Mit dem Pfadoperator / gibt man an, daß der nächstgenannte Knoten direkt unterhalb dem Zuvorgenannten liegt. Verwendet man jedoch den Suchoperator, dann liegt der nächstgenannte Knoten an beliebiger Stelle der Hierarchie unterhalb des Zuvorgenannten. Der XPfad //div findet z.B. sämtliche div-Elemente des Dokumentes - gleichgültig wo sie sich in der Hierarchie befinden.

Auf den ersten Blick scheint es verführerisch den Suchoperator großzügig einzusetzen und statt /html/head/title einfach nur //title zu schreiben. Diese Lösung ist allerdings recht ineffizient, da die XPath-Engine so gezwungen wird, jeden Knoten des gesamten Dokumentes zu überprüfen. Den Suchoperator sollte man sich also für Dokumente aufsparen, deren Struktur wirklich unbekannt ist.

Man kann den Suchoperator allerdings nicht nur am Anfang eines XPfades benutzen, sondern überall, wo auch der Pfadoperator erlaubt ist. Wenn man also z.B. code-Elemente sucht, von denen man weiss, daß sie irgendwo unterhalb von p-Elementen liegen, die ihrerseits unterhalb von einem div-Element mit der ID "content" liegen, könnte man den XPfad so formulieren: /html/body//div[@id="content"]/p//code. Dieser XPfad ist nicht nur spezifischer, sondern auch effizienter als //code.

Genau wie Dateipfade kann man XPfade sowohl absolut, als auch relativ angeben. XPfade, die mit einem Slash oder mit einem Doppelslash beginnen, sind absolut. Xpfade, die mit einem Knoten beginnen, sind relativ zum in PHP angegebenen Kontextknoten. Wenn also z.B. das head-Element der aktuelle Kontextknoten ist, lautet der vollständige relative XPfad zum title-Element einfach nur title. Der Kontextknoten selbst kann über den Punkt . angesprochen werden - ähnlich wie das Arbeitsverzeichnis bei Dateipfaden.

Attributknoten spricht man an, indem man dem Namen des Attributes ein @ voranstellt. //link[@rel="stylesheet" and @type="text/css"]/@href wären z.B. die URLs aller Stylesheets.

Selektiert wird immer der letzte Location Step. /html/head/title selektiert also die title-Elemente. /html/head selektiert alle head-Elemente. usw.

Mit dem Vereinigungsoperator | kann man aus den Knotenmengen mehrerer XPfade eine einzige machen. Nehmen wir an, man interessiert sich nicht nur für das title-Element, sondern auch für sämtliche meta-Elemente eines X?HTML Dokumentes. Der absolute XPfad dafür ist: /html/title|/html/meta. Mit dem head-Element als Kontextelement verkürzt sich dieser XPfad auf title|meta

Knotentests


Element- und Attributknoten kann man über ihren Namen ansprechen, doch nicht alle Knoten tragen Namen oder sind namentlich bekannt.
KnotentestBedeutung
*Elementknoten beliebigen Namens
@*Attributknoten beliebigen Namens
text()Textknoten (Bei Mixed Content - wie z.B. eins <b>zwei</b> drei - liefert text() sämtlichen Textinhalt sämtlicher Kindknoten - im Beispiel also: eins zwei drei.)
comment()Kommentarknoten
processing-instruction([string typ])Processing Instruction - optional: Typ der PI. Der Typ wird im öffnenden Tag angegeben: <?{typ} Die XML-Deklaration kann man so allerdings nicht ansprechen, denn die ist bereits prozessiert.
node()alle Arten von Knoten ausser Attributen, also: Element-, Text-, Kommentarknoten und PIs. node() ist identisch zu: *|text()|comment()|processing-instruction()

Beispiele für Location Steps


BeispielBedeutung
/der Wurzelknoten des Dokumentes
/aaadas Dokumentelement aaa
/aaa/bbbbbb-Elemente unterhalb des Dokumentelementes aaa
aaaaaa-Elemente unterhalb des Kontextknotens
./aaadito
aaa/bbbbbb-Elemente unterhalb der aaa-Elemente unterhalb des Kontextknotens
//aaasämtliche aaa-Elemente - unabhängig von ihrer Lage im Dokument
aaa//bbbsämtliche bbb-Elemente, die direkt oder indirekt unterhalb von aaa-Elementen unterhalb des Kontextknotens liegen
.//aaasämtliche aaa-Elemente direkt oder indirekt unterhalb des Kontextknotens
@aaaaaa-Attribut des Kontextknotens
aaa/@bbbbbb-Attribute der aaa-Elemente unterhalb des Kontextknotens
//aaa|//bbbsämtliche aaa-Elemente und sämtliche bbb-Elemente - jeweils unabhängig von ihrer Lage im Dokument
aaa/bbb|cccbbb-Elemente unterhalb von aaa-Elementen unterhalb des Kontextknotens und alle ccc-Elemente unterhalb des Kontextknotens
/aaa|/aaa/bbbdas Dokumentelement aaa und die bbb-Elemente unterhalb des Dokumentelementes
aaa/@bbb|aaa/@cccdie bbb- und ccc-Attribute der aaa-Elemente unterhalb des Kontextknotens
aaa/*alle Elemente unmittelbar unterhalb der aaa-Elemente unterhalb des Kontextknotens
aaa/@*alle Attribute der aaa-Elemente unterhalb des Kontextknotens
aaa/text()Textinhalte der aaa-Elemente unterhalb des Kontextknotens
//processing-instruction("php")sämtliche PHP-Sektionen des Dokumentes

Prädikate


Einleitung


Location Steps alleine sind meist zu unspezifisch, da oftmals mehrere Knoten gleichen Namens existieren, von denen nur wenige - oder sogar nur ein einziger - gesucht werden. Prädikate erlauben es - in etwa analog zu dem if-Konstrukt in PHP - Bedingungen, die die Knoten erfüllen müssen, festzulegen. Ist das Prädikat für den jeweiligen Knoten wahr, bleibt er in der Knotenmenge - ist es falsch, wird er aussortiert. Innerhalb eines Prädikates kann man den Location Step, auf den sich das Prädikate bezieht, mit dem Punkt . ansprechen.

Zahlen als Prädikate


Im einfachsten Fall enthält das Prädikat nur eine Zahl n. Das Prädikat ist in diesem Falle wahr, wenn der Knoten der n-te der Knotenmenge ist. Man beachte dabei, daß anders als bei Arrays der erste Knoten auf Position 1 liegt und nicht auf Position 0!
BeispielBedeutung
aaa[1]das erste aaa-Element
aaa[last()]das letzte aaa-Element

Knotenmengen als Prädikate


Knotenmengen werden als wahr gewertet, wenn mindestens ein Knoten der Menge ein Kind des aktuellen Location Steps ist.
BeispielBedeutung
aaa[bbb]alle aaa-Elemente, die bbb-Kindelemente haben
aaa[bbb/ccc]alle aaa-Elemente, die bbb-Kindelemente haben, die ihrerseits ccc-Kindelemente haben
aaa[@bbb]alle aaa-Elemente, die ein bbb-Attribut haben
*[@aaa|@bbb]alle Elemente, die ein aaa- oder ein bbb-Attribut haben

Boolsche Ausdrücke als Prädikate


Diese Variante entspricht am ehesten dem if-Konstrukt in PHP, schliesst jedoch die beiden anderen Varianten mit ein, d.h. Zahlen und Knotenmengen sind als Teilausdrücke erlaubt.
BeispielBedeutung
aaa[position()=1]das erste aaa-Element - identisch zu aaa[1]
aaa[position()>5]alle aaa-Elemente nach dem fünften
aaa[@bbb="test"]alle aaa-Elemente, deren bbb-Attribut gleich "test" ist
aaa[contains(@bbb,"test")]alle aaa-Elemente, in deren bbb-Attribut der Teilstring "test" vorkommt
aaa[@bbb="test"]/ccc[2]das zweite ccc-Element unterhalb der aaa-Elemente mit einem bbb-Attribut, das gleich "test" ist
*[starts-with(name(.),"a") and string-length(.)>=20]alle Elemente deren Name mit "a" beginnt und deren Inhalt mindestens 20 Zeichen lang ist
aaa[*|text()]alle aaa-Elemente, die Element- oder Textknoten enthalten
aaa[text() and count(node())>1]alle aaa-Elemente, die Mixed Content enthalten
aaa[not(node())]alle leeren aaa-Elemente
aaa[number(text())>10]alle aaa-Elemente deren Wert größer als 10 ist
aaa[.>10]dito
aaa/bbb[1 and @ccc]alle bbb-Elemente, die an erster Stelle unter aaa-Elementen liegen und ein ccc-Attribut haben
aaa/bbb[1][@ccc]dito

Typen


XPath kennt neben Knotenmengen noch drei skalare Typen: Strings, Numbers und Booleans. Knotenmengen, die sich zu Text evaluieren lassen, können in Ausdrücken wie Skalare behandelt werden. Wenn die Operatoren oder Funktionen einen skalaren Wert erfordern, ruft die XPath Engine automatisch text() für den ersten Knoten der Knotenmenge auf und castet das Ergebnis zu einem der drei skalaren Typen. Es ist also nicht nötig p[string-length(string(text()))>10] zu schreiben. p[string-length(.)>10] reicht völlig. Bei allen XPath-Funktionen, die nur einen einzigen optionalen Parameter haben, ist der Default-Wert der aktuelle Knoten des Location Steps. Den Punkt könnte man in diesem Beispiel also auch noch weglassen.

Operatoren


Boolsche Operatoren:
orandnot()
not() ist kein Operator, sondern eine Funktion und hier nur der Vollständigkeit halber gelistet.

Mathematische Operatoren:
+-*divmod
Da der Slash bereits als Pfadoperator benutzt wird, steht er nicht als Divisionsoperator zur Verfügung. Daher wird in XPath "div" zur Division verwendet. Der XPath-Operator "div" darf nicht mit dem gleichnamigen PHP-Operator verwechselt werden!

Vergleichsoperatoren:
=!=<<=>>=
Da es in XPath keine Zuweisungen gibt, wird ein einfaches(!) Gleichheitszeichen für Vergleiche benutzt.

Funktionen


KnotenmengenfunktionenErgebnisBedeutung
position ()numberliefert die Position, die der aktuelle Knoten im Location Step einnimmt
last ()numberliefert die Position des letzten Knotens im Location Step
count (nodeset)numberliefert die Anzahl der Knoten einer Knotenmenge
sum (nodeset)numberliefert die Summe aller Werte einer Knotenmenge
id (string [, string])nodesetliefert die Menge der Knoten mit den angegebenen IDs. Diese Funktion ist das XPath-Equivalent zur DOM-Funktion getElementById . Unglücklicherweise wird die RFC für IDs sehr spitzfindig ausgelegt. id() findet nur IDs mit dem Präfix xml. Beispiel: <node xml:id="inhalt"/>
local-name ([nodeset])stringliefert einen Knotennamen ohne Namespace-Präfix. Wird eine Knotenmenge angegeben, liefert die Funktion den lokalen Namen des ersten Knotens der Knotenmenge. Default-Wert ist der aktuelle Knoten des Location Steps
namespace-uri ([nodeset])stringliefert die Namespace-URI des ersten Knotens der Knotenmenge. Default-Wert ist der aktuelle Knoten des Location Steps
name ([nodeset])stringliefert den erweiterten Namen eines Knotens - bestehend aus Namespace-Präfix und lokalem Namen. Bei Angabe einer Knotenmege wird der erweiterte Name des ersten Knotens geliefert. Default-Wert ist der aktuelle Knoten des Location Steps
String-FunktionenErgebnisBedeutung
string ([mixed])stringkonvertiert den Parameter in einen String. Ist der Parameter eine Knotenmenge wird zuvor text() für ihren ersten Knoten aufgerufen. Default-Wert ist der aktuelle Knoten des Location Steps. Achtung: string(true()) wird in den String "true" verwandelt!
concat (string, string [,string])stringfügt zwei oder mehr Strings zu einem neuen String zusammen.
starts-with (string, string)booleangibt an, ob der erste String mit dem zweiten String beginnt.
  • Beispiel: starts-with("2004-04-24","2004") = true()
contains (string, string)booleangibt an ob der erste String den zweiten String enthält
  • Beispiel: contains("2004-04-24","4") = true()
substring-before (string, string)stringliefert den Teilstring des ersten Parameters, der vor dem ersten Auftauchen des zweiten Paramters steht.
  • Beispiel: substring-before("2004-04-24","-") = "2004"
substring-after (string, string)stringliefert den Teilstring des ersten Parameters, der nach dem ersten Auftauchen des zweiten Paramters steht.
  • Beispiel: substring-after("2004-04-24","-") = "04-24"
substring (string, number [, number])stringliefert den Teilstring ab der Position des zweiten Parameters. Der optionale dritte Parameter gibt die Länge des Teilstrings an.
  • Beispiel 1: substring("abcde", 2) = "bcde"
  • Beispiel 2: substring("abcde", 3, 2) = "cd"
string-length ([string])numberliefert die Textlänge des ersten Knotens der Knotenmenge. Default-Wert ist der aktuelle Knoten des Location Steps
normalize-space ([string])stringnormalisiert den String, d.h. Whitespace am Anfang und Ende des Strings wird entfernt und aller Whitespace im Reststring wird auf jeweils ein einziges Space reduziert. Default-Wert ist der aktuelle Knoten des Location Steps
translate (string, string, string)stringersetzt im ersten String die Zeichen aus dem zweiten String durch die Zeichen aus dem dritten String.
  • Beispiel: translate("abcdef", "da", "12") = "2bc1ef"
Number-FunktionenErgebnisBedeutung
number ([mixed])numberkonvertiert den Parameter in eine Zahl. Ist der Parameter eine Knotenmenge wird zuvor text() für ihren ersten Knoten aufgerufen. Default-Wert ist der aktuelle Knoten des Location Steps
floor (number)numberrundet die Zahl ab und liefert eine Ganzzahl
ceiling (number)numberrundet die Zahl auf und liefert eine Ganzzahl
round (number)numberrundet die Zahl und liefert eine Ganzzahl
Boolean-FunktionenErgebnisBedeutung
boolean ([mixed])booleankonvertiert den Parameter in einen Boolean. Ist der Parameter eine Knotenmenge wird zuvor text() für ihren ersten Knoten aufgerufen. Default-Wert ist der aktuelle Knoten des Location Steps
not (boolean)booleannegiert den Parameter
true ()booleanliefert den Wert TRUE
false ()booleanliefert den Wert FALSE
lang (string)booleangibt an, ob das xml:lang Attribut des Location Steps gleich dem Parameter ist. Bei <div xml:lang="en">..</div> wäre also:
  • Beispiel 1: lang("en") = true()
  • Beispiel 2: lang("de") = false()
(Die eckigen Klammern um einem Parameter bedeuten, daß er optional ist.)

Achsen


Standardmäßig bewegen sich die Location Steps von Elternknoten zu Kindknoten. Mit Hilfe von Achsen kann man jedoch andere Bewegungsrichtungen festlegen. Die Standardachse ist child .

AchseBedeutung
ancestor::aaaalle aaa-Elemente oberhalb des Kontextknotens
ancestor::*alle Elemente oberhalb des Kontextknotens
ancestor-or-self::aaaalle aaa-Elemente oberhalb des Kontextknotens und der Kontextknotens selbst, falls er ein aaa-Element ist
ancestor-or-self::*alle Elemente oberhalb des Kontextknotens und der Kontextknotens selbst
attribute::aaadas aaa-Attribut des Kontextknotens - identisch zu @aaa
child::aaaalle aaa-Elemente direkt unterhalb des Kontextknotens - identisch zu aaa
descendant::aaaalle aaa-Elemente direkt oder indirekt unterhalb des Kontextknotens - identisch zu .//aaa
descendant-or-self::aaaalle aaa-Elemente direkt oder indirekt unterhalb des Kontextknotens und der Kontextknoten selbst, falls er ein aaa Element ist - identisch zu .[name(.)="aaa"]|.//aaa
following::aaaalle im Dokumenttext folgenden aaa-Elemente - unabhängig von ihrer Lage in der Hierarchie
following-sibling::aaaalle im Dokumenttext folgenden aaa-Elemente auf der gleichen Stufe der Hierarchie
parent::aaalle aaa-Elemente unmittelbar oberhalb in der Hierarchie
preceding::aaalle im Dokumenttext vorhergenden aaa-Elemente - unabhängig von ihrer Lage in der Hierarchie
preceding-sibling::aaalle im Dokumenttext vorhergenden aaa-Elemente auf der gleichen Stufe der Hierarchie
self::*der Kontextknoten - identisch zu .
self::aaader Kontextknoten, falls er ein aaa-Element ist - identisch zu .[name(.)=aaa]

SimpleXML


Einleitung


Bei SimpleXML ist die XPath-Engine über die Methode SimpleXMLElement::xpath erreichbar. Der Kontextknoten des XPfades ist das SimpleXMLElement, von dem aus die xpath-Methode aufgerufen wurde. Da also immer ein Kontextknoten zur Verfügung steht, sind relative XPfade meist sinnvoller als absolute.

Knotenmengen


Bei einem XPfad, der zu einer Knotenmenge evaluiert, gibt SimpleXMLElement::xpath ein Array von SimpleXMLElementen zurück. Wenn man nur einen einzigen Knoten als Rückgabewert erwartet, kann man sich mit dem list Konstrukt behelfen.
PHP Quellcode:
// Das Dokumentelement wird automatisch zum Kontextknoten:
$zahlen = simplexml_load_string('<'.'?xml version="1.0" ?'.'>
<zahlen>
    <zahl>eins</zahl>
    <zahl>zwei</zahl>
    <zahl>drei</zahl>
</zahlen>'
);

//=== 1.) relative XPfade ========================================

// das zweite zahl-Element ausgeben:
if (list($zwei) = $zahlen->xpath('zahl[2]')) {
    echo $zwei;  // Ausgabe: "zwei"
}

// alle drei zahl-Elemente ausgeben:
foreach ($zahlen->xpath('zahl') as $zahl) {
    echo $zahl;
}
// Ausgabe: "einszweidrei"

//=== 2.) absolute XPfade ========================================

// das zweite zahl-Element ausgeben:
if (list($zwei) = $zahlen->xpath('/zahlen/zahl[2]')) {
    echo $zwei;  // Ausgabe: "zwei"
}

// alle drei zahl-Elemente ausgeben:
foreach ($zahlen->xpath('/zahlen/zahl') as $zahl) {
    echo $zahl;
}
// Ausgabe: "einszweidrei"

Skalare


SimpleXMLElement::xpath ist nicht in der Lage XPfade zu verarbeiten, die zu Skalaren evaluieren. Der Rückgabewert ist in jedem Falle FALSE. Man könnte jedoch mit Hilfe des DOMs eine Methode für Skalare nachrüsten. (Näheres dazu steht im DOMXPath Abschnitt.)
PHP Quellcode:
$xml = '<'.'?xml version="1.0" ?'.'>
<zahlen>
    <zahl>eins</zahl>
    <zahl>zwei</zahl>
    <zahl>drei</zahl>
</zahlen>'
;

$zahlen = new SimpleXMLElement($xml);

// Dieser XPfad müsste eigentlich zu "einszweidrei" evaluieren:
if ($zahlen->xpath('string(.)') === FALSE) {
    // Klappt aber nicht!
    echo "Der XPfad kann mit SimpleXMLElement::xpath nicht ausgewertet werden";
}

//=== die Behelfslösung ==========================================

class SXE extends SimpleXMLElement
{
    /**
     * verarbeitet XPfade, die zu Skalaren evaluieren
     *
     * @param string $xpath der zu vearbeitende XPfad
     */

    function scalarXPath ($xpath)
    {
        static $domXPath = NULL;
        $context = dom_import_simplexml($this);
        if (is_null($domXPath)) {
            $domXPath = new DOMXPath($context->ownerDocument);
        }
        $result = $domXPath->evaluate($xpath, $context);
        if ($result instanceof DOMNodeList) {
            throw new Exception($xpath.' ergibt keinen skalaren Wert.');
        }
        return $result;
    }
}

$zahlen = new SXE($xml);
// der Name des ersten Kindelementes von "zahlen":
if ($result = $zahlen->scalarXPath('name(*[1])')) {
    echo $result; // Ausgabe: "zahl"
}

Sonderfall: Löschen von Knoten


Das von SimpleXMLElement::xpath gelieferte Array erlaubt es nicht nur die gefundenen Knoten auszulesen; es können auch ihre Inhalte verändert oder neue Elemente oder Attribute hinzugefügt werden. Es ist allerdings nicht möglich, die mit XPath gefundenen Knoten aus dem Dokument zu löschen. Das liegt daran, daß SimpleXML über keine eigene Methode zum Löschen von Knoten verfügt. Stattdessen behilft man sich mit unset. Da das von SimpleXMLElement::xpath gelieferte Array aber nur Verweise auf die Knoten enthält - und nicht die Knoten selbst - ist ein unset hier zwecklos. Mit Hilfe des DOMs kann man aber die fehlende Löschmethode nachrüsten.
PHP Quellcode:
class SXE extends SimpleXMLElement
{
    /**
     * löscht ein SimpleXMLElement (inklusive aller Kindknoten)
     */

    function remove ()
    {
        $domNode = dom_import_simplexml($this);
        $domNode->parentNode->removeChild($domNode);
    }
}

$xml = '<test><eins/><zwei/><drei/><!-- vier --></test>';
$test = new SXE($xml);

// Lösche alle Kindelemente von "test", ausser dem Ersten:
foreach ($test->xpath('*[position()>1]') as $node) {
    $node->remove();
}
echo $sxe->asXML(); // Ausgabe: "<test><eins/><!-- vier --></test>"

DOMXPath


Einleitung


DOMXPath ist eine separate Klasse. Man erzeugt deshalb zunächst ein DOMXPath-Objekt mit dem DOMDocument als Parameter. Dabei ist die Reihenfolge wichtig. Das DOMXPath-Objekt muss nach dem Laden der Daten erzeugt werden. Andernfalls werden die XPfade auf ein leeres Dokument angewandt.
PHP Quellcode:
$dom = new DOMDocument;
$dom->load('datei.xml');
$xpath = new DOMXPath($dom);

Knotenmengen


XPfade, die zu Knotenmengen evaluieren, kann man sowohl mit DOMXPath::evaluate, als auch mit DOMXPath::query auslesen. Beide Methoden liefern ein Objekt vom Typ DOMNodeList zurück. Da man bei DOMXPath den Kontextknoten explizit erzeugen muss, sind absolute XPfade oft praktischer als relative.
PHP Quellcode:
$dom = new DOMDocument;
$dom->loadXML('<'.'?xml version="1.0" ?'.'>
<zahlen>
    <zahl>eins</zahl>
    <zahl>zwei</zahl>
    <zahl>drei</zahl>
</zahlen>'
);

$xpath = new DOMXPath($dom);

//=== 1.) absolute XPfade ========================================

// Einen einzelnen Wert einer DOMNodeList erhält man mit der item-Methode:
if ($zwei = $xpath->evaluate('/zahlen/zahl[2]')->item(0)) {
    echo $zwei->nodeValue;  // Ausgabe: "zwei"
}

// Dank der Magie des Iterator-Interfaces kann man ein DOMNodeList-Objekt
// in einer foreach-Schleife benutzen, als wäre es ein Array:
foreach ($xpath->evaluate('/zahlen/zahl') as $node) {
    echo $node->nodeValue;
}
// Ausgabe: "einszweidrei"

//=== 2.) relative XPfade ========================================

// Zunächst braucht man einen Kontextknoten. Dieser muss nicht unbedingt
// mit XPath ermittelt werden. In diesem Fall ginge z.B. auch:
// $context = $dom->documentElement;
$context = $xpath->evaluate('/zahlen')->item(0);

// bei der evaluate- und der query-Methode gibt es einen optionalen
// zweiten Parameter: den Kontextknoten
if ($zwei = $xpath->evaluate('zahl[2]', $context)->item(0)) {
    echo $zwei->nodeValue;  // Ausgabe: "zwei"
}

foreach ($xpath->evaluate('zahl', $context) as $node) {
    echo $node->nodeValue;
}
// Ausgabe: "einszweidrei"

Skalare


XPfade, die zu Skalaren evaluieren, kann man nur mit DOMXPath::evaluate auswerten. DOMXPath::query gibt anstatt eines Skalars eine leere DOMNodeList zurück. Knotenmengen muss man ausdrücklich mit string(), number() oder boolean() zu Skalaren konvertieren. text() zu benutzen, ist an dieser Stelle sinnlos, denn auch Textknoten sind Knotenmengen und keine Skalare. Bei Ausdrücken, die ohnehin zu Skalaren evaluieren, ist keine Konversion erforderlich. Bei XPfaden vom Typ String, die kein Ergebnis liefern, wird ein leerer String zurückgegeben. Fehlschlagende Boolean-XPfade geben FALSE zurück. Fehlschlagende Number-XPfade geben NAN zurück.
PHP Quellcode:
$dom = new DOMDocument;
$dom->loadXML('<'.'?xml version="1.0" ?'.'>
<zahlen>
    <zahl>10</zahl>
    <zahl>2</zahl>
    <zahl>6</zahl>
</zahlen>'
);

$xpath = new DOMXPath($dom);

// bei einer Knotenmenge ist ein Konversion mit number() erforderlich:
$q = 'number(/zahlen/zahl[3])';
// is_finite gibt falsch zurück, wenn der XPath fehlschlägt und NAN zurückgibt:
if (is_finite($result = $xpath->evaluate($q)))
    echo 4 + $result; // Ausgabe: "10"
}

// keine Konversion erforderlich - das Ergebnis ist sowieso vom Typ Number:
$q = 'sum(zahl) div count(zahl)';
if (is_finite($result = $xpath->evaluate($q, $dom->documentElement))) {
    echo $result; // Ausgabe: "6" (der durchschnittliche Wert von "zahl")
}

echo gettype($xpath->evaluate('2+2')); // Ausgabe: "double"

PHP-Funktionen in XPath


Als besonderes Goodie erlaubt es DOMXPath::registerPhpFunctions die sehr beschränkte Funktionsbibliothek von XPath um beliebige PHP-Funktionen zu ergänzen. Knotenmengen werden an PHP-Funktionen als Arrays von DOMElement-Objekten übergeben. Text- und Attributknoten muss man vor der Übergabe an die PHP-Funktion ausdrücklich mit den XPath-Funktionen string(), number() oder boolean() zu Skalaren casten, sonst werden sie ebenfalls als Knotenmengen gewertet. Bei Ausdrücken, die Skalare erzeugen - wie z.B. substring-before(@href,":") oder preis[1] + preis[2] - ist kein Casting erforderlich. Zulässige Rückgabewerte sind Strings, Zahlen (sowohl Integers als auch Floats) und Booleans. Es gibt keine Möglichkeit Knotenmengen aus einer PHP-Funktion an XPath zur übergeben. Es lassen sich nur gewöhnliche Funktionen bei der XPath-Engine registrieren. Sie ist nicht in der Lage Methoden oder anonyme Funktionen aufzurufen.
PHP Quellcode:
$dom = new DOMDocument;
$dom->preserveWhiteSpace = FALSE;
$dom->loadXML('<'.'?xml version="1.0" ?'.'>
<demo>
    <shopping-list>
        <item>Frischkäse</item>
        <item id="zwei">Schweizer Käse</item>
        <item>Käsekuchen</item>
        <item><!-- hier kommt noch was hin --></item>
        <item/><!-- ist schon im Kühlschrank -->
    </shopping-list>
    <dates>
        <date>2009-04-14</date>
        <date>2010-04-14</date>
        <date>2011-04-14</date>
    </dates>
</demo>'
);

$xpath = new DOMXPath($dom);

// Zunächst muss ein Namespace für die PHP-Funktionen registriert werden:
// (Das Präfix ist frei wählbar, aber die URI muss genau so aussehen!)
$xpath->registerNamespace('php', 'http://php.net/xpath');

// Jetzt werden die PHP-Funktionen bei der XPath-Engine registriert:
$xpath->registerPhpFunctions(
    array('preg_match', 'dateBetween', 'describeNodeset')
);

//=== Beispiel 1 =================================================

// finde die Items der Shopping-Liste, auf deren Inhalt
// der Reguläre Ausdruck /\bkäse\b/i passt:
// (Achtung: Der Rückgabewert von preg_match ist die Anzahl der gefundenen
// Matches. In XPath werden aber Zahlen als Positionsangaben gewertet.
// Deshalb wird hier der Rückgabewert in einen Boolschen Ausdruck verpackt.)
$q = '/demo/shopping-list/item[
    php:function("preg_match", "/\bkäse\b/i", string(.)) > 0
]'
;
foreach ($xpath->evaluate($q) as $n) {
    echo $n->nodeValue;
}
// Ausgabe: "Schweizer Käse"


//=== Beispiel 2 =================================================

// finde die date-Elemente, die zwischen 2010-04-10 und 2010-04-20 liegen:
$q = '/demo/dates/date[
    php:function("dateBetween", string(.), "2010-04-10", "2010-04-20")
]'
;
foreach ($xpath->evaluate($q) as $n) {
    echo $n->nodeValue;
}
// Ausgabe: "2010-04-14"


//=== Beispiel 3 =================================================

// beschreibe die Knotenmenge /demo/shopping-list/*:
$q = 'php:function("describeNodeset",/demo/shopping-list/*)';
echo $xpath->evaluate($q);
/* Ausgabe:
element (name: item): Frischkäse
element (name: item): Schweizer Käse (attributes: id)
element (name: item): Käsekuchen
element (name: item): 1 child
element (name: item): empty
*/



//=== die Funktionen =============================================

/**
 * testet ob ein Datum im Format YYYY-MM-DD zwischen zwei anderen Daten liegt
 *
 * @param string $date das zu testende Datum
 * @param string $dateBefore das Anfangsdatum des Testintervalls
 * @param string $dateAfter das Enddatum des Testintervalls
 * @return bool ob $date zwischen $dateBefore und $dateAfter liegt
 */

function dateBetween ($date, $dateBefore, $dateAfter) {
    if (!($d = DateTime::createFromFormat('Y-m-d', $date))) return FALSE;
    if ($d < (new DateTime($dateBefore))) return FALSE;
    if ($d > (new DateTime($dateAfter))) return FALSE;
    return TRUE;
}

/**
 * liefert eine simple Beschreibung einer Knotenmenge
 *
 * @param array $nodeset ein Array aus DOMElement-Objekten
 * @return string die Beschreibung der Knotenmenge
 */

function describeNodeset (array $nodeset)
{
    $descriptions = array();
    foreach ($nodeset as $n) {
        switch ($n->nodeType) {
            case XML_ELEMENT_NODE:
                $d = 'element (name: '.$n->nodeName.'): ';
                if ($l = $n->childNodes->length) {
                    if ($n->firstChild->nodeType == XML_TEXT_NODE) {
                        $d .= excerpt($n->nodeValue);
                    } else {
                        $d .= $l.($l==1?' child':' children');
                    }
                } else {
                    $d .= 'empty';
                }
                if ($n->hasAttributes()) {
                    $names = array();
                    foreach ($n->attributes as $a) {
                        $names[] = $a->nodeName;
                    }
                    $d .= ' (attributes: '.implode(',', $names).')';
                }
                break;
            case XML_ATTRIBUTE_NODE:
                $d = 'attribute (name: '.$n->nodeName.'): '.excerpt($n->nodeValue); break;
            case XML_TEXT_NODE:
                $d = 'text: '.excerpt($n->nodeValue); break;
            case XML_COMMENT_NODE:
                $d = 'comment: <![CDATA['.excerpt($n->data).']]>'; break;
            case XML_PI_NODE:
                $d = $n->target.' processing-instruction: <[!CDATA['.excerpt($n->data).']]>'; break;
            case XML_DOCUMENT_NODE:
                $d = 'document'; break;
        }
        $descriptions[] = $d;
    }
    return implode("\r\n", $descriptions);
}

/**
 * normalisiert Text und kürzt ihn auf max. 50 Zeichen
 *
 * @param string $text der zu bearbeitende Text
 * @return string der normalisierte und gekürzte Text
 */

function excerpt ($text)
{
    $t = preg_replace('/\s+/', ' ', trim($text));
    if (strlen($t) > 50) $t = substr($t, 0, 47).'...';
    return $t;
}

Namespaces


Namespaces mit Präfix


Der Zugriff auf Namespaces ist relativ einfach. Man muss nur dem Namen des Elementes oder des Attributes das Namespace-Präfix voranstellen. Leider ist die XPath-Engine aber nicht in der Lage selbstständig alle Präfixe des Dokumentes zu registrieren. Das muss man von Hand nachholen. Bei SimpleXML benutzt man dazu SimpleXMLElement::registerXPathNamespace und beim DOM DOMXPath::registerNamespace. Die Namespace-Präfixe müssen dabei nicht denen im Dokument entsprechen - die Namespace-URIs allerdings schon! Namespaces, auf die man nicht zugreifen will, braucht man auch nicht zu registrieren.
PHP Quellcode:
$xml = '<'.'?xml version="1.0" ?'.'>
<test xmlns:ns1="http://example.org/namespaces/eins">
    <ns1:namespaced>
        <ns1:eins>1</ns1:eins>
    </ns1:namespaced>
    <ns2:namespaced xmlns:ns2="http://example.org/namespaces/zwei">
        <ns2:zwei>2</ns2:zwei>
    </ns2:namespaced>
</test>'
;

// SimpleXML:

$sxe = simplexml_load_string($xml);
$sxe->registerXPathNamespace('ns1', 'http://example.org/namespaces/eins');
if (list($ns1Eins) = $sxe->xpath('ns1:namespaced/ns1:eins')) {
    echo $ns1Eins; // Ausgabe: "1"
}

// DOM:

$dom = new DOMDocument;
$dom->loadXML($xml);
$xpath = new DOMXPath($dom);
$xpath->registerNamespace('ns2', 'http://example.org/namespaces/zwei');
if ($ns2zwei = $xpath->evaluate('/test/ns2:namespaced/ns2:zwei')->item(0)) {
    echo $ns2zwei->nodeValue; // Ausgabe: "2"
}

Default-Namespaces


Unglücklicherweise ist der Zugriff auf Default-Namespaces nicht einfacher. Bei einem Default-Namespace registriert das Dokument kein Namespace-Präfix. Dem Anwender bleibt es so erspart, dieses vor jeden Knoten schreiben zu müssen. Bei XHTML ist es z.B. üblich einen Default-Namespace zu verwenden. XPath besteht aber leider bei jedem Knoten, der in einem Namespace liegt, auch auf einem Präfix. Man muss also auch für Default-Namespaces ein Präfix registrieren. Auch hier darf man jeden gültigen XML-Namen als Präfix angeben. Ein leerer String ist kein gültiger XML-Name!
PHP Quellcode:
$xhtml = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>Auswertung eines Default-Namespaces</title>
    </head>
    <body>
        <div id="content">hello world!</div>
    </body>
</html>'
;

// SimpleXML:

$sxe = simplexml_load_string($xhtml);
$sxe->registerXPathNamespace('x', 'http://www.w3.org/1999/xhtml');
if (list($title) = $sxe->xpath('x:head/x:title')) {
    echo $title; // Ausgabe: "Auswertung eines Default-Namespaces"
}

// DOM:

$dom = new DOMDocument;
$dom->loadXML($xhtml);
$xpath = new DOMXPath($dom);
$xpath->registerNamespace('x', 'http://www.w3.org/1999/xhtml');
if ($title = $xpath->evaluate('string(//x:div[@id="content"])')) {
    echo $title; // Ausgabe: "hello world!"
}

Auswertung der vorhandenen Namespaces


Mit SimpleXMLElement::getDocNamespaces kann man sich einen Überblick über die Namespaces zu verschaffen. Die Methode gibt ein Array aller verwendeten Namespaces mit den Präfixen als Schlüsseln und den URIs als Werten zurück.

Das DOM bietet keine derart einfache Methode. Man kann sie aber mit Hilfe von SimpleXML nachrüsten.
PHP Quellcode:
class MyDOM extends DOMDocument
{
    /**
     * liefert ein Array aller Namespaces in der Form [präfix] => [uri]
     *
     * @return array Liste aller Namespaces des Dokumentes
     */

    function getDocNamespaces ()
    {
        return simplexml_import_dom($this)->getDocNamespaces(TRUE);
    }
}

$dom = new MyDOM;
$dom->loadXML($xml);
print_r($dom->getDocNamespaces());

Links


Auszug aus dem Buch "Effective XML": Navigate with XPath
ZVON Tutorial: XPath
Understanding XPath
Wikipedia: de en
Die offizielle RFC: XML Path Language (XPath), Version 1.0
PHP Manual: SimpleXMLElement::xpath
PHP Manual: DOMXPath

Mozilla Developer Center: Introduction to using XPath in JavaScript

FireXPath ist ein Plugin für die Firefox-Extension Firebug, mit dem man XPfade direkt im Browser benutzen kann
XPath Builder (ein Windows-Programm zum Testen von XPfaden - benötigt .NET 2.0 Framework)


Mitwirkende: pecos, Jens Clasen
Erstellt von pecos, 27.04.2010 am 09:10
Zuletzt bearbeitet von pecos, 30.04.2010 am 00:40
6 Kommentare , 13388 Betrachtungen

Dieser Text steht unter der GNU-Lizenz für freie Dokumentation


 

Lesezeichen

Stichworte
dom, simplexml, xhtml, xml, xpath

Artikel-Optionen
Ansicht

Forumregeln
Es ist Ihnen nicht erlaubt, neue Themen zu verfassen.
Es ist Ihnen nicht erlaubt, auf Beiträge zu antworten.
Es ist Ihnen nicht erlaubt, Anhänge hochzuladen.
Es ist Ihnen nicht erlaubt, Ihre Beiträge zu bearbeiten.

BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus.

Gehe zu
Ähnliche Themen
Thema Autor Forum Antworten Letzter Beitrag
PHP XPATH/XML-Problem photomm PHP 0 18.09.2008 15:53
xpath -> XML -> HTML oliwol PHP 0 17.04.2008 11:21
xpath und apache PHP 2 09.02.2006 15:16
domxml und xpath PHP 2 11.08.2005 20:30
Php.xpath?!? LeOn PHP 0 18.11.2003 15:32


Alle Zeitangaben in WEZ +2. Es ist jetzt 06:56 Uhr.


Powered by vBulletin® Version 3.8.8 (Deutsch)
Copyright ©2000 - 2018, Jelsoft Enterprises Ltd.
Powered by NuWiki v1.3 RC1 Copyright ©2006-2007, NuHit, LLC