Cómo extraer datos de Indeed con Python (y evitar los errores 403)

Última actualización: April 16, 2026

En algún punto, más o menos por quincuagésima vez que copié y pegué un puesto de Indeed en una hoja de cálculo, empecé a cuestionarme mis decisiones profesionales. Si alguna vez intentaste extraer datos estructurados de Indeed de forma programática, ya sabes cuál es el chiste: el error 403 no es un fallo, es una función del sistema de defensa de Indeed.

Indeed es el portal de empleo más grande del mundo, con unos , en un momento dado y presencia en . Eso lo convierte en una de las fuentes más ricas de datos del mercado laboral del planeta, y también en una de las más difíciles de raspar. El extractor de código abierto JobFunnel (con miles de estrellas en GitHub) fue literalmente en diciembre de 2025 después de años perdiendo la guerra contra los bots. En palabras del propio mantenedor: "All users can scrape some jobs, but are quickly hit by captcha, and the scraping fails, yielding no jobs." Otro colaborador informó haber recibido un CAPTCHA . Así que sí: este no es un objetivo sencillo. En esta guía te mostraré todos los métodos prácticos para extraer datos de Indeed con Python, cómo sobrevivir de verdad al muro de los 403 y, para quienes prefieran saltarse por completo la depuración, una alternativa sin código con .

¿Qué significa extraer datos de Indeed con Python?

El web scraping, en esencia, consiste en extraer automáticamente datos estructurados de páginas web. Cuando hablamos de extraer datos de Indeed con Python, nos referimos a escribir un script que visite las páginas de resultados y de detalle de vacantes de Indeed, lea el HTML subyacente (o los datos incrustados) y saque campos como el título del puesto, la empresa, la ubicación, el salario y la descripción en un formato utilizable: CSV, una base de datos o una hoja de Google.

Las bibliotecas de Python más habituales para esto son Requests (para llamadas HTTP), BeautifulSoup (para analizar HTML) y Selenium o Playwright (para automatización del navegador). Pero Indeed no es un sitio estático sencillo. Es un híbrido: HTML renderizado en servidor con un bloque JSON incrustado, todo protegido por Cloudflare Bot Management. Eso significa que tu extractor tiene que lidiar con contenido renderizado con JavaScript, nombres de clase CSS que cambian constantemente y protecciones anti-bot agresivas, y todo eso antes de parsear ni un solo título de empleo.

Además, en 2026 no existe una API oficial, gratuita y de solo lectura para Indeed. La antigua Publisher Jobs API quedó obsoleta alrededor de 2020, y lo que queda está orientado solo a empleadores (Job Sync, Sponsored Jobs). Así que las opciones reales son raspar el sitio o pagar a un proveedor externo de datos.

¿Por qué extraer datos de ofertas de Indeed?

La razón de negocio es sencilla: revisar manualmente miles de anuncios es poco práctico, y los datos que contienen son realmente valiosos.

indeed_stats_dca2a43cec.png

Caso de usoQuién se beneficiaEjemplo
Generación de leadsEquipos de ventas y reclutamientoCrear listas de empresas que están contratando con datos de contacto
Investigación del mercado laboralAnalistas, equipos de RR. HH.Identificar habilidades en tendencia y rangos salariales por región
Inteligencia competitivaEmpleadores, agencias de staffingVigilar patrones de contratación y ofertas salariales de la competencia
Automatización personal de búsqueda de empleoCandidatosAgrupar ofertas que cumplan tus criterios en distintas ubicaciones
Datos de entrenamiento para modelos de MLCientíficos de datosCrear modelos de predicción salarial a partir de datos históricos

La propia investigación de Indeed Hiring Lab que los datos de publicaciones siguen de cerca el BLS JOLTS y pueden servir como un proxy casi en tiempo real de las condiciones del mercado laboral en EE. UU. Los fondos de cobertura usan la velocidad de publicación de vacantes como señal de datos alternativos. Los equipos de RR. HH. comparan compensaciones usando rangos salariales extraídos. Y los reclutadores construyen listas de prospectos a partir de empresas que están contratando activamente.

Un detalle importante: los datos salariales en Indeed han mejorado, pero siguen incompletos. A mediados de 2025, alrededor del incluían información salarial, pero solo cerca del mostraban una cifra exacta; el resto eran rangos. Cualquier análisis salarial basado en datos de Indeed debería tener en cuenta esa falta de cobertura.

Cómo elegir tu método para extraer datos de Indeed con Python

No existe una única forma “correcta” de raspar Indeed. La mejor opción depende de tu nivel, de la cantidad de datos que necesites y de cuánto mantenimiento estés dispuesto a asumir. He probado los cuatro enfoques principales y así se comparan:

