Saltar al contenido
Empezar gratis
Volver al blog

Buenas prácticas en el scraping web: una guía práctica sobre el ciclo de vida con consejos y código funcional

Anda MiuțescuÚltima actualización el 35 min read
Buenas prácticas en el scraping web: una guía práctica sobre el ciclo de vida con consejos y código funcional
En resumen: Las buenas prácticas de web scraping son los hábitos que mantienen un scraper legal, respetuoso, preciso y barato de ejecutar. Redacta un contrato de datos, prefiere APIs o JSON antes que HTML, sigue robots.txt tal como lo define la RFC 9309, regula el ritmo por host, respeta Retry-After y valida cada registro antes de guardarlo. Esta guía las aplica a cada etapa del ciclo de vida de un scraper, con fragmentos de Python que funcionan.

Las mejores prácticas de web scraping son los hábitos que garantizan que un scraper sea legal, respetuoso, preciso y económico de mantener: planifica los datos que necesitas, respeta el archivo robots.txt y las condiciones de uso del sitio web, regula el ritmo de las solicitudes por servidor, gestiona los errores 429 con retrasos, analiza de forma defensiva, valida cada registro, supervisa cada ejecución, comprueba el progreso y realiza un seguimiento del coste por registro válido.

La guía que figura a continuación aplica estas buenas prácticas de scraping web a cada etapa del ciclo de vida de un scraper, desde la planificación inicial hasta la factura mensual, con un breve fragmento de código en Python siempre que el código ayude a aclarar el concepto.

Los fragmentos de código requieren Python 3.10 o una versión posterior y dos bibliotecas:

pip install requests beautifulsoup4

Guarda cada fragmento con el nombre de archivo que aparece encima y ejecútalo desde esa carpeta, por ejemplo con python polite_fetch.py.

Etapa

Objetivo

Práctica clave

Sección

Plan

Saber cómo se define «terminado»

Contrato de datos más orden de las fuentes

Planificación

Legal y educado

Mantén la bienvenida

RFC 9309 robots.txt, User-Agent honesto

Scraping legal y respetuoso

Frecuencia

No sobrecargues

Limitación por host, Retry-After, backoff

Tasa de solicitudes

Solicitudes

Menos fallos

Tiempos de espera, sesiones, códigos 304

Hábitos de solicitud

Bloqueos

Recupera con calma

Diagnosticar y, a continuación, escalar un nivel

Bloqueos

Renderización

Paga solo cuando sea necesario

Primero JSON, luego el navegador

Navegadores sin interfaz gráfica

Análisis sintáctico

Supera los rediseños

Hooks estables, soluciones alternativas, bytes de entrada

Analizadores

Calidad de los datos

Confía en el resultado

Esquema, motivos de rechazo, deduplicación

Calidad de los datos

Supervisión

Aviso previo

Métricas de ejecución, pruebas canario, códigos de salida

Supervisión

Escalabilidad

Crecer sin caos

Frontier, puntos de control transaccionales

Escalabilidad

Coste

Mantén la rentabilidad

Coste por registro válido

Coste

¿Cuáles son las mejores prácticas más importantes en materia de web scraping?

Si solo vas a adoptar diez hábitos, que sean estos: redacta un contrato de datos, da prioridad a las API y al formato JSON frente al HTML, respeta correctamente el archivo robots.txt, identifica tu bot, regula la frecuencia de acceso a cada servidor, respeta el atributo Retry-After, establece tiempos de espera, valida los registros antes de guardarlos, avisa cuando las páginas estén vacías o contengan poca información, y guarda puntos de control con los datos que describen.

Cada una de estas diez prácticas recomendadas para el web scraping incluye un enlace a la sección donde se explica.

  1. Redacta un contrato de datos que establezca los campos, los tipos, una clave única y la actualidad de los datos antes de que exista ningún código.
  2. Da preferencia a una API oficial o al propio JSON de la página en lugar de analizar el HTML.
  3. Aplica el archivo robots.txt tal y como lo define el RFC 9309, no como lo haga tu biblioteca.
  4. Identifica tu bot con un User-Agent veraz y una dirección de contacto.
  5. Regula el ritmo de cada servidor por separado, empezando despacio y adaptándote al servidor.
  6. Respeta el Retry-After y retrocede exponencialmente con jitter.
  7. Establece tiempos de espera de conexión y lectura en cada solicitud.
  8. Valida los registros antes de guardarlos y conserva los motivos de cada rechazo.
  9. Avisa cuando las páginas estén vacías o contengan poca información, no solo en caso de fallos del sistema.
  10. Confirma los puntos de control junto con los datos que describen, para que un fallo nunca deje una página a medio guardar.

¿Cómo se debe planificar un proyecto de web scraping?

Planifica partiendo de los datos. Anota los campos, los tipos, la actualidad y el volumen que necesitas; a continuación, elige la fuente fiable más económica. Clasifica las técnicas de web scraping por coste: primero una API oficial, luego el JSON que ya carga la página, después los mapas del sitio para el descubrimiento, a continuación el HTML estático y, por último, un navegador sin interfaz gráfica solo como último recurso. Presupuesta las necesidades antes de escribir código.

Si aún estás decidiendo si realizar el scraping o no, empieza por las preguntas que debes responder antes de escribir un scraper.

Redacta primero un contrato de datos

Un contrato de datos es una breve tabla que especifica exactamente lo que debe proporcionar el rastreador. Aquí tienes uno para un monitor de precios de libros:

Campo

Tipo

Obligatorio

Regla

url

cadena (absoluta)

sí

clave única

título

cadena

sí

no vacía

precio

decimal

sí

> 0

moneda

Código ISO 4217

sí

p. ej., GBP

en_stock

booleano

sí

a partir del texto de disponibilidad

valoración

entero 1–5

no

se permite el valor nulo

fecha_de_recuperación

marca de tiempo UTC

sí

establecida por el programa de extracción

El monitor necesita datos actualizados a diario, a razón de unas 1.000 filas por ejecución.

El analizador cumple este requisito, la validación lo garantiza y la supervisión emite una alerta cuando una ejecución no alcanza el nivel requerido.

Elige la fuente fiable más económica

Revisa esta lista y detente en la primera fuente que cuente con todos los campos de tu contrato:

  1. Una API oficial. Cuando un sitio web ofrece una, extraer datos de una API es mejor que analizar el HTML: obtienes nombres de campos estables, límites documentados y condiciones de uso explícitas.
  2. El JSON que la página ya carga. Abre DevTools, filtra la pestaña «Red» por «Fetch/XHR» y vuelve a cargar la página. Muchos sitios web recogen sus datos en formato JSON antes de mostrarlos en pantalla.
  3. Mapas del sitio, para el descubrimiento. Utiliza los mapas del sitio en formato XML para descubrir las URL en lugar de rastrear cada enlace. Sus lastmod fechas también indican qué páginas han cambiado desde tu última ejecución.
  4. HTML estático. Recupera y analiza la página cuando los datos se encuentren en su código fuente.
  5. Una página renderizada. Ejecuta un navegador sin interfaz gráfica solo cuando los datos no existan en ningún otro sitio.

Cada paso hacia abajo supone más solicitudes, más código de análisis y más errores cuando el sitio cambia.

Aquí tienes el paso 2 en la práctica: la versión en JavaScript de quotes.toscrape.com carga sus cotizaciones desde un punto final JSON paginado, por lo que nunca se toca el HTML.

json_endpoint.py

"""Prefer the JSON a page already loads over parsing its HTML."""
import requests

url = "https://quotes.toscrape.com/api/quotes"
page, quotes = 1, []
while True:
    data = requests.get(url, params={"page": page}, timeout=(5, 30),
                        headers={"User-Agent": "acme-quotes-bot/0.1 (+mailto:data@example.com)"}).json()
    quotes += [(q["text"], q["author"]["name"]) for q in data["quotes"]]
    if not data["has_next"]:
        break
    page += 1
print(len(quotes), "quotes from", page, "JSON pages; first:", quotes[0][1])
  • /api/quotes?page=N es la misma solicitud que realiza la página en el navegador, tal y como aparece en la pestaña «Red».
  • has_next Es la condición de parada, por lo que nunca tienes que adivinar cuál es la última página ni recuperar páginas vacías.
  • No hay ningún analizador de HTML, porque los campos llegan con nombre y tipo definidos.

