XML e XPath

Boa formação, validação, namespaces, árvores, caminhos XPath, predicados e funções.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

O catálogo pode representar cada livro com um elemento livro, um atributo id e os filhos titulo e preco. XML permite definir esse vocabulário; XPath percorre a árvore para selecionar livros ou calcular valores. SVG, XHTML e MathML também usam vocabulários XML, com regras próprias.

Documento bem formado

<?xml version="1.0" encoding="UTF-8"?>
<catalogo>
  <livro id="r1">
    <titulo>Redes</titulo>
    <preco>18</preco>
  </livro>
  <livro id="a1">
    <titulo>Algoritmos</titulo>
    <preco>25</preco>
  </livro>
  <livro id="s1">
    <titulo>SQL</titulo>
    <preco>12</preco>
  </livro>
</catalogo>

Existe um único elemento raiz, catalogo. As etiquetas fecham pela ordem inversa à abertura, os nomes distinguem maiúsculas e os valores de atributos usam aspas. <livro id=r1> não é bem formado; <Titulo>Redes</titulo> também não. Elementos vazios podem usar <aviso/>.

O nó raiz do documento contém o elemento catalogo; não são o mesmo nó. Para o primeiro livro, o percurso até ao texto do título é:

Documento
└── catalogo
    └── livro  [atributo id="r1"]
        ├── titulo
        │   └── texto "Redes"
        └── preco
            └── texto "18"

A árvore omite os outros livros e os nós de texto da indentação. O atributo id pertence ao elemento, mas não é um filho no eixo child de XPath. Por isso usamos @id para o selecionar e titulo/text() para chegar ao texto Redes.

&amp; representa &, e &lt; representa < em texto. Uma secção <![CDATA[...]]> permite texto que não é interpretado como marcação, mas não pode conter o seu próprio terminador ]]>. Comentários não podem conter -- no interior.

Boa formação e validade

Boa formação verifica as regras sintáticas do XML. Validade exige também cumprir um contrato, como DTD, XML Schema ou Relax NG. Um documento com <preco>caro</preco> pode ser bem formado e falhar um esquema que exija número.

Uma DTD possível para o catálogo, sem repetir aqui o documento inteiro:

<!ELEMENT catalogo (livro*)>
<!ELEMENT livro (titulo, preco)>
<!ATTLIST livro id ID #REQUIRED>
<!ELEMENT titulo (#PCDATA)>
<!ELEMENT preco (#PCDATA)>

livro* permite zero ou mais livros. (titulo, preco) exige os dois nessa ordem. O atributo id é obrigatório, com unicidade e formato de ID. #PCDATA admite texto, mas esta DTD não exige que o preço seja numérico. Um XSD pode acrescentar tipos, como xs:decimal, e restrições. Uma DTD não é uma base de dados nem substitui a verificação de regras de negócio.

Ao processar XML não confiável, não atives resolução arbitrária de entidades externas nem carregamento de recursos do cliente. Esses mecanismos podem expor ficheiros ou fazer pedidos do servidor. O formato estar bem formado não o torna seguro.

Caminhos XPath

Vamos usar XPath 1.0, como no DOMXPath de PHP. O contexto é o nó a partir do qual uma expressão é avaliada. Resolve um caminho por etapas: parte da raiz, escolhe os livro filhos de catalogo, filtra os que têm preço abaixo de 20 e só depois escolhe titulo. Cada predicado filtra o passo onde aparece, antes de avançar. A tabela seguinte resume os casos.

ExpressãoResultado sobre o catálogo
/catalogo/livroOs três elementos livro
/catalogo/livro/tituloOs três elementos titulo
/catalogo/livro/titulo/text()Os seus nós de texto
//livroLivros descendentes do documento, em qualquer profundidade
/catalogo/livro/@idAtributos id dos livros
/catalogo/livro[@id='s1']/tituloO elemento cujo texto é SQL
/catalogo/livro[preco < 20]/tituloTítulos Redes e SQL
count(/catalogo/livro)O número 3
sum(/catalogo/livro/preco)O número 55

/ no início fixa o nó raiz do documento. Entre passos, / liga o eixo de filhos habitual. // abrevia um percurso por descendentes, não apenas filhos diretos. . representa o contexto e .. o pai. @ seleciona atributos; * seleciona elementos com qualquer nome nesse passo.

Predicados e posição

Um predicado entre [] filtra os candidatos do passo em que aparece. [preco < 20] avalia o preço de cada livro candidato. [2] equivale a uma condição sobre a posição, que começa em 1.

/catalogo/livro[2]/titulo
/catalogo/livro[preco < 20][1]/titulo
/catalogo/livro[1][preco < 20]/titulo

A primeira dá Algoritmos. A segunda primeiro filtra baratos e depois escolhe o primeiro, Redes. A terceira escolhe o primeiro livro e só depois verifica o preço. Neste catálogo também dá Redes, mas não daria o mesmo se o primeiro livro custasse 30. A ordem dos filtros faz parte da pergunta.

//livro[1] escolhe livros que são o primeiro livro entre os irmãos em cada grupo relevante. (//livro)[1] escolhe um único primeiro livro do conjunto global. Quando só há um catalogo, podem coincidir; com várias secções, a diferença torna-se visível.

Os eixos explicitam a relação: child:: é o percurso habitual, descendant:: desce vários níveis, ancestor:: sobe e following-sibling:: segue irmãos. //livro abrevia descendant-or-self::node()/child::livro. Podemos combinar condições com and, or e not. contains(titulo, 'Red') procura texto contido; starts-with verifica prefixo; normalize-space remove espaços exteriores e reduz sequências de espaços. last() usa a última posição. | une conjuntos de nós.

Com vários nós, compara existência e ausência com cuidado. Sobre <livro><categoria>redes</categoria><categoria>web</categoria></livro>, categoria = 'web' é verdadeiro, porque existe uma categoria web. Mas categoria != 'web' também é verdadeiro, porque existe uma categoria diferente.

Para pedir “nenhuma categoria é web”, nega a igualdade completa com not(categoria = 'web'). A comparação com vários nós é verdadeira quando algum nó a satisfaz, sem exigir todos.

Executar em PHP

O exemplo completo carrega XML de uma string, consulta e conta:

Consultar o catálogo com XPath
<?php
$documento = new DOMDocument();
$documento->loadXML('<catalogo><livro id="r1"><titulo>Redes</titulo><preco>18</preco></livro><livro id="a1"><titulo>Algoritmos</titulo><preco>25</preco></livro><livro id="s1"><titulo>SQL</titulo><preco>12</preco></livro></catalogo>', LIBXML_NONET);
$xpath = new DOMXPath($documento);
foreach ($xpath->query('/catalogo/livro[preco < 20]/titulo') as $titulo) {
    echo $titulo->textContent, "
";
}
echo 'Total: ', $xpath->evaluate('count(/catalogo/livro)'), "
";
Resultado

Executa o código para ver a saída e os gráficos.

Obténs Redes, SQL e Total: 3.

loadXML constrói a árvore a partir de texto. query devolve nós; evaluate também serve para resultados escalares, como count. No navegador, document.evaluate avalia XPath com um nó de contexto e um tipo de resultado pedido. Para percorrer nós por ordem do documento, pede XPathResult.ORDERED_NODE_ITERATOR_TYPE e lê-os com iterateNext() até devolver null. Para obter o texto dos elementos escolhidos, usa textContent. Não confundas um conjunto vazio com a string vazia de um elemento existente.

Namespaces

Um namespace distingue vocabulários através de um URI. O prefixo é uma abreviatura local, não a identidade definitiva do nome:

<c:catalogo xmlns:c="urn:biblioteca">
  <c:livro id="r1"><c:titulo>Redes</c:titulo></c:livro>
</c:catalogo>

c:livro pertence ao namespace urn:biblioteca. Outro documento pode usar prefixo b para o mesmo URI, sem mudar esse nome expandido. Declarar xmlns="urn:biblioteca" coloca elementos sem prefixo nesse namespace. A declaração por defeito não coloca automaticamente atributos sem prefixo no namespace.

Em XPath 1.0, um nome sem prefixo procura elementos sem namespace. Para consultar um documento com namespace por defeito, regista um prefixo na consulta:

$xpath->registerNamespace('b', 'urn:biblioteca');
$livros = $xpath->query('/b:catalogo/b:livro');

O prefixo b da consulta não precisa de ser o c do documento. Precisa de apontar para o mesmo URI. O URI identifica o namespace; não implica que o programa deva descarregar algo desse endereço.

Tecnologias relacionadas

XPath seleciona; XSLT transforma XML, por exemplo em HTML ou noutro XML; XQuery consulta e constrói resultados; XSL-FO descreve formatação de documentos, frequentemente para PDF. SVG é um vocabulário XML para gráficos.

Um documento XML não ganha validade, namespaces ou um esquema só por usar etiquetas; é o formato e o processamento escolhidos que dão essas propriedades. JSON também pode ter contratos de validação, por isso a escolha não se reduz a “XML validado, JSON sem regras”.

Exercícios

Distinguir primeiro local e global

Um documento tem duas secções, cada uma com dois livros. Na ordem do documento, os IDs são a,b na primeira e c,d na segunda. Compara //livro[1] e (//livro)[1].

Pista

O predicado do passo usa a posição no grupo desse percurso.

Solução

//livro[1] escolhe a e c, primeiros livros entre os irmãos em cada secção. (//livro)[1] escolhe apenas a, primeiro do conjunto global de livros. Os índices começam em 1, não em 0. Sem várias secções, o exemplo poderia esconder a diferença.

Confere a tua resposta:

  • Obtive a/c no caso local.
  • Obtive só a no conjunto global.

Erros frequentes

Não assumir que todos os usos de [1] significam um único nó no documento.

Voltar à explicação
Consultar um namespace por defeito

Um documento começa com <catalogo xmlns="urn:livros"><livro id="a"/></catalogo>. Porque /catalogo/livro não encontra o livro em XPath 1.0? Como o consultas e a que namespace pertence id?

Pista

Nomes sem prefixo em XPath 1.0 referem o namespace vazio.

Mais uma pista

Regista um prefixo para urn:livros e usa-o nos dois passos.

Solução

Regista b -> urn:livros e consulta /b:catalogo/b:livro. O prefixo é escolhido pela consulta, independentemente de o documento não ter prefixos. O atributo id sem prefixo está no namespace vazio, porque o namespace por defeito se aplica aos elementos e não automaticamente aos atributos.

Confere a tua resposta:

  • Associei prefixo ao URI na consulta.
  • Mantive id sem namespace.

Erros frequentes

Não tentar descarregar urn:livros nem usar o prefixo como identidade do namespace.

Voltar à explicação
Distinguir boa formação e validade

Uma DTD exige livro com filhos titulo e preco, nessa ordem, ambos #PCDATA. O documento contém <livro><preco>caro</preco><titulo>Redes</titulo></livro>. É bem formado? É válido nessa DTD? Trocar a ordem obriga preco a ser numérico?

Pista

Boa formação não verifica o modelo da DTD.

Mais uma pista

PCDATA aceita texto, não declara um tipo decimal.

Solução

O fragmento está bem formado se fizer parte de um documento com raiz única e restantes regras sintáticas respeitadas. Não é válido nessa DTD porque a ordem dos filhos está trocada. Corrigir para titulo seguido de preco resolve essa restrição, mas caro continua admitido como texto #PCDATA. Um esquema com tipo numérico ou validação do programa deve impor o formato pretendido.

Confere a tua resposta:

  • Separei sintaxe e ordem do contrato.
  • Expliquei o limite de #PCDATA.

Erros frequentes

Não confundir etiquetas equilibradas com validade nem assumir que o nome preco define o tipo.

Voltar à explicação
Aplicar filtros pela ordem certa

Usa este documento para comparar /catalogo/livro[preco < 20][1]/@id e /catalogo/livro[1][preco < 20]/@id. No primeiro livro, avalia também categoria = 'web', categoria != 'web' e not(categoria = 'web').

<catalogo>
  <livro id="a"><preco>30</preco><categoria>web</categoria><categoria>redes</categoria></livro>
  <livro id="b"><preco>12</preco><categoria>web</categoria></livro>
</catalogo>

Pista

Cada predicado atua sobre o resultado do passo anterior.

Mais uma pista

Uma comparação com um conjunto de nós é verdadeira quando existe uma comparação que a satisfaça.

Solução

O primeiro caminho aplica os predicados por ordem: filtra livros abaixo de 20 e fica apenas b; depois [1] escolhe o primeiro desse conjunto, que é b. O segundo escolhe primeiro o livro na posição 1, que é a; depois aplica [preco < 20]; como a custa 30, o conjunto fica vazio e nenhum atributo é selecionado. Em a, a igualdade é true pela categoria web e a desigualdade é true pela categoria redes. A negação da igualdade é false, porque existe web. Para excluir qualquer livro com web, usa [not(categoria = 'web')].

Confere a tua resposta:

  • Obtive b e conjunto vazio nos dois caminhos.
  • Obtive true, true e false nas condições.

Erros frequentes

Não trocar livremente a ordem dos predicados nem usar != como negação de uma igualdade entre conjunto de nós e string.

Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.