XPath-Tester

Wertet XPath aus und zeigt jeden Treffer, samt Fallstricken.

Eingabe
WartetFügen Sie ein Dokument ein. Die Prüfung läuft während der Eingabe.

Alles läuft in diesem Tab. Nichts, was Sie einfügen, wird hochgeladen, protokolliert oder irgendwohin gesendet. Öffnen Sie Ihr Netzwerkpanel und prüfen Sie es.

Fügen Sie ein Dokument ein, tippen Sie einen Ausdruck, und jeder passende Knoten erscheint mit seinem Typ, dem Pfad, der zu ihm führt (/catalog/book[2]/title), und seinem Wert; Trefferzahl und Auswertungsdauer stehen über der Liste. Die Auswertung läuft über document.evaluate auf der Engine des Browsers, diese Seite liefert also keine XPath-Bibliothek aus, und nichts, was Sie einfügen, verlässt den Tab.

Man greift dazu, wenn ein Ausdruck gleich an einen Ort wandert, der schlecht zu debuggen ist: eine Schematron-Regel, ein XSLT-match-Muster, ein Camel-Splitter, ein Scraper-Selektor. Hier zu erfahren, dass //book nichts auswählt, ist billiger als es an einem stillen leeren Ergebnis in Produktion zu merken.

Anders ist die Behandlung von Namensräumen. Die meisten kostenlosen Tester übergeben document.evaluate einen Null-Resolver, wodurch jeder präfigierte Ausdruck eine Ausnahme wirft und jedes Dokument mit Standard-Namensraum ohne Erklärung null Treffer liefert. Dieses Werkzeug sammelt jede xmlns-Deklaration des Dokuments ein, bindet sie alle und benennt die Falle des Standard-Namensraums, bevor es ein leeres Ergebnis zurückgibt.

Der Ausdruck, der auf nichts passt

Das ist der häufigste XPath-Fehlschlag, und sein Symptom ist nicht davon zu unterscheiden, dass das Element wirklich fehlt. Nehmen Sie ein Dokument, dessen Wurzel xmlns="urn:books" trägt. Das Element darin heißt nicht book; sein erweiterter Name ist {urn:books}book. XPath 1.0 kennt keinen Standard-Namensraum, ein Name ohne Präfix bedeutet also „in keinem Namensraum“, und //book fragt nach {}book. So einen Knoten gibt es nicht. Null Treffer, kein Fehler.

MDN sagt es geradeheraus: In XPath gibt es keine Möglichkeit, den Standard-Namensraum aufzugreifen, wie er auf eine gewöhnliche Elementreferenz angewendet wird. Binden Sie stattdessen selbst ein Präfix an die URI. Das Präfix ist lokal zum Ausdruck: Steht im Dokument xmlns:b="urn:books", dürfen Sie trotzdem //x:book schreiben, sofern Sie x binden.

Findet diese Seite einen Standard-Namensraum, bindet sie das Präfix ns daran, //ns:book funktioniert also ohne Vorbereitung, und die Falle wird über dem Ergebnis ausgegeben, mit Ihrer tatsächlichen URI darin. Das Namensraumfeld nimmt eigene Bindungen als präfix=uri-Paare entgegen, und diese überschreiben, was aus dem Dokument geerntet wurde.

  • Ein Präfix binden: //ns:book/ns:title. Am kürzesten, und das, was Sie in Code wollen.
  • Namensräume ignorieren: //*[local-name()="book"]. Passt auf ein book in jedem Namensraum und ist gut zu kennen, wenn Sie den Resolver nicht steuern können.
  • Ohne Präfix exakt sein: //*[namespace-uri()="urn:books" and local-name()="book"].
  • Attribute sind anders. Ein Standard-Namensraum gilt nie für Attributnamen: In <book xmlns="urn:books" id="7"/> ist das Element {urn:books}book, das Attribut aber schlicht {}id. Wählen Sie es als @id, nicht als @ns:id.

