Manipulando XMLs com XPath no Delphi

Categoria: Artigos
Categoria Pai: Addicted 2 Delphi!
Acessos: 13672
Imagem meramente ilustrativa

Antigamente eu achava muito complexa a forma de manipulação de arquivos XML e de fato, usar apenas os métodos das classes disponíveis nas units XMLDoc, XMLDom, XMLIntf, etc., torna a manipulação desta notação extremamente maçante. Talvez um xml simples e pequeno seja facilmente manipulado desta forma, mas, quanto mais complexo (e grande) for o arquivo as chances de se perder nos nós é muito grande. Felizmente, entretanto, há alguns meses eu descobri o XPath que, para resumir, é uma forma simples de acessar nós diretamente dentro de um XML.

A partir da seleção direta de um nó é possível realizar outras operações, como adição, remoção ou atualização de filhos e atributos de cada nó utilizando os métodos existentes nas classes e interfaces de manipulação XML do Delphi.

Como é um XPath?

O XPath utiliza expressões textuais simples e lógicas para identificar com exatidão aquilo que se quer manipular dentro do XML. O XPath é como uma string de consulta, ou seja, com ele você só seleciona um nó, mas não pode alterá-lo. Veja a imagem abaixo:

A imagem acima mostra um html qualquer e como se sabe, o formato HTML é irmão do formato XML, logo, eles contém muitas semelhanças e podem ser processados com o XPath. O último tag circulado (<a>) é o alvo, isto é, o nó que queremos acessar e para isso precisamos percorrer todo o caminho de tags que foram circulados.

Pensando no XML como uma estrutura de dados que pode ser percorrida, se estivermos no nó inicial (<div id="rb_shell">), podemos fazer a listagem a partir dos filhos deste nó até chagar ao nosso destino, por isso, o XPath que seleciona o tag <a> quando estamos posicionados em <div id="rb_shell"> seria:

head/nav/ul/li[2]/div/nav/div/ul/li/a

Se você pensar no XPath como um Path no estilo UNIX você pode estar pensando o que acontece ao se usar o seguinte XPath no mesmo exemplo:

/head/nav/ul/li[2]/div/nav/div/ul/li/a

De forma análoga ao que acontece no UNIX, ao incluir uma barra no início do XPath, estamos informando que o caminho é absoluto. E ao aplicar este XPath no nosso exemplo, invariavelmente obteríamos um erro, pois o nó <head> não é o nó raiz (primeiro nó) do documento XML. Se você quiser usar um caminho absoluto no XPath, você tem que montar o caminho desde o começo do documento, a partir do primeiro nó, logo, no nosso exemplo, um XPath absoluto e válido seria:

/div/head/nav/ul/li[2]/div/nav/div/ul/li/a

Este XPath trará corretamente o nosso alvo (<a>). Em todos os exemplos deste artigo eu vou usar sempre XPaths absolutos para facilitar o aprendizado, mas mesmo nos meus testes e desenvolvimentos do mundo real eu sempre os uso desta forma, pois o entendimento fica muito melhor e dá pra saber exatamente onde se quer ir, apenas olhando o XPath!


O XML de exemplo

Todos os exemplos deste artigo vão fazer referência a um XML simpes, o qual é mostrado a seguir:

<?xml version="1.0" encoding="UTF-8"?>
<bookstore>
  <book category="cooking">
    <title lang="en">Everyday Italian</title>
    <author>Giada De Laurentiis</author>
    <year>2005</year>
    <price>30.00</price>
  </book>

  <book category="children">
    <title lang="en">Harry Potter</title>
    <author>J K. Rowling</author>
    <year>2005</year>
    <price>29.99</price>
  </book>

  <book category="web">
    <title lang="en">XQuery Kick Start</title>
    <author>James McGovern</author>
    <author>Per Bothner</author>
    <author>Kurt Cagle</author>
    <author>James Linn</author>
    <author>Vaidyanathan Nagarajan</author>
    <year>2003</year>
    <price>49.99</price>
  </book>

  <book category="web">
    <title lang="en">Learning XML</title>
    <author>Erik T. Ray</author>
    <year>2003</year>
    <price>39.95</price>
  </book>

</bookstore>

Esse XML representa os livros à venda em uma livraria (book store). Vale ressaltar que o primeiro elemento visto no arquivo (<?xml version="1.0" encoding="UTF-8"?>) não é o nó raiz. O nó raiz de um arquivo XML seria o nó que contém todos os outros nós e, no caso deste exemplo, o nó raiz seria <bookstore>.

