Cómo extraer datos de ofertas de empleo

Siga este tutorial paso a paso y aprenda a crear un script de Python para Indeed que realice el Scraping web para extraer automáticamente datos sobre ofertas de empleo.
28 min de lectura
How to scrape jobs data

Siga este tutorial paso a paso y aprenda a crear un script de Python para Indeed que realice el Scraping web para extraer automáticamente datos sobre ofertas de empleo.

Esta guía tratará los siguientes temas:

  • ¿Por qué extraer datos de empleo de la web?
  • Bibliotecas y herramientas para extraer datos de Indeed
  • Rastrear datos de empleo de Indeed con Selenium

¿Por qué extraer datos de empleo mediante Scraping web?

Scraping web para recopilar datos sobre empleos es útil por varias razones, entre ellas:

  • Estudio de mercado: permite a las empresas y a los analistas del mercado laboral recopilar información sobre las tendencias del sector. Por ejemplo, esto incluye qué habilidades tienen una gran demanda o las regiones geográficas que experimentan un crecimiento del empleo. También permite supervisar las actividades de contratación de la competencia.
  • Optimización de la búsqueda y la selección de empleo: ayuda a los solicitantes de empleo a buscar ofertas de trabajo en múltiples fuentes para encontrar puestos que se ajusten a sus cualificaciones y preferencias.
  • Optimización de la contratación y los recursos humanos: apoya el proceso de contratación facilitando la contratación y ayudando a comprender las tendencias salariales del mercado y los beneficios que buscan los candidatos.

Por lo tanto, los datos sobre empleo son útiles tanto para los empleadores como para los solicitantes de empleo.

En lo que respecta a los Scrapers de ofertas de empleo, hay un aspecto esencial que hay que destacar. La plataforma objetivo debe ser pública. En otras palabras, debe permitir que incluso los usuarios que no hayan iniciado sesión puedan realizar búsquedas de empleo. Esto se debe a que rastrear datos bajo un muro de inicio de sesión puede acarrear problemas legales

Eso significa descartar LinkedIn. ¿Qué otras plataformas de empleo quedan? ¡Indeed, una de las principales plataformas de empleo online!

Bibliotecas y herramientas para extraer datos de Indeed

Python se considera uno de los mejores lenguajes para el Scraping web gracias a su sintaxis, su facilidad de uso y su rico ecosistema de bibliotecas. Así que, vamos a por ello. Echa un vistazo a nuestra guía sobre Scraping web con Python.

Ahora tienes que elegir las bibliotecas de scraping adecuadas entre las muchas disponibles. Para tomar una decisión informada, explora Indeed en tu navegador. Verás que la mayoría de los datos del sitio se recuperan tras la interacción. Esto significa que el sitio utiliza mucho AJAX para cargar y actualizar el contenido de forma dinámica sin necesidad de recargar la página. Para hacer Scraping web en un sitio así, necesitas una herramienta que sea capaz de ejecutar JavaScript. ¡Esa herramienta es Selenium!

Selenium permite extraer datos de sitios web dinámicos en Python. Representa los sitios en un navegador web controlable, realizando las operaciones que usted le indique. Gracias a Selenium, puede extraer datos incluso si el sitio de destino utiliza JavaScript para la representación o la recuperación de datos.

¡Aprenda a extraer ofertas de empleo de sitios web como Indeed!

Rastrear datos de empleo de Indeed con Selenium

Sigue este tutorial paso a paso y descubre cómo crear un script de Python para realizar Scraping web de Indeed.

Paso 1: Configuración del proyecto

Antes de realizar tareas de scraping web, asegúrate de que cumples estos requisitos previos:

¡Ahora ya tiene todo lo necesario para configurar un proyecto Python!

Abra el terminal y ejecute los siguientes comandos para:

  1. Crear una carpeta indeed-scraper
  2. Entrar en ella
  3. Inicializarla con un entorno virtual Python

mkdir indeed-Scraper
cd indeed-Scraper
python -m venv env

En Linux o macOS, ejecute el siguiente comando para activar el entorno:

./env/bin/activate

En Windows, ejecute:

envScriptsactivate.ps1

A continuación, inicialice un archivo scraper.py que contenga la siguiente línea en la carpeta del proyecto:

print("¡Hola, mundo!")

Por ahora, solo imprime «¡Hola, mundo!», pero pronto contendrá la lógica de scraping de Indeed.

Ejecútelo para verificar que funciona con:

python Scraper.py

Si todo ha salido según lo previsto, debería imprimir este mensaje en la terminal:

¡Hola, mundo!

Ahora que sabes que el script funciona, abre la carpeta del proyecto en tu IDE de Python.

¡Bien hecho! ¡Prepárate para escribir código Python!

Paso 2: Instalar las bibliotecas de scraping