CriterioBS4 + RequestsSeleniumJSON oculto (window.mosaic)Sin código (Thunderbit)
DificultadPrincipianteIntermedioIntermedio-avanzadoNinguna (2 clics)
VelocidadRápidoLento (renderizado del navegador)RápidoRápido (scraping en la nube)
Contenido renderizado por JSNoSí (datos incrustados)
Resistencia anti-botBajaMedia (detectable)Media-altaAlta (gestionada automáticamente)
Mantenimiento cuando cambia el HTMLAlto (se rompen selectores)AltoMedio (la estructura JSON es más estable)Ninguno (la IA se adapta)
Mejor paraPrototipos rápidosPáginas dinámicas, contenido con loginGrandes volúmenes de datos estructuradosUsuarios no técnicos, resultados rápidos

Esta guía recorre cada método. Si eres desarrollador Python, te interesarán las secciones de BS4, JSON oculto y Selenium. Si no programas —o simplemente estás cansado de depurar 403—, salta directamente a la sección de Thunderbit.

Antes de empezar

  • Dificultad: Principiante a intermedio (secciones de Python); ninguna (sección Thunderbit)
  • Tiempo necesario: ~20–60 minutos para configurar Python y hacer el primer scraping; ~2 minutos con Thunderbit
  • Lo que necesitas: Python 3.9+, un editor de código, navegador Chrome y, para la ruta sin código, la

Preparar tu entorno de Python para extraer datos de Indeed

Antes de escribir código de scraping, deja listo tu entorno.

Instala las bibliotecas necesarias

Crea un entorno virtual e instala los paquetes que vas a usar:

1python -m venv indeed_env
2source indeed_env/bin/activate  # En Windows: indeed_env\Scripts\activate
3# Para el enfoque HTTP + parsing
4pip install requests beautifulsoup4 lxml httpx
5# Para el enfoque de JSON oculto (recomendado)
6pip install curl_cffi parsel tenacity
7# Para el enfoque de automatización del navegador
8pip install selenium

Algunas notas:

  • curl_cffi es en 2026 la opción por defecto para raspar sitios protegidos por Cloudflare. Emula huellas TLS reales del navegador, algo que requests y httpx no pueden hacer por sí solos. Más adelante verás por qué esto importa en la sección anti-bot.
  • Selenium 4.6+ incluye Selenium Manager, así que ya no hace falta descargar manualmente ChromeDriver: el binario del navegador se gestiona automáticamente.
  • Usa lxml como backend de parsing para BeautifulSoup. Es aproximadamente que el html.parser de la biblioteca estándar.

Crea la estructura de tu proyecto

Manténlo simple:

1indeed_scraper/
2├── scraper.py
3├── requirements.txt
4└── output/

Todos los ejemplos de código de abajo se basan en scraper.py.

Cómo extraer datos de Indeed con Python usando BeautifulSoup

Este es el enfoque apto para principiantes: usar requests para obtener la página y BeautifulSoup para analizar el HTML. Es el más rápido de configurar, pero también el más frágil en Indeed.

Paso 1: construye la URL de búsqueda de Indeed

Las URLs de búsqueda de Indeed siguen un patrón predecible:

1https://www.indeed.com/jobs?q=<consulta>&l=<ubicación>&start=<desplazamiento>

Por ejemplo, buscar "data analyst" en "Austin, TX" desde la primera página:

1from urllib.parse import urlencode
2params = {
3    "q": "data analyst",
4    "l": "Austin, TX",
5    "start": 0,
6}
7url = f"https://www.indeed.com/jobs?{urlencode(params)}"
8print(url)
9# https://www.indeed.com/jobs?q=data+analyst&l=Austin%2C+TX&start=0

Indeed pagina en incrementos de 10, con un máximo duro de 1.000 resultados (start &lt;= 990). Cualquier valor superior a 990 devuelve silenciosamente la misma página.

Paso 2: envía una solicitud HTTP con cabeceras correctas

Indeed bloquea de inmediato las solicitudes con el user-agent por defecto de Python. Necesitas cabeceras realistas:

1import requests
2headers = {
3    "User-Agent": (
4        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
5        "(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"
6    ),
7    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
8    "Accept-Language": "en-US,en;q=0.9",
9    "Accept-Encoding": "gzip, deflate, br",
10    "Referer": "https://www.indeed.com/",
11}
12response = requests.get(url, headers=headers, timeout=30)
13print(response.status_code)

Si recibes un 200, por ahora has entrado. Si recibes un 403, Cloudflare te ha detectado. (Más adelante veremos cómo sobrevivir a eso.)

Paso 3: analiza las ofertas desde el HTML

Usa BeautifulSoup para seleccionar los elementos de las tarjetas de empleo. Prioriza los atributos data-testid; suelen ser más estables que los nombres de clase CSS aleatorios de Indeed:

