Web scraping con BeautifulSoup#

En este script hacemos una pequeña introducción al uso de BeautifulSoup para extraer datos de una página HTML.

En ocasiones deseamos trabajar con datos que están dispersos en páginas HTML.

En este ejemplo mostramos cómo obtener datos de precios de libros del sitio web de una librería ficticia

http://books.toscrape.com/

Para cada uno de los libros en venta en este sitio, deseamos saber lo siguiente: - título - precio - disponibilidad - rating

import pandas as pd
from urllib.request import urlopen
from bs4 import BeautifulSoup
import re

URL_LIBROS = 'http://books.toscrape.com/catalogue/'

Esta funciones nos ayudará a convertir los datos a pandas

def extraer_info(articulo):
    """
    La información de cada libro viene contenida en una etiqueta "article", como en este ejemplo

    <article class="product_pod">
      <div class="image_container">
        <a href="a-light-in-the-attic_1000/index.html"><img alt="A Light in the Attic" class="thumbnail" src="../media/cache/2c/da/2cdad67c44b002e7ead0cc35693c0e8b.jpg"/>
        </a>
         <p class="star-rating Three">
           <i class="icon-star"/>
           <i class="icon-star"/>
           <i class="icon-star"/>
           <i class="icon-star"/>
           <i class="icon-star"/>
         </p>
         <h3>
           <a href="a-light-in-the-attic_1000/index.html" title="A Light in the Attic">A Light in the ...</a>
         </h3>
         <div class="product_price">
           <p class="price_color">£51.77</p>
           <p class="instock availability">
           <i class="icon-ok"/>

                        In stock

           </p>
           <form>
             <button class="btn btn-primary btn-block" data-loading-text="Adding..." type="submit">Add to basket</button>
           </form>
         </div>
       </div>
    </article>


    Esta función extrae el título, precio, disponibilidad y rating y lo devuelve como una tupla.

    """
    titulo = articulo.h3.a['title']
    precio = articulo.find('p', attrs=['price_color']).get_text()[1:]
    disponible = articulo.find('p', attrs=['instock availability']).get_text().strip()
    rating = articulo.find('p', attrs=[re.compile('star-rating ([A-Za-z]+)')])['class']
    return (titulo, precio, disponible, rating.split()[1])

def procesar_pagina(n):
    """
    Procesa la página número "n" del sitio http://books.toscrape.com/

    Devuelve un dataframe de pandas con columnas título, precio, disponibilidad y rating,
    donde cada fila corresponde a un libro.
    """

    with urlopen(URL_LIBROS + f'page-{n}.html') as archivo:
        soup = BeautifulSoup(archivo, 'xml', from_encoding='UTF-8')
        datos_crudos = [extraer_info(articulo) for articulo in soup.findAll('article')]
        return pd.DataFrame(datos_crudos, columns=('Título', 'Precio', 'Disponibilidad', 'Rating'))

Procesar toda la librería#

datos = pd.concat([procesar_pagina(n) for n in range(1, 51)], ignore_index=True)

Convertir datos de precio a tipo numérico

datos['Precio'] = datos['Precio'].astype(float)

Información de la tabla de datos

datos.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1000 entries, 0 to 999
Data columns (total 4 columns):
 #   Column          Non-Null Count  Dtype  
---  ------          --------------  -----  
 0   Título          1000 non-null   object 
 1   Precio          1000 non-null   float64
 2   Disponibilidad  1000 non-null   object 
 3   Rating          1000 non-null   object 
dtypes: float64(1), object(3)
memory usage: 31.4+ KB

Descripción de las columnas numéricas

datos.describe()
Precio
count 1000.00000
mean 35.07035
std 14.44669
min 10.00000
25% 22.10750
50% 35.98000
75% 47.45750
max 59.99000

¿Son más caros los libros mejor calificados?

print("Precio por rating", datos.groupby('Rating')['Precio'].mean(), sep='\n\n')
Precio por rating

Rating
Five     35.374490
Four     36.093296
One      34.561195
Three    34.692020
Two      34.810918
Name: Precio, dtype: float64

Convirtamos los datos de Rating a Categorías

datos['Rating'] = pd.Categorical(datos['Rating'], categories=('One', 'Two', 'Three', 'Four', 'Five'), ordered=True)

print("Precio por rating, ya ordenados", datos.groupby('Rating')['Precio'].mean(), sep='\n\n')
Precio por rating, ya ordenados

Rating
One      34.561195
Two      34.810918
Three    34.692020
Four     36.093296
Five     35.374490
Name: Precio, dtype: float64