A função que faz tudo acontecer

Admito, eu achei esta função na Internet, contudo, meu objetivo é espalhar esta informação e dar um pouco mais de detalhes a respeito do assunto. Abaixo está a função:

function SelectXMLNode(AXMLRoot: IXmlNode; const ANodePath: String): IXmlNode;
var
  DomNodeSelect : IDomNodeSelect;
  DomNode : IDomNode;
  XMLDocumentAccess : IXmlDocumentAccess;
  XMLDocument: TXmlDocument;
begin
  Result := nil;

  if not Assigned(AXMLRoot) or not Supports(AXMLRoot.DOMNode, IDomNodeSelect, DomNodeSelect) then
    Exit;

  DomNode := DomNodeSelect.selectNode(ANodePath);

  if Assigned(DomNode) then
  begin
    if Supports(AXMLRoot.OwnerDocument, IXmlDocumentAccess, XMLDocumentAccess) then
      XMLDocument := XMLDocumentAccess.DocumentObject
    else
      XMLDocument := nil;

    Result := TXmlNode.Create(DomNode, nil, XMLDocument);
  end;
end;

Esta função possui dois parâmetros de significados muito simples e diretos. AXMLRoot é o nó (IXMLNode) a partir do qual o a expressão XPath será executada. ANodePath é a expressão XPath que se quer executar. A função, claro, retorna uma referência para o nó que o XPath tenta selecionar. Com o resultado dessa função se pode aplicar uma série de outras funções específicas a fim de criar ou remover filhos do nó selecionado ou alterar seus atributos, etc. O limite é a sua imaginação!


Algumas expressões XPath

Antes de falar a respeito de algumas expressões XPath, é bom que se frise que elas servem apenas para selecionar nós, e não para modificá-los. Boa parte da complexidade em se trabalhar com arquivos XML no Delphi é decorrente da dificuldade em se posicionar corretamente no arquivo XML. Essa dificuldade é totalmente inexistente ao se usar expressões XPath para selecionar nós e, a partir daí, realizar outras operações usando métodos específicos. Em suma, XPath = seleção de nós.

O escopo deste artigo não contempla a explicação completa de todas as expressões XPath e muito menos de termos referentes ao XML, tais como, parent, children, siblings, ancestors e descendants. A fim de manter o artigo sucinto explicarei apenas as expressões básicas que abordarão a maior parte dos cenários ao se utilizar XPath no Delphi. Caso você queira aprender um pouco mais sobre XPath eu recomendo esta série de artigos.

Como foi dito antes, expressões XPath são como a representação de diretórios em sistemas UNIX com alguns elementos extras. A seguir, algumas expressões XPath muito relevantes aplicadas ao XML de exemplo:

Expressão XPath  Resultado
bookstore

Seleciona todos os elementos de nome "bookstore" que são filhos imediatos do nó atual. Caso informemos como nó atual o nó raiz do documento, informar apenas bookstore irá selecionar o elemento bookstore mais externo do documento. Como temos apenas um elemento chamado bookstore, apenas um nó será retornado por esta expressão, o nó de nível superior chamado "bookstore". Caso o nó atual não seja o nó raiz, neste exemplo, a seleção falhará

/bookstore

Seleciona o elemento de nome bookstore que é o primeiro elemento da hierarquia do XML. Em outras palavras seleciona o nó raiz de nome bookstore. Ao utilizar a barra (/) no início da expressão XPath estamos indicando que se trata de um XPath absoluto. Utilizar XPaths absolutos é altamente recomendável para selecionar nós do documento, porque isso evita que tenhamos de nos posicionar em locais específicos dentro do mesmo. A não ser que você esteja desenvolvendo uma rotina iterativa que percorra todos os nós de um XML recursivamente, utilzar um XPath absoluto é sempre a melhor opção.

OBS.: O XPath absoluto nunca considera o nó atual, isto é, se o nó atual é um elemento mais interno do XML e utilizarmos um XPath absoluto, a seleção vai acontecer sem erros considerando o XML como um todo. Em outras palavras, ao usar um XPath absoluto, o parâmetro AXMLRoot da função SelectXMLNode pode ser qualquer nó dentro do arquivo XML, portanto, este parâmetro só faz sentido quando se usa um XPath relativo, o qual será sempre relativo ao nó indicado no parâmetro AXMLRoot