1from bs4 import BeautifulSoup
2soup = BeautifulSoup(response.text, "lxml")
3cards = soup.find_all("div", attrs={"data-testid": "slider_item"})
4jobs = []
5for card in cards:
6    title_el = card.find("h2", class_="jobTitle")
7    title = title_el.get_text(strip=True) if title_el else None
8    company = card.find(attrs={"data-testid": "company-name"})
9    location = card.find(attrs={"data-testid": "text-location"})
10    link = title_el.find("a")["href"] if title_el and title_el.find("a") else None
11    jobs.append({
12        "title": title,
13        "company": company.get_text(strip=True) if company else None,
14        "location": location.get_text(strip=True) if location else None,
15        "url": f"https://www.indeed.com{link}" if link else None,
16    })
17print(f"Encontrados {len(jobs)} empleos")

Paso 4: maneja la paginación

Recorre las páginas incrementando el parámetro start:

1import time, random
2all_jobs = []
3for page in range(0, 50, 10):  # Primeras 5 páginas
4    params["start"] = page
5    url = f"https://www.indeed.com/jobs?{urlencode(params)}"
6    response = requests.get(url, headers=headers, timeout=30)
7    # ... analizar como arriba ...
8    all_jobs.extend(jobs)
9    time.sleep(random.uniform(3, 6))

Limitaciones de este enfoque

Voy a ser claro: BS4 + Requests es el método más débil para Indeed en 2026. requests usa la biblioteca TLS estándar de Python, lo que genera una que Cloudflare identifica al instante como “no es un navegador”. Además, no soporta HTTP/2, que sí sirve Indeed. Lo normal es que te bloqueen tras unas pocas páginas. ¿Y los selectores CSS? Indeed rota nombres de clase como css-1m4cuuf y jobsearch-JobComponent-embeddedBody-1n0gh5s , así que cualquier selector basado en ellos es una bomba de tiempo.

Usa este método solo para prototipos rápidos en una sola página. Para algo a mayor escala, mejor el enfoque de JSON oculto.

Cómo extraer datos de Indeed con Python usando JSON oculto

Este es el método que recomiendo para la mayoría de desarrolladores Python. En lugar de analizar elementos HTML frágiles, extraes datos estructurados de una variable JavaScript incrustada en el código fuente de Indeed: window.mosaic.providerData["mosaic-provider-jobcards"].

Cada campo que te interesa —título, empresa, ubicación, salario, clave de la oferta, fecha de publicación, indicador de remoto— ya está dentro de ese bloque JSON. No hace falta ejecutar JavaScript. El esquema ha sido , así que es mucho más resistente que los selectores del DOM.

Paso 1: obtén el HTML de la página

Usa curl_cffi en lugar de requests: emula huellas TLS reales del navegador, algo clave para sobrevivir a Cloudflare:

1from curl_cffi import requests as cffi_requests
2response = cffi_requests.get(
3    "https://www.indeed.com/jobs?q=python+developer&l=Remote&start=0",
4    impersonate="chrome124",
5    headers={
6        "Accept-Language": "en-US,en;q=0.9",
7        "Referer": "https://www.indeed.com/",
8    },
9    timeout=30,
10)
11print(response.status_code, len(response.text))

¿Por qué curl_cffi? Es una implementación en Python sobre curl-impersonate, que reproduce exactamente el ClientHello TLS, el frame HTTP/2 SETTINGS y el orden de cabeceras de un navegador real. Es el único cliente HTTP de Python que, de forma activa, derrota en una sola llamada. Entre los objetivos compatibles de impersonación están chrome120, chrome124, chrome131, Safari y variantes de Edge.

Paso 2: extrae el JSON con una expresión regular

El bloque JSON está incrustado dentro de una etiqueta <script>. Sácalo con una regex:

1import re, json
2MOSAIC_RE = re.compile(
3    r'window\.mosaic\.providerData\["mosaic-provider-jobcards"\]=(\{.+?\});',
4    re.DOTALL,
5)
6match = MOSAIC_RE.search(response.text)
7if match:
8    data = json.loads(match.group(1))
9    results = data["metaData"]["mosaicProviderJobCardsModel"]["results"]
10    print(f"Encontrados {len(results)} empleos en el JSON oculto")
11else:
12    print("No se encontró el JSON oculto: posible bloqueo o cambio en la página")

Paso 3: extrae los campos de cada empleo desde el JSON

Cada elemento en results contiene más datos de los que se ven en pantalla:

1jobs = []
2for job in results:
3    jobs.append({
4        "jobkey": job["jobkey"],
5        "title": job["title"],
6        "company": job.get("company"),
7        "location": job.get("formattedLocation"),
8        "remote": job.get("remoteLocation"),
9        "salary": (job.get("salarySnippet") or {}).get("text"),
10        "posted": job.get("formattedRelativeTime"),
11        "job_type": job.get("jobTypes"),
12        "easy_apply": job.get("indeedApplyEnabled"),
13        "url": f"https://www.indeed.com/viewjob?jk={job['jobkey']}",
14    })

