Guía para analizar HTML con BeautifulSoup en Python

Este artículo le brindará un curso intensivo sobre web scraping en Python con Beautiful Soup, una popular biblioteca de Python para analizar HTML y XML.

Introducción

El raspado web recopila información mediante programación de varios sitios web. Si bien hay muchas bibliotecas y marcos en varios idiomas que pueden extraer datos web, Python ha sido durante mucho tiempo una opción popular debido a su gran cantidad de opciones para el web scraping.

Este artículo le brindará un curso intensivo sobre web scraping en Python con Beautiful Soup, una popular biblioteca de Python para analizar HTML y XML.

Web Scraping ético

El web scraping es omnipresente y nos brinda datos como los obtendríamos con una API. Sin embargo, como buenos ciudadanos de Internet, es nuestra responsabilidad respetar a los propietarios de los sitios de los que raspamos. Aquí hay algunos principios a los que debe adherirse un raspador web:

  • No reclamar el contenido raspado como propio. Los propietarios de sitios web a veces dedican mucho tiempo a crear artículos, recopilar detalles sobre productos o recolectar otro contenido. Debemos respetar su labor y originalidad.
  • No elimine un sitio web que no quiera ser eliminado. Los sitios web a veces vienen con un archivo robots.txt, que define las partes de un sitio web que se pueden raspar. Muchos sitios web también tienen Términos de uso que pueden no permitir el raspado. Debemos respetar los sitios web que no quieren ser raspados.
  • ¿Ya hay una API disponible? Espléndido, no hay necesidad de que escribamos un raspador. Las API se crean para proporcionar acceso a los datos de forma controlada según lo definido por los propietarios de los datos. Preferimos usar API si están disponibles.
  • Hacer solicitudes a un sitio web puede afectar el rendimiento de un sitio web. Un raspador web que realiza demasiadas solicitudes puede ser tan debilitante como un ataque DDOS. Debemos raspar de manera responsable para no causar ninguna interrupción en el funcionamiento normal del sitio web.

Resumen de Beautiful Soup

El contenido HTML de las páginas web se puede analizar y raspar con Beautiful Soup. En la siguiente sección, cubriremos aquellas funciones que son útiles para raspar páginas web.

Lo que hace que Beautiful Soup sea tan útil es la miríada de funciones que proporciona para extraer datos de HTML. Esta imagen a continuación ilustra algunas de las funciones que podemos usar:

BeautifulSoup - Resumen

Pongámonos manos a la obra y veamos cómo podemos analizar HTML con Beautiful Soup. Considere la siguiente página HTML guardada en un archivo como doc.html:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<html>
<head>
  <title>Head's title</title>
</head>

<body>
  <p class="title"><b>Body's title</b></p>
  <p class="story">line begins
    <a href="http://example.com/element1" class="element" id="link1">1</a>
    <a href="http://example.com/element2" class="element" id="link2">2</a>
    <a href="http://example.com/avatar1" class="avatar" id="link3">3</a>
  <p> line ends</p>
</body>
</html>

Los siguientes fragmentos de código se prueban en Ubuntu 20.04.1 LTS. Puede instalar el módulo BeautifulSoup escribiendo el siguiente comando en la terminal:

1
$ pip3 install beautifulsoup4

Es necesario preparar el archivo HTML doc.html. Esto se hace pasando el archivo al constructor BeautifulSoup, usemos el shell interactivo de Python para esto, para que podamos imprimir instantáneamente el contenido de una parte específica de una página:

1
2
3
4
from bs4 import BeautifulSoup

with open("doc.html") as fp:
    soup = BeautifulSoup(fp, "html.parser")

Ahora podemos usar Beautiful Soup para navegar por nuestro sitio web y extraer datos.

Desde el objeto de sopa creado en la sección anterior, obtengamos la etiqueta de título de doc.html:

1
soup.head.title   # returns <title>Head's title</title>

Aquí hay un desglose de cada componente que usamos para obtener el título:

Navegando etiquetas específicas

