Conocimiento

Schema Drift en Datos Extraídos: Detectando Extractores Rotos Antes que tus Usuarios

Los scrapers rara vez fallan cuando un sitio cambia. Siguen funcionando y devuelven datos sutilmente incorrectos. Cómo los contratos de datos y el perfilado por lotes detectan la deriva a tiempo.

Matt Brown

Matt Brown

29 de septiembre de 2026 · 9 min de lectura

Los peores fallos de un scraper no parecen fallos. El trabajo se ejecuta, las peticiones tienen éxito, las filas llegan al almacén de datos según lo programado. Entonces alguien de finanzas pregunta por qué el precio medio se triplicó de la noche a la mañana, o por qué la mitad del catálogo aparece de repente sin stock, y la investigación acaba llevando a un rediseño del sitio web de hace tres semanas que nadie había notado.

Esto es el schema drift (deriva del esquema): los datos siguen fluyendo, pero su forma o significado ha cambiado silenciosamente. Es el modo de fallo normal de los datos web, porque las fuentes cambian sin aviso y los extractores siguen produciendo resultados. Esta guía cubre las dos capas que lo detectan: contratos a nivel de registro que rechazan valores incorrectos, y perfiles a nivel de lote que detectan cuándo los datos en su conjunto dejan de parecerse a sí mismos. También muestra, con una pequeña prueba, por qué se necesitan ambas.

Conclusiones clave

  • La deriva suele ser silenciosa. Una página modificada rara vez rompe el extractor; hace que el extractor devuelva algo plausible y erróneo.
  • Los contratos a nivel de registro comprueban cada registro según unas reglas: campos obligatorios, tipos, rangos y formatos. Detectan las roturas evidentes.
  • Los perfiles a nivel de lote comparan cada ejecución con una línea base: tasas de relleno, mezcla de tipos y medianas. Detectan las roturas que los contratos no pueden ver.
  • En nuestra prueba de tres derivas realistas, los contratos detectaron una. Las otras dos pasaron todas las comprobaciones a nivel de registro y solo el perfil de lote las detectó.
  • Alerta ante la deriva antes de que los datos se envíen, pon el lote en cuarentena y registra qué sitio y qué versión del extractor la produjo.

Cómo ocurre realmente la deriva

CausaQué hacen los datos
El rediseño cambia el marcadoUn selector coincide con un elemento distinto, o con ninguno
Cambia el formato del precio”9.99” se convierte en “€9.99”, “9,99” o 999 en unidades menores
Un campo pasa a depender de JavaScriptEl HTML estático ya no lo contiene, así que vuelve vacío
Localización o variante geográficaAparece una moneda, idioma o unidad distinta para algunas páginas
Prueba A/BUna fracción de las páginas usa un diseño nuevo, así que una fracción de los registros se rompe
Página de bloqueo o desafíoLa página carga, el extractor se ejecuta, y devuelve nada o basura

El hilo común es que ninguno de estos casos genera una excepción. El último, páginas que cargan correctamente pero que no son la página que se quería, se trata en la tasa de fallo silencioso. El resto son el extractor procesando fielmente una página que ha cambiado por debajo de él.

Capa 1: contratos a nivel de registro

Un contrato de datos establece cómo es un registro válido: qué campos son obligatorios, sus tipos, sus rangos y formatos permitidos. Cada registro se comprueba antes de aceptarlo, y las infracciones se cuentan y se ponen en cuarentena en lugar de almacenarse silenciosamente.

import re
import statistics
from collections import Counter

CONTRACT = {
    "name":     {"type": str, "required": True},
    "price":    {"type": float, "required": True, "min": 0.01, "max": 100_000},
    "currency": {"type": str, "required": True, "pattern": r"^[A-Z]{3}$"},
    "in_stock": {"type": bool, "required": False},
}


def violations(record, contract=CONTRACT):
    """Record-level checks: presence, type, range, format."""
    problems = []
    for field, rule in contract.items():
        value = record.get(field)
        if value in (None, ""):
            if rule.get("required"):
                problems.append(f"{field}: missing")
            continue
        if rule["type"] is float and isinstance(value, int) and not isinstance(value, bool):
            value = float(value)
        if not isinstance(value, rule["type"]):
            problems.append(f"{field}: expected {rule['type'].__name__}, got {type(value).__name__}")
            continue
        if "min" in rule and value < rule["min"] or "max" in rule and value > rule["max"]:
            problems.append(f"{field}: {value} out of range")
        if "pattern" in rule and not re.match(rule["pattern"], value):
            problems.append(f"{field}: {value!r} bad format")
    return problems

Un registro como {"name": "x", "price": "€9.99", "currency": "eur"} falla dos veces: el precio es una cadena de texto, y la moneda no es un código de tres letras en mayúscula. Los contratos son baratos, explícitos y fáciles de razonar. Su límite es que cada registro se juzga por separado, y buena parte de la deriva produce registros que son individualmente válidos.

Capa 2: perfiles a nivel de lote

Un perfil resume un lote completo: para cada campo, con qué frecuencia está relleno, qué tipos aparecen y, para los números, la mediana. Comparar el perfil de cada ejecución con una línea base de ejecuciones sanas recientes muestra cuándo los datos en su conjunto cambian de forma, incluso si cada registro pasa su contrato.

