Testador de XPath
Avalia XPath e mostra cada resultado, com as armadilhas explicadas.
Tudo roda nesta aba. Nada do que você colar é enviado, registrado ou transmitido para lugar nenhum. Abra o painel de rede e confira.
Cole um documento, digite uma expressão, e cada nó correspondente é listado com o tipo, o caminho que chega até ele (/catalog/book[2]/title) e o valor, com a contagem de correspondências e o tempo de avaliação acima da lista. A avaliação roda no próprio motor do navegador via document.evaluate, então esta página não envia nenhuma biblioteca de XPath e nada do que você colar sai da aba.
Você recorre a isto quando uma expressão está prestes a ir para um lugar difícil de depurar: uma regra Schematron, um padrão match de XSLT, um splitter do Camel, um seletor de scraping. Descobrir aqui que //book não seleciona nada sai mais barato do que descobrir por um resultado vazio e silencioso em produção.
O que é diferente é o tratamento de namespaces. A maioria dos testadores gratuitos entrega ao document.evaluate um resolvedor nulo, então qualquer expressão com prefixo lança erro e qualquer documento com namespace padrão devolve zero correspondências sem explicação. Este aqui coleta todas as declarações xmlns do documento, vincula todas, e nomeia a armadilha do namespace padrão antes de devolver um resultado vazio.
A expressão que não casa com nada
Esta é a falha mais comum de XPath, e o sintoma é indistinguível de o elemento realmente não existir. Pegue um documento cuja raiz carrega xmlns="urn:books". O elemento lá dentro não se chama book; o nome expandido dele é {urn:books}book. O XPath 1.0 não tem conceito de namespace padrão, então um nome sem prefixo significa "em nenhum namespace", e //book pede {}book. Não existe esse nó. Zero correspondências, nenhum erro.
O MDN diz sem rodeios: não há como, em XPath, captar o namespace padrão tal como aplicado a uma referência normal de elemento. Vincule você mesmo um prefixo à URI. O prefixo é local à expressão, então se o documento diz xmlns:b="urn:books" você ainda pode escrever //x:book, desde que vincule x.
Quando esta página encontra um namespace padrão, ela vincula o prefixo ns a ele, então //ns:book funciona sem preparo, e ela imprime a armadilha acima do resultado com a sua URI real dentro. A caixa de namespaces aceita as suas próprias vinculações como pares prefixo=uri, e essas têm prioridade sobre o que foi colhido do documento.
- Vincule um prefixo: //ns:book/ns:title. O mais curto, e o que você quer em código.
- Ignore namespaces: //*[local-name()="book"]. Casa com um book em qualquer namespace, e vale conhecer quando você não controla o resolvedor.
- Seja exato sem prefixo: //*[namespace-uri()="urn:books" and local-name()="book"].
- Atributos são diferentes. Um namespace padrão nunca se aplica a nomes de atributo, então em <book xmlns="urn:books" id="7"/> o elemento é {urn:books}book mas o atributo é simplesmente {}id. Selecione como @id, não como @ns:id.
Barras, predicados e posições
/ é um passo de filho. // é abreviação de /descendant-or-self::node()/, e é por isso que /catalog/book encontra só os elementos book diretamente sob a raiz enquanto //book os encontra em qualquer profundidade. O segundo é mais tolerante e consideravelmente mais lento num documento grande, porque visita todo nó em vez dos filhos de um só.
Os índices de predicado começam em 1, não em 0, então uma expressão terminada em [0] devolve nada em silêncio. A armadilha mais sutil é que um predicado se prende ao passo dele e não à expressão inteira: //book[1] quer dizer "todo book que é o primeiro filho book do próprio pai", então um documento com três catálogos devolve três nós. Para a primeira correspondência geral você precisa de parênteses: (//book)[1].
position() e last() são funções do contexto, a lista de nós que o passo atual produziu: book[last()] é o último book sob cada pai. Um número solto é abreviação de [position() = 2], e por isso //book[@lang="en"][1] e //book[1][@lang="en"] são conjuntos diferentes. O primeiro filtra e então pega um; o segundo pega um e então filtra.
- child:: é o eixo padrão, então book e child::book são a mesma expressão.
- descendant:: busca para baixo; parent:: (..) e ancestor:: buscam para cima.
- following-sibling:: e preceding-sibling:: ficam num mesmo nível, que é como se diz "o price depois deste title".
- attribute:: se escreve @, e self:: se escreve . numa expressão abreviada.
- namespace:: está na especificação mas o Firefox não implementa. Não construa em cima disso.
O que o XPath 1.0 não tem
Navegadores implementam XPath 1.0 e mais nada. A API veio do DOM Level 3 XPath, hoje uma Nota do W3C aposentada, e sobrevive na seção 8 do padrão DOM do WHATWG. Nenhum navegador tem 2.0 e nenhum vai ter, então esta página declara o teto em vez de anunciar uma versão que não consegue entregar.
O XPath 1.0 tem quatro tipos: conjunto de nós, string, número e booleano. O 2.0 trocou esse modelo por sequências e tipagem ciente de esquema e trouxe o que mais faz falta: matches(), replace() e tokenize(), tipos de data de verdade, expressões for e if. O 3.1 acrescentou mapas, arrays e a seta =>. Tudo isso falha aqui, e igualmente no DOMXPath do PHP, no XPathNavigator do .NET e no javax.xml.xpath do Java, que também são 1.0. Os contornos que você mais vai usar são substring-before e substring-after no lugar de tokenize, e translate() no lugar de uma classe de caracteres.
Lendo o resultado
Nem toda expressão devolve nós. count(//book) devolve um número e string(/catalog/@id) devolve uma string, então o painel diz qual dos quatro tipos voltou e imprime escalares como valores, não como lista vazia. Um 0 escalar e um conjunto de nós vazio se parecem na maioria das ferramentas e significam coisas diferentes.
Cada correspondência mostra o tipo de nó (elemento, atributo, texto, cdata, comentário, instrução de processamento), o caminho que chega até ela e o valor: XML serializado para um elemento, o valor do atributo para um atributo. O conteúdo correspondente é escrito na página como texto e nunca como marcação, então um documento contendo um elemento script não consegue executar nada. O documento precisa estar bem formado antes de uma expressão rodar, um prefixo não vinculado é reportado pelo nome junto com os prefixos disponíveis, e as primeiras 1.000 correspondências são renderizadas enquanto a contagem acima da lista é o total real.
Fazendo isso em código
Todos os seis implementam XPath 1.0, e todos os seis fazem você registrar os prefixos de namespace por conta própria. Os prefixos do próprio documento nunca são captados automaticamente, e é por isso que o argumento de namespaces aparece em todos os exemplos.
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.xmlDuas coisas se repetem nos seis. Os prefixos de namespace são seus para declarar, não do documento para fornecer, e uma expressão inválida é sinalizada de um jeito diferente em cada um (uma DOMException lançada, um XPathEvalError levantado, um false devolvido, um status de saída 10), e por isso nenhum destes exemplos chama a consulta e usa o resultado na mesma linha.
Perguntas frequentes
Por que meu XPath não devolve resultados?
Normalmente porque o documento tem um namespace padrão e a sua expressão não tem prefixo. Se a raiz carrega xmlns="urn:something", o book lá dentro é na verdade {urn:something}book, e //book pede um book sem namespace. Zero correspondências e nenhum erro, porque do ponto de vista do XPath nada deu errado.
Esta página detecta isso, nomeia a URI que encontrou, e vincula o prefixo ns a ela para que //ns:book funcione de cara. Se você preferir evitar prefixos, //*[local-name()="book"] casa pelo nome local independentemente do namespace. As outras causas a descartar são maiúsculas e minúsculas, já que o XPath diferencia, e um predicado [0] quando os índices começam em 1.
Meu XML é enviado quando testo uma expressão?
Não. A varredura de boa formação roda num Web Worker desta aba e a expressão é avaliada pelo document.evaluate, uma API local do navegador. Não existe componente de servidor aqui para onde mandar nada.
Isso importa mais no XPath do que na maioria das ferramentas, porque os documentos contra os quais se escrevem expressões são payloads reais, não amostras: uma resposta capturada da API de um parceiro, uma mensagem tirada de uma fila, uma asserção SAML. Abra a aba Rede, cole um documento, rode uma expressão, e veja ela continuar vazia.
Qual versão de XPath é suportada?
XPath 1.0, porque é o que os navegadores implementam e não há alternativa. A avaliação roda no document.evaluate, definido na seção 8 do padrão DOM do WHATWG. Chrome, Firefox e Safari são todos 1.0 e nenhum anunciou intenção de ir além.
Então matches(), replace(), tokenize(), expressões for e if, tipos de data, sequências, mapas e arrays falham todos aqui, como falham no DOMXPath do PHP, no XPathNavigator do .NET e no javax.xml.xpath do Java. Se você precisa de 2.0 ou 3.1, isso significa Saxon: Saxon-JS no navegador, Saxon-HE na JVM ou no .NET.
Qual a diferença entre / e // no XPath?
/ seleciona um filho direto; // é abreviação de /descendant-or-self::node()/ e seleciona em qualquer profundidade. Então /catalog/book casa com elementos book imediatamente dentro da raiz catalog, enquanto //book casa com book em qualquer lugar. Uma / inicial ancora na raiz do documento, e é por isso que /book falha num documento cuja raiz é catalog.
A armadilha é combinar // com um predicado. //book[1] não quer dizer "o primeiro book do documento"; o predicado se aplica ao passo, então quer dizer "todo book que é o primeiro filho book do seu pai", e três catálogos dão três nós. Envolva para conseguir o que você queria: (//book)[1].
Como seleciono um atributo em vez de um elemento?
Ponha @ antes do nome. //book/@id seleciona o nó atributo id e o painel mostra o valor, o caminho a que pertence e o tipo. Para filtrar por um atributo em vez de selecioná-lo, coloque-o num predicado: //book[@id="b1"] seleciona elementos book, não atributos.
Atributos têm a própria regra de namespace, e é a que as pessoas erram. Uma declaração de namespace padrão nunca se aplica a nomes de atributo, então em <book xmlns="urn:books" id="7"/> o atributo é simplesmente {}id: selecione como @id, porque @ns:id não casa com nada. Um atributo só tem namespace quando você mesmo escreve o prefixo, como em xlink:href ou xsi:schemaLocation.
Posso testar XPath contra HTML aqui?
Só se o HTML for XML bem formado, o que a maior parte não é. A entrada é analisada como application/xml, então tags br não fechadas, valores de atributo sem aspas e "e comercial" solto em URLs são rejeitados antes de qualquer expressão rodar. Isso é deliberado: o XPath se comporta de outro jeito contra um DOM de HTML, onde nomes de elemento viram minúsculas e tudo fica no namespace do XHTML.
Contra um XMLDocument estrito, como aqui, os testes de nó diferenciam maiúsculas e os namespaces se comportam conforme a especificação. Para HTML do mundo real, use um analisador tolerante: lxml.html em Python, jsoup em Java, ou querySelector quando um seletor CSS bastar. XHTML, SVG e um trecho que você já arrumou são analisados aqui sem problema.