Testeur XPath
Évalue XPath et montre chaque résultat, pièges expliqués.
Tout s'exécute dans cet onglet. Rien de ce que vous collez n'est envoyé, journalisé ni transmis où que ce soit. Ouvrez votre panneau réseau et vérifiez.
Collez un document, tapez une expression, et chaque nœud correspondant est listé avec son type, le chemin qui y mène (/catalog/book[2]/title) et sa valeur, le nombre de correspondances et le temps d’évaluation s’affichant au-dessus de la liste. L’évaluation tourne sur le moteur du navigateur via document.evaluate : cette page n’embarque donc aucune bibliothèque XPath, et rien de ce que vous collez ne quitte l’onglet.
On y vient quand une expression s’apprête à partir dans un endroit pénible à déboguer : une règle Schematron, un motif match XSLT, un splitter Camel, un sélecteur de scraping. Apprendre ici que //book ne sélectionne rien coûte moins cher que de l’apprendre d’un résultat vide et silencieux en production.
La différence tient à la gestion des espaces de noms. La plupart des testeurs gratuits passent à document.evaluate un résolveur nul : toute expression préfixée lève une erreur et tout document avec un espace de noms par défaut renvoie zéro correspondance sans explication. Celui-ci collecte chaque déclaration xmlns du document, les lie toutes, et nomme le piège de l’espace de noms par défaut avant de vous rendre un résultat vide.
L’expression qui ne correspond à rien
C’est l’échec XPath le plus courant, et son symptôme est indiscernable de l’absence réelle de l’élément. Prenez un document dont la racine porte xmlns="urn:books". L’élément qui s’y trouve ne s’appelle pas book ; son nom étendu est {urn:books}book. XPath 1.0 n’a pas la notion d’espace de noms par défaut : un nom sans préfixe signifie « dans aucun espace de noms », et //book demande {}book. Ce nœud n’existe pas. Zéro correspondance, aucune erreur.
MDN le dit sans détour : il n’existe aucun moyen, en XPath, de récupérer l’espace de noms par défaut tel qu’appliqué à une référence d’élément ordinaire. Liez vous-même un préfixe à l’URI. Le préfixe est local à l’expression : si le document dit xmlns:b="urn:books", vous pouvez tout de même écrire //x:book, à condition de lier x.
Quand cette page trouve un espace de noms par défaut, elle y lie le préfixe ns : //ns:book fonctionne donc sans préparation, et le piège est imprimé au-dessus du résultat avec votre URI réelle dedans. Le champ des espaces de noms accepte vos propres liaisons sous forme de paires préfixe=uri, et celles-ci l’emportent sur ce qui a été récolté dans le document.
- Liez un préfixe : //ns:book/ns:title. Le plus court, et ce que vous voulez en code.
- Ignorez les espaces de noms : //*[local-name()="book"]. Correspond à un book dans n’importe quel espace de noms, utile quand vous ne maîtrisez pas le résolveur.
- Soyez exact sans préfixe : //*[namespace-uri()="urn:books" and local-name()="book"].
- Les attributs diffèrent. Un espace de noms par défaut ne s’applique jamais aux noms d’attributs : dans <book xmlns="urn:books" id="7"/>, l’élément est {urn:books}book mais l’attribut est simplement {}id. Sélectionnez-le par @id, pas par @ns:id.
Barres obliques, prédicats et positions
/ est un pas vers un enfant. // est l’abréviation de /descendant-or-self::node()/, d’où le fait que /catalog/book ne trouve que les éléments book directement sous la racine tandis que //book les trouve à n’importe quelle profondeur. Le second est plus tolérant et nettement plus lent sur un gros document, car il visite chaque nœud plutôt que les enfants d’un seul.
Les indices de prédicat commencent à 1, pas à 0 : une expression finissant par [0] ne renvoie silencieusement rien. Le piège plus subtil est qu’un prédicat se rattache à son pas et non à l’expression entière : //book[1] signifie « tout book qui est le premier enfant book de son propre parent », donc un document à trois catalogues renvoie trois nœuds. Pour la première correspondance globale, il faut des parenthèses : (//book)[1].
position() et last() sont des fonctions du contexte, la liste de nœuds produite par le pas courant : book[last()] est le dernier book sous chaque parent. Un nombre nu est l’abréviation de [position() = 2], et c’est pourquoi //book[@lang="en"][1] et //book[1][@lang="en"] désignent des ensembles différents. Le premier filtre puis prend un ; le second prend un puis filtre.
- child:: est l’axe par défaut : book et child::book sont la même expression.
- descendant:: cherche vers le bas ; parent:: (..) et ancestor:: cherchent vers le haut.
- following-sibling:: et preceding-sibling:: restent à un même niveau : c’est ainsi qu’on dit « le price qui suit ce title ».
- attribute:: s’écrit @, self:: s’écrit . dans une expression abrégée.
- namespace:: figure dans la spécification mais Firefox ne l’implémente pas. N’y construisez rien.
Ce que XPath 1.0 n’a pas
Les navigateurs implémentent XPath 1.0 et rien d’autre. L’API vient de DOM Level 3 XPath, aujourd’hui une Note W3C retirée, et survit dans la section 8 du standard DOM du WHATWG. Aucun navigateur n’a la 2.0 et aucun ne l’aura : cette page annonce donc le plafond plutôt que de vendre une version qu’elle ne peut pas livrer.
XPath 1.0 a quatre types : ensemble de nœuds, chaîne, nombre, booléen. La 2.0 a remplacé ce modèle par des séquences et un typage conscient du schéma, et a apporté ce qui manque le plus : matches(), replace() et tokenize(), de vrais types de date, les expressions for et if. La 3.1 a ajouté les maps, les tableaux et la flèche =>. Tout cela échoue ici, comme dans DOMXPath de PHP, XPathNavigator de .NET et javax.xml.xpath de Java, qui sont aussi en 1.0. Les contournements les plus fréquents sont substring-before et substring-after à la place de tokenize, et translate() à la place d’une classe de caractères.
Lire le résultat
Toutes les expressions ne renvoient pas des nœuds. count(//book) renvoie un nombre et string(/catalog/@id) une chaîne : le panneau indique donc lequel des quatre types est revenu et imprime les scalaires comme valeurs plutôt que comme liste vide. Un 0 scalaire et un ensemble de nœuds vide se ressemblent dans la plupart des outils et ne veulent pas dire la même chose.
Chaque correspondance affiche son type de nœud (élément, attribut, texte, cdata, commentaire, instruction de traitement), le chemin qui y mène et sa valeur : du XML sérialisé pour un élément, la valeur de l’attribut pour un attribut. Le contenu correspondant est écrit dans la page comme du texte et jamais comme du balisage : un document contenant un élément script ne peut donc rien exécuter. Le document doit être bien formé avant qu’une expression ne s’exécute, un préfixe non lié est signalé par son nom à côté des préfixes disponibles, et les 1 000 premières correspondances sont rendues tandis que le décompte au-dessus de la liste est le total réel.
Le faire en code
Les six implémentent XPath 1.0, et les six vous font enregistrer vous-même les préfixes d’espace de noms. Les préfixes du document ne sont jamais repris automatiquement, et c’est pourquoi l’argument des espaces de noms apparaît dans chaque exemple.
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.xmlDeux choses se répètent dans les six. Les préfixes d’espace de noms sont à vous de déclarer, pas au document de fournir, et une expression invalide se signale différemment dans chacun (une DOMException levée, une XPathEvalError déclenchée, un false renvoyé, un code de sortie 10) : aucun de ces exemples n’appelle donc la requête et n’en utilise le résultat sur une seule ligne.
Questions fréquentes
Pourquoi mon XPath ne renvoie-t-il aucun résultat ?
En général parce que le document a un espace de noms par défaut et que votre expression n’a pas de préfixe. Si la racine porte xmlns="urn:something", le book qui s’y trouve est en réalité {urn:something}book, et //book demande un book sans espace de noms. Zéro correspondance et aucune erreur, car du point de vue de XPath rien ne s’est mal passé.
Cette page le détecte, nomme l’URI trouvée et y lie le préfixe ns, si bien que //ns:book fonctionne immédiatement. Si vous préférez éviter les préfixes, //*[local-name()="book"] correspond sur le nom local quel que soit l’espace de noms. Les autres causes à écarter sont la casse, XPath y étant sensible, et un prédicat [0] alors que les indices commencent à 1.
Mon XML est-il envoyé quand je teste une expression ?
Non. Le contrôle de bonne formation tourne dans un Web Worker de cet onglet et l’expression est évaluée par document.evaluate, une API locale du navigateur. Il n’y a ici aucun composant serveur à qui envoyer quoi que ce soit.
Cela compte davantage pour XPath que pour la plupart des outils, car les documents contre lesquels on écrit des expressions sont de vraies charges utiles et non des échantillons : une réponse capturée de l’API d’un partenaire, un message pris sur une file, une assertion SAML. Ouvrez l’onglet Réseau, collez un document, lancez une expression, et regardez-le rester vide.
Quelle version de XPath est prise en charge ?
XPath 1.0, parce que c’est ce que les navigateurs implémentent et qu’il n’y a pas d’alternative. L’évaluation passe par document.evaluate, défini à la section 8 du standard DOM du WHATWG. Chrome, Firefox et Safari sont tous en 1.0 et aucun n’a annoncé l’intention d’aller plus loin.
Donc matches(), replace(), tokenize(), les expressions for et if, les types de date, les séquences, les maps et les tableaux échouent tous ici, comme dans DOMXPath de PHP, XPathNavigator de .NET et javax.xml.xpath de Java. Si vous avez besoin de 2.0 ou 3.1, cela veut dire Saxon : Saxon-JS dans un navigateur, Saxon-HE sur la JVM ou .NET.
Quelle différence entre / et // en XPath ?
/ sélectionne un enfant direct ; // est l’abréviation de /descendant-or-self::node()/ et sélectionne à n’importe quelle profondeur. Ainsi /catalog/book correspond aux éléments book immédiatement dans la racine catalog, tandis que //book correspond à book n’importe où. Une / initiale ancre à la racine du document, et c’est pourquoi /book échoue sur un document dont la racine est catalog.
Le piège est de combiner // avec un prédicat. //book[1] ne veut pas dire « le premier book du document » ; le prédicat s’applique au pas, donc cela veut dire « tout book qui est le premier enfant book de son parent », et trois catalogues donnent trois nœuds. Encadrez pour obtenir ce que vous vouliez : (//book)[1].
Comment sélectionner un attribut plutôt qu’un élément ?
Mettez @ devant le nom. //book/@id sélectionne le nœud attribut id, et le panneau montre sa valeur, le chemin auquel il appartient et son type. Pour filtrer sur un attribut au lieu de le sélectionner, mettez-le dans un prédicat : //book[@id="b1"] sélectionne des éléments book, pas des attributs.
Les attributs ont leur propre règle d’espace de noms, et c’est celle qu’on se trompe le plus. Une déclaration d’espace de noms par défaut ne s’applique jamais aux noms d’attributs : dans <book xmlns="urn:books" id="7"/>, l’attribut est simplement {}id, sélectionnez-le par @id, car @ns:id ne correspond à rien. Un attribut n’est dans un espace de noms que si vous écrivez vous-même le préfixe, comme xlink:href ou xsi:schemaLocation.
Puis-je tester du XPath sur du HTML ici ?
Seulement si le HTML est du XML bien formé, ce que la plupart n’est pas. L’entrée est analysée comme application/xml : les balises br non fermées, les valeurs d’attribut sans guillemets et les esperluettes brutes dans les URL sont donc rejetées avant qu’aucune expression ne s’exécute. C’est délibéré : XPath se comporte différemment contre un DOM HTML, où les noms d’éléments sont mis en minuscules et où tout se trouve dans l’espace de noms XHTML.
Contre un XMLDocument strict, comme ici, les tests de nœud sont sensibles à la casse et les espaces de noms se comportent comme spécifié. Pour du HTML réel, utilisez plutôt un analyseur tolérant : lxml.html en Python, jsoup en Java, ou querySelector quand un sélecteur CSS suffit. XHTML, SVG et un fragment que vous avez déjà nettoyé s’analysent ici sans problème.