Calcular el presupuesto antes de escribir el código

Un presupuesto aproximado muestra si el plan es siquiera viable: páginas × frecuencia ÷ tasa permitida = duración de la ejecución.

Supongamos que necesitas 10 000 páginas de productos al día y que el sitio tolera una solicitud cada dos segundos. Eso son 10 000 × 2 = 20 000 segundos, o unas 5,6 horas de rastreo al día. Si eso no encaja, necesitas menos páginas, un rastreo incremental o una fuente con más registros por solicitud. Rastrear más rápido no es una opción, ya que el sitio establece la tasa.

Conserva las respuestas sin procesar durante unos días

Almacena el código HTML o JSON sin procesar que obtengas durante unos días, comprimido e indexado por URL y hora de obtención. De este modo, si un selector deja de funcionar o aparece un nuevo campo, bastará con volver a analizar las copias almacenadas, en lugar de volver a rastrear el sitio web.

Extrae únicamente datos públicos que tengas motivos legítimos para recopilar, lee las condiciones de uso del sitio y mantente alejado de las áreas que requieran inicio de sesión, a menos que se te permita el acceso. Recupera el archivo robots.txt y aplícalo tal y como define la RFC 9309, identifica a tu bot con una URL de contacto, evita los datos personales que no necesites y nunca te hagas pasar por un rastreador de un motor de búsqueda.

¿Cómo funciona realmente la coincidencia con el archivo robots.txt?