Como se mencionó anteriormente, Selenium es una gran herramienta cuando se trata de Scraping web de ofertas de empleo de Indeed. Ejecuta el siguiente comando en el entorno virtual Python activado para añadirlo a las dependencias del proyecto:

pip install selenium

Esto puede tardar un poco, así que ten paciencia.

Ten en cuenta que este tutorial hace referencia a Selenium 4.11.2, que incluye funciones de detección automática de controladores. Si tienes una versión anterior de Selenium instalada en tu PC, actualízala con:

pip install selenium -U

Ahora, borre scraper.py. A continuación, importe el paquete e inicialice un Scraper de Selenium con:

from selenium import webdriver
from selenium.webdriver.chrome.service import Service

# configurar una instancia de Chrome controlable
# en modo sin interfaz gráfica
service = Service()
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
driver = webdriver.Chrome(
    service=service,
    options=options
)

# lógica de rastreo...

# cerrar el navegador y liberar los recursos
driver.quit()

Este script instancia una instancia de WebDriver para controlar mediante programación una instancia de Chrome. El navegador se abrirá en segundo plano en modo sin interfaz gráfica, lo que significa que no habrá GUI. Esa es una configuración habitual para la producción. Si, por el contrario, prefieres seguir las operaciones que ejecuta el script de trabajos de Scraping web en la página, comenta esa opción. Esto resulta útil en el desarrollo.

Asegúrate de que tu IDE de Python no informe de ningún error. Ignora las advertencias que puedas recibir debido a las importaciones no utilizadas. ¡Estás a punto de utilizar las bibliotecas para extraer datos del repositorio de GitHub!

¡Perfecto! Es hora de crear su Scraper web Indeed Python.

Paso 3: Conéctese a la página web de destino

Abre Indded y busca los trabajos que te interesan. En esta guía, verás cómo extraer ofertas de trabajo remotas para ingenieros de software en Nueva York. Ten en cuenta que cualquier otra búsqueda de trabajo en Indeed servirá. La lógica de extracción será la misma.

Así es como se ve la página de destino en el navegador en el momento de escribir este artículo:

Indeed gif: remote software engineer jobs in New York

En concreto, así es como se ve la URL de la página de destino:

https://www.indeed.com/jobs?q=software+engineer&l=New+York%2C+NY&sc=0kf%3Aattr%28DSQF7%29%3B&radius=100

Como puede ver, se trata de una URL dinámica que cambia en función de algunos parámetros de consulta.

A continuación, puede utilizar Selenium para conectarse a la página de destino con:

driver.get("https://www.indeed.com/jobs?q=software+engineer&l=New+York%2C+NY&sc=0kf%3Aattr%28DSQF7%29%3B&radius=100")

La función get() indica al navegador que visite la página especificada por la URL pasada como parámetro.

Después de abrir la página, debe establecer el tamaño de la ventana para asegurarse de que todos los elementos sean visibles:

driver.set_window_size(1920, 1080)

Así es como se ve tu script de scraping de Indeed hasta ahora:

from selenium import webdriver
from selenium.webdriver.chrome.service import Service

# configurar una instancia de Chrome controlable
# en modo sin interfaz gráfica
service = Service()
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
driver = webdriver.Chrome(
    service=service,
    options=options
)

# establecer el tamaño de la ventana para asegurarse de que las páginas
# no se rendericen en modo responsivo
driver.set_window_size(1920, 1080)

# abrir la página de destino  en el navegador
driver.get("https://www.indeed.com/jobs?q=software+engineer&l=New+York%2C+NY&sc=0kf%3Aattr%28DSQF7%29%3B&radius=100")

# lógica de scraping...

# cerrar el navegador y liberar los recursos
driver.quit()

Comenta la opción para habilitar el modo sin interfaz gráfica y ejecuta el script. Se abrirá la ventana siguiente durante una fracción de segundo antes de cerrarse:

Selenium script for scraping Indeed's software engineer listings in New York

Tenga en cuenta el aviso «Chrome está siendo controlado por software automatizado». Eso garantiza que Selenium funciona como se espera.

Paso 4: Familiarícese con la estructura de la página

Antes de sumergirse en el scraping, hay otro paso crucial que debe llevar a cabo. El scraping de datos de un sitio implica seleccionar elementos HTML y extraer datos de ellos. Encontrar una forma de obtener los nodos deseados del DOM no siempre es fácil. Por eso debe dedicar algo de tiempo a analizar la estructura de la página para comprender cómo definir una estrategia de selección eficaz.

Abra su navegador y visite la página de búsqueda de empleo de Indeed. Haga clic con el botón derecho del ratón en cualquier elemento y seleccione la opción «Inspeccionar» para abrir las herramientas de desarrollo de su navegador:

Screenshot Using 'Inspect' in the browser on the Indeed job search page

Aquí verás que la mayoría de los elementos que contienen datos interesantes tienen clases CSS como las siguientes:

  • css-j45z4f, css-1m4cuuf, …
  • e37uo190, eu4oa1w0, …
  • job_f27ade40cc1a3686, job_1a53a17f1faeae92, …

Dado que parecen generarse aleatoriamente en el momento de la compilación, no debes basarte en ellas para el scraping. En su lugar, debes basar la lógica de selección en clases como:

  • jobsearch-JobInfoHeader-title
  • date
  • cardOutline

O ID como:

  • companyRatings
  • applyButtonLinkContainer
  • jobDetailsSection

Además, tenga en cuenta que algunos nodos tienen atributos HTML únicos:

  • data-company-name
  • data-testid

Es información útil que hay que tener en cuenta para los trabajos de Scraping web de Indeed. Interactúa con la página para estudiar cómo reacciona y qué datos muestra. Te darás cuenta de que las diferentes ofertas de empleo tienen diferentes atributos de información.

Siga inspeccionando el sitio de destino y familiarícese con su estructura DOM hasta que se sienta preparado para continuar.

Paso 5: Comience a extraer los datos de los puestos de trabajo

Una sola página de búsqueda de Indeed incluye varias ofertas de empleo. Por lo tanto, necesitas una matriz para realizar un seguimiento de los empleos extraídos de la página:

jobs = []

Como habrás observado en el paso anterior, las ofertas de empleo se muestran en tarjetas .cardOutline:

Job postings displayed in .cardOutline cards on Indeed

Selecciónalas todas con:

job_cards = driver.find_elements(By.CSS_SELECTOR, ".cardOutline")

El método find_elements() de Selenium te permite localizar elementos web en una página web. Del mismo modo, también existe el método find_element() para obtener el primer nodo que coincida con la consulta de selección.

By.CSS_SELECTOR indica al controlador que utilice una estrategia de selector CSS. Selenium también admite:

  • By.ID: para buscar un elemento por el atributo HTML id
  • By.TAG_NAME: para buscar elementos basándose en su etiqueta HTML
  • By.XPATH: para buscar elementos mediante una expresión XPath

Importar By con:

from selenium.webdriver.common.by import By

Iterar sobre la lista de tarjetas de trabajo e inicializar un diccionario Python donde almacenar los detalles del trabajo:

for job_card in job_cards:
    # inicializar un diccionario para almacenar los datos de trabajo extraídos
    job = {}
    # lógica de extracción de datos de trabajo...

Una oferta de trabajo puede tener varios atributos. Dado que solo una pequeña parte de ellos son obligatorios, inicializa inmediatamente una lista de variables con valores predeterminados:

posted_at = None
applications = None
title = None
company_name = None
company_rating = None
company_reviews = None
location = None
location_type = None
apply_link = None
pay = None
job_type = None
benefits = None
description = None

Ahora que ya estás familiarizado con la página, sabes que algunos detalles se encuentran en la ficha de trabajo general. Otros, en cambio, se encuentran en la pestaña de detalles que aparece al interactuar.

Por ejemplo, la fecha de creación y el número de solicitudes se encuentran en la pestaña de resumen:

Summary' tab showing creation date and number of applications

Extraiga ambos con:

try:
    date_element = job_card.find_element(By.CSS_SELECTOR, ".date")
    date_element_text = date_element.text
    posted_at_text = date_element_text

    if "•" in date_element_text:
        date_element_text_array = date_element_text.split("•")
        posted_at_text = date_element_text_array[0]
        applications = date_element_text_array[1] 
            .replace("applications", "") 
            .replace("in progress", "") 
            .strip()

    posted_at = posted_at_text 
        .replace("Posted", "") 
        .replace("Employer", "") 
        .replace("Active", "") 
        .strip()
except NoSuchElementException:
    pass

Este fragmento destaca algunos patrones que son clave para el Scraping web de ofertas de empleo de Indeed. Como la mayoría de los elementos de información son opcionales, debes protegerte contra el siguiente error:

selenium.common.exceptions.NoSuchElementException: Mensaje: no existe tal elemento

Selenium lo lanza cuando intenta seleccionar un elemento HTML que no se encuentra actualmente en la página.

Importa la excepción con:

from selenium.common import NoSuchElementException

La instrucción try ... catch garantiza que, si el elemento de destino no se encuentra en el DOM, el script continuará sin fallos.

Además, parte de la información del trabajo se encuentra en cadenas como:

<info_1> • <info_2>

Si falta <info_2>, el formato de la cadena es, en su lugar:

<info_1>

Por lo tanto, es necesario cambiar la lógica de extracción de datos en función de la presencia del carácter «``•``».

