Tester XPath

Valuta XPath e mostra ogni risultato, trappole spiegate.

Input
In attesaIncolla un documento per controllarlo. La convalida gira mentre scrivi.

Tutto viene eseguito in questa scheda. Nulla di ciò che incolli viene caricato, registrato o inviato da qualche parte. Apri il pannello di rete e verifica.

Incolla un documento, scrivi un’espressione, e ogni nodo corrispondente viene elencato con il suo tipo, il percorso che lo raggiunge (/catalog/book[2]/title) e il suo valore, con il conteggio delle corrispondenze e il tempo di valutazione sopra l’elenco. La valutazione gira sul motore del browser tramite document.evaluate, quindi questa pagina non spedisce alcuna libreria XPath e nulla di ciò che incolli esce dalla scheda.

Ci si arriva quando un’espressione sta per finire in un posto scomodo da debuggare: una regola Schematron, un pattern match XSLT, uno splitter di Camel, un selettore per lo scraping. Scoprire qui che //book non seleziona nulla costa meno che scoprirlo da un risultato vuoto e silenzioso in produzione.

La differenza sta nella gestione dei namespace. La maggior parte dei tester gratuiti passa a document.evaluate un resolver nullo: qualunque espressione con prefisso solleva un’eccezione e qualunque documento con namespace predefinito restituisce zero corrispondenze senza spiegazioni. Questo raccoglie ogni dichiarazione xmlns del documento, le associa tutte, e nomina la trappola del namespace predefinito prima di restituirti un risultato vuoto.

L’espressione che non corrisponde a nulla

È il fallimento XPath più comune, e il sintomo è indistinguibile dal fatto che l’elemento davvero non ci sia. Prendi un documento la cui radice porta xmlns="urn:books". L’elemento al suo interno non si chiama book; il suo nome espanso è {urn:books}book. XPath 1.0 non ha il concetto di namespace predefinito, quindi un nome senza prefisso significa "in nessun namespace", e //book chiede {}book. Quel nodo non esiste. Zero corrispondenze, nessun errore.

MDN lo dice senza giri di parole: in XPath non c’è modo di raccogliere il namespace predefinito così come viene applicato a un riferimento di elemento ordinario. Associa tu stesso un prefisso all’URI. Il prefisso è locale all’espressione, quindi se il documento dice xmlns:b="urn:books" puoi comunque scrivere //x:book, purché associ x.

Quando questa pagina trova un namespace predefinito gli associa il prefisso ns, così //ns:book funziona senza preparativi, e stampa la trappola sopra il risultato con dentro la tua URI reale. Il campo dei namespace accetta le tue associazioni come coppie prefisso=uri, e queste prevalgono su ciò che è stato raccolto dal documento.

  • Associa un prefisso: //ns:book/ns:title. Il più breve, e ciò che vuoi nel codice.
  • Ignora i namespace: //*[local-name()="book"]. Corrisponde a un book in qualsiasi namespace, utile quando non controlli il resolver.
  • Sii esatto senza prefisso: //*[namespace-uri()="urn:books" and local-name()="book"].
  • Gli attributi fanno eccezione. Un namespace predefinito non si applica mai ai nomi degli attributi: in <book xmlns="urn:books" id="7"/> l’elemento è {urn:books}book ma l’attributo è semplicemente {}id. Selezionalo come @id, non come @ns:id.

Barre, predicati e posizioni

/ è un passo verso un figlio. // è l’abbreviazione di /descendant-or-self::node()/, ed è per questo che /catalog/book trova solo gli elementi book direttamente sotto la radice mentre //book li trova a qualsiasi profondità. Il secondo è più tollerante e notevolmente più lento su un documento grande, perché visita ogni nodo invece dei figli di uno solo.

Gli indici dei predicati partono da 1, non da 0, quindi un’espressione che finisce con [0] non restituisce nulla in silenzio. La trappola più sottile è che un predicato si lega al suo passo e non all’intera espressione: //book[1] significa "ogni book che è il primo figlio book del proprio genitore", quindi un documento con tre cataloghi restituisce tre nodi. Per la prima corrispondenza in assoluto servono le parentesi: (//book)[1].

