Web Scraping con Python

Web Scraping con Python

Scraping es una técnica para leer y extraer datos de un sitio web cuando no existe un medio que nos permita obtener está información "por el buen camino", como una API Rest o RSS. El scraping está relacionado a lo que hoy conocemos como crawler, o simplemente bot.

Sin ir más lejos Google y los demás buscadores tienen bots que indexan la web constantemente almacenando toda la información que luego ves en los resultados de búsqueda. Pero no solo los buscadores utilizan scraping, también sitios como los comparadores de precios de productos, hoteles, vuelos, y muchos otros más.

Lo que quiero mostrarte en este post es que nosotros podemos programar nuestro propio bot, ya que se trata de una técnica que podemos aprender sin mayores problemas. No vas a salir programando el próximo Google, pero por ejemplo vas a poder programar un bot para hacer un seguimiento del precio de ese producto que tanto queres, o para consultar los titulares de los sitios de noticias que lees a diario. Y lo cool es que con un poco de ingenio podemos lograr cosas mucho más increíbles.

Si te interesa conocer esta técnica más a fondo te recomiendo el libro "Web Scraping with Python". Con solo unos capítulos vas a entender lo potente que es el scraping, y como implementarlo con Python 🐍.

Let's go!

¿Cómo funciona el scraping?

Existen distintas técnicas, pero lo normal es scrapear un sitio web a través del código HTML, indicándole a nuestro bot que tags y atributos debe buscar para encontrar la información que queremos.

Imaginemos que deseamos obtener los titulares de un sitio de noticias que tiene la siguiente estructura HTML:

<section class="layout-articles">  
    <!-- Noticia 1 -->
    <article>
        <h1 class="title">
            <a href="/noticia-1" title="Noticia 1">
                Noticia 1
            </a>
        </h1>
        <img src="noticia-1.jpg">
    </article>
    <!-- Noticia 2 -->
    <article>
        <h1 class="title">
            <a href="/noticia-2" title="Noticia 2">
                Noticia 2
            </a>
        </h1>
        <img src="noticia-2.jpg">
    </article>
</section>  



Nuestro software debería buscar la etiqueta section class="layout-articles" que actúa como un wrapper de las noticias, obtener todos los tags h1 class="title", y de allí extraer la etiqueta a que contiene el título y la URL de las noticias.

Esto es solo un sencillo ejemplo para que vayas entendiendo la idea, y que te servirá para comprender mejor lo que vamos a programar.

¿Qué vamos a programar?

Existe un sitio genial llamado Simple Desktops que contiene una colección super cool de wallpapers totalmente fancy, y nuestro bot se encargará de recorrer las distintas páginas de esta web y descargar automáticamente todos los wallpapers 👏👏👏.

Homer's bird

Como ves, lo divertido del scraping es que el limite lo pone tu imaginación.

Pero primero analicemos la estructura del sitio y el código HTML, ya que esto nos permitirá comprender que pasos debe seguir nuestro bot para cumplir su objetivo:

  • La web contiene una paginación tipo /browse/, /browse/1/, /browse/2/, etc., donde se muestran los wallpapers.
  • Cada wallpaper es un div class="desktop" que dentro contiene una etiqueta img. El atributo src de esta etiqueta es lo que nos interesa ya que contiene la URL para descargar el wallpaper.
  • El sitio usa un generador de thumbnails que viene implícito en la URL de las imágenes, pero si eliminamos el texto que hace referencia al resize accedemos a la imagen original: Apple_Park.png.295x184_q100.png 😎.
  • La URL hacia la próxima página la podemos obtener del tag a class="more".



Con la información anterior podemos ver que el algoritmo debe hacer lo siguiente:

  1. Comenzará haciendo un request a la URL /browse/
  2. Del código HTML debe obtener las URL de los wallpapers
  3. Luego debe formatear las URL para quitar el resize
  4. Procesar las descargas de las imágenes
  5. Obtener la URL de la página siguiente y volver a comenzar

Genial, ahora que ya sabemos lo que tenemos que hacer, empecemos con la parte divertida... ¡a codear! 🎈

¿Como programar un bot en Python?

Estas son las librerías que vamos a usar:

  • os: para manejo de rutas de archivos y carpetas
  • re: para expresiones regulares
  • shutil: para operaciones con archivos
  • requests: para realizar peticiones HTTP
  • BeautifulSoup: para parsear el código HTML, es el corazón de nuestro bot ❤️



BeautifulSoup y requests son dos librerías que no vienen incluidas con Python, por lo que tendrás que instalarlas con pip:

$ pip install beautifulsoup4
$ pip install requests



Para hacer el código más legible y fácil de debugear iremos separando la lógica en funciones, cada una de las cuales cumplirá una tarea particular.

