Publicado originalmente en medium.com

¿Te bloquean al hacer web scraping? 21 técnicas prácticas con código Python para scrapear sin bloqueos: proxies rotativos, cabeceras, huellas TLS y trucos anti-bot.
Tu scraper no falla por tu código.
Falla porque grita "soy un bot" en cada petición.
Si quieres evitar bloqueos al hacer web scraping, primero tienes que entender una cosa: las webs modernas no bloquean scrapers, bloquean señales. Una cabecera que falta. Una IP de un datacenter. Una petición cada 200 milisegundos, perfectamente cronometrada, 24 horas al día.
Si estás:
esta guía es para ti.
Abajo tienes las 21 técnicas que uso para hacer web scraping sin que me bloqueen, agrupadas en cinco bloques, cada una con un pequeño ejemplo en Python que puedes copiar.
mode=auto solo escala cuando hace falta, y el plan gratuito incluye 5.000 créditos al mes.Los sistemas anti-bot como Cloudflare Bot Management, DataDome, Akamai o PerimeterX puntúan cada petición. Cuando la puntuación supera un umbral, recibes un 403 Forbidden, un CAPTCHA o algo peor: una respuesta 200 con datos falsos o vacíos.
Se fijan en cuatro cosas:

Arregla una e ignora las demás, y te seguirán bloqueando.
Ese es el problema real. El bloqueo funciona por capas, así que tu estrategia para evitarlo también tiene que hacerlo.
El User-Agent por defecto python-requests/2.x es la forma más rápida de que te baneen. Usa cadenas de navegadores reales y rótalas.
import random
import requests
USER_AGENTS = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15",
"Mozilla/5.0 (X11; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0",
]
headers = {"User-Agent": random.choice(USER_AGENTS)}
r = requests.get("https://httpbin.io/headers", headers=headers)
print(r.json())
Mantén la lista actualizada. Una versión de Chrome de hace tres años es casi tan sospechosa como no enviar User-Agent. 📖 MDN: cabecera User-Agent
Un navegador real nunca envía solo un User-Agent. Envía Accept, Accept-Language, Accept-Encoding, Sec-Fetch-* y más.
headers = {
"User-Agent": USER_AGENTS[0],
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
}
Truco: abre httpbin.io/headers en tu navegador y luego llámalo desde tu script. Compara las dos salidas. Cada diferencia es una señal.
Si tu User-Agent dice Firefox pero envías cabeceras exclusivas de Chrome como sec-ch-ua, te acabas de delatar. Cada conjunto de cabeceras debe coincidir con el navegador que dice ser.
Una regla sencilla: crea perfiles de cabeceras (uno por navegador) y rota el perfil completo, nunca cabeceras sueltas. 📖 MDN: client hints Sec-CH-UA
Los usuarios reales llegan desde algún sitio. Normalmente Google, u otra página de la misma web.
headers["Referer"] = "https://www.google.com/"
Para páginas profundas, pon como Referer la página de categoría o listado que enlaza a ellas.
Muchas webs ponen cookies en la primera visita y esperan recibirlas de vuelta. Un scraper que nunca devuelve cookies parece mil visitantes nuevos.
session = requests.Session()
session.headers.update(headers)
session.get("https://example.com/") # obtiene las cookies
r = session.get("https://example.com/products") # las devuelve
Aun así, las cabeceras solas no te salvarán de los bloqueos.
Si todas las peticiones salen de la misma IP, lo demás da igual.
Enviar miles de peticiones desde una sola IP es la señal de alarma más antigua del web scraping. Usa un pool de proxies y rota las IPs.
import itertools
PROXIES = [
"http://user:pass@proxy1.example.com:8000",
"http://user:pass@proxy2.example.com:8000",
"http://user:pass@proxy3.example.com:8000",
]
proxy_pool = itertools.cycle(PROXIES)
for url in urls:
proxy = next(proxy_pool)
r = requests.get(url, headers=headers, proxies={"http": proxy, "https": proxy}, timeout=30)
Las IPs de datacenter son baratas y rápidas, y los sistemas anti-bot conocen todos sus rangos. Los proxies residenciales usan IPs de proveedores de internet reales, así que inspiran mucha más confianza.
Usa proxies de datacenter para objetivos fáciles. Pasa a residenciales cuando empieces a ver errores 403 Forbidden.
Scrapear un e-commerce español desde una IP de Singapur resulta raro. Además, puede devolverte precios, monedas o contenido distintos. Elige proxies del país que espera la web.
Los proxies rotativos son geniales hasta que necesitas iniciar sesión, paginar o mantener un carrito. Cambiar de IP a mitad de sesión es una señal clásica de bot. Mantén la misma IP durante todo el flujo y luego rota.
Gestionar tú mismo un pool de proxies residenciales, la geolocalización y las sesiones fijas es un proyecto en sí mismo.
Aquí es donde la mayoría abandona sin hacer ruido.
Olvídate de la fontanería de proxies
ZenRows te da proxies residenciales rotativos (premium_proxy), selección de país (proxy_country) y sesiones fijas (session_id) como simples parámetros de la API.
→ Prueba ZenRows gratis
Machacar un servidor es de mala educación y además se nota. Limita la concurrencia y las peticiones por segundo a cada dominio.
import time
for url in urls:
r = session.get(url)
time.sleep(1.5)
Los humanos son irregulares. Los bots, perfectamente regulares. Un sleep(1) fijo ya es una huella en sí mismo.
time.sleep(random.uniform(1.2, 4.5))
Cuando el servidor te dice "frena", hazle caso. Reintentar al instante solo confirma que eres un bot y hace que bloqueen tu IP antes.
def fetch_with_backoff(url, max_retries=5):
for attempt in range(max_retries):
r = session.get(url, timeout=30)
if r.status_code not in (429, 503):
return r
wait = int(r.headers.get("Retry-After", 2 ** attempt))
time.sleep(wait + random.random())
return r
Revisar el robots.txt te mantiene lejos de las trampas (URLs honeypot) y de las zonas que la web vigila más de cerca. También es la base ética de cualquier proyecto de web scraping.
from urllib.robotparser import RobotFileParser
rp = RobotFileParser("https://example.com/robots.txt")
rp.read()
print(rp.can_fetch("*", "https://example.com/products"))
print(rp.crawl_delay("*"))
Tu tráfico llama menos la atención si lo repartes a lo largo del día, y cargas menos la web si lo haces de noche en su zona horaria. Los trabajos grandes por lotes van ahí.
El comportamiento es donde más rápido mejora cualquier scraper casero.
Pero la capa anti-bot más dura todavía está por llegar.
Chrome headless sin más filtra señales como navigator.webdriver = true. Usa parches stealth para ocultarlas.
# pip install playwright playwright-stealth
# playwright install chromium
from playwright.sync_api import sync_playwright
from playwright_stealth import Stealth
with Stealth().use_sync(sync_playwright()) as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://example.com")
print(page.title())
browser.close()
Esta sorprende a mucha gente. Aunque tengas cabeceras perfectas, el handshake TLS de requests en Python no se parece al de Chrome. Los sistemas anti-bot lo comprueban (huellas JA3/JA4).
curl_cffi imita a un navegador real a nivel TLS:
# pip install curl_cffi
from curl_cffi import requests as cffi_requests
r = cffi_requests.get("https://example.com", impersonate="chrome")
print(r.status_code)
Muchas webs cargan los datos con JavaScript, y algunos desafíos anti-bot exigen ejecutar JS para superarlos. Pero un navegador headless es lento y caro. Úsalo en las páginas que lo necesitan, no en todas.
Si ves CAPTCHAs, el sistema anti-bot ya desconfía de ti. Los servicios de resolución funcionan, pero son lentos y cobran por cada CAPTCHA. Corregir las señales de las técnicas 1–17 sale más barato que resolver el CAPTCHA que provocan.
Los navegadores stealth y la imitación TLS funcionan.
Hasta que el proveedor anti-bot saca una actualización y vuelves a estar depurando un viernes por la noche.
Abre DevTools → Network → Fetch/XHR y recarga la página. Muchas webs cargan sus datos desde un endpoint JSON. Llamarlo directamente es más rápido, más limpio y más ligero que parsear HTML.
r = session.get("https://example.com/api/products?page=1")
data = r.json()
La petición que no envías no puede hacer que te bloqueen. Guarda las respuestas, sáltate las URLs que ya has scrapeado y refresca solo lo que ha cambiado.
# pip install requests-cache
import requests_cache
session = requests_cache.CachedSession("scrape_cache", expire_after=3600)
Las técnicas 1 a 18 son un trabajo a tiempo completo: proxies rotativos, perfiles de cabeceras, imitación TLS, navegadores headless, gestión de CAPTCHAs, reintentos. Y todo se rompe cada vez que los proveedores anti-bot cambian sus reglas.
Una API de web scraping mete todo eso detrás de una sola petición.
Después de probar varias opciones, ZenRows es la que uso cuando una web se defiende. Estos son los motivos:
**mode=auto**) empieza con la configuración más barata y solo escala a renderizado JS o proxies premium cuando hace falta. Solo pagas la configuración que funciona. Docspremium_proxy=true y proxy_country (por ejemplo es, us, de). Docsjs_render=true) junto con wait_for y js_instructions para páginas dinámicas.session_id (cualquier número del 1 al 99999), que mantiene la misma IP durante 10 minutos. DocsSec-Ch-Ua, Sec-Fetch-* y similares, así que siempre encajan con la huella.response_type=markdown) para pipelines con LLMs.Esa única técnica cubre la mayoría de las otras 20.