Schrägstriche, Prädikate und Positionen

/ ist ein Schritt zu einem Kind. // ist die Kurzform für /descendant-or-self::node()/, weshalb /catalog/book nur book-Elemente direkt unter der Wurzel findet, während //book sie in jeder Tiefe findet. Das zweite ist nachsichtiger und auf einem großen Dokument deutlich langsamer, weil es jeden Knoten besucht statt der Kinder eines einzigen.

Prädikatindizes beginnen bei 1, nicht bei 0, ein Ausdruck, der auf [0] endet, liefert also stillschweigend nichts. Die subtilere Falle: Ein Prädikat bindet an seinen Schritt, nicht an den ganzen Ausdruck. //book[1] heißt „jedes book, das das erste book-Kind seines eigenen Elternteils ist“, ein Dokument mit drei Katalogen liefert also drei Knoten. Für den ersten Treffer insgesamt brauchen Sie Klammern: (//book)[1].

position() und last() sind Funktionen des Kontexts, also der Knotenliste, die der aktuelle Schritt erzeugt hat: book[last()] ist das letzte book unter jedem Elternteil. Eine nackte Zahl ist die Kurzform von [position() = 2], weshalb //book[@lang="en"][1] und //book[1][@lang="en"] verschiedene Mengen sind. Das erste filtert und nimmt dann eines; das zweite nimmt eines und filtert dann.

  • child:: ist die Standardachse, book und child::book sind also derselbe Ausdruck.
  • descendant:: sucht nach unten; parent:: (..) und ancestor:: suchen nach oben.
  • following-sibling:: und preceding-sibling:: bleiben auf einer Ebene – so sagt man „der price nach diesem title“.
  • attribute:: schreibt man @, self:: schreibt man . in einem abgekürzten Ausdruck.
  • namespace:: steht in der Spezifikation, aber Firefox implementiert es nicht. Bauen Sie nichts darauf.

Was XPath 1.0 nicht hat

Browser implementieren XPath 1.0 und sonst nichts. Die API stammt aus DOM Level 3 XPath, heute eine zurückgezogene W3C-Note, und lebt in Abschnitt 8 des WHATWG-DOM-Standards weiter. Kein Browser hat 2.0, und keiner bekommt sie, also nennt diese Seite die Obergrenze, statt eine Version zu bewerben, die sie nicht liefern kann.

XPath 1.0 hat vier Typen: Knotenmenge, Zeichenkette, Zahl, Wahrheitswert. 2.0 ersetzte dieses Modell durch Sequenzen und schemabewusste Typisierung und brachte, was am meisten fehlt: matches(), replace() und tokenize(), echte Datumstypen, for- und if-Ausdrücke. 3.1 kam mit Maps, Arrays und dem Pfeil =>. All das scheitert hier, ebenso in PHPs DOMXPath, .NETs XPathNavigator und Javas javax.xml.xpath, die ebenfalls 1.0 sind. Die Behelfe, die Sie am häufigsten brauchen, sind substring-before und substring-after statt tokenize sowie translate() statt einer Zeichenklasse.

Das Ergebnis lesen