/bookstore/*

Seleciona todos os nós filhos do nó de nome "bookstore". O asterisco no XPath, bem como o ponto (.) e ou duplo ponto (..) tem o mesmo significado para os paths UNIX, ou seja, o asterisco significa TODOS, o ponto simples significa o NÓ ATUAL e o duplo ponto significa o NÓ PAI. Não entraremos em maiores detalhes a respeito da utilização dos caracteres curinga ponto e duplo ponto porque eles só fazem sentido quando utilizamos XPaths relativos e como foi dito antes, o uso de XPaths relativos é desencorajado. A explicação do significado do asterisco só foi incluída porque posteriormente neste artigo usarei um artifício que utiliza este caractere curinga

/bookstore/book

Seleciona todos os nós de nome "book" que estão posicionados imediatamente abaixo do nó de nome "bookstore", o qual é o nó raiz do arquivo XML, pois trata-se de um XPath absoluto.

OBS.: No nosso exemplo existem 4 livros (book), logo, essa expressão vai retornar um array de 4 nós. A não ser que você de fato queira percorrer programaticamente estes 4 nós, é recomendável aperfeiçoar seu XPath de forma que ele indique diretamente o nó que você deseja acessar. Vaja o próximo exemplo.

/bookstore/book[2]

Seleciona o segundo nó de nome "book" que está posicionado imediatamente abaixo do nó raiz de nome "bookstore".

OBS.: Nas expressões XPath a indexação de arrays é feita considerando-se o primeiro elemento do mesmo como o elemento de índice 1, o segundo como sendo o elemento de índice 2 e assim sucessivamente. É um erro utilizar, por exemplo, /bookstore/book[0], que retornaria invariavelmente um nó nulo (nil) 

/bookstore/book[last()]

Seleciona o último nó de nome "book" que está posicionado imediatamente abaixo do nó raiz de nome "bookstore"

/bookstore/book[last() - 1]

Seleciona o penúltimo nó de nome "book" que está posicionado imediatamente abaixo do nó raiz de nome "bookstore"

/bookstore/book[position() < 3]

Seleciona os dois primeiros nós de nome "book" que estão posicionados imediatamente abaixo do nó raiz de nome "bookstore".

OBS.: Mais precisamente seleciona os nós de posição 1 e 2 dentre aqueles existentes no array. No nosso exemplo estes nós existem, pois temos 4 livros no XML. Se usássemos a expressão /bookstore/book[position() < 20], o resultado seria bem sucedido e todos os 4 livros seriam retornados

/bookstore/book[position() <= 3]

Seleciona os três primeiros nós de nome "book" que estão posicionados imediatamente abaixo do nó raiz de nome "bookstore"

/bookstore/book[position() = 3]

Seleciona apenas o terceiro nó de nome "book" que está posicionado imediatamente abaixo do nó raiz de nome "bookstore"

 /bookstore/book[@category]

Seleciona todos os nós de nome "book" que possuem um atributo de nome "category", e que estão posicionados imediatamente abaixo do nó raiz de nome "bookstore"

OBS.: Usar apenas o nome do atributo sem um valor, é o mesmo que perguntar se existe aquele atributo. Para especificar um valor é simples. Veja o próximo exemplo.

/bookstore/book[@category = "web"]

Seleciona todos os nós de nome "book" que possuem um atributo de nome "category" cujo valor é "web", e que estão posicionados imediatamente abaixo do nó raiz de nome "bookstore"

/bookstore/book[price > 35.00]

Seleciona todos os nós de nome "book" e que possuem um nó filho de nome "price" cujo valor é maior que "35.00", e que estão posicionados imediatamente abaixo do nó raiz de nome "bookstore".

OBS.: Em outras palavras essa expressão significa: Me retorne todos os livros cujo valor é maior que 35.00

/bookstore/book[price > 35.00]/title

Seleciona todos os nós de nome "title" que são filhos imediatos dos nós de nome "book" que possuem um nó filho de nome "price" cujo valor é maior que "35.00". Os nós de nome "book" precisam estar posicionados imediatamente abaixo do nó raiz de nome "bookstore"

/bookstore/book[price > 35.00]/title[@lang = "pt"]

Seleciona todos os nós de nome "title" que possuem um atributo de nome "lang", cujo valor é "pt" e que são filhos imediatos dos nós de nome "book" que possuem um nó filho de nome "price" cujo valor é maior que "35.00". Os nós de nome "book" precisam estar posicionados imediatamente abaixo do nó raiz de nome "bookstore".

OBS.: Esta expressão não vai retornar resultados porque no nosso exemplo nenhum dos atributos "lang" dos nós "title" tem o valor "pt"

/bookstore/book[1]/price/text()

Seleciona o valor do nó "price" que é filho direto do primeiro nó de nome "book", o qual, por sua vez, é filho direto do nó raiz "bookstore"

/bookstore/book[@category="web"][1]/author[3]

Seleciona o terceiro nó de nome "author" que é filho imediato do primeiro nó de nome "book" cujo atributo de nome "category" tem o valor "web" e que está posicionado imediatamente abaixo do nó raiz de nome "bookstore"

Você pode testar estas expressões no site https://www.freeformatter.com/xpath-tester.html. Basta carregar o XML de exemplo e informar a expressão XPath. A forma de funcionamento deste site é exatamente a mesma utilizada na função SelectXMLNode. Todas as expressões são executadas considerando-se que estamos posicionados no nó raiz do documento. Este site possui outros exemplos de expressões XPath, caso você queira aprender um pouco mais a respeito disso.


Utilizando a função SelectXMLNode

A utilização desta função é muito simples. Abaixo está um trecho de código com um exemplo que carrega um arquivo XML e seleciona um de seus nós:

var
  XML: IXMLDocument;
  XMLNode: IXMLNode;
begin
  XML := NewXMLDocument;
  XML.LoadFromFile('Teste.xml'); // ou XML.XML.Text := 'conteúdo do XML';
  XMLNode := SelectXMLNode(XML.DocumentElement,'/bookstore/book[@category="web"][1]/author[3]');
  ShowMessage('Valor do nó: ' + XMLNode.Text);
  ShowMessage('Nós filhos: ' + IntToStr(XMLNode.ChildNodes.Count));
  ShowMessage('Primeiro filho: ' + XMLNode.ChildNodes[0].NodeName);
end;

A recomendação que eu faço para utilização sem traumas do XPath no Delphi é que se carregue o XML desta forma, e utilize-se como primeiro parâmetro da função, o nó raiz do XML carregado, no caso do exemplo, representado por XML.DocumentElement. Fazendo isso e usando XPaths absolutos irá tornar o uso e o entendimento dos XPaths muito mais fáceis!

Um caso especial: namespaces sem prefixo

O que me motivou a aprender um pouco mais sobre XPaths foi a necessidade que eu tive de ler os arquivos de projeto do Delphi (arquivos .dproj). Estes arquivos são XMLs do MSBuild e eu precisei manipulá-los. Tudo me levava a crer que seria uma tarefa simples, mas não foi. Simplesmente os XPaths não estavam funcionando. Abaixo está um exemplo de um arquivo .dproj:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <ProjectGuid>{C52988A6-58D7-4F31-8088-D03418907848}</ProjectGuid>
        <MainSource>DPK.dpk</MainSource>
        <FrameworkType>None</FrameworkType>
        <Config Condition="'$(Config)'==''">Base</Config>
        <Platform Condition="'$(Platform)'==''">Win32</Platform>
        <TargetedPlatforms>1</TargetedPlatforms>
        <AppType>Package</AppType>
    </PropertyGroup>
    <PropertyGroup Condition="'$(Config)'=='Base' or '$(Base)'!=''" />
    <PropertyGroup Condition="'$(Base)'!=''">
		<DesignOnlyPackage>true</DesignOnlyPackage>
		<RuntimeOnlyPackage>false</RuntimeOnlyPackage>
		<DCC_OutputNeverBuildDcps>false</DCC_OutputNeverBuildDcps> <!-- false = Rebuild as Needed -->
		<DCC_BplOutput>PACKAGE_OUTPUT_DIRECTORY</DCC_BplOutput>
        <DCC_DcpOutput>DCP_OUTPUT_DIRECTORY</DCC_DcpOutput>
        <GenDll>true</GenDll>
        <DCC_SysLibRoot>SYSTEM_LIBRARY_ROOT_PATH</DCC_SysLibRoot>
        <GenPackage>true</GenPackage>
        <DllPrefix>LIB_PREFIX</DllPrefix>
        <DCC_Description>DESCRIPTION</DCC_Description>
        <DllPrefixDefined>true</DllPrefixDefined>
        <DllSuffix>LIB_SUFFIX</DllSuffix>
        <DCC_FrameworkPath>FRAMEWORK_SEARCH_PATH</DCC_FrameworkPath>
        <DCC_UnitAlias>UNIT_ALIASES</DCC_UnitAlias>
        <DCC_UnitSearchPath>SEARCH_PATH</DCC_UnitSearchPath>
        <DCC_Namespace>UNIT_SCOPE_NAMES</DCC_Namespace>
        <DllVersion>LIB_VERSION</DllVersion>
        <DCC_Define>CONDITIONAL_DEFINES</DCC_Define>
        <DCC_DcuOutput>UNIT_OUTPUT_DIRECTORY</DCC_DcuOutput>
    </PropertyGroup>
    <ProjectExtensions>
        <Borland.ProjectType>Package</Borland.ProjectType>
        <BorlandProject>
            <Delphi.Personality>
                <Source>
                    <Source Name="MainSource">DPK.dpk</Source>
                </Source>
            </Delphi.Personality>
            <Deployment/>
            <Platforms>
                <Platform value="Win32">True</Platform>
            </Platforms>
        </BorlandProject>
    </ProjectExtensions>
    <Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" Condition="Exists('$(BDS)\Bin\CodeGear.Delphi.Targets')"/>
    <Import Project="$(APPDATA)\Embarcadero\$(BDSAPPDATABASEDIR)\$(PRODUCTVERSION)\UserTools.proj" Condition="Exists('$(APPDATA)\Embarcadero\$(BDSAPPDATABASEDIR)\$(PRODUCTVERSION)\UserTools.proj')"/>
</Project>

Este XML não tem nada de especial a não ser por um detalhe que passou despercebido: ele possui um namespace sem prefixo. Não vou me aprofundar neste assunto, mas, grosso modo, um namespace é uma forma de desambiguação dentro de um XML. Usa-se um namespace para diferenciar, digamos, nós que agrupam características de uma pessoa física das características de uma pessoa jurídica. Neste exemplo, no XML haveriam dois nós "caracteristicas", um seria, por exemplo, <pf:caracteristicas> e o outro seria <pj:caracteristicas>, sendo pf e pj os prefixos (ou aliases) de dois namespaces. Veja o XML abaixo para entender melhor:

<root xmlns:pf="http://www.pessoa.fisica.org/" xmlns:pj="http://www.pessoa.juridica.org">
	<pf:caracteristicas>
		<nome>Christian Bale</nome>
		<profissao>Ator</profissao>
	</pf:caracteristicas>
	<pj:caracteristicas>
		<nome>United Artists</nome>
		<tipo>Associação</tipo>
	</pj:caracteristicas>
</root>

O XPath consegue facilmente acessar qualquer um destes nós. Para acessar o nó de características de uma pessoa jurídica, por exemplo, se usaria a expressão /root/pj:caracteristicas, mas note que o XML do arquivo .dproj possui um namespace sem prefixo algum (xmlns="http://schemas.microsoft.com/developer/msbuild/2003"). A minha primeira tentativa apenas ignorou a presença deste namespace, mas ao fazer isso eu não obtive qualquer resposta. Ao usar o XPath "/Project/PropertyGroup[1]/MainSource" para obter o nó que contém o nome do arquivo DPK (observe o XML do arquivo .dproj) nenhuma resposta foi encontrada porque não foi possível achar o XPath.

Após algumas consultas na web eu descobri que a falta de um alias (ou prefixo) para o namespace estava tornando todos os XPaths inefetivos e eu tinha duas alternativas. A primeira delas seria alterar o xml e incluir um alias, por exemplo, xmlns:ns="http://schemas.microsoft.com/developer/msbuild/2003". Fazendo isso a expressão "/ns:Project/ns:PropertyGroup[1]/ns:MainSource" funcionaria, no entanto eu descartei essa alternativa porque eu não queria um passo adicional para alterar parte do XML para só depois poder lê-lo de forma plena. Restou apenas a alternativa mais hardcore e bizarra, alterar a expressão XPath para:

/*[name()="Project"]/*[name()="PropertyGroup"][1]/*[name()="MainSource"]

Por favor, ignore as quebras de linha na expressão acima. Esta expressão estranha seleciona exatamente o que eu preciso, mas precisou ser escrita desta forma porque o XML não possui um prefixo para seu namespace. A explicação é mais simples do que parece:

Tendo em mente as 3 explicações acima, vejamos a primeira parte da expressão (as outras partes seguem o mesmo raciocínio):

/*[name()="Project"]

Esta expressão pode ser traduzida como (observe as cores e o que elas representam na expressão XPath):

Selecione todos os nós e dentre estes, retorne apenas aqueles cujo nome é "Project"

Esta forma aparentemente bizarra de XPath consegue realizar a seleção básica de nós quando o XML possui namespaces sem prefixos. É força bruta, mas funciona que é uma beleza!