El JSON suele incluir estimaciones salariales, atributos taxonómicos (etiquetas de habilidades) y valoraciones de empresa que no siempre aparecen en el HTML renderizado.

Paso 4: extrae varias páginas

Usa tierSummaries en el JSON para entender el total de resultados y luego recorre las páginas:

1import time, random
2all_jobs = []
3for start in range(0, 50, 10):  # Primeras 5 páginas
4    url = f"https://www.indeed.com/jobs?q=python+developer&l=Remote&start={start}&sort=date"
5    response = cffi_requests.get(
6        url,
7        impersonate="chrome124",
8        headers={"Accept-Language": "en-US,en;q=0.9", "Referer": "https://www.indeed.com/"},
9        timeout=30,
10    )
11    match = MOSAIC_RE.search(response.text)
12    if match:
13        data = json.loads(match.group(1))
14        results = data["metaData"]["mosaicProviderJobCardsModel"]["results"]
15        all_jobs.extend([{
16            "jobkey": j["jobkey"],
17            "title": j["title"],
18            "company": j.get("company"),
19            "location": j.get("formattedLocation"),
20            "salary": (j.get("salarySnippet") or {}).get("text"),
21            "url": f"https://www.indeed.com/viewjob?jk={j['jobkey']}",
22        } for j in results])
23    time.sleep(random.uniform(3, 7))
24print(f"Total: {len(all_jobs)} empleos extraídos")

Por qué el JSON oculto es más resistente

La estructura window.mosaic.providerData cambia con mucha menos frecuencia que los nombres de clase CSS. Obtienes datos limpios y estructurados sin tener que pelearte con HTML confuso. Aun así, necesitas mitigación anti-bot —cabeceras, pausas y proxies—, y eso lo veremos a continuación.

Cómo extraer datos de Indeed con Python usando Selenium

Selenium es el enfoque de automatización del navegador. Es útil cuando necesitas interactuar con la página: hacer clic en paneles de detalle, manejar contenido detrás de login o extraer descripciones cargadas dinámicamente que no aparecen en el HTML inicial.

Cuándo usar Selenium en lugar de clientes HTTP

  • Indeed carga parte del contenido de forma dinámica (descripciones completas en el panel lateral derecho)
  • Necesitas raspar páginas que requieren sesión o inicio de sesión
  • Harás scraping a pequeña escala y la velocidad no es crítica

Guía rápida

1from selenium import webdriver
2from selenium.webdriver.common.by import By
3from selenium.webdriver.chrome.options import Options
4import time
5options = Options()
6options.add_argument("--disable-blink-features=AutomationControlled")
7# options.add_argument("--headless=new")  # El modo headless es más detectable; úsalo con cautela
8driver = webdriver.Chrome(options=options)
9driver.get("https://www.indeed.com/jobs?q=data+engineer&l=New+York")
10time.sleep(3)
11cards = driver.find_elements(By.CSS_SELECTOR, "[data-testid='slider_item']")
12for card in cards:
13    try:
14        title = card.find_element(By.CSS_SELECTOR, "h2.jobTitle").text
15        company = card.find_element(By.CSS_SELECTOR, "[data-testid='company-name']").text
16        location = card.find_element(By.CSS_SELECTOR, "[data-testid='text-location']").text
17        print(f"{title} | {company} | {location}")
18    except Exception:
19        continue
20driver.quit()

Limitaciones

Selenium es lento: cada página requiere renderizado completo del navegador. Chrome en modo headless es (Cloudflare revisa navigator.webdriver, cadenas del vendor de WebGL, conteo de plugins y más). Incluso undetected-chromedriver solo retrasa la detección; no la evita para siempre. Y, al igual que con BS4, tus selectores se romperán cuando Indeed cambie su interfaz.

Para la mayoría de casos, el enfoque de JSON oculto te da los mismos datos más rápido y con menos mantenimiento. Reserva Selenium para situaciones límite en las que de verdad necesites un navegador.

Cómo evitar errores 403 al extraer datos de Indeed con Python

Esta es la parte más importante. Si llegaste aquí desde una búsqueda frustrada en Google, estás en el lugar correcto.

indeed_antibot_374d080ff4.png

Por qué Indeed bloquea tu extractor

Indeed usa , no DataDome ni PerimeterX. Las cabeceras de respuesta lo confirman: server: cloudflare, cf-ray y la cookie de gestión de bots __cf_bm. Cloudflare inspecciona tu huella TLS (JA3/JA4), el orden de las cabeceras HTTP/2, los patrones de solicitud y señales de comportamiento del navegador. Si algo de eso parece no humano, recibirás un 403, un 429, un 503 o —el caso más traicionero— un 200 OK con una página de desafío Turnstile en lugar de datos reales de empleo.

Rota el User-Agent y las cabeceras de solicitud