Nicht jeder Ausdruck liefert Knoten. count(//book) liefert eine Zahl und string(/catalog/@id) eine Zeichenkette, das Panel nennt also, welcher der vier Typen zurückkam, und gibt Skalare als Werte statt als leere Liste aus. Eine skalare 0 und eine leere Knotenmenge sehen in den meisten Werkzeugen gleich aus und bedeuten Verschiedenes.

Jeder Treffer zeigt seinen Knotentyp (Element, Attribut, Text, CDATA, Kommentar, Verarbeitungsanweisung), den Pfad dorthin und seinen Wert: serialisiertes XML für ein Element, den Attributwert für ein Attribut. Getroffener Inhalt wird als Text in die Seite geschrieben und nie als Markup, ein Dokument mit einem script-Element kann also nichts ausführen. Das Dokument muss wohlgeformt sein, bevor ein Ausdruck läuft, ein ungebundenes Präfix wird namentlich samt der verfügbaren Präfixe gemeldet, und die ersten 1.000 Treffer werden dargestellt, während die Zahl über der Liste die tatsächliche Gesamtzahl ist.

Dasselbe in Code

Alle sechs implementieren XPath 1.0, und alle sechs lassen Sie die Namensraum-Präfixe selbst registrieren. Die Präfixe des Dokuments werden nie automatisch übernommen – deshalb taucht das Namensraum-Argument in jedem Beispiel auf.

const source = `<?xml version="1.0"?>
<library xmlns="urn:books">
  <book id="b1"><title>XML in a Nutshell</title></book>
</library>`;

const doc = new DOMParser().parseFromString(source, 'application/xml');
if (doc.querySelector('parsererror')) throw new Error('not well-formed');

// document.createNSResolver is deprecated: it now returns its input
// unchanged and is kept only for compatibility. Write the resolver
// yourself. These prefixes are local to the expression and need not
// match the ones the document uses.
const NS = { bk: 'urn:books' };
const resolve = (prefix) => NS[prefix] ?? null;

// Snapshot, not iterator: an iterator result is invalidated by any
// mutation of the document while you are still walking it.
const snap = doc.evaluate(
  '//bk:book[@id="b1"]/bk:title',   // //title would match nothing
  doc,
  resolve,
  XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
  null,
);

for (let i = 0; i < snap.snapshotLength; i++) {
  console.log(snap.snapshotItem(i).textContent);
}
from lxml import etree

# lxml's defaults resolve entities and fetch external DTDs. All three
# flags below are needed to close that off.
parser = etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False)
tree = etree.fromstring(source.encode('utf-8'), parser)

# lxml refuses a None key in the namespaces map, for the same reason the
# spec does: XPath 1.0 cannot address a default namespace. Give it a prefix.
ns = {'bk': 'urn:books'}

for title in tree.xpath('//bk:book/bk:title', namespaces=ns):
    print(title.text)

# An invalid expression raises rather than returning empty.
try:
    tree.xpath('//bk:book[')
except etree.XPathEvalError as e:
    print(f'bad expression: {e}')

# The escape hatch when you cannot register prefixes:
tree.xpath("//*[local-name()='book']")
import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.*;
import org.w3c.dom.NodeList;
import java.util.Iterator;
import java.util.Map;

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

// setNamespaceAware is FALSE by default. Leave it off and every prefixed
// element is treated as one long local name, so bk:book never matches.
// This is the usual cause of "it works in xmllint but not in Java".
dbf.setNamespaceAware(true);
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);

var doc = dbf.newDocumentBuilder()
    .parse(new java.io.ByteArrayInputStream(bytes));

XPathFactory xpf = XPathFactory.newInstance();
xpf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
XPath xpath = xpf.newXPath();

Map<String, String> ns = Map.of("bk", "urn:books");
xpath.setNamespaceContext(new NamespaceContext() {
    public String getNamespaceURI(String prefix) {
        return ns.getOrDefault(prefix, XMLConstants.NULL_NS_URI);
    }
    public String getPrefix(String uri) { return null; }
    public Iterator<String> getPrefixes(String uri) { return null; }
});

NodeList nodes = (NodeList) xpath.evaluate(
    "//bk:book/bk:title", doc, XPathConstants.NODESET);

for (int i = 0; i < nodes.getLength(); i++) {
    System.out.println(nodes.item(i).getTextContent());
}
using System.Xml;
using System.Xml.XPath;

var settings = new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Prohibit,
    XmlResolver = null,
};

using var reader = XmlReader.Create(new StringReader(source), settings);
var nav = new XPathDocument(reader).CreateNavigator();