Dado un elemento HTML, puede acceder a su contenido de texto con el atributo text. Utilice las cadenas replace() de Python para limpiar las cadenas recopiladas.

Paso 6: Lidiar con las medidas antiscraping de Indeed

Indeed adopta algunas técnicas y tecnologías para evitar que los bots accedan a sus datos. Por ejemplo, al interactuar con las tarjetas de empleo, tiende a abrir este modal de vez en cuando:

Modal appearing on Indeed as an anti-scraping measure

Esta ventana emergente bloquea la interacción. Si no se aborda adecuadamente, detendrá su script Selenium Indeed. Inspecciónelo en DevTools y preste atención al botón de cierre:

Popup in Indeed disrupting Selenium script, highlighting the close button in DevTools

Cierre este modal en Selenium con:

try:
    dialog_element = driver.find_element(By.CSS_SELECTOR, "[role=dialog]")
    close_button = dialog_element.find_element(By.CSS_SELECTOR, ".icl-CloseButton")
    close_button.click()
except NoSuchElementException:
    pass

El método click() de Selenium te permite hacer clic en el elemento seleccionado en el navegador controlado.

¡Genial! Esto cerrará la ventana emergente y le permitirá continuar con la interacción.

Otra técnica de protección de datos que hay que tener muy en cuenta es Cloudflare. Cuando se interactúa demasiado con la página y se producen demasiadas solicitudes, Indeed muestra esta pantalla antibots:

Cloudflare anti-bot screen on Indeed after excessive interactions

La resolución de CAPTCHA de Cloudflare desde Selenium es una tarea muy difícil que requiere un producto premium. Al fin y al cabo, extraer datos de Indeed no es tan fácil. Afortunadamente, puedes evitarlos introduciendo algunos retrasos aleatorios en tu script.

Asegúrate de que la última operación de tu bucle «for» sea:

time.sleep(random.uniform(1, 5))

Esto detendrá el script durante un número aleatorio de segundos, de 1 a 5.

Importa los paquetes necesarios de la biblioteca estándar de Python con:

import random
import time

¡Muy bien! Nada impedirá que tu script automatizado extraiga datos de Indeed.

Paso 7: Abre la ficha de detalles del puesto

Cuando haces clic en una tarjeta de resumen de trabajo, Indeed realiza una llamada AJAX para recuperar los detalles sobre la marcha. Mientras se esperan estos datos, la página muestra un marcador de posición animado:

Animated placeholder on Indeed while loading job details after clicking an outline card

Puedes verificar que las secciones de detalles se han cargado cuando el elemento siguiente aparece en la página:

Element indicating the job details section has loaded on Indeed

Por lo tanto, para acceder a los datos de los detalles del puesto de trabajo en Selenium, debes:

  • Realizar la operación de clic
  • Esperar a que la página contenga los datos de interés

Consíguelo con:

job_card.click()

try:
    title_element = WebDriverWait(driver, 5) 
        .until(EC.presence_of_element_located((By.CSS_SELECTOR, ".jobsearch-JobInfoHeader-title")))
    title = title_element.text.replace("n- job post", "")
except NoSuchElementException:
    continue

El objeto WebDriverWait de Selenium te permite esperar a que se produzca una condición específica. En este caso, el script espera hasta 5 segundos a que .jobsearch-JobInfoHeader-title aparezca en la página. Después de eso, lanzará una excepción TimeoutException.

Tenga en cuenta que el fragmento anterior también recupera el título de la oferta de empleo.

Importa WebDriverWait y EC:

from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

A partir de ahora, el elemento en el que hay que centrarse es esta columna de detalles:

Selenium script using WebDriverWait for '.jobsearch-JobInfoHeader-title', highlighting the job title retrieval and the detail column

Selecciónelo con:

job_details_element = driver.find_element(By.CSS_SELECTOR, ".jobsearch-RightPane")

¡Fantástico! ¡Ya está todo listo para extraer algunos datos de empleo!

Paso 8: Extraer los detalles del trabajo

Es hora de rellenar las variables que definimos en el paso 4 con algunos datos de empleo.

Obtenga el nombre de la empresa que ofrece la vacante:

try:
    company_link_element = job_details_element.find_element(By.CSS_SELECTOR, "div[data-company-name='true'] a")
    company_name = company_link_element.text
except NoSuchElementException:
    pass

A continuación, extrae la información sobre las valoraciones de los usuarios y el número de reseñas de la empresa:

Extracting company's user ratings and number of reviews on Indeed

Como puede ver, no hay una forma fácil de acceder al elemento que almacena el número de reseñas.

