1.2. Datos semi-estructurados#

Introducción#

Los datos semi-estructurados poseen cierta organización, pero sin cumplir reglas estrictas como los estructurados. Ejemplos de estos datos son archivos XML, JSON, algunos archivos de logs, archivos HTML, etc.

Los datos semi-estructurados generalmente provienen de la web, logs, mails, sensores, etc.

La Web es considerada semi-estructurada ya que define un lenguage de marcado llamado HTML cuya función es describir la estructura y el contenido de una página web. Los navegadores (como Chrome, Firefox, Edge, Safari) leen HTML para saber qué mostrar y cómo organizar el contenido.

Las elementos mas destacados del lenguaje HTML son:

  • Etiquetas (Tags): Son la base de HTML. Cada etiqueta define un tipo de contenido o estructura. Ejemplo:

    • <h1>Un título</h1>

    • <p>Un párrafo</p>

  • Elementos: Pueden estar anidados e incluyen una etiqueta de apertura, contenido, y una etiqueta de cierre. El elemento <html> es considerado raiz. Debe iniciar con <html> y finalizar con </html>. Existen otros elementos como: <body>, <h1> y <p>.

  • Atributos: Proveen información adicional de los elementos. Siempre se especifican en el tag de inicio y se escriben de la forma nombre-"valor". Ejemplos:

    • <a href=”http://www.HOLA.com”> Visitar HOLA</a>

    • <img src=”mi_imagen.jpg” alt=”Es una imagen de la playa”>

Hay muchas páginas que enseñan el lenguaje HTML. Por ejemplo:

  • W3C HTML: describe todos los componentes HTML con ejemplos que se pueden ejecutar.

  • FreeCodeCamp: provee una breve introducción a los fundamentos del lenguaje HTML con ejemplos simples.

Pero nosotros no debemos hacernos expertos en el lenguaje, sólo debemos poder leerlo para extraer información útil para poder estructurarla.

Web scraping#

Un scraper es un programa que se centra en extraer datos específicos de los sitios Web y exportarlos para un almacenamiento en formato XML, Excel, JSON o SQL. Su automatización permite recopilar datos para aplicar luego análisis sobre los mismos.

Librerías a utilizar:

  • Requests: para realizar requerimientos HTTP (HTTP requests).

  • Urllib: similar a requests, pero es nativo de python y un poco menos intuituvo.

  • BeautifulSoup: para analizar (parsing) HTML y extraer los datos.

Comenzamos importando las librerías:

import requests
from bs4 import BeautifulSoup

MOMENTO CHEATSHEET

Este es el momento de recurrir a una cheatsheet que provea un resumen breve y práctico de información clave sobre esas librerías.

Uso del requests#

Vamos con un ejemplo simple de un HTML de laboratorio. Ingresar a la url Aerolineas. Podemos también inspeccionar su código HTML desde el navegador.

Comenzamos indicando el url de donde esta ubicada la página aerolineas.html

# indico sobre la URL.
url = 'https://cursoepa.fi.uncoma.edu.ar/archivos_datasets/aerolineas.html'

# los headers son necesarios para que no nos rechace la conexión.
headers = {"User-Agent": "Mozilla/5.0"}

# realizo el request
resultado = requests.get(url, headers=headers)

# Fuerza la codificación a UTF-8 (para acentos y caracteres especiales)
resultado.encoding = 'utf-8'

Podemos ver la respuesta obtenida en formato texto con resultado.text. En esa variable tenemos todo el HTML de la página en formato texto. Podemos imprimir solo algunos caracteres.

html_respuesta=resultado.text
# devuelvo el HTML como respuesta (desde el caracter 700 al 1500)
print(html_respuesta[675:1000])
  <h1>Información de Vuelos - Aerolínea Casi A Tiempo Air</h1>

  <table>
    <thead>
      <tr>
        <th data-code="nro_vue">N° de Vuelo</th>
        <th data-code="tipo_vue">Tipo de Vuelo</th>
        <th data-code="origen_vue">Origen</th>
        <th data-code="fechaS_vue">Fecha de Salida</th>
        <th data-code="h

Uso de beautifulSoup#

Para comenzar a trabajar con la librería BeautifulSoup debemos definir una variable que posea el código HTML recuperado.

Entonces, creamos una variable soup con el contenido de html_respuesta:

soup = BeautifulSoup(html_respuesta, 'lxml')
#print(soup.prettify())

Método find()

El método find() se utiliza cuando existe sólo un elemento que coincida con los criterios de búsqueda o si solo se desea el primero, ya que devuelve el primer elemento que coincida.

Ahora podemos probar obtener el elemento h1, es decir la primera etiqueta <h1> en la página HTML:

h1text = soup.find("h1")
h1text
<h1>Información de Vuelos - Aerolínea Casi A Tiempo Air</h1>

Método find_all()

El método find_all() se utiliza cuando existen varios elementos que coinciden con los criterios de búsqueda. Devuelve un ResultSet. Este tipo de datos se utiliza generalmente para las respuestas de las BDs. Vamos a usar sólo su método get() para obtener elementos dentro del mismo.

