---
title: "Manuseio de arquivos XML com o Python"
subtitle: "Uso do módulo `lxml`"
author: "[Walmes Marques Zeviani](http://leg.ufpr.br/~walmes)"
output:
  # rmdformats::html_clean:
  rmdformats::readthedown:
    #theme: yeti
    highlight: haddock
    fig_caption: yes
    fig_align: center
---

<!-- TODO http://uribo.github.io/rpkg_showcase/index.html -->

# Usando o módulo `lxml`

## XML como string

  * <https://lxml.de/>.
  * <http://infohost.nmt.edu/tcc/help/pubs/pylxml/web/index.html>.
  * <http://stanford.edu/~mgorkove/cgi-bin/rpython_tutorials/webscraping_with_lxml.php>.
  * <https://stackabuse.com/introduction-to-the-python-lxml-library/>.
  * <https://pythontips.com/2018/06/20/an-intro-to-web-scraping-with-lxml-and-python/>.

```{python}
import see
import lxml.etree
```

  * O pacote [`xml2`] é uma interface em R para a [`libxml2`].
  * A [`libxml2`] é uma biblioteca em C.
  * Tem recursos para *parsing* de arquivos XML/HTML, acesso por meio de
    expressões XPath, dentre outros recursos.
  * É usada por diversas outras linguagens, como Python e R.
  * Para acesso ao código fonte, visite o [repositório da `libxml2`].

```{python}
text = '''<?xml version="1.0"?>
<catalog>
   <book id="bk101">
      <author>Gambardella, Matthew</author>
      <title>XML Developer\'s Guide</title>
      <genre>Computer</genre>
      <price>44.95</price>
      <publish_date>2000-10-01</publish_date>
      <description>An in-depth look at creating applications
      with XML.</description>
   </book>
   <book id="bk102">
      <author>Ralls, Kim</author>
      <title>Midnight Rain</title>
      <genre>Fantasy</genre>
      <price>5.95</price>
      <publish_date>2000-12-16</publish_date>
      <description>A former architect battles corporate zombies,
      an evil sorceress, and her own childhood to become queen
      of the world.</description>
   </book>
</catalog>'''

# Exame/análise.
doc = lxml.etree.fromstring(text)
doc

# Classe e métodos disponíveis.
type(doc)
see.see(doc)

# Funções extratoras.
xml_name(doc)
xml_length(doc)

# Ponteiros.
str(doc[[1]])

# doc.cssselect()
# doc.findall()
# doc.attrib

# Extração de conteúdo com XPath.
doc.xpath(_path = ".//book")
doc.xpath(_path = ".//author")
doc.xpath(_path = ".//author/text()")

# Extração de conteúdo por nome de tag ou caminho (Não é XPath).
u = doc.findall(path = "book")
type(u)

[node.tag for node in u]
[node.attrib for node in u]
[node.text for node in u]

# Extração dos autores.
[node.text for node in doc.xpath(_path = ".//author")]

# Extração dos preços.
[node.text for node in doc.xpath(_path = ".//price")]

help(doc.getchildren)
help(doc.xpath)
help(doc.get)
help(doc.values)

# Extração do nome dos autores por posição.
doc[0][0].text
doc[1][0].text

# Coerção para lista dentro de lista.
[list(node) for node in list(doc)]

[node[0].text for node in list(doc)]
[node[1].text for node in list(doc)]
[node.get('author') for node in list(doc)]

# .get() para extração dos atributos.
doc[0].get('id')
doc[1].get('id')

# Todos os atributos na forma de dicionário.
doc[0].attrib
doc[1].attrib

# Extrai a lista (sem nomes) dos atributos.
doc[0].values()
doc[1].values()

dir(doc)

# Pega o vizinho a direita e esquerda.
doc[0].getnext().attrib
doc[1].getprevious().attrib

doc[1][0].getnext().tag
doc[1][1].getprevious().tag
```

## Importação - catálogo botânico

```{python}
# Importação de um arquivo XML.
import requests

url = "https://www.w3schools.com/Xml/plant_catalog.xml"
txt = requests.get(url).content

doc = lxml.etree.fromstring(txt)
type(doc)

# COMMON
# PRICE
# REMOVE PRICE
#

# Extração dos nomes comuns.
[node.text for node in doc.xpath(_path = ".//COMMON")]

# Extração do preço com conversão para numérico.
[float(node.text.replace("$", "")) for node in doc.xpath(_path = ".//PRICE")]

# Elimina o preço da primeira planta.
doc[0].tag
doc[0][4].tag
doc[0].remove(doc[0][4])
doc[0][4].tag

# Extração do preço com conversão para numérico.
u = [float(node.text.replace("$", "")) for node in doc.xpath(_path = ".//PRICE")]
len(u)

# ATTENTION: A ausência do preço faltante é refletida no comprimento mas
# não sinalizada com um NaN/None ou outra coisa.

dir(doc)
len(list(doc))

[node.find("COMMON").text for node in doc.iterchildren("PLANT")]

# Isso dá erro.
# [node.find("PRICE").text for node in doc.iterchildren("PLANT")]
# --> AttributeError: 'NoneType' object has no attribute 'text'

# Função para extrair o preço com tratamento de excessão
def get_value(node, tag):
    try:
        u = node.find(tag).text
        return(u)
    except AttributeError:
        return("")

# Extração com o ausente representado por string vazia.
[get_value(node, "PRICE") for node in doc.iterchildren("PLANT")]

# Função para extrair uma lista de tags de cada elemento.
def get_values(node, tags):
    # Na forma de lista.
    # u = [get_value(node, tag) for tag in tags]
    # Na forma de dicionário.
    u = dict([(tag, get_value(node, tag)) for tag in tags])
    return(u)

[get_values(node, ["COMMON", "PRICE"])
 for node in doc.iterchildren("PLANT")]
```