Bien, primero te recomiendo que crees una carpeta para este proyecto y dentro el archivo simpledesktop-bot.py, al cual comenzaremos importando las librerías:

import os  
import re  
import shutil  
import requests  
from requests.exceptions import HTTPError  
from bs4 import BeautifulSoup  



El punto de entrada a nuestra app será la función init(), que funciona como un constructor. Allí setearemos los datos iniciales, como la URL del sitio web, el path desde donde comenzara a correr el bot, y el directorio donde guardaremos los wallpapers. Por defecto se creará la carpeta wallpapers dentro del directorio de nuestro proyecto y allí haremos las descargas. Para ello primero chequeamos con os.path.exists si este directorio ya existe, y de no existir lo creamos con os.makedirs.

Por último llamamos a la función processPage() que iniciará el scraping.

def init():  
    url = 'http://simpledesktops.com'
    first_path = '/browse/'
    download_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'wallpapers')

    if not os.path.exists(download_directory):
        os.makedirs(download_directory)

    processPage(url, first_path, download_directory)



processPage() es un wrapper que se encargará de realizar las llamadas a las demás funciones. Por parámetro recibirá la URL inicial y con cada ejecución se vuelve a llamar recursivamente recibiendo la nueva página a procesar, lo cual nos permite hacer un loop que finalizará al alcanzar la última página.

La primera función llamada es getPageContent(), que recibe por parámetros las variables url y path, con las cuales hace el request HTTP y devuelve un diccionario con estos datos:

  • images: es una lista con las URL de los wallpapers a descargar
  • next_page: contiene la URL de la siguiente página a procesar

La segunda función que ejecuta es downloadWallpaper() a la cual le pasaremos las URL de los wallpapers y se encargará de procesar las descargas.

Por último tenemos la recursión, verificamos si next_page tiene un valor asignado y volvemos a llamar a processPage() con el nuevo path.

def processPage(url, path, download_directory):  
    print('\nPATH:', path)
    print('=========================')

    wallpapers = getPageContent(url + path)
    if wallpapers['images']:
        downloadWallpaper(wallpapers['images'], download_directory)
    else:
        print('This page does not contain any wallpaper')
    if wallpapers['next_page']:
        processPage(url, wallpapers['next_page'], download_directory)
    else:
        print('THIS IS THE END, BUDDY')



Como dije anteriormente getPageContent() es la primer función llamada por processPage(), y su objetivo es retornar las URL de los wallpapers y la URL de la página siguiente.

Primero definimos las variables image y next_page que guardarán los datos a retornar, y las inicializamos con valores por defecto.

Luego llamamos a la función requestPage() (para que el código sea más fácil de leer he abstraído el request HTTP a su propia función, que veremos más adelante) que nos retornará el código HTML listo para ser manipulado. Y aquí es donde veremos la magia de BeautifulSoup! Primero vamos usar el método find_all para obtener todos los wallpapers que, tal como vimos en el análisis de la estructura HTML, están representados por el tag div class="desktop", y los guardamos en la variable wallpapers. Luego iteramos estos elementos y usando el método find buscaremos la etiqueta img, de la cual obtendremos el atributo src que contiene la URL del wallpaper. Con este dato iremos completando nuestra lista images.

Por último buscamos el tag a class="more", extraemos su URL del atributo href y lo asignamos a la variable next_page que creamos al principio de la función.

Retornamos images y next_page en un diccionario.

def getPageContent(url):  
    images = []
    next_page = None

    html = requestPage(url)
    if html is not None:
        # Search wallpapers URL
        wallpapers = html.find_all('div', {'class': 'desktop'})
        for wp in wallpapers:
            img = wp.find('img')
            images.append(img.attrs['src'])

        # Search for next page URL
        try:
            more_button = html.find('a', {'class':'more'})
            next_page = more_button.attrs['href']
        except:
            pass

    return {'images': images, 'next_page': next_page}



Anteriormente usamos la función requestPage(), que tal como dijimos, se encarga del request HTTP y de retornar el HTML ya parseado. Para el request usamos requests.get y guardamos el payload en la variable raw_html. Por último parseamos el HTML plano con BeautifulSoup y lo retornamos.

Con try/except nos aseguramos de interceptar los errores y mostrar sus respectivos mensajes de error.

def requestPage(url):  
    try:
        raw_html = requests.get(url)
        try:
            html = BeautifulSoup(raw_html.text, features='html.parser')
            return html
        except:
            print('Error parsing HTML code')
            return None
    except HTTPError as e:
        print(e.reason)
        return None



La segunda función llamada por processPage() es downloadWallpaper(), que recibe la lista de URL y descarga los wallpapers.