Podemos probar obtener todas las etiquetas <th> de la página HTML, e imprimimos el tipo de la respuesta:

resultado_th = soup.find_all("th")
print(type(resultado_th))   
<class 'bs4.element.ResultSet'>

Un vez que tenemos el resultado podemos realizar operaciones para manipularlo.

Primero iteramos sobre el resultado resultado_th e imprimimos cada elemento:

for elemento in resultado_th:
    print(elemento)
<th data-code="nro_vue">N° de Vuelo</th>
<th data-code="tipo_vue">Tipo de Vuelo</th>
<th data-code="origen_vue">Origen</th>
<th data-code="fechaS_vue">Fecha de Salida</th>
<th data-code="horaS_vue">Hora de Salida</th>
<th data-code="destino_vue">Destino</th>
<th data-code="fechaA_vue">Fecha de Arribo</th>
<th data-code="horaA_vue">Hora de Arrivo</th>
<th data-code="matricula_avi">Matrícula del Avión</th>
<th data-code="descripcion">Descripción del vuelo</th>

Como vemos, cada elemento es un nodo completo donde inicia y termina la etiqueta <th>…</th>.

Ahora vamos a recuperar todas las etiquetas <td> y listar sólo las primeras 10:

resultado_td = soup.find_all("td")
#imprimo solo los primeros 10 elementos donde inicio y termina un nodo <td>....</td>
print(resultado_td[0:10])
[<td data-code="nro_vue">AL0025
        </td>, <td data-code="tipo_vue">Cabotaje
        </td>, <td data-code="origen_vue">COM
        </td>, <td data-code="fechaS_vue">2024-02-16
        </td>, <td data-code="horaS_vue">21:00:00
        </td>, <td data-code="destino_vue">BUE
        </td>, <td data-code="fechaA_vue">2024-02-16
        </td>, <td data-code="horaA_vue">23:15:00
        </td>, <td data-code="matricula_avi">LV-BYY
        </td>, <td data-code="descripcion">El avíón despegó bien, pero tuvo que bajar en NQN porque el baño no funcionaba. LLegó tarde, pero poco.
      </td>]

Dataframes#

Luego, así como hicimos con los datos estructurados, debemos pasar esta información a un dataframe para poder manipularlo facilmente.

Recordando dataframes

Los dataframes son estructuras tipo tabla, donde cada columna (column) representa el índice del eje 1 (axis=1) y son de tipo Series.

Para ponerle un nombre a las columnas del dataframe debemos recorrer las etiquetas <th> (ya guardadas en resultado_th) y obtener el data-code de cada una. Luego armar una lista de nombres de columnas:

columnas=[]
for elemento in resultado_th:
    columnas.append(elemento.get("data-code"))
columnas
['nro_vue',
 'tipo_vue',
 'origen_vue',
 'fechaS_vue',
 'horaS_vue',
 'destino_vue',
 'fechaA_vue',
 'horaA_vue',
 'matricula_avi',
 'descripcion']

Primero vamos a crear un dataframe solo con las columnas (debemos importar pandas, no lo hicimos antes):

import pandas as pd

#creo un dataframe vacio con los nombres de las columnas que ya tenia
df = pd.DataFrame([], columns=columnas)
df
nro_vue tipo_vue origen_vue fechaS_vue horaS_vue destino_vue fechaA_vue horaA_vue matricula_avi descripcion

Ahora tenemos buscar y armar cada una de las filas. Para eso debemos recuperar el contenido de cada etiqueta <td>.

Volvemos a utilizar resultado_td que posee todas las etiquetas completas <td> y recuperamos su texto:

%%script echo 'No lo ejecuto aca porque es muy largo'
for texto in resultado_td:
    #imprimo el texto de cada fila <td>
    print(texto.get_text())
No lo ejecuto aca porque es muy largo

Una vez que ya podemos obtener los textos de cada fila de etiquetas <td> debemos crear las filas del dataframe. Para esto sabemos que van de 10 en 10 ya que hay 10 columnas. Entonces debemos hacer varias iteraciones.

Iteramos sobre el arreglo resultado_td nuevamente y armamos las filas con 10 valores, hasta llegar al final. Usamos también el tipo Tuple para armar el dataframe:

# el tamaño  de resultado_td me brinda las iteraciones que debo hacer
# en este caso el tamaño es de 50, lo divido en 10, con lo que debo iterar 5 veces.

#en este caso cantidad vale 50
cantidad = len(resultado_td)
#lo paso a entero porque no puede iterar sobre float
# cantidad_de_a_10/10 es igual a 5
cantidad_de_a_10 = round(cantidad/10)
#creo un tipo de dato tupla para ir guardando cada fila
mis_tuplas = []

#inicializo indice con 1 para ir armando la fila
# va a iiciar en 0 y va a terminar con el valor 50
indice = 0 