intentar:
    elemento_calificación_empresa = elemento_detalles_empleo.find_element(By.ID, "companyRatings")
    calificación_empresa = elemento_calificación_empresa.get_attribute("aria-label").split("out")[0].strip()
    company_reviews_element = job_details_element.find_element(By.CSS_SELECTOR, "[data-testid='inlineHeader-companyReviewLink']")
    company_reviews = company_reviews_element.text.replace(" reviews", "")
except NoSuchElementException:
    pass

A continuación, céntrate en la ubicación de la empresa:

Extracting the company location

De nuevo, debes aplicar el patrón «``•``» mencionado en el paso 4:

try:
    company_location_element = job_details_element.find_element(By.CSS_SELECTOR,
                                                                "[data-testid='inlineHeader-companyLocation']")
    company_location_element_text = company_location_element.text

    location = company_location_element_text

    if "•" in company_location_element_text:
        company_location_element_text_array = company_location_element_text.split("•")
        location = company_location_element_text_array[0]
        location_type = company_location_element_text_array[1]
except NoSuchElementException:
    pass

Como es posible que desee solicitar rápidamente el puesto de trabajo, eche un vistazo también al botón «Solicitar en el sitio web de la empresa» de Indeed:

Apply on company site' button on Indeed

Recupera la URL de destino del botón con:

try:
    apply_link_element = job_details_element.find_element(By.CSS_SELECTOR, "#applyButtonLinkContainer button")
    apply_link = apply_link_element.get_attribute("href")
except NoSuchElementException:
    pass

La función get_attribute() de Selenium devuelve el valor del atributo HTML especificado.

Ahora comienza la parte complicada.

Si inspeccionas la sección «Detalles del trabajo», notarás que no hay una forma fácil de seleccionar los elementos de salario y tipo de trabajo:

'Job details' section on Indeed showing pay and job type elements

Lo que puedes hacer es:

  1. Obtener todos los <div>dentro del <div> «Detalles del trabajo»
  2. Iterar sobre ellos
  3. Si el texto del <div>actual contiene «Salario» o «Tipo de trabajo», obtener el siguiente elemento hermano
  4. Extraer los datos de interés

En otras palabras, tienes que implementar la siguiente lógica:

for div in job_details_element.find_elements(By.CSS_SELECTOR, "#jobDetailsSection div"):
    if div.text == "Pay":
        pay_element = div.find_element(By.XPATH, "following-sibling::*")
        pay = pay_element.text
    elif div.text == "Job Type":
        job_type_element = div.find_element(By.XPATH, "following-sibling::*")
        job_type = job_type_element.text

Selenium no proporciona un método de utilidad para acceder a los hermanos de un nodo. Lo que puedes hacer en su lugar es utilizar la expresión Xpath following-sibling::*.

Ahora, céntrate en las prestaciones del trabajo. Normalmente, hay más de una:

Para recuperarlos todos, debes inicializar una lista y rellenarla con:

try:
    benefits_element = job_details_element.find_element(By.ID, "benefits")
    benefits = []
    for benefit_element in benefits_element.find_elements(By.TAG_NAME, "li"):
        benefit = benefit_element.text
        benefits.append(benefit)
except NoSuchElementException:
    pass

Por último, obtenga la descripción del puesto sin formato:

Raw job description section on Indeed

Extraiga el texto de la descripción con:

try:
    description_element = job_details_element.find_element(By.ID, "jobDescriptionText")
    description = description_element.text
except NoSuchElementException:
    pass

Rellene el diccionario de empleos y añádalo a la lista de empleos:

job["posted_at"] = posted_at
job["applications"] = applications
job["title"] = title
job["company_name"] = company_name
job["company_rating"] = company_rating
job["company_reviews"] = company_reviews
job["location"] = location
job["location_type"] = location_type
job["apply_link"] = apply_link
job["pay"] = pay
job["job_type"] = job_type
job["benefits"] = benefits
job["description"] = description

jobs.append(job)

También puede añadir una instrucción de registro para verificar que el script funciona según lo esperado:

print(job)

Ejecute el script:

python Scraper.py

Esto producirá un resultado similar al siguiente:

{'posted_at': '17 days ago', 'applications': '50+', 'title': 'Software Support Engineer', 'company_name': 'Integrated DNA Technologies (IDT)', 'company_rating': '3.5', 'company_reviews': '95', 'location': 'New York, NY 10001', 'tipo_ubicación': 'Remoto', 'enlace_solicitud': 'https://www.indeed.com/applystart?jk=c00120130a9c933b&from=vj&pos=bottom&mvj=0&jobsearchTk=1h9fpft0fj3t3800&spon=0&sjdu=YmZE5d5THV8u75cuc0H6Y26AwfY51UOGmh3Z9h4OvXiYhWlsa56nLum9aT96NeA9XAwdulcUk0atwlDdDDqlBQ&vjfrom=tp-semfirstjob&astse=bcf3778ad128bc26&assa=2447', 'pay': '80 000-100 000 $ al año', 'job_type': 'A tiempo completo', 'benefits': ['401(k)', '401(k) matching', 'Seguro dental', 'Seguro médico', 'Permiso parental remunerado', 'Permiso remunerado', 'Permiso parental', 'Seguro oftalmológico'], 'description': «Integrated DNA Technologies (IDT) es el fabricante líder de oligonucleótidos personalizados y tecnologías patentadas para (omitido por brevedad...)»}