Beautiful Soup es poderoso porque nuestros objetos de Python coinciden con la estructura anidada del documento HTML que estamos raspando.

Para obtener el texto de la primera etiqueta <a>, ingrese esto:

1
soup.body.a.text  # returns '1'

Para obtener el título dentro de la etiqueta del cuerpo del HTML (indicado por la clase "título"), escriba lo siguiente en su terminal:

1
soup.body.p.b     # returns <b>Body's title</b>

Para documentos HTML profundamente anidados, la navegación podría volverse tediosa rápidamente. Afortunadamente, Beautiful Soup viene con una función de búsqueda para que no tengamos que navegar para recuperar elementos HTML.

Búsqueda de elementos de etiquetas

El método find_all() toma una etiqueta HTML como argumento de cadena y devuelve la lista de elementos que coinciden con la etiqueta proporcionada. Por ejemplo, si queremos todas las etiquetas a en doc.html:

1
soup.find_all("a")

Veremos esta lista de etiquetas a como salida:

1
[<a class="element" href="http://example.com/element1" id="link1">1</a>, <a class="element" href="http://example.com/element2" id="link2">2</a>, <a class="element" href="http://example.com/element3" id="link3">3</a>]

Aquí hay un desglose de cada componente que usamos para buscar una etiqueta:

Buscando Elementos de Etiquetas

También podemos buscar etiquetas de una clase específica proporcionando el argumento class_. Beautiful Soup usa class_ porque class es una palabra clave reservada en Python. Busquemos todas las etiquetas a que tengan la clase "elemento":

1
soup.find_all("a", class_="element")

Como solo tenemos dos enlaces con la clase "elemento", verás este resultado:

1
[<a class="element" href="http://example.com/element1" id="link1">1</a>, <a class="element" href="http://example.com/element2" id="link2">2</a>]

¿Qué pasaría si quisiéramos obtener los enlaces incrustados dentro de las etiquetas a? Recuperemos el atributo href de un enlace usando la opción find(). Funciona como find_all() pero devuelve el primer elemento coincidente en lugar de una lista. Escriba esto en su shell:

1
soup.find("a", href=True)["href"] # returns http://example.com/element1

Las funciones find() y find_all() también aceptan una expresión regular en lugar de una cadena. Detrás de escena, el texto se filtrará usando el método search() de la expresión regular compilada. Por ejemplo:

1
2
3
4
import re

for tag in soup.find_all(re.compile("^b")):
    print(tag)

La lista tras la iteración obtiene las etiquetas que comienzan con el carácter b, que incluye <cuerpo> y <b>:

1
2
3
4
5
6
7
8
9
<body>
 <p class="title"><b>Body's title</b></p>
 <p class="story">line begins
       <a class="element" href="http://example.com/element1" id="link1">1</a>
 <a class="element" href="http://example.com/element2" id="link2">2</a>
 <a class="element" href="http://example.com/element3" id="link3">3</a>
 <p> line ends</p>
 </p></body>
 <b>Body's title</b>

Hemos cubierto las formas más populares de obtener etiquetas y sus atributos. A veces, especialmente para páginas web menos dinámicas, solo queremos el texto. ¡Veamos cómo podemos conseguirlo!

Obtener el texto completo

La función get_text() recupera todo el texto del documento HTML. Obtengamos todo el texto del documento HTML:

1
soup.get_text()

Tu salida debería ser así:

1
2
3
4
5
6
7
8
9
Head's title


Body's title
line begins
      1
2
3
 line ends

A veces, los caracteres de nueva línea se imprimen, por lo que su salida también puede verse así:

1
"\n\nHead's title\n\n\nBody's title\nline begins\n    1\n2\n3\n line ends\n\n"

Ahora que tenemos una idea de cómo usar Beautiful Soup, ¡hagamos un sitio web!

Beautiful Soup en acción: raspar una lista de libros