#el for lo va a hacer 5 veces
for x in range(cantidad_de_a_10):
    #cada vez que termina de hacer un fila con 10 valores, se reinicia a vacia
    fila=[]
    # el for lo hace 10 veces, una por cada columna
    for n in range(10):
        #armo la fila para luego transformar, desde 0 hasta 50
        valor = resultado_td[indice].get_text()
        #como el texto lo genera con caracteres en blanco y otras cosas, lo limpio
        #es un str, asi que puedo usar las operaciones conocidas
        #saco espacios en blanco al inicio y al final del valor
        valor = valor.strip()
        fila.append(valor)        
        #print("cada fila aca posee: " , valor)
        #sumo el indice de a 1 para ir moviendome sobre resultado_td
        indice+=1
    # aca tengo la lista completa y creo una tupla de cada una    
    mi_tupla = tuple(fila)
    # voy adicionando las tuplas a una lista de tuplas
    mis_tuplas.append(mi_tupla)  

#print(mis_tuplas)

#creo el dataframe  con los nombres de las columnas que ya tenia y las tuplas
df = pd.DataFrame(mis_tuplas, columns=columnas)
#print(df.head(20))

#creamos un .csv como hicimos con los datos estructurados, pero le agregamos la
#codificacion correcta para que reconozca los acentos
df.to_csv('archivosGenerados/datosvuelo.csv')
df
nro_vue tipo_vue origen_vue fechaS_vue horaS_vue destino_vue fechaA_vue horaA_vue matricula_avi descripcion
0 AL0025 Cabotaje COM 2024-02-16 21:00:00 BUE 2024-02-16 23:15:00 LV-BYY El avíón despegó bien, pero tuvo que bajar en ...
1 AT0334 Cabotaje NQN 2024-02-18 08:00:00 BUE 2024-02-18 10:00:00 LV-CPJ El avíón despegó con niebla y los pasajeros se...
2 AP0221 Cabotaje SAL 2024-02-15 13:20:00 NQN 2024-02-15 16:30:00 LV-GKP El avíón despegó bien, al llegar habíaviento y...
3 AT0334 Cabotaje NQN 2024-02-18 08:00:00 BUE 2024-02-18 10:00:00 LV-CPJ El avíón despegó con niebla y los pasajeros se...
4 AV2375 Cabotaje COR 2024-02-14 09:20:00 ROS 2024-02-14 10:00:00 LV-CPJ El avíón despegó con lluvia y mucho calor. Se ...

Vamos a ver otro ejemplo de características un poco diferentes, ya que el HTML es distinto.

Creamos un HTML que tiene:

html = """
<div class="Producto">
  <ul class="detalles">
    <li class="id_producto"><strong>Id:</strong> 1</li>
    <li class="nombre"><strong>Nombre:</strong> Reloj</li>
  </ul>
</div>
<div class="Producto">
  <ul class="detalles">
    <li class="id_producto"><strong>Id:</strong> 2</li>
    <li class="nombre"><strong>Nombre:</strong> Telefono</li>
  </ul>
</div>
"""

Ahora obtenemos el HTMLcreado en el formato de soup para trabajarlo:

soup = BeautifulSoup(html, 'html.parser')

Recuperamos todas las etiquetas div que tengas la class_ con el nombre de Producto:

resultado = soup.find_all('div', class_='Producto')
resultado
[<div class="Producto">
 <ul class="detalles">
 <li class="id_producto"><strong>Id:</strong> 1</li>
 <li class="nombre"><strong>Nombre:</strong> Reloj</li>
 </ul>
 </div>,
 <div class="Producto">
 <ul class="detalles">
 <li class="id_producto"><strong>Id:</strong> 2</li>
 <li class="nombre"><strong>Nombre:</strong> Telefono</li>
 </ul>
 </div>]

Armamos listas para guardar los productos y sus nombres:

# creo listas para guardar id_productos y nombre
productos, nombres = [], []

Recorremos el resultado para obtener las class internas:

for elemento in resultado:
    #buscamos la etiqueta li y el nombre de la clase, y recuperamos el valor
    idproducto = elemento.find('li', class_ = 'id_producto').text
    #como me devuelve Id:valor, tengo que sacar la palabra Id: y quedarme solo con el valor
    idproducto = idproducto.replace("Id:", "").strip()
    
    nombre = elemento.find('li', class_ = 'nombre').text 
    #como me devuelve Nombre:valor(Reloj o Telefono), tengo que sacar la palabra Nombre: 
    #y quedarme solo con el valor
    nombre = nombre.replace("Nombre:", "").strip()

    #lo voy agregando en una lista
    productos.append(idproducto)
    nombres.append(nombre)
    

print(productos)
print(nombres)
['1', '2']
['Reloj', 'Telefono']

Ahora si podemos crear un dataframe con esos datos:

df_productos = pd.DataFrame({
    'idproducto': productos,
    'Nombre': nombres
})

df_productos
idproducto Nombre
0 1 Reloj
1 2 Telefono