Usando xpath para manipular XML. (Python)
Overview
Bem-vindos de volta à nossa jornada através do mundo fascinante dos arquivos XML com Python! No segundo capítulo desta série empolgante, mergulharemos nas profundezas do XPath, explorando suas versões, usos e algumas dicas essenciais. Prepare-se para uma aventura repleta de códigos, exemplos práticos, e, claro, um pouco de humor nerd ao longo do caminho. Arregace as mangas, pois está na hora de decifrar os segredos do XPath e levar suas habilidades de programação Python a um novo patamar!
Este será o segundo post da serie “Manipulando XML com Python”. No primeiro post, demonstrei como extrair dados de um XML simples, mas pulei a explicação de como utilizar o xpath para fazer isso. Com um novo exemplo, vou mostrar como utilizar o xpath e alguns cuidados que devemos tomar.
Breve história…
De acordo com a Wikipedia, existem diversas versões do XPath em uso. A primeira veio em 1999, a segunda em 2007 (com uma revisão em 2010), a 3.0 em 2014 e a 3.1 em 2017. Todavia, a versão 1.0 ainda é a mais utilizada.
Legal, né? Existem várias versões, mas pare que serve o XPath?
Bom, ele serve para possibilitar a navegação dentro de uma árvore, que é a representação de um arquivo XML. A sintaxe que utilizamos com esta linguagem é relativamente simples. Veja o XML de exemplo:
<A>
<B>
<C/>
</B>
</A>
Para referenciar o elemento <C/>, a sintaxe XPath seria: /A/B/C (Selecione o elemento C, que está dentro de um elemento B, que por sua vez, está dentro do elemento A.)
Utilizando XPath
Ok, agora que já passamos por um contexto do que é e para que serve o XPath, vamos começar a explorar o XPath. Neste post, vou utilizar o seguinte XML de exemplo:
<?xml version="1.0"?>
<catalog>
<product description="Cardigan Sweater" product_image="cardigan.jpg">
<catalog_item gender="Men's">
<item_number>QWZ5671</item_number>
<price>39.95</price>
<size description="Medium">
<color_swatch image="red_cardigan.jpg">Red</color_swatch>
<color_swatch image="burgundy_cardigan.jpg">Burgundy</color_swatch>
</size>
<size description="Large">
<color_swatch image="red_cardigan.jpg">Red</color_swatch>
<color_swatch image="burgundy_cardigan.jpg">Burgundy</color_swatch>
</size>
</catalog_item>
<catalog_item gender="Women's">
<item_number>RRX9856</item_number>
<price>42.50</price>
<size description="Small">
<color_swatch image="red_cardigan.jpg">Red</color_swatch>
<color_swatch image="navy_cardigan.jpg">Navy</color_swatch>
<color_swatch image="burgundy_cardigan.jpg">Burgundy</color_swatch>
</size>
<size description="Medium">
<color_swatch image="red_cardigan.jpg">Red</color_swatch>
<color_swatch image="navy_cardigan.jpg">Navy</color_swatch>
<color_swatch image="burgundy_cardigan.jpg">Burgundy</color_swatch>
<color_swatch image="black_cardigan_medium.jpg">Black</color_swatch>
</size>
<size description="Large">
<color_swatch image="navy_cardigan.jpg">Navy</color_swatch>
<color_swatch image="black_cardigan_large.jpg">Black</color_swatch>
</size>
<size description="Extra Large">
<color_swatch image="burgundy_cardigan.jpg">Burgundy</color_swatch>
<color_swatch image="black_cardigan_xlarge.jpg">Black</color_swatch>
</size>
</catalog_item>
</product>
</catalog>
Ele é um mini catálogo que possui 1 produto, dividido em duas categorias (masculino e feminino), depois por tamanho e cor. Não se preocupe em copiar o XML, ao final deste post, tem o link para o Github com o exemplo completo! 🙂
Um aviso: O XML não é meu. Ele foi levemente alterado, mas sua forma original pode ser encontrada aqui, no site do autor.
Estava experimentando o Heroku e acabei fazendo um webservice que retorna estes dados (tanto em json quanto em xml), o endereço para a api é: https://raccoon-ninja-dummy-api.herokuapp.com/api/v1/catalog?&format=xml
XML parser
Para fins didaticos, neste post vou utilizar dois parsers de XML, o xml.etree e o lxml. O primeiro é mais comum (pelo menos de acordo com as minhas pesquisas), mas o segundo é mais abrangente.
Salvo engano, o xml.etree já vem com o Python 3.6, mas o LXML tem que ser instalado. Para fazer isso, no prompt, utilize o seguinte comando:
pip install lxml
Em instalações mais recentes, não deve ocorrer nenhum problema. Todavia, percebi que algumas versões mais antigas (por volta do Python 3.4) estavam apresentando problemas na hora de instalar no Windows.
Recuperando o root do XML
Independente do parser que você escolha (xml.etree ou lxml), a forma para pegar o root do arquivo XML é a mesma:
# Usando xml.etree
# from xml.etree import ElementTree as ET_xml
# OU usando o lxml
# from lxml import etree as ET_lxml
xml_filename = ".\\samples\\prod_catalog.xml"
root = ET_xml.parse(xml_filename).getroot()
No código acima seria necessário remover o comentário de uma das linhas de import, mas a sintaxe para obter o root do arquivo é o mesmo.
Agora vamos aos exemplos de utilização! Para os exemplos, considere que você possui uma variável chamada root e que ela possui a raiz do XML. (Conforme demonstrado acima.)
01: Extraindo o valor do atributo “gender” para todos os elementos.
for i, item in enumerate(root.findall("product/catalog_item")):
print("{}: {}".format(i, item.attrib["gender"]))
No exemplo acima, utilizei a sintaxe: “product/catalog_item”. Isso retornou para mim todos os elementos a partir do nível da tag <catalog_item …>. O que fiz a partir daí foi extrair o valor do atributo gender (item.attrib[“gender”]).
Este código funciona com os dois parsers.
02: Extraindo o item_number de cada catalog_item.
for i, item in enumerate(root.findall("product/catalog_item/item_number")):
print("\t{}: {}".format(i, item.text))
Neste exemplo utilizei a sintaxe: “product/catalog_item/item_number”. Ela retornou para mim todos os elementos que estão com a tag <item_number> e que estão dentro catalog_item e que são filhas da tag product.
Para extrair o conteúdo da tag, utilizei a propriedade text (item.text).
A sintaxe utilizada também funciona para os dois parsers.
03a: Buscando o valor de todos os color_swatches para itens de tamanho medium.
for i, item in enumerate(root.findall("product/catalog_item/size[@description='Medium']")):
swatches = [s.text for s in item.findall("color_swatch")]
print("\t{} Color swatches form MEDIUM: {}".format(i, ", ".join(swatches)))
Agora está começando a complicar um pouco, mas ainda está fácil. Neste exemplo, utilizei duas sintaxes:
- “product/catalog_item/size[@description=“Medium”]”: Localiza todos os elementos (tags) size onde o atributo description é igual a Medium.
- Com este resultado, mandei buscar todos os elementos color_swatch.
Apenas para título de esclarecimento: Na segunda linha, utilizei uma técnica chamada list comprehension para gerar a lista com as cores. A mesma funcionalidade pode ser reescrita assim:
swatches = list()
for swatch in item.findall("color_swatch"):
swatches.append(swatch.text)
03b: Buscando o valor de todos os color_swatches para itens de tamanho medium.
for i, item in enumerate(root.findall("product/catalog_item/size[@description='Medium']/color_swatch")):
print("\t{} Color: {}".format(i, item.text))
Achei importante dividir o exemplo três, por uma razão simples: Mais demonstrações de como utilizar o XPath.
No exemplo 3b, utilizei a seguinte sintaxe: “product/catalog_item/size[@description=“Medium”]/color_swatch”.
Esta sintaxe combina as duas do exemplo 03a. A diferença é que você tem menos controle sobre as tags. No exemplo anterior, você recebeu todas as tags size onde a descrição é igual a Medium. Depois você extraiu as tags color_swatch. Neste exemplo, fez a mesma coisa, mas “pulou” a etapa de extrair primeiro as tags size. Dependendo do que você quiser extrair, o exemplo 03a pode ser melhor.
03: Resultados
Como eu divide o exemplo 03 em dois, veja abaixo como fica o resultado de cada um deles:
Resultado do exemplo 03a:
0 Color swatches form MEDIUM: Red, Burgundy
1 Color swatches form MEDIUM: Red, Navy, Burgundy, Black
Resultado do exemplo 03b:
0 Color: Red
1 Color: Burgundy
2 Color: Red
3 Color: Navy
4 Color: Burgundy
5 Color: Black
Ambos exemplos (e sintaxes) funcionam com os dois parsers.
04: Extraindo informações das tags color_swatch, mas apenas para os sizes que contenham a palavra Large e onde a cor seja Black.
xpath_pattern = "product/catalog_item/size[contains(@description, 'Large')]/color_swatch[text()='Black']"
for i, item in enumerate(root.xpath(xpath_pattern)):
print("\t{}: Image: {}, Tag: {}, Text: {}".format(i, item.attrib["image"], item.tag, item.text))
Ok, o titulo ficou um pouco ruim, mas a ideia é essa: Buscar todas as tags (elementos) color_swatch que estejam dentro de uma tag size onde o description contem a palavra Large e que o texto da tag color_swatch seja Black.
Pelo tamanho da descrição, é possível imaginar que a sintaxe também ficou grande. A utilizada foi: “product/catalog_item/size[contains(@description, “Large”)]/color_swatch[text()=“Black”]”
Importante: Esta sintaxe não funciona com ambos parsers. Inclusive, ela não funciona com a função find ou findall (que foi utilizada nos outros exemplos). Para utilizar sintaxes com funções, comparações e operações em geral, precisamos chamar a função xpath ao invés da findall.
Para ver este exemplo todo de forma mais estruturada, acesse o meu Github. Coloquei o exemplo (incluindo o XML) lá.
Espero ter ajudado!