Ahora que hemos dominado los componentes de Beautiful Soup, es hora de poner en práctica nuestro aprendizaje. Construyamos un raspador para extraer datos de https://books.toscrape.com/ y guardarlos en un archivo CSV. El sitio contiene datos aleatorios sobre libros y es un gran espacio para probar sus técnicas de web scraping.

Primero, cree un nuevo archivo llamado scraper.py. Importemos todas las bibliotecas que necesitamos para este script:

1
2
3
4
5
import requests
import time
import csv
import re
from bs4 import BeautifulSoup

En los módulos mencionados anteriormente:

  • requests - realiza la solicitud de URL y obtiene el HTML del sitio web
  • time - limita cuántas veces raspamos la página a la vez
  • csv - nos ayuda a exportar nuestros datos raspados a un archivo CSV
  • re - nos permite escribir expresiones regulares que serán útiles para seleccionar texto en función de su patrón
  • bs4 - Atentamente, el módulo de raspado para analizar el HTML

Tendría bs4 ya instalado, y time, csv y re son paquetes integrados en Python. Deberá instalar el módulo requests directamente de esta manera:

1
$ pip3 install requests

Antes de comenzar, debe comprender cómo está estructurado el HTML de la página web. En su navegador, vayamos a http://books.toscrape.com/catalogue/page-1.html. Luego, haga clic con el botón derecho en los componentes de la página web que desea raspar y haga clic en el botón inspeccionar para comprender la jerarquía de las etiquetas, como se muestra a continuación.

Esto le mostrará el HTML subyacente de lo que está inspeccionando. La siguiente imagen ilustra estos pasos:

Entendiendo las etiquetas HTML

Al inspeccionar el HTML, aprendemos cómo acceder a la URL del libro, la imagen de portada, el título, la calificación, el precio y más campos del HTML. Escribamos una función que raspe un elemento de libro y extraiga sus datos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def scrape(source_url, soup):  # Takes the driver and the subdomain for concats as params
    # Find the elements of the article tag
    books = soup.find_all("article", class_="product_pod")

    # Iterate over each book article tag
    for each_book in books:
        info_url = source_url+"/"+each_book.h3.find("a")["href"]
        cover_url = source_url+"/catalogue" + \
            each_book.a.img["src"].replace("..", "")

        title = each_book.h3.find("a")["title"]
        rating = each_book.find("p", class_="star-rating")["class"][1]
        # can also be written as : each_book.h3.find("a").get("title")
        price = each_book.find("p", class_="price_color").text.strip().encode(
            "ascii", "ignore").decode("ascii")
        availability = each_book.find(
            "p", class_="instock availability").text.strip()

        # Invoke the write_to_csv function
        write_to_csv([info_url, cover_url, title, rating, price, availability])

La última línea del fragmento anterior apunta a una función para escribir la lista de cadenas extraídas en un archivo CSV. Agreguemos esa función ahora:

1
2
3
4
5
6
7
8
def write_to_csv(list_input):
    # The scraped info will be written to a CSV here.
    try:
        with open("allBooks.csv", "a") as fopen:  # Open the csv file.
            csv_writer = csv.writer(fopen)
            csv_writer.writerow(list_input)
    except:
        return False

Como tenemos una función que puede raspar una página y exportarla a CSV, queremos otra función que rastree el sitio web paginado, recopilando datos de libros en cada página.

Para hacer esto, echemos un vistazo a la URL para la que estamos escribiendo este raspador:

1
"http://books.toscrape.com/catalogue/page-1.html"

El único elemento variable en la URL es el número de página. Podemos formatear la URL dinámicamente para que se convierta en una URL inicial:

1
"http://books.toscrape.com/catalogue/page-{}.html".format(str(page_number))