def profile(records, fields=CONTRACT):
    """Batch-level shape: how often each field is filled, its types, and numeric medians."""
    n = max(1, len(records))
    out = {}
    for field in fields:
        values = [r.get(field) for r in records]
        present = [v for v in values if v not in (None, "")]
        numbers = [float(v) for v in present if isinstance(v, (int, float)) and not isinstance(v, bool)]
        out[field] = {
            "fill_rate": len(present) / n,
            "types": Counter(type(v).__name__ for v in present),
            "median": statistics.median(numbers) if numbers else None,
            "distinct": len(set(map(str, present))),
        }
    return out


def drift(baseline, current, fill_drop=0.1, median_ratio=3.0):
    """Compare two batch profiles and describe what changed shape."""
    alerts = []
    for field, base in baseline.items():
        cur = current[field]
        if base["fill_rate"] - cur["fill_rate"] > fill_drop:
            alerts.append(f"{field}: filled {base['fill_rate']:.0%} -> {cur['fill_rate']:.0%}")
        if set(cur["types"]) - set(base["types"]):
            alerts.append(f"{field}: new types {sorted(set(cur['types']) - set(base['types']))}")
        if base["median"] and cur["median"]:
            ratio = cur["median"] / base["median"]
            if ratio > median_ratio or ratio < 1 / median_ratio:
                alerts.append(f"{field}: median {base['median']:g} -> {cur['median']:g}")
    return alerts

Los umbrales son puntos de partida. Una caída de 10 puntos en la tasa de relleno o un cambio de tres veces en una mediana rara vez son normales; ajusta ambos por campo una vez que tengas unas semanas de historial.

Por qué se necesitan ambas: una pequeña prueba

Generamos una línea base sana de 1.000 registros de productos, luego simulamos tres roturas realistas y ejecutamos ambas capas sobre cada una.

DerivaQué ocurrióContrato a nivel de registroPerfil de lote
Precio con formatoUn rediseño puso el símbolo de moneda dentro del precio en el 30% de las páginasDetectado: 300 registros rechazadosDetectado: nuevo tipo str en price
Unidades menoresLos precios empezaron a llegar como 6305 en lugar de 63.05No detectado: todos los registros pasaronDetectado: mediana de 63.05 a 6305, y un nuevo tipo int
Selector de stock rotoEl campo in-stock volvió vacío en el 80% de las páginasNo detectado: el campo es opcionalDetectado: relleno del 100% al 20%

El contrato detectó la única deriva que produjo valores obviamente malformados. Las otras dos produjeron registros individualmente válidos, un número positivo dentro de rango y un campo opcional dejado vacío, y solo la vista de lote mostró que algo había cambiado. El caso de las unidades menores no es hipotético: un endpoint de producto real que examinamos la semana pasada devolvió 10000 para un zapato de $100.00, como se describe en deja de analizar HTML.

Qué hacer cuando salta la deriva

  1. Pon el lote en cuarentena. No envíes datos de un lote con alertas de deriva hasta que alguien lo haya revisado. Un conjunto de datos tardío es mejor que uno erróneo.
  2. Localiza el alcance. Desglosa la alerta por sitio, tipo de página y versión del extractor. La deriva suele empezar en un sitio tras un cambio.
  3. Compara una muestra. Mira un puñado de registros afectados junto a las páginas de las que provienen. La causa suele ser obvia en unos minutos.
  4. Corrige y rellena hacia atrás. Actualiza el extractor, y luego vuelve a extraer el periodo afectado a partir de las páginas almacenadas si las conservas, o vuelve a recopilarlas si no.
  5. Actualiza la línea base de forma deliberada. Cuando un cambio es legítimo, como un sitio que realmente cambia de moneda, restablece la línea base a propósito, nunca de forma automática.

Reduce la probabilidad de deriva desde el principio

Algunas fuentes derivan menos que otras. Los datos estructurados incrustados para los motores de búsqueda cambian con mucha menos frecuencia que el diseño de la página, por eso extraerlos primero reduce la frecuencia con la que saltan los contratos. Vigilar las propias páginas también ayuda: un cambio de plantilla detectado por la detección de cambios a gran escala es una alerta temprana de que la extracción puede estar a punto de romperse, y una tasa creciente de infracciones de contrato en un sitio es una entrada importante para una puntuación de salud del objetivo. Recopilar de forma consistente desde un solo mercado por trabajo también elimina toda una clase de deriva causada por variantes geográficas y de moneda.

La conclusión

Los datos web derivan porque la web cambia sin avisar. El peligro no es que los extractores se rompan, sino que sigan funcionando en páginas que ya no son lo que eran cuando se construyeron, y produzcan datos que parecen correctos registro a registro.

Comprueba cada registro contra un contrato, y comprueba cada lote contra su propio historial reciente. En nuestra prueba, el contrato por sí solo detectó una deriva de tres; el perfil de lote detectó las tres. Juntos, convierten “el panel se veía raro durante tres semanas” en una alerta la mañana en que empezó.

Fuentes y referencias

  • Prueba realizada por Shifter el 29 de septiembre de 2026 con el código anterior, sobre 1.000 registros de productos generados y tres derivas simuladas.

¿Listo para empezar?

Prueba los proxies residenciales de Shifter, más de 205M IPs, más de 195 países, desde 0,75 $/GB.

Comenzar