Automatización de Web Scraping en Portales de Empleo con Navegadores sin Interfaz y Análisis DOM

Al realizar extracción de datos en portales de empleo modernos, frecuentemente nos encontramos con que la navegación entre páginas no altera la URL del navegador. Este comportamiento indica que la información se carga dinámicamente mediante peticiones asíncronas en lugar de recargas completas del documento.

En lugar de invertir tiempo en ingeniería inversa para descubrir los endpoints de la API subyacente, una alternativa altamente efectiva es utilizar herrameintas de automatización para simular la interacción del usuario, como hacer clic en el botón de paginación. Para lograr esto de manera eficiente y sin consumir recursos gráficos, emplearemos un navegador sin interfaz gráfica (headless browser) controlado por Selenium.

Extracción de datos mediante selectores nativos de Selenium

En este primer enfoque, configuramos un navegador en modo headless e inycetamos cabeceras HTTP personalizadas para evadir bloqueos básicos. Posteriormente, utilizamos los métodos de localización de elementos de Selenium para extraer la información de cada vacante y almacenarla en un archivo CSV.


import time
import random
import csv
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.options import Options

# Configuración del navegador sin interfaz gráfica
opciones_chrome = Options()
opciones_chrome.add_argument("--headless")
opciones_chrome.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36")

driver = webdriver.Chrome(options=opciones_chrome)
wait = WebDriverWait(driver, 15)

url_objetivo = "https://www.example-jobs.com/list?query=developer"
driver.get(url_objetivo)
time.sleep(3)

# Gestión del archivo de salida
with open('vacantes_selenium.csv', mode='w', encoding='utf-8', newline='') as archivo_csv:
    escritor = csv.writer(archivo_csv)
    escritor.writerow(['Titulo', 'Fecha', 'Empresa', 'Salario', 'Experiencia', 'Ubicacion', 'Requisitos'])

    while True:
        # Esperar a que la lista de trabajos esté presente en el DOM
        wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, "ul.job-list li")))
        vacantes = driver.find_elements(By.CSS_SELECTOR, "ul.job-list li")

        for vacante in vacantes:
            titulo = vacante.find_element(By.CSS_SELECTOR, "h3.title").text
            fecha_pub = vacante.find_element(By.CSS_SELECTOR, "span.date").text
            empresa = vacante.find_element(By.CSS_SELECTOR, "div.company a").text
            salario = vacante.find_element(By.CSS_SELECTOR, "span.salary").text
            
            # Extraer y limpiar experiencia
            info_extra = vacante.find_element(By.CSS_SELECTOR, "div.details").text.split()
            experiencia = info_extra[1] if len(info_extra) > 1 else "N/A"
            
            ubicacion = vacante.find_element(By.CSS_SELECTOR, "span.location").text
            
            # Filtrar requisitos (excluyendo el primer elemento que suele ser el salario)
            spans_requisitos = vacante.find_elements(By.CSS_SELECTOR, "div.details span")
            requisitos = " ".join([span.text for span in spans_requisitos[1:]])

            escritor.writerow([titulo, fecha_pub, empresa, salario, experiencia, ubicacion, requisitos])

        # Lógica de paginación
        pagina_source = driver.page_source
        if "disabled next-page" not in pagina_source:
            boton_siguiente = wait.until(EC.element_to_be_clickable((By.CLASS_NAME, "next-page")))
            boton_siguiente.click()
            time.sleep(2 + random.uniform(0.5, 1.5))
        else:
            break

driver.quit()

Optmiización del parsing con BeautifulSoup

El método anterior puede presentar cuellos de botella si la página posee un DOM muy complejo, ya que Selenium interactúa con el motor del navegador para cada búsqueda de elementos. Una optimización significativa consiste en obtener el código fuente completo de la página ya renderizada (page_source) y procesarlo con una librería de análisis sintáctico rápida como BeautifulSoup. Esto reduce drásticamente el tiempo de extracción de los atributos.


import time
import random
import re
import csv
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.options import Options
from bs4 import BeautifulSoup

# Configuración headless
opciones_chrome = Options()
opciones_chrome.add_argument("--headless")
driver = webdriver.Chrome(options=opciones_chrome)
wait = WebDriverWait(driver, 15)

driver.get("https://www.example-jobs.com/list?query=developer")
time.sleep(3)

with open('vacantes_bs4.csv', mode='w', encoding='utf-8', newline='') as archivo_csv:
    escritor = csv.writer(archivo_csv)
    escritor.writerow(['Titulo', 'Fecha', 'Empresa', 'Salario', 'Experiencia', 'Ubicacion', 'Requisitos'])

    while True:
        wait.until(EC.presence_of_element_located((By.CLASS_NAME, "job-list")))
        
        # Obtener el HTML renderizado y parsearlo con BeautifulSoup
        html_renderizado = driver.page_source
        sopa = BeautifulSoup(html_renderizado, 'lxml')
        vacantes = sopa.select('ul.job-list li')

        for vacante in vacantes:
            titulo = vacante.select_one('h3.title').text
            fecha_pub = vacante.select_one('span.date').text
            empresa = vacante.select_one('div.company a').text
            salario = vacante.select_one('span.salary').text
            
            # Uso de expresiones regulares para limpiar la experiencia
            texto_detalles = vacante.select_one('div.details').text
            match_exp = re.search(r'Experiencia:\s*(\d+\s*años|\d+\s*mes)', texto_detalles)
            experiencia = match_exp.group(1) if match_exp else "N/A"
            
            ubicacion = vacante.select_one('span.location').text
            
            # Extracción de requisitos
            spans = vacante.select('div.details span')
            requisitos = " ".join([span.text for span in spans[1:]])

            escritor.writerow([titulo, fecha_pub, empresa, salario, experiencia, ubicacion, requisitos])

        # Control de paginación
        if "disabled next-page" not in driver.page_source:
            wait.until(EC.element_to_be_clickable((By.CLASS_NAME, "next-page"))).click()
            time.sleep(1.5 + random.uniform(0.5, 1.0))
        else:
            break

driver.quit()

Etiquetas: Selenium BeautifulSoup web-scraping headless-browser Python

Publicado el 10-5 00:00