Esta URL con formato de cadena con el número de página se puede obtener mediante el método requests.get(). Entonces podemos crear un nuevo objeto BeautifulSoup. Cada vez que obtenemos el objeto sopa, se comprueba la presencia del botón "siguiente" para que podamos detenernos en la última página. Realizamos un seguimiento de un contador para el número de página que se incrementa en 1 después de raspar con éxito una página.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def browse_and_scrape(seed_url, page_number=1):
    # Fetch the URL - We will be using this to append to images and info routes
    url_pat = re.compile(r"(http://.*\.com)")
    source_url = url_pat.search(seed_url).group(0)

   # Page_number from the argument gets formatted in the URL & Fetched
    formatted_url = seed_url.format(str(page_number))

    try:
        html_text = requests.get(formatted_url).text
        # Prepare the soup
        soup = BeautifulSoup(html_text, "html.parser")
        print(f"Now Scraping - {formatted_url}")

        # This if clause stops the script when it hits an empty page
        if soup.find("li", class_="next") != None:
            scrape(source_url, soup)     # Invoke the scrape function
            # Be a responsible citizen by waiting before you hit again
            time.sleep(3)
            page_number += 1
            # Recursively invoke the same function with the increment
            browse_and_scrape(seed_url, page_number)
        else:
            scrape(source_url, soup)     # The script exits here
            return True
        return True
    except Exception as e:
        return e

La función anterior, browse_and_scrape(), se llama recursivamente hasta que la función soup.find("li",class_="next") devuelve Ninguno. En este punto, el código raspará la parte restante de la página web y saldrá.

Para la pieza final del rompecabezas, iniciamos el flujo de raspado. Definimos seed_url y llamamos a browse_and_scrape() para obtener los datos. Esto se hace bajo el bloque if __name__ == "__main__":

1
2
3
4
5
6
7
8
if __name__ == "__main__":
    seed_url = "http://books.toscrape.com/catalogue/page-{}.html"
    print("Web scraping has begun")
    result = browse_and_scrape(seed_url)
    if result == True:
        print("Web scraping is now complete!")
    else:
        print(f"Oops, That doesn't seem right!!! - {result}")

Si desea obtener más información sobre el bloque if __name__ == "__main__", consulte nuestra guía sobre cómo funciona.

Puede ejecutar el script como se muestra a continuación en su terminal y obtener el resultado como:

1
$ python scraper.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Web scraping has begun
Now Scraping - http://books.toscrape.com/catalogue/page-1.html
Now Scraping - http://books.toscrape.com/catalogue/page-2.html
Now Scraping - http://books.toscrape.com/catalogue/page-3.html
.
.
.
Now Scraping - http://books.toscrape.com/catalogue/page-49.html
Now Scraping - http://books.toscrape.com/catalogue/page-50.html
Web scraping is now complete!

Los datos extraídos se pueden encontrar en el directorio de trabajo actual con el nombre de archivo allBooks.csv. Aquí hay una muestra del contenido del archivo:

1
2
3
http://books.toscrape.com/a-light-in-the-attic_1000/index.html,http://books.toscrape.com/catalogue/media/cache/2c/da/2cdad67c44b002e7ead0cc35693c0e8b.jpg,A Light in the Attic,Three,51.77,In stock
http://books.toscrape.com/tipping-the-velvet_999/index.html,http://books.toscrape.com/catalogue/media/cache/26/0c/260c6ae16bce31c8f8c95daddd9f4a1c.jpg,Tipping the Velvet,One,53.74,In stock
http://books.toscrape.com/soumission_998/index.html,http://books.toscrape.com/catalogue/media/cache/3e/ef/3eef99c9d9adef34639f510662022830.jpg,Soumission,One,50.10,In stock

¡Buen trabajo! Si desea ver el código del raspador en su totalidad, puede encontrarlo en GitHub.

Conclusión

En este tutorial, aprendimos la ética de escribir buenos web scrapers. Luego usamos Beautiful Soup para extraer datos de un archivo HTML usando las propiedades de objeto de Beautiful Soup, y sus varios métodos como find(), find_all() y get_text(). Luego construimos un raspador que recupera una lista de libros en línea y la exporta a CSV.

El web scraping es una habilidad útil que ayuda en diversas actividades, como la extracción de datos como una API, la realización de control de calidad en un sitio web, la verificación de URL rotas en un sitio web y más. ¿Cuál es el próximo raspador que vas a construir?