Un único User-Agent estático es la forma más rápida de acabar bloqueado. Rota entre varios strings actuales y realistas. Importante: los campos de versión menor de Chrome están desde la reducción del User-Agent, así que no inventes versiones menores no nulas o los sistemas anti-bot lo detectarán.

1import random
2USER_AGENTS = [
3    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
4    "(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
5    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
6    "(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
7    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
8    "(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.3800.97",
9    "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
10    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 "
11    "(KHTML, like Gecko) Version/17.4 Safari/605.1.15",
12]
13headers = {
14    "User-Agent": random.choice(USER_AGENTS),
15    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
16    "Accept-Language": "en-US,en;q=0.9",
17    "Accept-Encoding": "gzip, deflate, br, zstd",
18    "Referer": "https://www.indeed.com/",
19    "Sec-Fetch-Dest": "document",
20    "Sec-Fetch-Mode": "navigate",
21    "Sec-Fetch-Site": "same-origin",
22}

Asegúrate también de que tus Client Hints sec-ch-ua coincidan con la versión del User-Agent. Un sec-ch-ua: "Chrome";v="131" junto a un User-Agent que dice Chrome 145 es una bandera roja instantánea.

Añade retrasos aleatorios entre solicitudes

Los intervalos fijos activan la detección por patrones. Usa variación aleatoria:

1import time, random
2# Entre cada solicitud
3time.sleep(random.uniform(3, 6))
4# En reintentos tras un bloqueo
5def backoff_sleep(attempt):
6    base = 4
7    sleep_time = base * (2 ** attempt) + random.uniform(0, 2)
8    time.sleep(min(sleep_time, 60))

El consenso práctico de y es dejar 3–6 segundos entre solicitudes por IP, con un límite duro de unas 100 solicitudes por IP y sesión antes de rotar.

Usa rotación de proxies

Este es el factor que más determina el éxito. Los proxies de centros de datos en rangos de AWS/GCP tienen una tasa de éxito de aproximadamente 5–15% contra objetivos protegidos por Cloudflare Enterprise; en Indeed, eso es prácticamente inutilizable. Los proxies residenciales, junto con la huella TLS correcta, pueden subir a un 80–95% de éxito.

1PROXIES = [
2    "http://user:pass@us.residential.example:7777",
3    "http://user:pass@us.residential.example:7778",
4    "http://user:pass@us.residential.example:7779",
5]
6proxy = random.choice(PROXIES)
7response = cffi_requests.get(
8    url,
9    impersonate="chrome124",
10    headers=headers,
11    proxies={"https": proxy},
12    timeout=30,
13)

En 2026, el precio de los proxies residenciales ronda los , según el proveedor y el compromiso contratado. Para Indeed, conviene empezar con un pool pequeño e ir escalando según sea necesario.

Maneja correctamente los códigos de estado 403, 429 y 503

No reintentes a ciegas. Cada código significa algo distinto:

1def fetch_with_retry(url, proxy_pool, max_retries=5):
2    for attempt in range(max_retries):
3        proxy = random.choice(proxy_pool)
4        headers["User-Agent"] = random.choice(USER_AGENTS)
5        try:
6            r = cffi_requests.get(
7                url,
8                impersonate=random.choice(["chrome124", "chrome120", "edge101"]),
9                headers=headers,
10                proxies={"https": proxy},
11                timeout=30,
12            )
13            # Comprueba el caso sutil de “200 con desafío”
14            if r.status_code == 200 and "cf-turnstile" not in r.text and "Just a moment" not in r.text:
15                return r
16            if r.status_code == 403:
17                print(f"403: bloqueado. Rotando proxy, intento {attempt + 1}")
18            elif r.status_code == 429:
19                print(f"429: demasiadas solicitudes. Bajando el ritmo.")
20            elif r.status_code == 503:
21                print(f"503: servidor saturado o desafío JS.")
22            backoff_sleep(attempt)
23        except Exception as e:
24            print(f"Error en la solicitud: {e}")
25            backoff_sleep(attempt)
26    raise RuntimeError(f"Falló tras {max_retries} reintentos: {url}")

El caso de “200 con desafío” es el más engañoso. Revisa siempre el cuerpo de la respuesta en busca de marcadores como cf-turnstile o Just a moment antes de considerar un 200 como éxito.

La alternativa más sencilla: deja que Thunderbit gestione el anti-bot

Para quienes no quieren crear y mantener pools de proxies, rotación de cabeceras y suplantación de huellas TLS, el scraping en la nube de maneja automáticamente CAPTCHAs, rotación de proxies y protecciones anti-bot. Sin configuración de proxies, sin curl_cffi, sin bibliotecas para resolver CAPTCHA. Es la ruta de menor fricción cuando solo necesitas los datos.

Por qué tu scraper de Indeed sigue rompiéndose (y cómo arreglarlo)

El muro 403 es el dolor agudo. El dolor crónico es el mantenimiento: scrapers que funcionan hoy se rompen la semana siguiente y, sin avisar, devuelven datos vacíos o resultados desactualizados.