// XmlNamespaceManager is not optional. .NET has no default-namespace
// concept in XPath either, so bind a prefix and use it.
var ns = new XmlNamespaceManager(nav.NameTable);
ns.AddNamespace("bk", "urn:books");

foreach (XPathNavigator node in nav.Select("//bk:book/bk:title", ns))
{
    Console.WriteLine(node.Value);
}

// Compile once if the expression is reused: Select() reparses every call.
XPathExpression expr = nav.Compile("count(//bk:book)");
expr.SetContext(ns);
Console.WriteLine((double)nav.Evaluate(expr));
<?php
$doc = new DOMDocument();
// LIBXML_NONET stops libxml2 fetching anything the document references.
if (!$doc->loadXML($source, LIBXML_NONET)) {
    fwrite(STDERR, "not well-formed\n");
    exit(1);
}

$xpath = new DOMXPath($doc);

// Prefixes must be registered even when the document declares them.
// registerNamespace('', ...) is accepted but useless: XPath 1.0 still
// cannot address the empty prefix.
$xpath->registerNamespace('bk', 'urn:books');

foreach ($xpath->query('//bk:book/bk:title') as $node) {
    echo $node->textContent, "\n";
}

// query() returns false on an invalid expression, not an empty list.
// A loose == comparison would read that false as "no results".
$result = $xpath->query('//bk:book[');
if ($result === false) {
    fwrite(STDERR, "invalid XPath expression\n");
}
# xmllint ships with libxml2 and is almost certainly already installed.
# --xpath has no way to register a namespace prefix, so against a
# namespaced document it prints "XPath set is empty" and exits 10.
xmllint --nonet --xpath '//book/title' doc.xml

# The interactive shell does have setns, and reads fine from a heredoc:
xmllint --nonet --shell doc.xml <<'EOF'
setns bk=urn:books
xpath //bk:book/bk:title
EOF

# Or sidestep prefixes entirely:
xmllint --nonet --xpath "//*[local-name()='title']/text()" doc.xml

# xmlstarlet is the friendlier option if you can install it:
xmlstarlet sel -N bk=urn:books -t -v '//bk:book/bk:title' -n doc.xml

Zwei Dinge wiederholen sich über alle sechs. Namensraum-Präfixe deklarieren Sie, nicht das Dokument liefert sie, und ein ungültiger Ausdruck wird jeweils anders signalisiert (eine geworfene DOMException, ein ausgelöster XPathEvalError, ein zurückgegebenes false, ein Exit-Status 10) – weshalb keines dieser Beispiele die Abfrage aufruft und ihr Ergebnis in einer Zeile weiterverwendet.

Häufige Fragen

Warum liefert mein XPath keine Ergebnisse?

Meist, weil das Dokument einen Standard-Namensraum hat und Ihr Ausdruck kein Präfix. Trägt die Wurzel xmlns="urn:something", ist das book darin in Wahrheit {urn:something}book, und //book fragt nach einem book ohne Namensraum. Null Treffer und kein Fehler, denn aus Sicht von XPath ist nichts schiefgegangen.

Diese Seite erkennt das, nennt die gefundene URI und bindet das Präfix ns daran, //ns:book funktioniert also sofort. Wenn Sie Präfixe lieber vermeiden, passt //*[local-name()="book"] auf den lokalen Namen, unabhängig vom Namensraum. Die weiteren Ursachen, die auszuschließen sind: Groß- und Kleinschreibung, denn XPath unterscheidet sie, und ein Prädikat [0], wo die Indizes bei 1 beginnen.

Wird mein XML hochgeladen, wenn ich einen Ausdruck teste?

Nein. Die Wohlgeformtheitsprüfung läuft in einem Web Worker in diesem Tab, und der Ausdruck wird von document.evaluate ausgewertet, einer lokalen Browser-API. Es gibt hier keine Serverkomponente, an die sich etwas senden ließe.