position() e last() sono funzioni del contesto, cioè dell’elenco di nodi prodotto dal passo corrente: book[last()] è l’ultimo book sotto ciascun genitore. Un numero nudo è l’abbreviazione di [position() = 2], ed è per questo che //book[@lang="en"][1] e //book[1][@lang="en"] sono insiemi diversi. Il primo filtra e poi ne prende uno; il secondo ne prende uno e poi filtra.

  • child:: è l’asse predefinito, quindi book e child::book sono la stessa espressione.
  • descendant:: cerca verso il basso; parent:: (..) e ancestor:: cercano verso l’alto.
  • following-sibling:: e preceding-sibling:: restano allo stesso livello: è così che si dice "il price dopo questo title".
  • attribute:: si scrive @, self:: si scrive . in un’espressione abbreviata.
  • namespace:: è nella specifica ma Firefox non lo implementa. Non costruirci sopra.

Che cosa XPath 1.0 non ha

I browser implementano XPath 1.0 e nient’altro. L’API viene da DOM Level 3 XPath, oggi una Nota W3C ritirata, e sopravvive nella sezione 8 dello standard DOM del WHATWG. Nessun browser ha la 2.0 e nessuno l’avrà, quindi questa pagina dichiara il tetto invece di reclamizzare una versione che non può fornire.

XPath 1.0 ha quattro tipi: insieme di nodi, stringa, numero, booleano. La 2.0 ha sostituito quel modello con sequenze e tipizzazione consapevole dello schema e ha portato ciò che manca di più: matches(), replace() e tokenize(), veri tipi di data, espressioni for e if. La 3.1 ha aggiunto mappe, array e la freccia =>. Tutto ciò fallisce qui, e ugualmente in DOMXPath di PHP, XPathNavigator di .NET e javax.xml.xpath di Java, che sono anch’essi 1.0. Gli espedienti che userai di più sono substring-before e substring-after al posto di tokenize, e translate() al posto di una classe di caratteri.

Leggere il risultato