Cómo Indeed rompe tus selectores

Indeed rota los nombres de clase CSS de forma agresiva. La guía de Bright Data que clases como css-1m4cuuf y css-1rqpxry “parecen generadas aleatoriamente, probablemente en tiempo de compilación”. Las pruebas A/B hacen que distintas sesiones vean diseños diferentes al mismo tiempo. Y la reestructuración del DOM ocurre sin aviso.

La historia de JobFunnel es bastante ilustrativa. Un colaborador informó: "CaptchaBuster has successfully mitigated the captcha, and the reason for still unsuccessfully scraping the page [is] due to outdated beautiful soup selectors." El extractor no estaba bloqueado: estaba leyendo los elementos equivocados.

Estrategia: prioriza el JSON oculto frente al parsing del DOM

El bloque window.mosaic.providerData ha mantenido un esquema estable desde al menos 2023. La ruta metaData.mosaicProviderJobCardsModel.results[] sigue siendo en 2026. Los selectores del DOM se rompen cada mes. La extracción desde JSON se rompe, como mucho, una vez al año.

Estrategia: usa atributos de datos en lugar de nombres de clase

Cuando tengas que tocar el DOM, apunta a atributos funcionales:

SelectorPropósito
[data-testid="slider_item"]Contenedor de cada tarjeta de empleo
[data-testid="job-title"] o h2.jobTitle > aEnlace del título del puesto
[data-testid="company-name"]Nombre del empleador
[data-testid="text-location"]Texto de la ubicación
data-jk="<jobkey>" en cada tarjetaEl ancla más estable: sin cambios desde 2019

Añade comprobaciones para detectar selectores obsoletos

Nunca dejes que tu scraper siga funcionando en silencio con cero resultados. Añade una comprobación después de cada descarga:

1results = parse_hidden_json(html)
2assert len(results) &gt; 0, (
3    f"Indeed devolvió un conjunto vacío en start={start} — "
4    "posible bloqueo, CAPTCHA o deriva de selectores. "
5    f"Primeros 500 caracteres de la respuesta: {html[:500]}"
6)

Registra los primeros 500–2000 caracteres de la respuesta en bruto cuando haya fallos. Así podrás saber enseguida si recibiste un desafío Turnstile, un muro de inicio de sesión o un cambio de esquema. Ejecuta una prueba de humo diaria a nivel de CI con una consulta fija (por ejemplo, q=python&l=remote) que valide que hay resultados no nulos.

La alternativa con IA: scrapers que no se rompen

La IA de Thunderbit vuelve a leer la estructura de la página cada vez; no depende de selectores codificados ni de patrones regex. Cuando Indeed cambia su HTML, Thunderbit se adapta automáticamente. Eso ataca directamente el problema de mantenimiento que tantos usuarios mencionan como su mayor frustración. Si alguna vez te despertaste con un mensaje en Slack diciendo “el scraper volvió a devolver filas vacías”, sabes el valor de no tener que arreglarlo tú mismo.

Extraer datos de Indeed sin escribir Python: la alternativa sin código

Todas las guías competidoras asumen que vas a escribir código Python. Pero los datos de los foros cuentan otra historia. Los usuarios dicen cosas como "it's just so difficult with constant bugs and errors" y algunos sugieren contratar a alguien en Fiverr solo para obtener los datos. Si eso te suena familiar, esta sección es tu salida.

Cómo extraer datos de Indeed con Thunderbit, paso a paso

Paso 1: Instala la desde Chrome Web Store. Es gratis para empezar.

Paso 2: Abre una página de resultados de Indeed en tu navegador; por ejemplo, https://www.indeed.com/jobs?q=data+analyst&l=Austin%2C+TX.

Paso 3: Haz clic en el icono de Thunderbit en la barra del navegador y luego en "AI Suggest Fields". La IA de Thunderbit analiza la página y detecta automáticamente columnas como título del puesto, empresa, ubicación, salario, URL del empleo y fecha de publicación. Puedes revisar y ajustar los campos sugeridos: quitar columnas que no necesites o añadir campos personalizados describiendo en lenguaje natural lo que quieres.

Paso 4: Haz clic en "Scrape". Thunderbit extrae los datos de la página y los muestra en una tabla estructurada. Verás filas de vacantes con los campos que configuraste.

Enriquecer con scraping de subpáginas

Después de extraer la lista principal, haz clic en "Scrape Subpages" para que Thunderbit visite cada página individual de detalle de empleo. Recupera descripciones completas, requisitos, beneficios y enlaces de solicitud, sin configuración adicional. Es el equivalente a escribir un segundo scraper en Python para visitar cada URL /viewjob?jk=<jobkey>, solo que aquí se hace con un clic.

Manejar la paginación automáticamente

