# XML na Prática (2): Fazendo (muito) mais com menos

No [último post](https://blog.matheusyuri.pro/xml-na-pratica), vimos como lidar com arquivos ODM-XML usando algumas bibliotecas Python para tanto, fazendo o processo menos temeroso e mais simples.

```python
## Lembra dessas bibliotecas? ##
import requests
from lxml import etree #Neste caso, usamos a API da biblioteca 'ElementTree' que está disponível na biblioteca 'lxml'
import pandas as pd
```

Essas bibliotecas (especialmente o `pandas` e o `lxml.etree`) foram pensados para otimizar a construção e manipulação de conjuntos de dados contidos em um documento XML. No entanto, é possível que encontremos alguns empecilhos no caminho, que não foram abordados no artigo anterior. Aqui estão alguns:

* **O documento XML é enorme**: Anteriormente, utilizamos um arquivo cujo tamanho é pequeno; sabemos que, na vida real, as coisas podem muito bem não vir assim. É possível que caia em nossas mãos um arquivo com 1GB de tamanho para mais;
    
* **Você não precisa de todos os dados contidos no arquivo**: Para poder manipular um documento XML, bibliotecas como `lxml` e `ElementTree` precisam, pela própria natureza desse tipo de arquivo, ler e interpretar um XML do início ao fim. Por razões como o problema já mencionado acima, isso custa tempo e memória, afetando a performance do seu *script* em Python.
    
* **Você não tem à disposição uma máquina robusta**: Os dois problemas acima são muito mais difíceis de resolver quando temos uma máquina mais limitada. Usar plataformas como o [Google Colab](https://colab.research.google.com/) podem ser uma alternativa de contorno a isso, mas nem sempre podemos lançar mão desse recurso.
    

A partir disso, você pode pensar algo como “voltei à estaca zero; como é que eu vou conseguir manipular esse bendito XML, e conseguir os dados de que preciso? Será que terei de ir atrás de outra alternativa senão o Python?”

O objetivo desse artigo é mostrar que ainda não é hora de desistir do Python. Mudando algumas coisas no código, você ganhará, com certeza, mais uma chance de enfrentar os “monstros” do XML, e finalmente conseguir extrair os dados e informações tão esperados.

## Conhecendo o obstáculo

Para começar, debruçemo-nos em algumas das primeiras linhas de código que estamos utilizando:

```python
from lxml import etree
import requests

url = 'https://github.com/cdisc-org/DataExchange-ODM/raw/main/examples/Demographics_RACE/Demographics_RACE_check_all_that_apply.xml' #Fonte do arquivo .xml que estamos utilizando
response = requests.get(url) #Requisição HTTP

tree = response.content #O conteúdo da requisição
```

Esse bloco de código, como é visto acima, faz três coisas: **1)** Armazena uma `string` de um endereço URL do GitHub; **2)** Faz uma requisição a esse URL via (`requests`)[https://requests.readthedocs.io/] para obter o conteúdo do mesmo; e **3)** Armazena o conteúdo da requisição em uma variável (i.e., o arquivo XML do qual extrairemos os dados).

O próximo passo é transformar o conteúdo da variável `tree` em algo que pode ser lido e explorado pelo [`lxml`](https://lxml.de/), usando a função `XML`. Assim:

```python
tree = etree.XML(tree, etree.XMLParser(remove_comments=True))
```

```python
print(tree)
<Element {http://www.cdisc.org/ns/odm/v2.0}ODM at 0x145a22f7a80>
```

O processo de [*parsing*](https://pymotw.com/3/xml.etree.ElementTree/parse.html), que é o processo aqui realizado pelo `lxml.etree`, consiste em transformar o conteúdo que foi extraído da URL do GitHub em uma árvore ODM com todos os seus elementos. Com isso, podemos explorar o conteúdo de todo o documento XML de forma completa, uma vez que a árvore de elementos estará na memória do computador, pronta para ser utilizada...

... e é aí que pode estar o problema.

## Entendendo o tamanho do problema

Como dito anteriormente, o exemplo que estamos utilizando pode ser considerado pequeno, nos mais variados sentidos. Além do tamanho, podemos assumir que a árvore ODM que é resultado do *parsing* também não é muito grande. Transferindo o exemplo para o seu computador e executando o *script* inteiro, é possível que o processamento seja rápido e não ocupe tanta memória.

Para casos mais próximos da vida real, nem sempre é assim. Exemplificando através de uma experiência pessoal, já me encontrei na situação de ter de lidar com arquivos XML que, uma vez transformados em DataFrames do pandas, possuíam **mais de 8 milhões de linhas**. Para se ter uma ideia, uma planilha do Excel possui um limite de linhas de um pouco mais de 1 milhão. Um verdadeiro "monstro".

Acontece que, para que a árvore ODM possa ser utilizada através de um `parse` do `lxml`, ela precisa ser totalmente "destrinchada", precisando alocar na memória, além do nó raiz (que pode ser visto, neste caso, usando o atributo `tree.tag` no `lxml`), os nós filhos, ou *children*, de toda a estrutura. Essa alocação toda não só gera toda uma complexidade da coisa, mas também pode custar muita memória do computador (um exemplo de como isso tudo estaria distribuído na memória é ilustrado no diagrama abaixo).

![image](https://mermaid.ink/img/pako:eNqNk91KwzAUgF8lxNtOcJPBKgjr2v2IVdAhQutF1p6uYWlT0lSdY4_k1R5hL2b6Mw1ubEsucnq-L4eG5KxwwEPAJp4LksVoavkpUiMvZnUCpAC4fHXv63w5-p4LyXYjKEFTLglDNs0znm6_34G9aRpqtW6R5Tm5FIUsBEH2o6txq-ID72G7QU-EfmloUCG7QkPKYo6u9qij0bZG7YoOvb4UdFZInu-xkfdCGBegE6ci44O7ajY5sOsvyuWSgTpyRBkzL6JeOQ11cL4A86LT6fw3rcaEbtSJomPmoDGDQLlHTbsxe70gOG46Z5vDxux2Zyf-c3S2OT7bnJwwIQ39tA53a8BIntsQoQQSLpYW48FidyvX5dSrNHHrg4YyNtvZ541WBPWNO70KNnACIiE0VN2yKkUfyxgS8LGpwhQKKQjzsZ-ulUrUI3pepgE21esHAwtezGNsRoTl6qvIQiLBpkQ1WfKbhZBKLty6H6u2XP8A7UMU8A?type=png align="left")

## Transformando o obstáculo no caminho

A ideia aqui é, em vez de abrir toda a árvore estrutural do XML e armazenar na memória, abrir os dados sem precisar processar o arquivo, de uma forma temporária, onde nós poderemos ler os dados em sequência. Essa abordagem é especialmente recomendada quando *não precisamos de todos os dados contidos no arquivo XML*. Para tanto, adicionaremos mais uma biblioteca Python em nossa "caixa de ferramentas": [`io`](https://docs.python.org/3/library/io.html).

Observe o *snippet* abaixo:

```python
import requests
from lxml import etree 
import pandas as pd
import io

url = 'https://github.com/cdisc-org/DataExchange-ODM/raw/main/examples/Demographics_RACE/Demographics_RACE_check_all_that_apply.xml'
response = requests.get(url)
tree = response.content

tree = io.BytesIO(tree)
```

Perceba que, após armazenar o conteúdo da requisição HTTP na variável `tree` usando o `requests`, o passo seguinte é a transformação desse conteúdo usando a função [`io.BytesIO()`](https://www.digitalocean.com/community/tutorials/python-io-bytesio-stringio). Mas, afinal, o que são essas coisas?

A biblioteca `io`, que compõe a Biblioteca Padrão do Python, vem lidar com os dados brutos da maneira mais simples possível, e de forma temporária. Isso quer dizer que, ao lermos o conteúdo da requisição que foi armazenada em `tree`, faremos apenas uma leitura do conteúdo, sem torná-lo uma árvore ODM bem mais complexa e pesada. Ou seja, a função `io.BytesIO()` *somente abre o arquivo*, e o deixa em um espaço temporário da memória (um [*buffer*](https://napoleon.com.br/glossario/o-que-e-buffer-memory)) para que seja possível acessá-lo enquanto o código é executado, através de uma sequência de bytes, conforme ilustrado no diagrama abaixo.

![image](https://mermaid.ink/img/pako:eNptkU1OwzAQha9iudtk07KpkZCIsqGlYkF3DYshHjdW_RP5B4iqHokVR-jFcHCBCDEjWc9Pn5-lmSNtLUfK6N5B35Ft1RiSysfnbFRDQH_3kN2xVrsN6vOHk0C2NoAitfS9Nef3F1RPE4yU5Q1Z76ooBDpyLw2CIxxz4AT8VT4MCtNDIZViM7Ecu_DB2QOy2WKx-EuuLyRfijn_h0TDs8hnq8D7GgXRqK0bKmXbw_dfV2NPEy66fJU8dGzev11PQshtsZqm0IJqdBokT3M8jmBDQ4caG8qSNBiDA9XQxpwSCjHYx8G0lAUXsaDOxn1HmQDl0y32HALWEtL49Y-LXAbrNnlTXws7fQKEgox2?type=png align="left")

A partir disso, podemos realizar uma exploração dos dados contidos na variável `tree` através de um outro método presente no `lxml.etree`, o [`etree.iterparse`](https://lxml.de/api/lxml.etree.iterparse-class.html).

```python
for _, element in etree.iterparse(io.BytesIO(den_xml2)): #DICA: Como o io.BytesIO(den_xml2) se trata de uma sequência temporária de caracteres, é mais interessante colocá-lo dentro da iteração em vez de tentar armazená-lo numa variável.
    ...
```
A partir daqui, é possível extrair os dados de maneira semelhante ao método que utilizamos no post anterior.

## Mas qual é a diferença?

Você pode estar se perguntando sobre qual é a diferença factual vista uma vez aplicada as mudanças sugeridas. É possível fazer vários testes de performance considerando o tempo de execução e a quantidade de memória utilizada; mas, para resumir, observe o gráfico a seguir, que mostra de maneira relativa o quanto de memória é ocupada por cada função, `etree.XML()` e `io.BytesIO()`:

![image](https://i.postimg.cc/85VyCYyr/a503f45f-0ffc-4a2c-b5f3-f20a08034fb1.png align="left")

Perceba que o gráfico faz a divisão entre os dados brutos e a árvore ODM parseada (lembra dos diagramas anteriores?); uma vez que o `io.BytesIO()` apenas armazena os dados brutos, sem precisar fazer nenhum *parsing* para acessar o conteúdo, ele ocupa apenas o espaço necessário para que os bytes desses dados estejam disponíveis; já a função do `etree`, além de armazenar os dados brutos, tem de manter disponível a árvore ODM inteira, assim tendo que ocupar mais memória. Essa característica é decisiva na hora de lidarmos com arquivos XML que rompem a barreira dos 1GB de tamanho, por exemplo; se não temos uma quantidade de memória o suficiente para que caiba todos os dados do arquivo, certamente teremos inúmeros problemas para extrair o que queremos. Isso sem falar na performance: quanto maior a necessidade de memória, mais tempo para processarmos tudo, por consequência.

Ao fim e ao cabo, independentemente do tamanho do "monstro" XML com o qual você terá de lidar, ainda é possível fazer tudo que se precisa de uma maneira mais fácil e menos dolorosa usando o Python. Cabe a nós agora decidirmos qual método se adapta melhor a cada caso, e seguir estudando sobre as ferramentas e conceitos que foram abordados até agora. E assim, da próxima vez que um obstáculo enorme aparecer em seu caminho, você pode parar pra respirar, avaliar e escolher a melhor ferramenta, para que esse obstáculo se torne uma parte mais simples e - por que não? - mais divertida do caminho.