## Importação - Liga da Justiça

```{python}
import os
os.getcwd()

# Importação de um arquivo XML.
url = "data/justice-league.xml"
doc = lxml.etree.parse(url)

# Cria a lista de nós apontando para os `result`.
doc_result = doc.xpath(_path = "./result")

# Extrair o atributo title, year e type.
[node.attrib.get("title") for node in doc_result]
[node.attrib.get("year") for node in doc_result]
[node.attrib.get("type") for node in doc_result]

# Extrair apenas os títulos com presença do atributo poster.
doc_result = doc.xpath(_path = "./result[@poster]")
[node.attrib.get("title") for node in doc_result]

# Extrair apenas os títulos com `type = "movie"`.
doc_result = doc.xpath(_path = "./result[@type = 'movie']")
[node.attrib.get("title") for node in doc_result]

# Que o título comece com `Lego`.
doc_result = doc.xpath(_path = "./result[starts-with(@title, 'Lego')]")
[node.attrib.get("title") for node in doc_result]

# Que o título contenha `Lego`.
doc_result = doc.xpath(_path = "./result[contains(@title, 'Lego')]")
[node.attrib.get("title") for node in doc_result]
```

## Importação - endereços

  * [Nominatim] é uma API do OpenStreetMap de busca de endereços.
  * A que é usada no código abaixo retorna informações geográficas para
    um termo de busca como latitude e longitude.

```{python}
url = "http://nominatim.openstreetmap.org/search" +\
    "?q=UFPR, Curitiba, BR&format=xml"
url

txt = requests.get(url).content

# Importação.
doc = lxml.etree.fromstring(txt)
len(doc)

# Extrair todos os atributos de cada resultado.
doc_place = doc.xpath(_path = "./place")
attr = [node.attrib for node in doc_place]
attr

len(attr)
type(attr)

attr[0].keys()
attr[0].values()

# Criar uma data frame.
import pandas as pd

attr[0]
attr[1]
attr[-1]

# FIXME? Deveria criar um DataFrame válido mas não.
# tb = pd.DataFrame(data = attr, columns = attr[0].keys())

# Determina as variáveis.
vars = set(attr[0].keys())
for d in attr[1:]:
    vars = vars.union(d.keys())

# Cria um dicionário de arrays.
dict_array = {v: [dic.get(v) for dic in attr] for v in vars}
dict_array

# Cria a tabela de dados.
tb = pd.DataFrame(data = dict_array, columns = vars)
tb.info()
tb.shape
tb.columns

# Converte para numérico.
tb.loc[:, ["lat", "lon"]] = \
    tb.loc[:, ["lat", "lon"]].apply(lambda x: x.astype("float"))

# Exibe as principais variáveis.
tb.loc[:, ["lat", "lon", "display_name"]]
```

## Importação - abalos sísmicos

  * O [U.S. Geological Survey](https://earthquake.usgs.gov) é um site
    americano que registra eventos sísmicos.
  * Ele possui um servido de API para aquisição em tempo real e consulta
    a dados históricos.

```{python}
import webbrowser as wb

# Quantos eventos sismicos no dia 02/02/2018?
# Retorna o número de eventos para no intervalo fornecido.
url = "https://earthquake.usgs.gov/fdsnws/event/1/" + \
      "count?" + \
      "format=xml&" + \
      "starttime=2018-02-02T00:00:00&" + \
      "endtime=2018-02-03T00:00:00"
# wb.open(url)

# Detalhes das ocorrências no intervalo fornecido.
url = "https://earthquake.usgs.gov/fdsnws/event/1/" + \
      "query?" + \
      "format=xml&" + \
      "starttime=2018-02-02T00:00:00&" + \
      "endtime=2018-02-03T00:00:00"
# wb.open(url)

doc = lxml.etree.parse("data/earth-quakes-2018-02-02.xml")
type(doc)
dir(doc)

# Passa o namespace como dicionário.
nsd = {"x": "http://quakeml.org/xmlns/bed/1.2"}
doc_event = doc.xpath(_path = ".//x:event", namespaces = nsd)
len(doc_event)

# Nome dos nós.
[node.tag.replace(nsd.get("x"), "") for node in doc_event[0]]

# Usando .find() e .xpath() com namespaces.
doc_event[0].find("x:origin/x:latitude/x:value",
                  namespaces = nsd).text
doc_event[0].xpath("./x:origin/x:latitude/x:value",
                   namespaces = nsd)[0].text

# Função para extrair o conteúdo os nós por expressão XPath.
def get_xpath_text(node, xpath, namespaces):
    u = node.xpath(_path = xpath, namespaces = namespaces)[0].text
    return(u)

# Extrai a latitude.
[get_xpath_text(node,
                xpath = "./x:origin/x:latitude/x:value",
                namespaces = nsd) for node in doc_event]

# Função recebe uma lista de expressões Xpath.
def get_xpaths_text(node, xpaths, namespaces):
    # u = dict([(xpath, get_xpath_text(node = node,\
    #                                  xpath = xpath,\
    #                                  namespaces = namespaces)) for\
    #           xpath in xpaths])
    u = [get_xpath_text(node = node,\
                        xpath = xpath,\
                        namespaces = namespaces) for\
         xpath in xpaths]
    return(u)

get_xpaths_text(doc_event[0],
                xpaths = ["./x:origin/x:latitude/x:value",
                          "./x:origin/x:longitude/x:value",
                          "./x:magnitude/x:mag/x:value"],
                namespaces = nsd)
```