¡Et voilà! Acabas de aprender a extraer ofertas de empleo de sitios web.

Paso 9: Extrae varias páginas de ofertas de empleo

Una búsqueda de empleo típica en Indeed produce una lista paginada con docenas de resultados. ¡Vimos cómo extraer cada página!

En primer lugar, inspecciona una página y toma nota de cómo se comporta Indeed. En concreto, muestra el siguiente elemento cuando hay una página siguiente disponible.

Element on Indeed indicating the availability of a next page

De lo contrario, falta el elemento de la página siguiente:

Indeed page missing the 'next page' element

Ten en cuenta que Indeed puede devolver una lista con cientos de ofertas de empleo. Como no quieres que tu script se ejecute indefinidamente, considera la posibilidad de añadir un límite al número de páginas extraídas.

Implementa el rastreo web en Indeed en Selenium con:

pages_scraped = 0
pages_to_scrape = 5
while pages_scraped < pages_to_scrape:
    job_cards = driver.find_elements(By.CSS_SELECTOR, ".cardOutline")

    for job_card in job_cards:
        # scraping logic...

    pages_scraped += 1

    # si esta no es la última página, pasa a la siguiente página
    # de lo contrario, rompe el bucle while
    try:
        next_page_element = driver.find_element(By.CSS_SELECTOR, "a[data-testid=pagination-page-next]")
        next_page_element.click()
    except NoSuchElementException:
        break

El Scraper de Indeed seguirá repitiendo el bucle hasta llegar a la última página o recorrer 5 páginas.

Paso 10: Exportar los datos extraídos a JSON

En este momento, los datos extraídos se almacenan en una lista de diccionarios Python. Exportarlos a JSON para facilitar su intercambio y lectura.

En primer lugar, crea un objeto de salida:

output = {
    "date": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
    "jobs": jobs
}

El atributo de fecha es necesario porque las fechas de publicación de las ofertas de empleo están en el formato «hace <X> días». Sin algún contexto sobre el día en que se extrajeron los datos de los empleos, sería difícil entenderlos.

Recuerde importar datetime:

from datetime import datetime

A continuación, expórtalo con:

import json

# lógica de recopilación...

with open("jobs.json", "w") as file:
    json.dump(output, file, indent=4)

El fragmento anterior inicializa un archivo de salida jobs.json con open() y lo rellena con datos JSON a través de json.dump(). Consulte nuestro artículo para obtener más información sobre cómo realizar el parseo y la serialización de datos a JSON en Python.

El paquete json proviene de la biblioteca estándar de Python, por lo que ni siquiera necesitas instalar una dependencia adicional para lograr el objetivo.

¡Vaya! Has partido de datos de empleo sin procesar contenidos en una página web y ahora tienes datos JSON semiestructurados. Ya estás listo para echar un vistazo al script completo de Indeed Python para el Scraping web.

Paso 11: Ponlo todo junto

Aquí está el archivo scraper.py completo:

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.common import NoSuchElementException
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import random
import time
from datetime import datetime
import json

# configurar una instancia controlable de Chrome
# en modo sin interfaz gráfica
service = Service()
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
driver = webdriver.Chrome(
    service=service,
    options=options
)

# abrir la página de destino  en el navegador
driver.get("https://www.indeed.com/jobs?q=software+engineer&l=New+York%2C+NY&sc=0kf%3Aattr%28DSQF7%29%3B&radius=100")
# establecer el tamaño de la ventana para asegurarse de que las páginas
# no se rendericen en modo responsivo
driver.set_window_size(1920, 1080)

# una estructura de datos donde almacenar las ofertas de empleo
# extraídas de la página
jobs = []

pages_scraped = 0
pages_to_scrape = 3
while pages_scraped < pages_to_scrape:
    # selecciona las tarjetas de ofertas de empleo de la página
    job_cards = driver.find_elements(By.CSS_SELECTOR, ".cardOutline")

    for job_card in job_cards:
        # inicializa un diccionario para almacenar los datos de empleo extraídos
        job = {}

        # inicializar los atributos del trabajo para recopilar
        posted_at = None
        applications = None
        title = None
        company_name = None
        company_rating = None
        company_reviews = None
        location = None
        location_type = None
        apply_link = None
        pay = None
        job_type = None
        benefits = None
        description = None

        # obtener los datos generales del empleo de la tarjeta de resumen
        try:
            date_element = job_card.find_element(By.CSS_SELECTOR, ".date")
            date_element_text = date_element.text
            posted_at_text = date_element_text

            if "•" in date_element_text:
                date_element_text_array = date_element_text.split("•")
                posted_at_text = date_element_text_array[0]
                applications = date_element_text_array[1] 
                    .replace("applications", "") 
                    .replace("en curso", "") 
                    .strip()