La primer tarea de esta función es hacer una pequeña modificación a las URL, porque recordemos que el sitio web usa un generador de thumbnails que viene implícito en el nombre de la imagen, y sin esta modificación estaríamos descargando un wallpaper de 300x189 px. Quitando este resize vamos a poder descargar las imágenes en su tamaño original.
Veamos un ejemplo:

http://static.simpledesktops.com/uploads/desktops/2020/03/30/piano.jpg.300x189_q100.png

Pongamos la atención en el nombre del wallpaper, vemos que en primer lugar se incluye el nombre del archivo con su extensión original (piano.jpg) y luego el "código" del resize (300x189_q100.png). Lo que necesitamos es obtener la misma URL pero sin esta última parte, para lo cual vamos a usar una expresión regular.

Con la regex '^.+?(\.png|jpg)' buscamos la primer ocurrencia de .png o .jpg comenzando desde el inicio de la URL. Si hay match obtenemos todo ese string, con lo cual nos quedará la URL sin el resize, y si no hay match quiere decir que no encuentra la extensión de la imagen y por lo tanto no es una URL válida.

Siguiendo con el código, en la variable file_path generamos la ruta completa a la imagen y chequeamos si ya existe en nuestro disco para no volver a descargarla. Si la imagen no existe haremos lo siguiente:

  • Usamos request.get para obtener la imagen por streaming y guardamos la referencia a este objeto en la variable wp_file.
  • Con la función open abrimos el archivo local (que será creado en este momento, aunque se encontrará vacío) en modo binario y escritura, y lo referenciamos con el nombre de variable output_file.
  • Por último usamos shutil.copyfileobj para copiar el contenido de wp_file dentro de output_file

Con esto ya tendremos descargado el wallpaper en nuestro disco.

Los bloques with nos permitirán liberar automáticamente la memoria usada por Python para el request HTTP y la creación del archivo local, por lo cual podemos proceder a descargar el siguiente wallpaper.

def downloadWallpaper(wallpapers, directory):  
    for url in wallpapers:
        match_url = re.match('^.+?(\.png|jpg)', url)
        if match_url:
            formated_url = match_url.group(0)
            filename = formated_url[formated_url.rfind('/')+1:]
            file_path = os.path.join(directory, filename)
            print(file_path)

            if not os.path.exists(file_path):
                with requests.get(formated_url, stream=True) as wp_file:
                    with open(file_path, 'wb') as output_file:
                        shutil.copyfileobj(wp_file.raw, output_file)
        else:
            print('Wallpaper URL is invalid')



Eso es todo, solo resta incluir la llamada a la función init() que da inicio a la ejecución del código:

init()  



Para poner a correr nuestro bot abrimos una consola en el directorio del proyecto y ejecutamos el comando python3 simpledesktop-bot.py, y veremos algo como lo siguiente:

$ python3 simpledesktop-bot.py

PATH: /browse/  
=========================
/Users/MyUser/simple-desktop-scraper/wallpapers/sphericalharmonics1.png
/Users/MyUser/simple-desktop-scraper/wallpapers/Dinosaur_eye_2.png
/Users/MyUser/simple-desktop-scraper/wallpapers/trippin.png
...

PATH: /browse/2/  
=========================
/Users/MyUser/simple-desktop-scraper/wallpapers/Apple_Park.png
/Users/MyUser/simple-desktop-scraper/wallpapers/triangles.png
/Users/MyUser/simple-desktop-scraper/wallpapers/thanksgiving_twelvewalls.png
...

PATH: /browse/3/  
=========================
/Users/MyUser/simple-desktop-scraper/wallpapers/minimalistic_rubik_cube_2880.png
/Users/MyUser/simple-desktop-scraper/wallpapers/Nesting_Dolls.png
/Users/MyUser/simple-desktop-scraper/wallpapers/flat_bamboo_wallpaper.png



El código completo lo podes encontrar en el repositorio de GitHub y si te gustó dejale una estrellita al repo 😉

SimpleDesktop-Bot

Conclusiones

En primer lugar gracias por haber llegado hasta acá, significa mucho para mí que hayas leído este post.

Espero que hayas podido aprender algo nuevo ya que ese es mi objetivo, y si es así entonces dejame un comment o mandame un tweet porque me gustaría saberlo.

También me gustaría conocer sus propios bot, así que si te animas a desarrollar uno contame porque me interesa saber. Y si crees que podes aportar a nuestro Simple Desktop bot (porque también es tuyo) entonces animate a mandarme un pull request. No hay cosa que me gustaría más.

Si te gusta mi contenido me ayudarías mucho difundiéndolo con tus amigos o en tu feed de Twitter, y si te gustaría que escriba sobre algo en particular dejame un comment. Espero tu feedback para ir mejorando con cada nuevo post ❤️.

comments powered by Disqus