El archivo robots.txt es un estándar formal desde 2022, cuando la IETF publicó el RFC 9309, el Protocolo de Exclusión de Robots. Las reglas que importan:

  • Busca primero tu grupo. Los grupos comienzan con User-agent líneas. Tu rastreador sigue el grupo que especifica su token de producto, y el * grupo solo se aplica si ninguna de ellas lo hace.
  • Prevalece la coincidencia más larga. Cuando varias reglas coinciden con una ruta, la más específica (la que tiene más caracteres) es la que prevalece.
  • En caso de empate, prevalece la regla que permita el acceso. Si una Allow y una Disallow regla coinciden con la misma longitud, la ruta se permite.
  • Dos caracteres especiales. * coincide con cualquier secuencia de caracteres y $ marca el final de la ruta, por lo que Disallow: /*.pdf$ bloquea todas las URL cuya ruta termine en .pdf.
  • Los códigos de estado cambian el significado. Un error 4xx en robots.txt significa que un rastreador puede acceder a todo. Un error 5xx o una solicitud fallida significa que debe asumirse una prohibición total.
  • El almacenamiento en caché tiene un límite. No sigas utilizando una copia en caché durante más de 24 horas.
  • El «crawl-delay» no figura en el estándar y Google lo ignora. Respétalo de todos modos cuando esté presente, ya que indica lo que desea el propietario del sitio.

El RFC también indica que estas reglas no constituyen una forma de autorización de acceso: el archivo es una solicitud publicada por el propietario del sitio, no un bloqueo.

Por qué tu analizador de robots.txt puede no ajustarse al estándar

urllib.robotparser, de Python, es la opción obvia para los rastreadores en Python, pero comprueba las reglas en el orden en que aparecen en el archivo y se detiene en la primera coincidencia en lugar de elegir la más larga. Además, trata las rutas de las reglas como prefijos simples, por lo que * y $ no tienen ningún significado especial.

El fragmento de código que aparece a continuación procesa un pequeño archivo robots.txt con ambos enfoques y muestra sus resultados uno al lado del otro. El «fetcher» educado importa posteriormente su parse_robots() y rfc9309_allowed() .

robots_match.py

"""robots.txt per RFC 9309: pick the group for your user agent, then the
longest matching rule wins, Allow wins ties, '*' matches any characters and
'$' anchors the end of the path."""
import re
import urllib.robotparser


def parse_robots(text, user_agent):
    """Return (rules, crawl_delay) for the group that applies to user_agent."""
    groups, current, in_agent_lines = [], None, False
    for raw in text.splitlines():
        line = raw.split("#", 1)[0].strip()
        if ":" not in line:
            continue
        key, value = (part.strip() for part in line.split(":", 1))
        key = key.lower()
        if key == "user-agent":
            if not in_agent_lines:  # a new group starts
                current = {"agents": [], "rules": [], "delay": None}
                groups.append(current)
            current["agents"].append(value.lower())
            in_agent_lines = True
            continue
        in_agent_lines = False
        if current is None:
            continue
        if key in ("allow", "disallow"):
            current["rules"].append((key, value))
        elif key == "crawl-delay":  # not in RFC 9309, but a clear signal
            try:
                current["delay"] = float(value)
            except ValueError:
                pass
    token = user_agent.split("/")[0].lower()  # product token, e.g. "acme-price-monitor"
    matched = [g for g in groups if token in g["agents"]] or \
              [g for g in groups if "*" in g["agents"]]
    rules = [rule for g in matched for rule in g["rules"]]
    delay = next((g["delay"] for g in matched if g["delay"] is not None), None)
    return rules, delay


def _pattern(rule_path):
    regex = re.escape(rule_path).replace(r"\*", ".*")
    if regex.endswith(r"\$"):
        regex = regex[:-2] + "$"
    return re.compile(regex)


def rfc9309_allowed(rules, path):
    """rules: list of ("allow"|"disallow", path) for the group that applies to you."""
    best_len, verdict = -1, True  # no matching rule means allowed
    for kind, rule_path in rules:
        if not rule_path:
            continue  # an empty Disallow allows everything
        if _pattern(rule_path).match(path):
            length = len(rule_path)
            if length > best_len or (length == best_len and kind == "allow"):
                best_len, verdict = length, kind == "allow"
    return verdict


if __name__ == "__main__":
    robots_txt = """User-agent: *
Disallow: /catalogue/
Allow: /catalogue/category/
Disallow: /*.pdf$

User-agent: acme-price-monitor
Disallow: /checkout/
Crawl-delay: 2
"""
    stdlib = urllib.robotparser.RobotFileParser()
    stdlib.parse(robots_txt.splitlines())
    rules, _ = parse_robots(robots_txt, "mybot/1.0")
    for path in ["/catalogue/category/books_1/index.html", "/catalogue/item_1.html",
                 "/files/report.pdf", "/files/report.pdf?v=2", "/index.html"]:
        print(f"{path:42} RFC 9309: {str(rfc9309_allowed(rules, path)):5}  "
              f"urllib.robotparser: {stdlib.can_fetch('mybot', path)}")
    rules, delay = parse_robots(robots_txt, "acme-price-monitor/1.2 (+https://example.com/bot)")
    print("acme-price-monitor group:", rules, "crawl-delay:", delay)
    print("/catalogue/item_1.html for acme-price-monitor:", rfc9309_allowed(rules, "/catalogue/item_1.html"))

Lo que muestra la comparación:

  • /files/report.pdf está permitido por urllib.robotparser, aunque Disallow: /*.pdf$ lo prohíba según la norma, ya que el analizador de la biblioteca estándar lee el * tal cual.
  • /catalogue/category/books_1/index.html queda bloqueado por urllib.robotparser, ya que Disallow: /catalogue/ aparece primero en el archivo. Según el estándar, el más largo Allow: /catalogue/category/ prevalece y la página se permite.
  • acme-price-monitor tiene su propio grupo, que sustituye al * grupo por completo. Para ese bot, /catalogue/item_1.html se permite con un retraso de rastreo de dos segundos, ya que ninguna de las * reglas se aplican.

Utiliza un comparador que cumpla con los estándares o, al menos, comprueba tu analizador con las reglas específicas de las que dependes.

Condiciones de servicio, inicios de sesión y datos personales

Unas pocas reglas cubren la mayoría de las situaciones:

  • Lee primero las condiciones. Iniciar sesión suele implicar aceptarlas, y muchas prohíben el acceso automatizado, así que trata las áreas autenticadas como zonas prohibidas sin permiso.
  • Recopila el mínimo de datos personales, o ninguno. Omite los nombres, correos electrónicos o detalles del perfil que no figuren en tu contrato de datos; las leyes de protección de datos, como el RGPD, se aplican a los datos personales, los hayas recopilado o no.
  • No eludas los muros de pago, los inicios de sesión ni los CAPTCHA. Son señales de que el propietario quiere limitar el acceso.

Se trata de hábitos de trabajo, no de asesoramiento jurídico. Para conocer los detalles legales, consulta nuestra guía sobre si el web scraping es legal.

Identifica tu scraper de forma honesta

Utiliza un User-Agent estable que indique el nombre de tu producto, su versión y una forma de ponerse en contacto contigo:

acme-price-monitor/1.2 (+https://example.com/bot; data@example.com)

El propietario de un sitio web que detecte un tráfico inusual procedente de esa cadena podrá ponerse en contacto contigo en lugar de bloquear un rango de IP, y los autores del archivo robots.txt obtendrán un token del producto al que dirigirse.

Nunca envíes un User-Agent de Googlebot. Google documenta cómo los propietarios de sitios web pueden verificar sus rastreadores mediante búsquedas DNS directas e inversas o sus rangos de IP publicados, por lo que uno falso no superará la comprobación y parecerá engañoso una vez detectado.

¿A qué velocidad puedes extraer datos sin sobrecargar un sitio web?

No existe una velocidad segura universal. Empieza con una o dos solicitudes simultáneas por servidor y una pausa de aproximadamente un segundo; después, deja que el servidor marque el ritmo: reduce la velocidad cuando aumente la latencia, detente y espera cuando recibas un error 429 o 503, respeta el Retry-After y retrocede exponencialmente ante las fluctuaciones.

Asigna un presupuesto por host, no por rastreador

Diez trabajadores que hagan una pausa cortés cada uno pueden seguir enviando diez solicitudes paralelas a un dominio. Controla el ritmo por donde pasa cada solicitud a un host: un objeto «fetcher» o un limitador compartido entre máquinas.

Deja que el servidor marque el ritmo

Un aumento de la latencia significa que el servidor está trabajando más, así que amplía el retraso antes de que empiecen a producirse errores. Programa los rastreos de gran envergadura para las horas de menor actividad del sitio web, normalmente por la noche en la zona horaria de su mercado principal.

Gestiona correctamente los códigos 429 y Retry-After

El código 429 «Too Many Requests» (RFC 6585) indica que se han enviado demasiadas solicitudes en un periodo de tiempo determinado, mientras que el 503 significa que el servidor está sobrecargado o fuera de servicio por mantenimiento. Ambos pueden incluir el encabezado «Retry-After», y la semántica HTTP (RFC 9110) permite dos formatos para su valor:

Retry-After: 120
Retry-After: Fri, 09 Oct 2026 07:30:00 GMT

El primero es en segundos y el segundo, una fecha HTTP; un rastreador que solo admita uno de ellos se bloqueará o ignorará el encabezado cuando se encuentre con el otro. Considera el valor como un tiempo de espera mínimo.

Retrasa el tiempo de espera de forma exponencial, con variación

Cuando no haya un «Retry-After», o si un reintento vuelve a fallar, espera más tiempo cada vez:

wait = min(cap, base × 2^attempt) × random(0.5 to 1.5)

Utiliza el mayor de entre ese tiempo de espera y cualquier valor de «Retry-After». El factor aleatorio es la variación: sin ella, los trabajadores que han fallado al mismo tiempo volverán a intentarlo juntos y saturarán el servidor en una oleada sincronizada.

Reintenta solo lo que se pueda reintentar

Reintenta las respuestas 429, 500, 502, 503 y 504, así como los tiempos de espera de red en solicitudes GET idempotentes. No reintentes un 404, ya que la página no va a volver, ni insistas con un 403, ya que la misma solicitud obtiene la misma respuesta. Limita los intentos y registra cada abandono para que las URL atascadas aparezcan en tus métricas.

Un recuperador «educado» en una sola clase

La PoliteFetcher clase que se muestra a continuación combina esta sección con la siguiente, importando parse_robots() y rfc9309_allowed() del fragmento de robots.txt, así que guarda ambos archivos en una misma carpeta.

polite_fetch.py

"""A polite, resilient fetcher: robots.txt, per-host pacing, timeouts,
retries with backoff and jitter, Retry-After, and conditional requests."""
import email.utils
import random
import time
from urllib.parse import urlsplit

import requests

from robots_match import parse_robots, rfc9309_allowed  # Snippet 2

USER_AGENT = "acme-price-monitor/1.2 (+https://example.com/bot; data@example.com)"
RETRYABLE = {429, 500, 502, 503, 504}
ROBOTS_TTL = 24 * 3600  # RFC 9309: don't trust a cached copy for more than 24 h


class PoliteFetcher:
    def __init__(self, min_delay=1.0, max_retries=4, timeout=(5, 30)):
        self.session = requests.Session()
        self.session.headers["User-Agent"] = USER_AGENT
        self.min_delay, self.max_retries, self.timeout = min_delay, max_retries, timeout
        self.robots, self.last_hit, self.validators = {}, {}, {}

    def _robots(self, host):
        cached = self.robots.get(host)
        if cached and time.monotonic() - cached[0] < ROBOTS_TTL:
            return cached[1], cached[2]
        try:
            resp = self.session.get(host + "/robots.txt", timeout=self.timeout)
        except requests.RequestException:
            resp = None
        if resp is None or resp.status_code >= 500:
            rules, delay = [("disallow", "/")], None  # unreachable: assume full disallow
        elif resp.status_code >= 400:
            rules, delay = [], None  # unavailable (e.g. 404): no restrictions
        else:
            rules, delay = parse_robots(resp.text, USER_AGENT)
        self.robots[host] = (time.monotonic(), rules, delay)
        return rules, delay

    def allowed(self, url):
        parts = urlsplit(url)
        rules, _ = self._robots(f"{parts.scheme}://{parts.netloc}")
        path = (parts.path or "/") + (f"?{parts.query}" if parts.query else "")
        return rfc9309_allowed(rules, path)

    def _pace(self, host):
        _, crawl_delay = self._robots(host)
        delay = max(self.min_delay, crawl_delay or 0)
        wait = self.last_hit.get(host, 0) + delay - time.monotonic()
        if wait > 0:
            time.sleep(wait)
        self.last_hit[host] = time.monotonic()

    @staticmethod
    def _retry_after(value):
        if not value:
            return None
        if value.isdigit():
            return int(value)
        when = email.utils.parsedate_to_datetime(value)  # HTTP-date form
        return max(0, when.timestamp() - time.time())

    def get(self, url):
        if not self.allowed(url):
            raise PermissionError(f"robots.txt disallows {url}")
        parts = urlsplit(url)
        host = f"{parts.scheme}://{parts.netloc}"
        headers = {}
        etag, modified = self.validators.get(url, (None, None))
        if etag:
            headers["If-None-Match"] = etag
        if modified:
            headers["If-Modified-Since"] = modified
        for attempt in range(self.max_retries + 1):
            self._pace(host)
            try:
                resp = self.session.get(url, headers=headers, timeout=self.timeout)
            except (requests.ConnectionError, requests.Timeout):
                resp = None
            if resp is not None and resp.status_code not in RETRYABLE:
                if resp.status_code == 200:
                    self.validators[url] = (resp.headers.get("ETag"),
                                            resp.headers.get("Last-Modified"))
                return resp  # 200, 304, or a non-retryable error for the caller
            if attempt == self.max_retries:
                break
            backoff = min(60, 2 ** attempt) * random.uniform(0.5, 1.5)
            server_hint = self._retry_after(resp.headers.get("Retry-After")) if resp is not None else None
            time.sleep(max(backoff, server_hint or 0))
        raise RuntimeError(f"gave up on {url} after {self.max_retries + 1} attempts")


if __name__ == "__main__":
    f = PoliteFetcher(min_delay=1.0, max_retries=2)
    for url in ["https://books.toscrape.com/", "https://httpbin.org/etag/abc123",
                "https://httpbin.org/etag/abc123", "https://httpbin.org/status/404"]:
        r = f.get(url)
        print(r.status_code, len(r.content), "bytes", url)
    for url in ["https://httpbin.org/deny", "https://httpbin.org/status/503"]:
        try:
            f.get(url)
        except (PermissionError, RuntimeError) as exc:
            print("expected:", exc)

Las líneas clave:

  • _robots() recoge el archivo robots.txt con tu propio User-Agent, interpreta un código 4xx como «sin restricciones» y un 5xx o un error de red como «no acceder», y vuelve a recogerlo una vez que la copia en caché tenga 24 horas de antigüedad.
  • _pace() Aplica el retraso por host y utiliza el Crawl-delay del sitio cuando este es mayor que tu propio mínimo.
  • RETRYABLE Limita los reintentos a los límites de frecuencia y a los errores del servidor. Un error 404 se devuelve directamente al solicitante.
  • _retry_after() gestiona tanto el formato en segundos como el de fecha HTTP, y el bucle permanece inactivo durante el tiempo que sea mayor entre el retardo de retroceso y la indicación del servidor.

La demostración que aparece al final obtiene un 304 en una solicitud repetida, devuelve un 404 sin volver a intentarlo, rechaza una ruta prohibida por robots antes de enviarla y desiste ante un 503 permanente tras agotar el número máximo de intentos.

¿Qué hábitos de solicitud evitan la mayoría de los fallos de scraping?

La mayoría de los fallos se deben a solicitudes descuidadas, no a defensas ingeniosas. Establece siempre tiempos de espera de conexión y lectura, reutiliza una sesión por host, envía un conjunto de encabezados coherente y honesto, acepta la compresión, omite las imágenes y fuentes que no necesites, normaliza las URL para evitar duplicados y utiliza solicitudes condicionales para que las páginas que no hayan cambiado devuelvan un 304 vacío.

  • Establece tiempos de espera de conexión y lectura independientes en cada solicitud, por ejemplo, 5 segundos y 30 segundos. En requests, una llamada sin tiempo de espera puede esperar indefinidamente y bloquear a un trabajador.
  • Utiliza uno requests.Session por servidor. De este modo se reutilizan las conexiones, se evitan los handshakes TLS repetidos y se alivia la carga del servidor.
  • Envía el mismo conjunto de encabezados realista y veraz en cada solicitud, con un User-Agent que te identifique. Nuestra guía de encabezados HTTP para el web scraping explica qué encabezados son importantes y en qué orden.
  • Mantén la compresión activada. requests solicita gzip de forma predeterminada; si creas los encabezados manualmente, mantén Accept-Encoding, ya que el HTML comprimido ocupa una fracción del tamaño en la red.
  • Limpia las URL antes de que entren en tu cola: elimina los parámetros de seguimiento y #fragments, sigue rel="canonical" enlaces y elimina duplicados.
  • Omite los píxeles de seguimiento y las vistas de paginación duplicadas; recupera solo lo que vayas a analizar.

Almacena las páginas en caché y utiliza solicitudes condicionales

Cuando una respuesta contenga un ETag o Last-Modified , guárdala y reenvíala la próxima vez como If-None-Match o If-Modified-Since. Con estas solicitudes condicionales, una página que no haya cambiado recibe un 304 Not Modified sin cuerpo, lo que te ahorra ancho de banda y trabajo al sitio web.

PoliteFetcher.get() Ya lo hace: su validators diccionario almacena el ETag y Last-Modified de las últimas 200 respuestas y las devuelve la próxima vez. Un código de estado 304 se devuelve tal cual, por lo que tu código reutiliza la copia almacenada.

¿Qué debes hacer cuando un sitio web empieza a bloquear tu rastreador?

Analiza la situación antes de escalar el problema. Interpreta la señal (403, 429, 503 o un 200 que muestra un CAPTCHA o una página vacía), comprueba primero tu propia frecuencia y tus encabezados, y luego ve subiendo un peldaño cada vez: reduce la velocidad, corrige los encabezados, añade proxies probados, renderiza la página y, por último, pasa los objetivos difíciles a una API de scraping gestionada.

Código de error

Causa probable

Primera respuesta

429 Demasiadas solicitudes

Límite de frecuencia

Respetar el «Retry-After»; reducir la concurrencia por host

503 con «Retry-After»

Sobrecarga o mantenimiento

Esperar el tiempo indicado; reducir la frecuencia

403 Prohibido

IP bloqueada, encabezado incorrecto o se requiere inicio de sesión

Comprueba los encabezados y las condiciones; no insistas con los reintentos

200 con una página de desafío o CAPTCHA

Defensa contra bots

Trátalo como un error de validación; reduce la velocidad y, a continuación, escala el problema

Código de estado 200 con un contenedor de datos vacío

Contenido renderizado con JavaScript

Busca la llamada JSON; renderiza solo si es necesario

Aumento de los tiempos de espera

Sobrecarga del servidor o limitación de tráfico

Aumenta los retrasos; reduce la concurrencia

¿No sabes qué significa un código? Consulta nuestra lista de errores de proxy y de estado HTTP.

Los bloqueos «suaves» son los más peligrosos: una respuesta 200 que contiene una página de desafío o carece de datos parece un éxito, y solo las comprobaciones de datos que se describen más adelante en esta guía lo detectan.

Los sitios web bloquean a los rastreadores porque el tráfico automatizado intenso sobrecarga los servidores y puede perjudicar al negocio. La lista de amenazas automatizadas de OWASP incluye el rastreo como una amenaza específica, la OAT-011, y la mayoría de las defensas se centran en el volumen y los patrones de abuso, por lo que el tráfico educado y bien identificado activa menos de ellas.

Actúa en este orden:

  1. Reduce la velocidad y la concurrencia en el servidor afectado.
  2. Corrige tus encabezados para que sean coherentes y estén completos.
  3. Añade proxies, empezando por elegir el tipo de proxy adecuado para el destino. Prueba tus proxies antes de ampliar la escala y, a continuación, distribuye la carga con proxies rotativos.
  4. Visualiza la página en un navegador sin interfaz gráfica si los datos solo aparecen tras la ejecución de JavaScript.
  5. Delega los objetivos más difíciles a una API de scraping gestionada.

Para la identificación de huellas digitales, los CAPTCHA y los navegadores ocultos, utiliza nuestra guía para realizar scraping sin que te bloqueen.

¿Cuándo necesitas realmente un navegador sin interfaz gráfica?

Solo cuando los datos no se encuentren en el HTML ni en una llamada JSON que realice la página. Compara el código fuente de la página con lo que muestran las DevTools; si los datos llegan a través de una solicitud XHR, llama directamente a ese punto final. Representa solo las páginas que realmente necesiten JavaScript, bloquea imágenes y fuentes, y reutiliza los contextos del navegador, ya que la representación es mucho más lenta y costosa.

Un navegador sin interfaz ejecuta el JavaScript de la página sin una ventana visible: es la forma más completa de recuperar una página, y la más costosa. Dos pruebas rápidas te dirán si necesitas uno:

  • Compara «Ver código fuente» con «Elementos». Busca en el código fuente de la página (Ctrl+U o Cmd+Opción+U) un valor que te interese, como un precio. Si está ahí, basta con una solicitud HTTP simple; si solo lo muestra el panel «Elementos» de DevTools, significa que lo ha generado JavaScript.
  • Comprueba en la pestaña «Red» si hay una solicitud Fetch/XHR que transporte los datos, como el ejemplo del punto final JSON anterior, y llámala directamente.

Si necesitas renderización, envía al navegador solo las URL que no superen esas pruebas y bloquea las imágenes, las fuentes y los archivos multimedia. Con Playwright o una herramienta similar, ejecuta un único proceso de navegador con un contexto nuevo por tarea, en lugar de un navegador nuevo por página.

Los usuarios de Scrapy pueden ejecutar JavaScript solo para las solicitudes que lo necesiten y mantener el resto del rastreo en el rápido descargador HTTP de Scrapy.

¿Cómo se escriben analizadores que resistan los cambios de diseño?

Ancla los selectores a elementos que rara vez cambian (identificadores, atributos de datos, estructura semántica, JSON-LD incrustado) en lugar de a clases de estilo; mantén una lista ordenada de selectores de reserva por campo; decodifica correctamente los bytes sin procesar; y haz que la falta de campos obligatorios provoque un error evidente. Un analizador que devuelve espacios en blanco sin avisar es peor que uno que se cuelga.

Elige selectores que perduren más allá de un rediseño

Las clases de estilo como .text-lg.font-bold cambian cada vez que un diseñador modifica la página. Los identificadores (ID) y data-* los atributos, itemprop los microdatos, las etiquetas semánticas y los bloques JSON-LD cambian con mucha menos frecuencia, ya que hay otro código que depende de ellos. Mantén también los selectores breves, ya que una ruta completa desde <body> se rompe cuando se mueve cualquier div contenedor.

Los selectores CSS son más fáciles de leer, mientras que XPath permite seleccionar por texto y recorrer el árbol hacia arriba. La comparación entre XPath y los selectores CSS aborda las ventajas e inconvenientes.

Mantén alternativas ordenadas para cada campo

Asigna a cada campo una lista de selectores, empezando por el más específico, y elige el primero que coincida. El SELECTORS diccionario del fragmento de código que aparece a continuación funciona de esta manera.

Decodifica bytes, no texto adivinado

Ten cuidado con el error de codificación del símbolo «£». Si un servidor envía Content-Type: text/html sin especificar un juego de caracteres, requests recurre a ISO-8859-1 al generarla resp.text, por lo que una página en UTF-8 se decodifica incorrectamente, «£» se convierte en «Â£» y no se puede analizar ningún precio. En su lugar, pasa resp.contentlos bytes sin procesar a tu analizador; BeautifulSoup leerá el propio <meta charset>.

Prueba con páginas guardadas

Guarda algunas páginas reales como casos de prueba. Cuando cambie un selector, vuelve a analizar las respuestas sin procesar almacenadas y compara el resultado antes y después.

Este fragmento de código convierte las fichas de libros de una página del catálogo de books.toscrape.com en registros tipificados, valida cada uno de ellos e imprime un informe de calidad; a continuación, pasa al analizador una ficha con una clase de precio renombrada para mostrar cómo se detecta un cambio en el diseño.

parse_validate.py

"""Parse defensively, normalize, validate, and report data quality."""
import json
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from decimal import Decimal, InvalidOperation
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup

# Ordered fallbacks: the first selector that matches wins.
SELECTORS = {
    "title": ["h3 a[title]", "h3 a"],
    "price": ["p.price_color", "[itemprop=price]"],
    "availability": ["p.availability", ".instock"],
    "url": ["h3 a[href]"],
}
RATINGS = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}


@dataclass
class Book:
    url: str
    title: str
    price: Decimal
    currency: str
    in_stock: bool
    rating: int | None
    fetched_at: str  # provenance: when this record was fetched (UTC)


def first(card, field):
    for css in SELECTORS[field]:
        node = card.select_one(css)
        if node is not None:
            return node
    return None


def parse_card(card, page_url, fetched_at=""):
    errors = []
    link, title_node = first(card, "url"), first(card, "title")
    price_node, stock_node = first(card, "price"), first(card, "availability")
    title = (title_node.get("title") or title_node.get_text(strip=True)) if title_node else ""
    if not title:
        errors.append("title missing")
    raw_price = price_node.get_text(strip=True) if price_node else ""
    currency = "GBP" if "£" in raw_price else "UNKNOWN"
    try:
        price = Decimal(raw_price.replace("£", "").replace(",", ""))
        if price <= 0:
            errors.append("price not positive")
    except InvalidOperation:
        price = Decimal("NaN")
        errors.append(f"price unparseable: {raw_price!r}")
    rating_node = card.select_one("p.star-rating")
    rating = next((RATINGS[c] for c in (rating_node.get("class", []) if rating_node else [])
                   if c in RATINGS), None)
    if link is None:
        errors.append("url missing")
    book = Book(url=urljoin(page_url, link["href"]) if link else "", title=title, price=price,
                currency=currency, in_stock="in stock" in (stock_node.get_text(" ", strip=True).lower()
                                                          if stock_node else ""),
                rating=rating, fetched_at=fetched_at)
    return book, errors


def scrape(page_url):
    resp = requests.get(page_url, timeout=(5, 30),
                        headers={"User-Agent": "acme-catalogue-bot/0.1 (+mailto:data@example.com)"})
    resp.raise_for_status()
    fetched_at = datetime.now(timezone.utc).isoformat(timespec="seconds")
    # Pass bytes, not resp.text: with no charset in Content-Type, requests guesses
    # ISO-8859-1 and "£" becomes "£". BeautifulSoup reads the page's <meta charset>.
    soup = BeautifulSoup(resp.content, "html.parser")
    cards = soup.select("article.product_pod")
    seen, good, rejected = set(), [], []
    for card in cards:
        book, errors = parse_card(card, page_url, fetched_at)
        if book.url in seen:
            errors.append("duplicate url")
        seen.add(book.url)
        (rejected if errors else good).append((book, errors))
    report = {
        "page": page_url,
        "cards_found": len(cards),
        "valid": len(good),
        "rejected": len(rejected),
        "missing_rating": sum(1 for b, _ in good if b.rating is None),
        "sample": {k: str(v) for k, v in asdict(good[0][0]).items()} if good else None,
    }
    return good, rejected, report


if __name__ == "__main__":
    _, rejected, report = scrape("https://books.toscrape.com/catalogue/page-1.html")
    print(json.dumps(report, indent=2))
    for book, errors in rejected:
        print("REJECTED", book.url, errors)
    # Simulate a layout change: the price class is renamed.
    html = '<article class="product_pod"><h3><a href="x.html" title="T">T</a></h3><p class="cost">£1.00</p></article>'
    card = BeautifulSoup(html, "html.parser").select_one("article")
    print("after layout change:", parse_card(card, "https://books.toscrape.com/")[1])
  • first() recorre la lista de valores por defecto de cada campo, de modo que una clase renombrada no vacíe una columna.
  • BeautifulSoup(resp.content, ...) Pasa bytes, lo que evita el error de codificación mencionado anteriormente.
  • parse_card() recoge los errores en lugar de generarlos, de modo que una ficha errónea no detiene la página y cada rechazo tiene una razón.
  • El cambio de diseño simulado devuelve «precio no analizable» en lugar de un precio en blanco, que es la señal que quieres ver en tus registros.

Encontrarás más información sobre cómo convertir páginas sin procesar en campos en nuestra guía de análisis de datos.

¿Cómo se comprueba la calidad de los datos extraídos?

Trata cada registro como no fiable hasta que supere un esquema: campos obligatorios presentes, tipos correctos, rangos razonables y una clave única. Utiliza el tipo «Decimal» para el dinero, normaliza las divisas y las fechas, elimina duplicados según la clave, almacena la URL de origen y la hora de obtención, y genera un informe por ejecución con los valores válidos, rechazados y que faltan.

La calidad de los datos es donde los rastreadores fallan de forma silenciosa: la obtención se realiza con éxito, el analizador se ejecuta y los valores erróneos se cuelan en los informes durante semanas. Estas prácticas recomendadas para el rastreo de datos evitan que eso ocurra:

  • Define el esquema en el código. La Book dataclass del fragmento de código de análisis es el contrato de datos en Python; pydantic añade más comprobaciones, pero una dataclass no necesita ninguna dependencia adicional.
  • Rechaza en lugar de forzar. Cuando un precio no se pueda analizar, rechaza el registro indicando el motivo en lugar de almacenar un cero; los motivos de rechazo son tu primera señal de alerta ante un cambio en el formato.
  • Almacena el importe como Decimal, nunca como un número flotante, ya que los números flotantes no pueden representar con exactitud la mayoría de los precios. Normaliza las divisas a códigos ISO 4217 y las marcas de tiempo a UTC.
  • Vigila las tasas de valores nulos a nivel de campo para detectar saltos respecto a la última ejecución.
  • Elimina duplicados basándote en la URL canónica o en una clave de negocio, como un SKU. Los duplicados suelen indicar un bucle de paginación o que el mismo artículo aparece en dos categorías.
  • Asigna a cada fila un source_url y fetched_at, de modo que cualquier valor se pueda rastrear hasta su página y momento.
  • Elabora un informe de calidad por ejecución: tarjetas encontradas, válidas, rechazadas y campos opcionales que faltan. El fragmento de código de análisis muestra exactamente esto, y la siguiente sección lo supervisa.

Comprueba

Regla de ejemplo

Detecta

Campos obligatorios

Título y precio presentes

Cambios en el diseño

Tipos

El precio se interpreta como un decimal

Errores de codificación, selector incorrecto

Rangos

0 < precio < 10 000

Confusiones entre unidades o divisas

Unicidad

una fila por URL

Bucles de paginación, duplicados

Tasa de valores nulos

Valoraciones nulas: no más de 5 puntos por encima de la última ejecución

Fallos parciales

Procedencia

source_url y fetched_at establecidos

Registros no rastreables

¿Cómo se supervisa un rastreador web en producción?

Realiza un seguimiento de algunas métricas de ejecución y activa alertas ante cualquier cambio, no solo ante fallos: tasa de éxito por código de estado, registros por página, páginas vacías, tasas de campos nulos, reintentos, latencia, actualidad y coste por registro válido. Haz que la salida de la tarea sea distinta de cero cuando se superen los umbrales, de modo que sea tu programador, y no un cliente, quien se dé cuenta primero.

Un rastreador que se cuelga es fácil de detectar; uno que recopila silenciosamente información errónea, no tanto. Considera los umbrales que se indican a continuación como puntos de partida para el ajuste, no como valores de referencia.

Métrica

Por qué es importante

Ejemplo de alerta

Tasa de éxito por código de estado

Detecta bloqueos e interrupciones del servicio

La proporción de códigos 2xx cae por debajo del 95 %

Registros por página

Detecta cambios en el diseño

Por debajo del 50 % de la mediana de los últimos 7 días

Páginas vacías

Errores ocultos

Cualquier página con 0 registros

Tasa de campos nulos

Fallo parcial

Hasta 10 puntos más que en la última ejecución

Reintentos por solicitud

Limitación

Media superior a 0,3

Latencia p95

Carga del servidor

Se duplica respecto al valor de referencia, por lo que se ralentiza

Actualización

Ejecuciones fallidas

Datos más antiguos que tu objetivo

Coste por registro válido

Desviación del presupuesto

Aumento del 25 % respecto a la semana anterior

Hay dos hábitos que hacen que estas métricas sean más útiles:

  • Recupera unas cuantas URL «canario» con valores conocidos, como un producto con un título fijo, en cada ejecución. Un «canario» erróneo significa que el analizador ha fallado, no los datos.
  • Alerta sobre las variaciones en lugar de sobre cifras fijas. Una caída en el número de registros por página con respecto a la mediana de los últimos 7 días es una mejor señal de alarma de cambio en el diseño que cualquier recuento absoluto.

El fragmento de rastreo de la siguiente sección termina con una health() función que señala los errores de recuperación, las páginas vacías y las páginas con poco contenido, y luego sale con el estado 1. Los programadores como cron, systemd, Airflow y los ejecutores de CI lo tratan como un fallo, por lo que la alerta proviene de herramientas que ya utilizas.

Para la programación, el envío y la repetición, consulta cómo crear tareas de scraping que se puedan recuperar.

¿Cómo se escala el rastreo web sin que deje de funcionar?

Escala el control de registros antes que los trabajadores, ya que las técnicas avanzadas de scraping web, como el rastreo distribuido, dependen de ello. Mantén una frontera de URL deduplicadas, confirma los registros de cada página y su indicador «completado» en una sola transacción, escribe de forma idempotente para que las repeticiones no dupliquen filas, aplica límites de frecuencia por dominio en todos los trabajadores y rastrea de forma incremental utilizando mapas del sitio y solicitudes condicionales en lugar de volver a recuperar todo.

Las mejores prácticas para el web scraping escalable tienen que ver principalmente con el estado, ya que un mayor número de trabajadores multiplica cualquier error en el seguimiento del progreso.

  • Utiliza una «frontera», no una lista: una cola de URL deduplicadas que permite al rastreo descubrir nuevas páginas sobre la marcha. ¿No conoces las fronteras? Empieza con el rastreo web en Python.
  • Haz que los puntos de control sean transaccionales. Confirma los registros de una página, los enlaces descubiertos y el indicador «hecho» en una sola transacción, de modo que, si se produce un fallo, se conserve todo el trabajo de esa página o ninguno.
  • Escribe de forma idempotente. Las claves primarias o las operaciones «upsert» hacen que una escritura duplicada no tenga ningún efecto, por lo que las repeticiones y los reintentos son seguros.
  • Reanuda en lugar de reiniciar. La siguiente ejecución continúa desde la primera URL que quedó sin terminar.

Para ver cómo funciona, rastrea quotes.toscrape.com con el fragmento de código que aparece a continuación, que mantiene una frontera SQLite y confirma el trabajo de cada página en una sola transacción. Ejecuta python resumable_crawl.py 3 para que se detenga tras tres páginas y, a continuación, ejecuta python resumable_crawl.py de nuevo: la segunda ejecución se reanuda donde se detuvo la primera, sin filas duplicadas.

resumable_crawl.py

"""Resumable pagination crawl with a SQLite checkpoint and a run health check."""
import json
import sqlite3
import sys
import time
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup

START = "https://quotes.toscrape.com/page/1/"
HEADERS = {"User-Agent": "acme-quotes-bot/0.1 (+mailto:data@example.com)"}


def open_db(path="crawl.db"):
    db = sqlite3.connect(path)
    db.executescript("""
        CREATE TABLE IF NOT EXISTS frontier (url TEXT PRIMARY KEY, done INTEGER DEFAULT 0);
        CREATE TABLE IF NOT EXISTS quotes (text TEXT, author TEXT, tags TEXT,
                                           page TEXT, PRIMARY KEY (text, author));
    """)
    db.execute("INSERT OR IGNORE INTO frontier(url) VALUES (?)", (START,))
    db.commit()
    return db


def crawl(db, max_pages):
    stats = {"pages": 0, "errors": 0, "rows": 0, "empty_pages": 0, "rejected": 0}
    while stats["pages"] < max_pages:
        row = db.execute("SELECT url FROM frontier WHERE done = 0 LIMIT 1").fetchone()
        if row is None:
            break  # frontier exhausted
        url = row[0]
        try:
            resp = requests.get(url, headers=HEADERS, timeout=(5, 30))
            resp.raise_for_status()
        except requests.RequestException:
            stats["errors"] += 1
            break  # leave it undone; the next run resumes here
        soup = BeautifulSoup(resp.content, "html.parser")
        quotes = soup.select("div.quote")
        stats["empty_pages"] += not quotes
        with db:  # rows, link discovery and the checkpoint commit together
            for q in quotes:
                text, author = q.select_one("span.text"), q.select_one("small.author")
                if text is None or author is None:
                    stats["rejected"] += 1  # malformed card: reject it, don't coerce
                    continue
                cur = db.execute(
                    "INSERT OR IGNORE INTO quotes VALUES (?, ?, ?, ?)",
                    (text.get_text(strip=True), author.get_text(strip=True),
                     ",".join(t.get_text(strip=True) for t in q.select("a.tag")), url))
                stats["rows"] += cur.rowcount
            nxt = soup.select_one("li.next a[href]")
            if nxt:
                db.execute("INSERT OR IGNORE INTO frontier(url) VALUES (?)",
                           (urljoin(url, nxt["href"]),))
            db.execute("UPDATE frontier SET done = 1 WHERE url = ?", (url,))
        stats["pages"] += 1
        time.sleep(1.0)  # pace requests to one host
    return stats


def health(stats, db, min_rows_per_page=5):
    problems = []
    if stats["errors"]:
        problems.append(f"{stats['errors']} fetch errors")
    if stats["empty_pages"]:
        problems.append(f"{stats['empty_pages']} pages had no records (layout change?)")
    if stats["rejected"]:
        problems.append(f"{stats['rejected']} cards missing text or author")
    if stats["pages"] and stats["rows"] / stats["pages"] < min_rows_per_page:
        problems.append("rows per page below threshold")
    total = db.execute("SELECT COUNT(*) FROM quotes").fetchone()[0]
    pending = db.execute("SELECT COUNT(*) FROM frontier WHERE done = 0").fetchone()[0]
    return {**stats, "total_rows": total, "pending_urls": pending, "problems": problems}


if __name__ == "__main__":
    max_pages = int(sys.argv[1]) if len(sys.argv) > 1 else 100
    db = open_db()
    report = health(crawl(db, max_pages), db)
    print(json.dumps(report))
    sys.exit(1 if report["problems"] else 0)
  • with db: agrupa las citas insertadas, el enlace a la página siguiente recién encontrado y la done = 1 actualización en una sola transacción.
  • INSERT OR IGNORE El uso de claves primarias hace que cada escritura sea idempotente, tanto para las URL de Frontier como para las citas.
  • Si falla una recuperación, su URL queda sin procesar y se detiene la ejecución, de modo que la siguiente ejecución la vuelve a intentar en primer lugar.
  • Las fichas a las que les falte una cita o un autor se omiten y se cuentan, en lugar de guardarse a medias.
  • health() convierte los contadores de la ejecución en una lista de problemas, y sys.exit(1) hace que esos problemas sean visibles para quienquiera que programe la tarea.

Una vez que el control de datos es sólido, otras tres prácticas añaden capacidad de forma segura:

  • Compartir los límites de tasa por dominio. Al haber varias máquinas, la regulación local ya no protege el sitio, así que mantén un presupuesto por dominio en un lugar compartido, como un «token bucket» en Redis.
  • Realiza el rastreo de forma incremental. Utiliza las fechas del mapa del sitio lastmod para poner en cola solo las URL nuevas o modificadas, y las solicitudes condicionales para convertir las páginas sin cambios en respuestas 304 vacías.
  • Escala horizontalmente en último lugar. Añade trabajadores solo una vez que los límites por host se apliquen de forma centralizada, de modo que más trabajadores signifiquen más hosts en paralelo, no más presión sobre uno solo.

¿Cómo se mantienen bajo control los costes del web scraping?

Mide el coste por registro válido, no el coste por solicitud. La mayor parte del ahorro proviene de realizar menos consultas: almacena en caché y utiliza solicitudes condicionales, da prioridad a los puntos finales JSON, genera el contenido solo cuando sea necesario, limita los reintentos, utiliza proxies solo para los hosts que los necesiten y realiza el rastreo de forma incremental. A continuación, compara esa cifra de forma honesta con una API de scraping gestionada antes de desarrollar más.

La fórmula es sencilla:

cost per valid record = (infrastructure + proxies + engineering hours) ÷ valid records delivered

Las horas de ingeniería van en el numerador, ya que un rastreador que necesita un desarrollador cada semana no es barato. El denominador cuenta solo los registros que han superado la validación.

Aprovecha

Efecto

Véase

Solicitudes condicionales y almacenamiento en caché

Las páginas sin cambios devuelven un 304 vacío

Hábitos de solicitud

Puntos finales JSON en lugar de HTML

Menos solicitudes, análisis más sencillo

Planificación

Renderizar solo cuando sea necesario

Evita la carga del navegador en páginas estáticas

Navegadores sin interfaz gráfica

Límites de reintentos y retrasos

Detiene las «tormentas de reintentos»

Frecuencia de solicitudes

Proxies solo cuando sea necesario

Paga por las direcciones IP solo en los servidores que bloquean

Bloqueos

Rastreos incrementales

Recuperar solo las URL nuevas o modificadas

Escalabilidad

Validación antes del almacenamiento

No pagues por almacenar ni enviar registros erróneos

Calidad de los datos

Desarrollar o comprar

Desarrollar internamente tiene sentido para unos pocos objetivos estables y con poca protección. Cuando los proxies, los reintentos, la representación y los bloqueos suponen más tiempo de ingeniería del que valen los datos, compara el coste por registro válido entre las opciones de nuestro resumen de rastreadores web y API de rastreo. Si prefieres no gestionar esa infraestructura en absoluto, la Scraper API de WebScrapingAPI es una API de scraping gestionada que se encarga de los proxies, los reintentos y la representación opcional en JavaScript. La representación en JavaScript y la geolocalización no suponen ningún coste adicional en créditos, y las solicitudes fallidas no se facturan.

¿Qué consejos populares sobre el scraping web deberías ignorar?

Ignora los consejos que cambian el acceso a corto plazo por riesgo: falsificar un User-Agent de Googlebot, rotar los User-Agents en cada solicitud con encabezados que no coinciden, movimientos «aleatorios» del ratón programados por defecto, retrasos fijos que ignoran las señales del servidor, reintentar cada error indefinidamente y extraer todo por si acaso. Cada uno de ellos añade costes, riesgos o datos erróneos.

«Utiliza un User-Agent de Googlebot para que los sitios te dejen entrar». Los propietarios de los sitios pueden desenmascarar un Googlebot falso mediante una consulta de DNS. Utiliza un User-Agent honesto con datos de contacto, tal y como se describe en la sección de identificación.

«Alterna los User-Agents en cada solicitud». Un User-Agent de navegador con encabezados que ningún navegador real enviaría llama más la atención que una identidad coherente, así que mantén un conjunto de encabezados coherente por sesión.

«Añade movimientos y clics aleatorios del ratón para parecer humano». Para la mayoría de los objetivos, esto añade complejidad y ralentiza cada página sin solucionar el problema habitual, el volumen de solicitudes, así que ajusta primero tu frecuencia y tus encabezados.

«Espera entre 5 y 10 segundos entre solicitudes y estarás a salvo». Un retraso fijo es demasiado lento para un sitio que funciona bien y demasiado rápido para uno que devuelve códigos 429. Utiliza un ritmo por host que se adapte a la latencia y al Retry-After.

«Vuelve a intentarlo hasta que funcione». Los reintentos ilimitados convierten una interrupción del servicio en una avalancha de reintentos. Vuelve a intentar solo los errores que se puedan reintentar, aplica jitter para espaciar los intentos y limita el número de intentos.

«Extrae todo, por si acaso». Las páginas adicionales consumen ancho de banda, almacenamiento y tiempo de servidor, y los campos adicionales aumentan tu exposición si contienen datos personales. Recopila solo lo que especifique tu contrato de datos.

«El Crawl-delay es un estándar que todo el mundo sigue». No forma parte del RFC 9309, y Google lo ignora. Respétalo cuando esté presente, pero no esperes que todos los rastreadores o analizadores lo interpreten de la misma manera.

«Mi biblioteca se encarga de robots.txt por mí». requests y BeautifulSoup no recogen ni aplican el archivo robots.txt en absoluto. Scrapy sí puede hacerlo, a través de su ROBOTSTXT_OBEY configuración, que se activa en los proyectos creados con scrapy startproject activar. Comprueba tu configuración y prueba el comparador con las reglas de las que dependes.

Lista de verificación de buenas prácticas para el scraping web

Antes de que un rastreador entre en producción, confirma que cuente con un contrato de datos por escrito, una comprobación del archivo robots.txt conforme a los estándares, un User-Agent veraz, control de ritmo por host con soporte para Retry-After, tiempos de espera, solicitudes condicionales, selectores de reserva, validación de esquemas, alertas de estado de ejecución, puntos de control transaccionales y un seguimiento del coste por registro válido.

Cada fila incluye una prueba de «listo cuando», para que puedas saber si un rastreador está listo para entrar en producción.

Fase

Comprobación

Se da por hecho cuando

Plan

Contrato de datos redactado

Campos, tipos, clave y vigencia acordados

Aspectos legales

Se han revisado los términos y el archivo robots.txt

Compatibilidad con los estándares en la ruta de obtención

Identidad

User-Agent veraz

Se incluyen el nombre, la versión y la URL de contacto

Frecuencia

Regulación por host

Se respeta el «Retry-After»; retroceso con fluctuación; límite de reintentos establecido

Solicitudes

Tiempos de espera y sesiones

Tiempos de espera de conexión y lectura configurados en todas partes

Almacenamiento en caché

Solicitudes condicionales

ETag y Last-Modified almacenados y enviados

Análisis

Selectores de reserva

Bytes pasados al analizador; fixtures en las pruebas

Calidad

Validación del esquema

Motivos de rechazo registrados; deduplicación por clave

Supervisión

Estado de la ejecución

Salida distinta de cero en caso de incumplimiento del umbral; «canarios» implementados

Escalabilidad

Puntos de control

Confirmación de datos y progreso en una sola transacción

Coste

Economía unitaria

Coste por registro válido, con seguimiento semanal

¿Quieres desarrollar estas habilidades desde cero? Elige entre nuestros mejores cursos de web scraping.

FAQ

¿Qué son las mejores prácticas de web scraping?

Las mejores prácticas de web scraping son los hábitos que garantizan que los scrapers sean legales, respetuosos, precisos y asequibles. Entre ellas se incluyen recopilar solo los datos que necesitas, respetar el archivo robots.txt y las condiciones del sitio web, identificar tu bot, dosificar las solicitudes por cada servidor, respetar el encabezado «Retry-After», validar los registros, supervisar las ejecuciones, establecer puntos de control del progreso y realizar un seguimiento del coste por registro válido.

¿Cuántas solicitudes por segundo son seguras al extraer datos de un sitio web?

No hay una cifra que sea segura en todos los casos. Empieza con una o dos solicitudes simultáneas por servidor, con un intervalo de aproximadamente un segundo entre ellas, y luego adáptate: reduce la velocidad a medida que aumentan los tiempos de respuesta, haz una pausa ante respuestas 429 o 503, respeta el Retry-After y ejecuta rastreos de gran volumen fuera de las horas punta.

¿Tengo que respetar el archivo robots.txt al realizar el scraping?

Técnicamente nada te lo impide, pero el archivo robots.txt es la norma publicada por el propietario para los clientes automatizados, e ignorarlo puede provocar bloqueos. Aplícalo tal y como define el RFC 9309, utilizando el grupo propio de tu bot si existe, y lee también los términos y condiciones del sitio.

¿Sigue el robotparser de Python el estándar robots.txt?

No del todo. El urllib.robotparser de Python aplica las reglas en el orden en que aparecen en el archivo, en lugar de seleccionar la coincidencia más larga, por lo que puede permitir URL que un sitio prohíbe o bloquear URL que permite. Además, ignora los comodines * y $ definidos en el RFC 9309.

¿Qué User-Agent debo utilizar para el web scraping?

Utiliza un User-Agent honesto y estable que identifique a tu bot y proporcione una URL de contacto o un correo electrónico, para que los propietarios de los sitios web puedan ponerse en contacto contigo en lugar de bloquearte. Nunca te hagas pasar por Googlebot, ya que Google publica cómo verificar sus rastreadores.

¿Cómo debe gestionar un rastreador una respuesta 429 «Too Many Requests»?

Deja de enviar solicitudes a ese servidor, lee el encabezado «Retry-After» y espera al menos ese tiempo; el valor puede estar expresado en segundos o en una fecha HTTP. Si no hay encabezado, reduce el ritmo de forma exponencial con jitter, limita el número de reintentos y reduce la concurrencia por servidor antes de reanudar.

¿Cómo evito que mi scraper deje de funcionar cuando una página web cambia su diseño?

No puedes evitar los rediseños, pero puedes sobrevivir a ellos: vincula los selectores a puntos de referencia estables, como identificadores (ID), atributos de datos o JSON-LD; mantén alternativas por campo; señala claramente los errores cuando falten campos obligatorios; y avisa cuando disminuya el número de registros por página.

¿Cómo compruebo la calidad de los datos extraídos?

Valida cada registro con un esquema antes de almacenarlo: campos obligatorios, tipos correctos, rangos razonables y una clave única. Utiliza el tipo «Decimal» para los precios, elimina duplicados según la clave, registra los motivos de rechazo y compara las tasas de valores nulos y el recuento de filas entre ejecuciones.

¿Necesito un navegador sin interfaz gráfica para el scraping web?

Normalmente no. Si los datos se encuentran en el código fuente de la página o en una solicitud JSON en la pestaña «Red», recógelos directamente. Utiliza un navegador sin interfaz gráfica solo para páginas cuyo contenido se genere con JavaScript y que bloqueen imágenes y fuentes.

¿Cómo puedo acelerar el scraping web sin que me bloqueen?

Recupera menos datos en lugar de insistir más: utiliza puntos finales JSON en lugar de páginas renderizadas, realiza solicitudes condicionales para que las páginas que no hayan cambiado devuelvan un 304, realiza rastreos incrementales basados en el mapa del sitio y reutiliza las conexiones. A continuación, paraleliza entre hosts, no dentro de uno mismo, estableciendo límites por host.

¿Está bien extraer datos que están protegidos por un inicio de sesión?

Ten cuidado. Iniciar sesión suele implicar aceptar los términos del sitio, muchos de los cuales prohíben el acceso automatizado, así que trata las áreas autenticadas como zonas prohibidas sin permiso o contrato. Si tienes permiso, guarda las credenciales en un gestor de secretos, limita los datos personales y respeta los límites de frecuencia habituales.

¿Cuándo debo utilizar una API de extracción web en lugar de crear mi propio extractor?

Desarrolla uno propio cuando los objetivos sean pocos, estables y estén poco protegidos. Utiliza una API de scraping gestionada cuando los proxies, los reintentos, la representación y los bloqueos consuman más tiempo de ingeniería del que valen los datos, o cuando necesites muchos países, y compara el coste por registro válido, incluidas las horas de ingeniería.

Dedica tu tiempo a los datos, no a la infraestructura

Todas las prácticas mencionadas anteriormente son tareas que puedes mantener internamente, y para muchos equipos esa es la decisión acertada. Cuando no lo sea, puedes probar la API de scraping con 5.000 solicitudes gratuitas y dejar que se encargue de los proxies, los reintentos y el renderizado opcional en JavaScript; ejecutar páginas dinámicas a través de la API del navegador con una única solicitud REST; añadir proxies residenciales de pago por uso a partir de 8,50 $ por GB, u obtener registros estructurados de 116 fuentes compatibles con la API de datos.

Acerca del autor

Anda Miuțescu, Redactor de contenidos técnicos @ WebScrapingAPI

Anda Miuțescu

Redactor de contenidos técnicos

Anda Miuțescu es redactora de contenidos técnicos en WebScrapingAPI, donde elabora contenidos claros y útiles que ayudan a los desarrolladores a comprender el producto y sus capacidades.

Tutorial de Python de Nodriver: Extracción de datos de navegadores de forma sigilosa, desde el primer script hasta la puesta en producción
La ciencia del web scraping

Tutorial de Python de Nodriver: Extracción de datos de navegadores de forma sigilosa, desde el primer script hasta la puesta en producción

TL;DR: En resumen: Nodriver es una biblioteca asíncrona de Python que controla Chromium mediante el Chrome DevTools Protocol, sin Selenium ni el binario chromedriver. Úsala cuando un navegador sigiloso autoalojado, más parecido a una sesión real de Chrome, sea mejor opción que una pila WebDriver, pero ten en cuenta que por sí sola no elimina los riesgos de IP, TLS o comportamiento. Este tutorial va desde un primer script hasta la producción en contenedores y una Browser API alojada.

Suciu Dan15 min read
Leer artículo
Cómo gestionar las URL de redireccionamiento de Google /goto y los tokens CAES en un rastreador de SERP
La ciencia del web scraping

Cómo gestionar las URL de redireccionamiento de Google /goto y los tokens CAES en un rastreador de SERP

TL;DR: En resumen: Las URL /goto de Google con un token CAES, y los envoltorios /url más antiguos, son capas de redirección, no destinos, así que un scraper de SERP que trata cada href como final rompe el seguimiento de posiciones y los almacenes de enlaces. Prefiere una API de SERP estructurada cuando devuelva los enlaces de destino. Si tienes que analizar HTML, clasifica cada href, elimina redirecciones duplicadas y resuélvelas con un GET simple. No descifres CAES.

Suciu Dan13 min read
Leer artículo
Cabeceras de Respuesta HTTP en cURL: Cada Bandera, Técnica y Receta de Scripting
La ciencia del web scraping

Cabeceras de Respuesta HTTP en cURL: Cada Bandera, Técnica y Receta de Scripting

TL;DR: cURL oculta las cabeceras de respuesta por defecto. Utilice -i para ver las cabeceras junto con el cuerpo, -I para una solicitud HEAD que sólo devuelva cabeceras, -v para una depuración completa de solicitud/respuesta y -D para guardar las cabeceras en un archivo. Para scripts modernos, cURL 7.83+ le permite extraer cabeceras individuales o volcarlas todas como JSON con la opción -w write-out.

Suciu Dan13 min read
Leer artículo

Empieza a crear

¿Estás listo para ampliar tu recopilación de datos?

Únete a más de 2000 empresas que utilizan WebScrapingAPI para extraer datos de la web a escala empresarial sin ningún gasto de infraestructura.