Thunderbit gestiona automáticamente la paginación basada en clics de Indeed. No hace falta construir manualmente URLs con offsets ni escribir bucles de paginación. Va pasando de página y agregando los resultados.

Exporta a tus herramientas favoritas

Exporta los datos extraídos a CSV, Excel, Google Sheets, Airtable o Notion — . No necesitas escribir código con csv.writer() ni pandas.to_csv().

Cuándo usar Python y cuándo usar Thunderbit

EscenarioMejor herramienta
Canales de datos personalizados, automatización programada con cron/AirflowPython
Integración dentro de una base de código mayorPython
Lógica de parsing altamente personalizadaPython
Investigación puntual o análisis de mercadoThunderbit
Miembros del equipo sin perfil técnico necesitan los datosThunderbit
Obtener los datos ahora mismo sin depurar 403Thunderbit
Enriquecimiento de subpáginas sin configuraciónThunderbit

Comparación de tiempo: configurar Python + depurar el anti-bot = horas o días, especialmente la primera vez. Thunderbit = menos de 2 minutos para obtener los mismos datos. No digo que Python esté mal; digo que depende de lo que necesites.

Ninguna de las guías mejor posicionadas sobre scraping de Indeed aborda la legalidad, lo cual sorprende dado lo mucho que aparece la pregunta "Is scraping Indeed legal?" en los foros. Esto no es asesoramiento legal, pero aquí tienes el panorama.

Términos de servicio de Indeed

Los Términos de uso de Indeed () no incluyen una cláusula general de “prohibido raspar”. La única prohibición explícita de automatización está en la sección A.3.5, que prohíbe "use of any automation, scripting, or bots to automate the Indeed Apply process". Eso está limitado al flujo de aplicación, no a la lectura pasiva de ofertas públicas. La principal herramienta de cumplimiento de Indeed es técnica: desafíos de Cloudflare, bloqueos de IP, huellas de dispositivo, no un pleito en tribunales.

Precedentes legales relevantes

El caso estadounidense más citado es hiQ Labs v. LinkedIn. El Noveno Circuito que raspar datos públicamente accesibles “probablemente no viola la CFAA” (Computer Fraud and Abuse Act). Sin embargo, más tarde se consideró que hiQ porque sus empleados habían creado perfiles falsos en LinkedIn y aceptado los Términos.

Más recientemente, Meta v. Bright Data (N.D. Cal., enero de 2024) dejó una decisión todavía más clara. El juez Chen que los Términos de Facebook e Instagram “no prohíben el scraping de datos públicos estando desconectado”. Meta retiró voluntariamente el resto de las reclamaciones al mes siguiente.

robots.txt de Indeed

El de Indeed prohíbe de forma amplia /jobs/ y /job/ para el User-agent: * por defecto, pero permite explícitamente que Googlebot y Bingbot accedan a /viewjob?, las páginas individuales de detalle de empleo. Los rastreadores de entrenamiento de IA (GPTBot, CCBot, anthropic-ai) están fuertemente restringidos. robots.txt no es jurídicamente vinculante en EE. UU., pero respetarlo es una buena práctica y una prueba de buena fe.

Guías prácticas para un scraping responsable

  • Extrae solo datos públicamente disponibles: nunca inicies sesión, nunca crees cuentas falsas
  • Respeta los límites de velocidad: 1 solicitud cada 3–6 segundos por IP, concurrencia de un solo dígito
  • No republicar los datos extraídos como si fueran tu propio portal de empleo
  • Usa los datos para investigación personal o interna, no para reventa comercial sin permiso
  • Elimina o hashea la PII que no necesites; establece un límite de retención para datos cercanos a información personal
  • Si operas a gran escala o en la UE/Reino Unido, consulta con un abogado: las obligaciones de transparencia del artículo 14 del RGPD se aplican a datos personales extraídos

El espectro de riesgo: la automatización personal de búsqueda de empleo está en el extremo bajo. La reventa comercial a gran escala de los datos de Indeed está en el extremo alto.

Conclusión y puntos clave

Extraer datos de Indeed con Python es posible, pero no es un proyecto de fin de semana que configuras y olvidas. La protección Cloudflare de Indeed, los selectores cambiantes y las medidas anti-bot agresivas hacen que tengas que abordar esto con las herramientas adecuadas y expectativas realistas.

Lo que me llevaría de todo esto es lo siguiente:

  • Indeed es la fuente más rica de datos del mercado laboral en la web — 350 millones de visitas mensuales, 130 millones de ofertas — pero pelea con fuerza contra los extractores.
  • La extracción desde JSON oculto (window.mosaic.providerData) es el enfoque de Python más resistente. El esquema lleva años estable, mientras que los selectores CSS se rompen cada mes.
  • curl_cffi con impersonación de navegador es el cliente HTTP por defecto en 2026 para sitios protegidos por Cloudflare. requests y httpx se bloquean solo por la huella TLS.
  • Usa siempre cabeceras rotativas, retrasos aleatorios y proxies residenciales para evitar los errores 403. Los proxies de centros de datos son prácticamente inútiles contra Cloudflare Enterprise.
  • Añade comprobaciones por assertions para saber al instante cuándo se rompen los selectores o cuándo te están sirviendo una página de desafío en lugar de datos reales.
  • Para usuarios no técnicos o cualquiera que solo quiera resultados rápidos, ofrece una vía sin código, impulsada por IA, que se adapta automáticamente a los cambios del sitio: sin proxies, sin depuración, sin mantenimiento.