Vamos a usar la página de pruebas de ZenRows, scrapingcourse.com/antibot-challenge, protegida por un desafío anti-bot. Primero, la versión casera:
import requests
url = "https://www.scrapingcourse.com/antibot-challenge"
r = requests.get(url, headers={"User-Agent": USER_AGENTS[0]}, timeout=30)
print("Status:", r.status_code)
print("Bypassed:", "You bypassed the Antibot challenge" in r.text)
Recibirás la página del desafío en lugar del contenido:
Bypassed: False
Ahora con ZenRows y Adaptive Stealth Mode:
# pip install requests
import requests
params = {
"url": "https://www.scrapingcourse.com/antibot-challenge",
"apikey": "YOUR_ZENROWS_API_KEY",
"mode": "auto",
}
r = requests.get("https://api.zenrows.com/v1/", params=params, timeout=180)
print("Bypassed:", "You bypassed the Antibot challenge" in r.text)
print("Cost (USD):", r.headers.get("X-Request-Cost"))
Bypassed: True
Cost (USD): 0.025
Sin lista de proxies. Sin perfiles de cabeceras. Sin parches stealth.
La cabecera X-Request-Cost muestra lo que ha costado la petición (0,001 USD = 1 crédito), así ves cuándo la API ha tenido que escalar. Dos detalles de la referencia de la API: usa un timeout de cliente de 180 segundos, porque las páginas protegidas pueden tardar, y no envíes js_render ni premium_proxy junto con mode=auto. El modo automático gestiona esos dos por ti.
¿Necesitas control en lugar de automatización? Pon tú los parámetros:
params = {
"url": "https://www.scrapingcourse.com/ecommerce/",
"apikey": "YOUR_ZENROWS_API_KEY",
"js_render": "true",
"premium_proxy": "true",
"proxy_country": "es",
"wait_for": ".product",
"css_extractor": '{"names": ".product-name", "prices": ".price", "links": "a.woocommerce-LoopProduct-link @href"}',
}
r = requests.get("https://api.zenrows.com/v1/", params=params, timeout=180)
print(r.json())
Algunas reglas que conviene saber:
wait_for (y wait) solo funcionan con js_render=true.proxy_country requiere premium_proxy=true.css_extractor devuelve JSON: si hay una coincidencia llega como texto, si hay varias como lista. Añade @href o @src para obtener atributos. DocsZenRows gestiona las cabeceras de navegador, pero puedes enviar tu propio Referer o Cookie. Activa custom_headers y pásalas con el argumento headers= de siempre:
params = {
"url": "https://httpbin.io/anything",
"apikey": "YOUR_ZENROWS_API_KEY",
"custom_headers": "true",
}
headers = {"Referer": "https://www.google.com/"}
r = requests.get("https://api.zenrows.com/v1/", params=params, headers=headers, timeout=180)
print(r.text)
Cabeceras como User-Agent o Sec-Fetch-* se ignoran aquí a propósito, para que nunca contradigan la huella del navegador. Docs
# pip install zenrows
from zenrows import ZenRowsClient
client = ZenRowsClient("YOUR_ZENROWS_API_KEY")
response = client.get("https://www.scrapingcourse.com/ecommerce/", params={
"js_render": True,
"premium_proxy": True,
"proxy_country": "us",
})
print(response.text)
A partir de aquí puedes construir:

❓ ¿Cómo evito que me bloqueen al hacer web scraping?
✅ Combina varias técnicas: cabeceras reales de navegador, proxies residenciales rotativos, pausas aleatorias con backoff ante errores 429 y una huella TLS que coincida con la de un navegador real. Si la web usa una protección anti-bot seria, una API de web scraping como ZenRows se encarga de esas capas por ti.
❓ ¿Por qué recibo un error 403 Forbidden al hacer web scraping?
✅ Un 403 suele significar que el sistema anti-bot ha marcado tu petición. Las causas más comunes son el User-Agent por defecto de Python, cabeceras de navegador que faltan, una IP de datacenter o una huella TLS que no es de navegador. Arregla primero las cabeceras y luego pasa a proxies residenciales.
❓ ¿Es legal el web scraping?
✅ Extraer datos públicos suele ser legal en muchas jurisdicciones, pero depende de los datos, de los términos de la web y de la legislación local (los datos personales bajo el RGPD, por ejemplo). No scrapees contenido tras un login sin permiso, respeta el robots.txt y consulta con un abogado si el proyecto es comercial.
❓ ¿Cuántas peticiones por segundo puedo enviar sin que me bloqueen?
✅ No hay un número universal. Empieza con una petición cada 1–3 segundos por dominio con variación aleatoria, vigila las respuestas 429 y ajusta. Los proxies rotativos te permiten repartir el volumen, pero no hacen invisible un scraping agresivo.
❓ ¿Necesito proxies residenciales para hacer web scraping?
✅ No siempre. Los proxies de datacenter funcionan bien en muchas webs. Necesitas residenciales cuando el objetivo usa una protección anti-bot seria (Cloudflare, DataDome, Akamai) o bloquea los rangos de IP de datacenter.
❓ ¿Hay alguna API de web scraping gratuita?
✅ ZenRows tiene un plan gratuito con 5.000 créditos al mes y sin tarjeta de crédito. Una petición estándar cuesta 1 crédito, con renderizado JS 5, con proxies premium 10, y con ambos 25. Ver precios.
Evitar bloqueos no va de un truco ingenioso.
Va de no darle al sistema anti-bot ni una sola razón para dudar de ti. Puedes construirlo tú mismo, capa a capa, o dejar las capas más difíciles a una API de web scraping y centrarte en los datos.
Deja de pelearte con los sistemas anti-bot
Proxies residenciales rotativos, renderizado JS y bypass anti-bot en una sola petición. 5.000 créditos gratis al mes, sin tarjeta.
→ Empieza a scrapear con ZenRowsMás guías prácticas sobre Python, APIs y automatización
Tutoriales, herramientas y flujos de trabajo reales para desarrolladores.
→ Visita kevinmeneses.com
¿Buscas contenido técnico para tu empresa? Puedo ayudarte · LinkedIn · kevinmenesesgonzalez@gmail.com