Non tutte le espressioni restituiscono nodi. count(//book) restituisce un numero e string(/catalog/@id) una stringa, quindi il pannello dice quale dei quattro tipi è tornato e stampa gli scalari come valori invece che come elenco vuoto. Uno 0 scalare e un insieme di nodi vuoto si somigliano nella maggior parte degli strumenti e significano cose diverse.

Ogni corrispondenza mostra il tipo di nodo (elemento, attributo, testo, cdata, commento, istruzione di elaborazione), il percorso che la raggiunge e il suo valore: XML serializzato per un elemento, il valore dell’attributo per un attributo. Il contenuto corrispondente viene scritto nella pagina come testo e mai come markup, quindi un documento che contenga un elemento script non può eseguire nulla. Il documento deve essere ben formato prima che un’espressione venga eseguita, un prefisso non associato viene segnalato per nome accanto ai prefissi disponibili, e vengono disegnate le prime 1.000 corrispondenze mentre il conteggio sopra l’elenco è il totale reale.

Farlo da codice

Tutti e sei implementano XPath 1.0, e tutti e sei ti fanno registrare da solo i prefissi di namespace. I prefissi del documento non vengono mai recepiti automaticamente, ed è per questo che l’argomento dei namespace compare in ogni esempio.

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

Due cose si ripetono in tutti e sei. I prefissi di namespace li dichiari tu, non li fornisce il documento, e un’espressione non valida viene segnalata in modo diverso in ciascuno (una DOMException lanciata, un XPathEvalError sollevato, un false restituito, un codice di uscita 10): per questo nessuno di questi esempi chiama la query e ne usa il risultato sulla stessa riga.

Domande frequenti

Perché il mio XPath non restituisce risultati?

Di solito perché il documento ha un namespace predefinito e la tua espressione non ha prefisso. Se la radice porta xmlns="urn:something", il book al suo interno è in realtà {urn:something}book, e //book chiede un book senza namespace. Zero corrispondenze e nessun errore, perché dal punto di vista di XPath non è andato storto nulla.

Questa pagina lo rileva, nomina l’URI trovata e vi associa il prefisso ns, così //ns:book funziona subito. Se preferisci evitare i prefissi, //*[local-name()="book"] corrisponde sul nome locale a prescindere dal namespace. Le altre cause da escludere sono le maiuscole, dato che XPath le distingue, e un predicato [0] quando gli indici partono da 1.

Il mio XML viene caricato quando provo un’espressione?

No. Il controllo di buona formazione gira in un Web Worker in questa scheda e l’espressione è valutata da document.evaluate, un’API locale del browser. Qui non c’è alcun componente server a cui inviare qualcosa.

Per XPath conta più che per la maggior parte degli strumenti, perché i documenti su cui si scrivono espressioni sono payload veri e non campioni: una risposta catturata dall’API di un partner, un messaggio preso da una coda, un’asserzione SAML. Apri la scheda Rete, incolla un documento, esegui un’espressione e guardala restare vuota.

Quale versione di XPath è supportata?

XPath 1.0, perché è ciò che implementano i browser e non esiste alternativa. La valutazione passa per document.evaluate, definito nella sezione 8 dello standard DOM del WHATWG. Chrome, Firefox e Safari sono tutti 1.0 e nessuno ha annunciato l’intenzione di andare oltre.

Quindi matches(), replace(), tokenize(), le espressioni for e if, i tipi di data, le sequenze, le mappe e gli array falliscono tutti qui, come falliscono in DOMXPath di PHP, XPathNavigator di .NET e javax.xml.xpath di Java. Se ti servono 2.0 o 3.1, significa Saxon: Saxon-JS nel browser, Saxon-HE su JVM o .NET.

Qual è la differenza fra / e // in XPath?

/ seleziona un figlio diretto; // è l’abbreviazione di /descendant-or-self::node()/ e seleziona a qualsiasi profondità. Quindi /catalog/book corrisponde agli elementi book immediatamente dentro la radice catalog, mentre //book corrisponde a book ovunque. Una / iniziale ancora alla radice del documento, ed è per questo che /book fallisce su un documento la cui radice è catalog.

La trappola è combinare // con un predicato. //book[1] non significa "il primo book del documento"; il predicato si applica al passo, quindi significa "ogni book che è il primo figlio book del suo genitore", e tre cataloghi danno tre nodi. Racchiudi per ottenere ciò che intendevi: (//book)[1].

Come seleziono un attributo invece di un elemento?

Metti @ davanti al nome. //book/@id seleziona il nodo attributo id e il pannello ne mostra il valore, il percorso di appartenenza e il tipo. Per filtrare su un attributo invece di selezionarlo, mettilo in un predicato: //book[@id="b1"] seleziona elementi book, non attributi.

Gli attributi hanno una regola di namespace tutta loro, ed è quella che si sbaglia più spesso. Una dichiarazione di namespace predefinito non si applica mai ai nomi degli attributi: in <book xmlns="urn:books" id="7"/> l’attributo è semplicemente {}id, quindi selezionalo come @id, perché @ns:id non corrisponde a nulla. Un attributo sta in un namespace solo se scrivi tu il prefisso, come in xlink:href o xsi:schemaLocation.

Posso provare XPath su HTML qui?

Solo se l’HTML è XML ben formato, cosa che la maggior parte non è. L’input viene analizzato come application/xml, quindi tag br non chiusi, valori di attributo senza virgolette e e commerciali nude nelle URL vengono rifiutati prima che qualsiasi espressione venga eseguita. È voluto: XPath si comporta diversamente su un DOM HTML, dove i nomi degli elementi vengono resi minuscoli e tutto sta nel namespace XHTML.

Su un XMLDocument rigoroso, come qui, i test sui nodi distinguono le maiuscole e i namespace si comportano come da specifica. Per l’HTML del mondo reale usa un parser tollerante: lxml.html in Python, jsoup in Java, o querySelector quando basta un selettore CSS. XHTML, SVG e un frammento che hai già ripulito si analizzano qui senza problemi.

Strumenti correlati

Approfondimenti

Errori che risolve