Bei XPath zählt das mehr als bei den meisten Werkzeugen, denn die Dokumente, gegen die man Ausdrücke schreibt, sind echte Nutzlasten und keine Beispiele: eine von einer Partner-API mitgeschnittene Antwort, eine Nachricht aus einer Queue, eine SAML-Assertion. Öffnen Sie den Netzwerk-Tab, fügen Sie ein Dokument ein, führen Sie einen Ausdruck aus – er bleibt leer.

Welche XPath-Version wird unterstützt?

XPath 1.0, weil Browser genau das implementieren und es keine Alternative gibt. Die Auswertung läuft über document.evaluate, definiert in Abschnitt 8 des WHATWG-DOM-Standards. Chrome, Firefox und Safari sind alle 1.0, und keiner hat angekündigt, weiterzugehen.

Also scheitern matches(), replace(), tokenize(), for- und if-Ausdrücke, Datumstypen, Sequenzen, Maps und Arrays hier allesamt, genau wie in PHPs DOMXPath, .NETs XPathNavigator und Javas javax.xml.xpath. Brauchen Sie 2.0 oder 3.1, heißt das Saxon: Saxon-JS im Browser, Saxon-HE auf der JVM oder unter .NET.

Was ist der Unterschied zwischen / und // in XPath?

/ wählt ein direktes Kind; // ist die Kurzform für /descendant-or-self::node()/ und wählt in jeder Tiefe. /catalog/book trifft also book-Elemente unmittelbar innerhalb der Wurzel catalog, während //book book überall trifft. Ein führendes / verankert an der Dokumentwurzel, weshalb /book an einem Dokument scheitert, dessen Wurzel catalog heißt.

Die Falle ist, // mit einem Prädikat zu kombinieren. //book[1] heißt nicht „das erste book im Dokument“; das Prädikat gilt für den Schritt, also heißt es „jedes book, das das erste book-Kind seines Elternteils ist“, und drei Kataloge geben drei Knoten. Klammern Sie, um das Gemeinte zu bekommen: (//book)[1].

Wie wähle ich ein Attribut statt eines Elements?

Setzen Sie @ vor den Namen. //book/@id wählt den Attributknoten id, und das Panel zeigt seinen Wert, den zugehörigen Pfad und seinen Typ. Um nach einem Attribut zu filtern, statt es zu wählen, setzen Sie es in ein Prädikat: //book[@id="b1"] wählt book-Elemente, keine Attribute.

Attribute haben ihre eigene Namensraumregel, und die wird am häufigsten falsch verstanden. Eine Standard-Namensraumdeklaration gilt nie für Attributnamen: In <book xmlns="urn:books" id="7"/> ist das Attribut schlicht {}id – wählen Sie es als @id, denn @ns:id trifft nichts. Ein Attribut liegt nur dann in einem Namensraum, wenn Sie das Präfix selbst schreiben, wie bei xlink:href oder xsi:schemaLocation.

Kann ich hier XPath gegen HTML testen?

Nur wenn das HTML wohlgeformtes XML ist, was das meiste nicht ist. Die Eingabe wird als application/xml geparst, nicht geschlossene br-Tags, Attributwerte ohne Anführungszeichen und rohe Und-Zeichen in URLs werden also abgelehnt, bevor ein Ausdruck läuft. Das ist Absicht: XPath verhält sich gegen ein HTML-DOM anders, wo Elementnamen kleingeschrieben werden und alles im XHTML-Namensraum liegt.

Gegen ein striktes XMLDocument, wie hier, sind Knotentests groß-/kleinschreibungsempfindlich und Namensräume verhalten sich wie spezifiziert. Für echtes HTML nehmen Sie einen nachsichtigen Parser: lxml.html in Python, jsoup in Java oder querySelector, wenn ein CSS-Selektor genügt. XHTML, SVG und ein bereits aufgeräumtes Fragment lassen sich hier parsen.

Verwandte Werkzeuge

Zum Weiterlesen

Fehler, die das behebt