posted_at = posted_at_text 
                .replace("Publicado", "") 
                .replace("Empleador", "") 
                .replace("Activo", "") 
                .strip()
excepto NoSuchElementException:
pasar

        # cerrar el modal anti-scraping
        try:
            dialog_element = driver.find_element(By.CSS_SELECTOR, "[role=dialog]")
            close_button = dialog_element.find_element(By.CSS_SELECTOR, ".icl-CloseButton")
            close_button.click()
        except NoSuchElementException:
            pass

        # cargar la tarjeta de detalles del trabajo
        job_card.click()

        # esperar a que se cargue la sección de detalles del trabajo después del clic
        try:
            title_element = WebDriverWait(driver, 5) 
                .until(EC.presence_of_element_located((By.CSS_SELECTOR, ".jobsearch-JobInfoHeader-title")))
            title = title_element.text.replace("n- job post", "")
        except NoSuchElementException:
            continue

        # extraer los detalles del trabajo
        job_details_element = driver.find_element(By.CSS_SELECTOR, ".jobsearch-RightPane")

        try:
            elemento_enlace_empresa = elemento_detalles_empleo.find_element(By.CSS_SELECTOR, "div[data-company-name='true'] a")
nombre_empresa = elemento_enlace_empresa.text
excepto NoSuchElementException:
pasar

intentar:
elemento_calificación_empresa = elemento_detalles_empleo.find_element(By.ID, "companyRatings")
            company_rating = company_rating_element.get_attribute("aria-label").split("out")[0].strip()
company_reviews_element = job_details_element.find_element(By.CSS_SELECTOR, "[data-testid='inlineHeader-companyReviewLink']")
            company_reviews = company_reviews_element.text.replace(" reviews", "")
excepto NoSuchElementException:
pasar

intentar:
company_location_element = job_details_element.find_element(By.CSS_SELECTOR,
                                                                        "[data-testid='inlineHeader-companyLocation']")
            company_location_element_text = company_location_element.text

            location = company_location_element_text

            if "•" in company_location_element_text:
                company_location_element_text_array = company_location_element_text.split("•")
                location = company_location_element_text_array[0]
                tipo_ubicación = empresa_ubicación_elemento_texto_matriz[1]
        excepto NoSuchElementException:
            pasar

        intentar:
            aplicar_enlace_elemento = detalles_trabajo_elemento.encontrar_elemento(By.CSS_SELECTOR, "#applyButtonLinkContainer botón")
            aplicar_enlace = aplicar_enlace_elemento.obtener_atributo("href")
        except NoSuchElementException:
            pass

        for div in job_details_element.find_elements(By.CSS_SELECTOR, "#jobDetailsSection div"):
            if div.text == "Pay":
                pay_element = div.find_element(By.XPATH, "following-sibling::*")
                pay = pay_element.text
            elif div.text == "Job Type":
                job_type_element = div.find_element(By.XPATH, "following-sibling::*")
                job_type = job_type_element.text

        try:
            benefits_element = job_details_element.find_element(By.ID, "benefits")
            benefits = []
            for benefit_element in benefits_element.find_elements(By.TAG_NAME, "li"):
                benefit = benefit_element.text
                benefits.append(benefit)
        except NoSuchElementException:
            pass

        try:
            description_element = job_details_element.find_element(By.ID, "jobDescriptionText")
            descripción = description_element.text
        except NoSuchElementException:
            pass

        # almacenar los datos extraídos
        job["posted_at"] = posted_at
        job["applications"] = applications
        job["title"] = title
        job["company_name"] = company_name
        job["company_rating"] = company_rating
        job["company_reviews"] = company_reviews
        job["location"] = location
        job["location_type"] = location_type
        job["apply_link"] = apply_link
        job["pay"] = pay
        job["job_type"] = job_type
        job["benefits"] = benefits
        job["description"] = description
        jobs.append(job)

        # esperar un número aleatorio de segundos entre 1 y 5
        # para evitar bloqueos por limitación de velocidad
        time.sleep(random.uniform(1, 5))

    # incrementar el contador de scraping
    pages_scraped += 1

    # si no es la última página, ir a la siguiente
    # de lo contrario, romper el bucle while
    try:
        next_page_element = driver.find_element(By.CSS_SELECTOR, "a[data-testid=pagination-page-next]")
        next_page_element.click()
    except NoSuchElementException:
        break