Si quieres probar la ruta sin código, para que lo pruebes en Indeed sin compromiso. Y si vas por la ruta de Python, los ejemplos de código anteriores son un buen punto de partida; solo recuerda tratar la resistencia anti-bot como una prioridad de primer nivel, no como algo secundario.

Para saber más sobre enfoques y herramientas de web scraping, consulta nuestras guías sobre , y . También puedes ver tutoriales en el .

Prueba Thunderbit para extraer datos de Indeed más rápido

Preguntas frecuentes

¿Qué bibliotecas de Python son mejores para extraer datos de Indeed?

Para solicitudes HTTP, curl_cffi es la opción más sólida en 2026: emula huellas TLS reales del navegador, algo esencial para sortear Cloudflare. httpx con HTTP/2 es una alternativa razonable para objetivos menos protegidos. Para analizar HTML, BeautifulSoup4 con lxml sigue siendo el estándar. Para automatización del navegador, Playwright (con playwright-stealth) o undetected-chromedriver funcionan, aunque ambos son cada vez más detectables. El enfoque de regex sobre JSON oculto (window.mosaic.providerData) evita por completo la necesidad de hacer parsing pesado.

¿Por qué sigo recibiendo errores 403 al extraer datos de Indeed?

Indeed usa Cloudflare Bot Management, que inspecciona tu huella TLS (JA3/JA4), el orden de las cabeceras HTTP/2, los patrones de solicitud y el comportamiento del navegador. Si usas requests a secas, tu huella TLS te delata de inmediato como script de Python; el 403 llega antes incluso de que se lean tus cabeceras. Solución: cambia a curl_cffi con impersonación de navegador, rota User-Agent realistas, añade retrasos aleatorios (3–6 segundos) y usa proxies residenciales. Revisa también el caso de “200 con desafío Turnstile”: busca marcadores cf-turnstile en el cuerpo de la respuesta.

¿Puedo extraer datos de Indeed sin programar?

Sí. Herramientas como te permiten extraer ofertas de Indeed en unos pocos clics: instala la extensión de Chrome, abre una página de búsqueda de Indeed, haz clic en "AI Suggest Fields" y luego en "Scrape". La IA de Thunderbit detecta automáticamente campos como título, empresa, ubicación y salario. Gestiona la paginación, el enriquecimiento de subpáginas (descripciones completas) y las protecciones anti-bot de forma automática. Exporta a CSV, Google Sheets, Airtable o Notion gratis.

¿Con qué frecuencia cambia la estructura HTML de Indeed?

Indeed rota con regularidad los nombres de clase CSS (por ejemplo, css-1m4cuuf, cadenas hash aleatorias) y reestructura elementos del DOM sin previo aviso. Las pruebas A/B hacen que distintos usuarios vean diseños diferentes simultáneamente. El enfoque de JSON oculto (window.mosaic.providerData) es mucho más estable: el esquema se ha mantenido consistente desde al menos 2023. Cuando tengas que usar selectores del DOM, prioriza los atributos data-testid y data-jk (clave del empleo) antes que las clases CSS.

El scraping de URLs públicas de Indeed sin iniciar sesión probablemente no genere responsabilidad bajo la CFAA en EE. UU., a la luz de la sentencia hiQ v. LinkedIn (2022) del Noveno Circuito y la decisión Meta v. Bright Data (2024). Los Términos de Indeed prohíben específicamente automatizar el proceso Apply, no la lectura pasiva de ofertas públicas. Aun así, haz scraping de forma responsable: no inicies sesión, no crees cuentas falsas, respeta los límites de velocidad, no republicar los datos como si fueran tu propio portal y maneja con cuidado cualquier dato personal (nombres de reclutadores, correos) bajo GDPR/CCPA. Para operaciones de escala comercial, consulta a un abogado.

Más información

Fawad Khan
Fawad Khan
Fawad writes for a living, and honestly, he kind of loves it. He's spent years figuring out what makes a line of copy stick — and what makes readers scroll past. Ask him about marketing, and he'll talk for hours. Ask him about carbonara, and he'll talk longer.
Tabla de contenidos

Prueba Thunderbit

Extrae leads y otros datos en solo 2 clics. Impulsado por IA.

Consigue Thunderbit Es gratis
Extrae datos usando IA
Transfiere datos fácilmente a Google Sheets, Airtable o Notion
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week