Tal y como se mencionó en la introducción, algunos sitios web establecen medidas de seguridad para evitar el acceso de crawlers. Tales medidas pueden abarcar desde una simple comprobación del User-Agent hasta un sofisticado sistema de registros y heurísticas que tratan de detectar a estos autómatas.
Nuestro trabajo será, precisamente, engañar a esos sistemas para evitar ser detectados y preparar una estrategia de recuperación en caso de bloqueo.
Presentando: Selenium
Cuando de trata de automatizar procesos sobre un navegador, Selenium sin duda la herramienta de referencia. Una de sus mayores ventajas es que, a diferencia de otras herramientas como Scrapy, que trabajan sobre documentos HTML descargados estáticamente, Selenium ejecuta y controla un navegador web al completo, por lo que es mucho más difícil de detectar.
Selenium esta disponible para multitud de lenguajes como Java, Javascript, Python, etc. Para este proyecto se ha decidido utilizar Python a lo largo de todo el proceso, tanto por la disponibilidad de las herramientas como por la popularidad del lenguaje entre sectores no-tic.
Configuración
Los pre-requisitos para seguir en proceso son:
python==3.6selenium==3.141.0
En el directorio actual, crearemos el script con el nombre que deseemos, por ejemplo, crawl.py:
Comprobaremos que las dependencias son correctas escribiendo en el script:
from selenium import webdriver
print('Done')
De este modo, ejecutando python crawl.py deberíamos ver escrito por pantalla el mensaje Done.
Obtenicón del WebDriver
Selenium no trae instalado por defecto ningún navegador, por ello se ha de escoger uno y obtener su WebDriver por separado. El WebDriver es lo que le permitirá tomar el control del navegador, y ha de ser específico para cada navegador distinto. En este caso, optaremos por Firefox al ser el navegador de código libre más popular y robusto. El WebDirver se puede encontrar en su repositorio oficial.
Una vez descargado y descomprimido en el directorio local, podemos probar su funcionamiento editando el script.
from selenium import webdriver
import os
import time
BASE_PATH = os.path.abspath(os.path.dirname(__file__))
DRIVER_PATH = os.path.join(BASE_PATH,'geckodriver')
CACHE_PATH = os.path.join(BASE_PATH,'cache')
driver = webdriver.Firefox(executable_path=DRIVER_PATH)
driver.get(uri)
time.sleep(5)
driver.quit()
print('Done')
Directivas
Como ha mostrado el ejemplo anterior, Selenium permite controlar el navegador a través del objeto driver con directivas como driver.get() para navegar a una URI.
Las directivas que usaremos en este script son las cuatro siguientes:
get(): Accede a una URIback(): Retrocede a la página anteriorfind_elements_by_css_selector(): Selecciona un elemento de la páginaquit(): Cierra el navegador
Guardado local de cada página
Recordamos que el objetivo de este script es guardar una copia local de cada página que encuentre para posteriormente ser analizada con otras herramientas.
Cuando el WebDriver carga una página, podemos acceder al contenido crudo de la misma mediante driver.page_source. De este modo, podemos guardar copias de las páginas HTML añadiendo:
with open(local_path, 'w') as local_page:
local_page.write(driver.page_source)
Selección de elementos HTML
De entre las muchas formas que ofrece Selenium de seleccionar elementos, el selector CSS es una de las más sencillas de utilizar. Este método será también utilizado en posteriores etapas con otras herramientas.
Usando la sintaxis propia de CSS, podemos aplicar criterios de selección del mismo modo que lo haríamos en la hoja de estilos:
links = driver.find_elements_by_css_selector("a.icon-arrow-right-after")
La llamada anterior devuelve una lista con todos los elementos de tipo <a> (enlaces HTML) que además tengan la clase icon-arrow-right-after. El nombre de dicha clase ha sido obtenido manualmente utilizando el inspector de código de Firefox. Una vez se conoce el selector que se desea utilizar, su aplicación es automática por parte de Selenium.
Estructura de los elementos
Los elementos que devuelven las llamadas a find_*() son objetos internos de Selenium que contienen, entre otras cosas, los atributos del elemento HTML original. De este modo, el siguiente código permite obtener la URL a la que apuntaba dicho enlace:
link = links[0].get_attribute("href")
Navegación
Con dicho enlace, ya podemos llamar a la función get() para navegar a la siguiente página.
driver.get(link)
De un modo análogo se pueden obtener los enlaces a los pisos individuales y visitarlos uno a uno:
links = driver.find_elements_by_css_selector("a.item-link")
for link in links:
driver.get(link)
# Do something in the new page
driver.back()
Check captcha
Una de las medidas que, sin duda, uno se puede encontrar cuando trabaja contra servicios protegidos contra bots son los captchas: retos sencillos de resolver por un humano pero difíciles para un autómata.
Para hacer frente a ellos, se ha optado por un enfoque semi-superviado en el que se observe el proceso ejecutarse de forma automática hasta que el portal web requiera la resolución de un captcha. En ese momento el script se detendrá a la espera de que el usuario supervisor resuelva el reto y éste pueda proseguir.
Para logar esto de forma cómoda y automática programaremos que, ante la detección de un captcha, el script entre en pausa un tiempo determinado y, tras ese tiempo, compruebe si el captcha ya ha desaparecido:
def check_captcha():
captcha = driver.find_elements_by_css_selector("div.g-recaptcha")
if len(captcha) > 0:
while len(captcha) > 0:
time.sleep(5)
captcha = driver.find_elements_by_css_selector("div.g-recaptcha")
Deberemos llamar a esta función cada vez que pidamos una página nueva con get(). De este modo, cuando se vuelva de la función podremos estar seguros de que la página ya no contiene ningún captcha.
Algoritmo completo
El algoritmo completo entonces resultaría:
- Visitar la primera página del listado de items
- Guardar una copia de dicha página
- Obtener los enlaces a cada item
- Para cada item
- Visitar el item
- Guardar una copia de la página del item
- Volver atrás
- Obtener el enlace a la siguiente página del listado
- Volver al primer punto
El código completo no ha sido compartido debido a que, en el proceso de desarrollo se implementó una estructura de guardado selectivo de páginas para facilitar las sucesivas ejecuciones del experimento que resulta totalmente innecesaria una vez de tiene controlado el proceso.
En el caso de contar con tiempo suficiente como para realizar una versión más concisa y legible, está será compartida junto con el resto del proyecto.