# cerrar el navegador y liberar los recursos
driver.quit()

# produce el objeto de salida
output = {
    "date": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
    "jobs": jobs
}

# expórtalo a JSON
with open("jobs.json", "w") as file:
    json.dump(output, file, indent=4)

En menos de 200 líneas de código, acabas de crear un Scraper web con todas las funciones para extraer datos de empleos de Indeed.

Ejecútalo con:

python Scraper.py

Espera unos minutos a que el script se complete

Al final del proceso de rastreo, aparecerá un archivo jobs.json en la carpeta raíz de tu proyecto. Ábrelo y verás:

{
    "date": "2023-09-02 19:56:44",
    "jobs": [
        {
            "posted_at": "7 days ago",
            "applications": "50+",
            "title": "Ingeniero de software - Todos los niveles",
            "company_name": "Listrak",
            "company_rating": "3",
            "company_reviews": "5",
            "location": "King of Prussia, PA",
            "location_type": "Remoto",
            "apply_link": "https://www.indeed.com/applystart?jk=f27ade40cc1a3686&from=vj&pos=bottom&mvj=0&jobsearchTk=1h9bge7mbhdj0800&spon=0&sjdu=YmZE5d5THV8u75cuc0H6Y26AwfY51UOGmh3Z9h4OvXgPYWebWpM-4nO05Ssl8I8z-BhdrQogdzP3xc9-PmOQTQ&vjfrom=vjs&astse=16430083478063d1&assa=2381",
            "pay": null,
            "job_type": null,
            "benefits": [
                "Gym membership",
                "Paid time off"
            ],
            "description": "Acerca de Listrak: Somos una empresa de SaaS que ofrece una plataforma integrada de marketing digital en la que confían más de 1000 minoristas y marcas líderes para el marketing por correo electrónico y mensajes de texto, la resolución de identidades, los desencadenantes de comportamiento y la coordinación entre canales. Nuestra sede se encuentra en (omitido por brevedad...)"
        },
        // omitido por brevedad...
        {
            «posted_at»: «Hace 9 días»,
            «applications»: null,
            «title»: «Ingeniero de software, Front End (híbrido-remoto)»,
            «company_name»: «Weill Cornell Medicine»,
            «company_rating»: «3,4»,
            "company_reviews": "41",
            "location": "Nueva York, NY 10021",
            "location_type": "Remoto",
            "apply_link": "https://www.indeed.com/applystart?jk=1a53a17f1faeae92&from=vj&pos=bottom&mvj=0&jobsearchTk=1h9bge7mbhdj0800&spon=0&sjdu=YmZE5d5THV8u75cuc0H6Y26AwfY51UOGmh3Z9h4OvXgZADiLYj9Y4htcvtDy_iaWMIfcMu539kP3i1FMxIq2rA&vjfrom=vjs&astse=90a9325429efdf13&assa=4615",
            «salario»: «99 800 $ - 123 200 $ al año»,
            «tipo de empleo»: nulo,
            «beneficios»: nulo,
            «descripción»: «Título: Ingeniero de software, Front End (híbrido-remoto)nTítulo: Ingeniero de software, Front End (híbrido-remoto)nUbicación: Upper East SidenUnidad organizativa: Olivier Elemento LabnDías laborables: de lunes a viernesnEstatus de exención: exentonRango salarial: 99 800,00 $ - 123 200,00 $nAs (omitido por brevedad...)"
        }
}

¡Enhorabuena! ¡Acabas de aprender a extraer datos de Indeed con Python!

Conclusión

En este tutorial, has comprendido por qué Indeed es uno de los mejores portales de empleo de la web y cómo extraer datos de él. En concreto, has visto cómo crear un Scraper en Python que puede recuperar datos de ofertas de empleo de este portal.

Como se muestra aquí, extraer datos de Indeed no es una tarea fácil. El sitio cuenta con una protección anti-scraping que podría bloquear tu script. Cuando se trata de sitios de este tipo, necesitas un navegador controlable que sea capaz de gestionar automáticamente CAPTCHAs, huellas digitales, reintentos automáticos y mucho más. ¡Esto es exactamente lo que ofrece nuestro nuevo Navegador de scraping!

¿No quieres lidiar con el Scraping web, pero te interesan los datos sobre empleos? Explora nuestros conjuntos de datos de Indeed y nuestro conjunto de datos de ofertas de empleo. Regístrate ahora y comienza tu prueba gratuita.

Nota: esta guía ha sido probada exhaustivamente por nuestro equipo en el momento de su redacción, pero dado que los sitios web actualizan con frecuencia su código y estructura, es posible que algunos pasos ya no funcionen como se esperaba.