Web Scraping con Python
Tags: python, scraping, html
Published: 2020-05-21 15:21:15

*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](https://www.amazon.com/-/es/Ryan-Mitchell/dp/1491910291)". 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](https://es.wikipedia.org/wiki/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:

```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](http://simpledesktops.com/) 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 👏👏👏. 
<br>

![Homer's bird](https://media.giphy.com/media/l41lUJ1YoZB1lHVPG/giphy.gif)

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](https://docs.python.org/3/library/os.html): para manejo de rutas de archivos y carpetas
* [re](https://docs.python.org/3/library/re.html): para expresiones regulares
* [shutil](https://docs.python.org/3/library/shutil.html): para operaciones con archivos
* [requests](https://requests.readthedocs.io/es/latest/): para realizar peticiones HTTP
* [BeautifulSoup](https://www.crummy.com/software/BeautifulSoup/bs4/doc/): 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:

```python
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.

```python
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`.

```python
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.

```python
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.

```python
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](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.

```python
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:

```python
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](https://github.com/luciano-im/SimpleDesktop-Bot)

<br><br>

## 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 ❤️.