Luciano Muñoz
Posted on May 21, 2020
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 👏👏👏.
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 etiquetaimg
. El atributosrc
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:
- Comenzará haciendo un request a la URL
/browse/
- Del código HTML debe obtener las URL de los wallpapers
- Luego debe formatear las URL para quitar el resize
- Procesar las descargas de las imágenes
- 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 variablewp_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 variableoutput_file
. - Por último usamos
shutil.copyfileobj
para copiar el contenido dewp_file
dentro deoutput_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 ❤️.
Posted on May 21, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.