Volver al blog

JSON-LD Web Scraping Con Python: Extraer Datos Estructurados

Emily Chen
Emily Chen

Advanced Data Extraction Specialist

30-Jul-2026

TL;DR:

  • JSON-LD suele ser el camino más corto desde una página a un registro tipificado. Busca <script type="application/ld+json"> antes de escribir selectores para un titular, autor, fecha de publicación, imagen o campos de producto.
  • Lee cada bloque JSON-LD. Una página puede dividir los datos de organización, ruta de navegación, artículo, producto y preguntas frecuentes a través de varios scripts.
  • Normaliza tres formas de nivel superior. Un bloque JSON-LD puede ser un objeto, un arreglo de objetos, o un objeto cuyo @graph contenga los nodos útiles.
  • Los campos de Schema.org son opcionales en la práctica. Selecciona el nodo por @type, mantén los campos ausentes como None y valida solo los campos que tu pipeline realmente requiere.
  • Beautiful Soup no ejecuta JavaScript. Si una página inyecta JSON-LD después de cargar, primero obtén HTML renderizado y ejecuta el mismo analizador sobre esa respuesta.
  • Puedes probar el analizador sin un modelo en la nube. El ejemplo completo de Python a continuación lee un nodo Article real, compara su titular con el H1 visible y valida la salida.
  • Empieza con páginas públicas y limitadas. Crea una cuenta gratuita en Scrapeless cuando tu objetivo necesite HTML renderizado.

JSON-LD suele contener los campos que un extractor está a punto de reconstruir a partir de elementos de página dispersos. En un artículo en vivo de Scrapeless, un solo objeto Article lleva el titular, autor, editor, fechas, URL canónica, imagen principal, descripción y palabras clave. La página visible sigue siendo importante, pero los metadatos estructurados dan al pipeline de extracción un punto de partida tipificado.

JSON-LD sigue la especificación JSON-LD 1.1, mientras que vocabularios como Article, Product y BreadcrumbList provienen del modelo de datos estructurados de Schema.org. Ningún estándar garantiza que cada editor complete cada propiedad. Tu analizador debe preservar esa incertidumbre en lugar de inventar valores.

Para qué es bueno el web scraping con JSON-LD

JSON-LD es útil cuando una página publica hechos legibles por máquina junto con su diseño orientado al ser humano. Los nodos comunes incluyen:

  • Article y NewsArticle para titulares, fechas, autores, imágenes y editores;
  • Product para nombres, marcas, ofertas, calificaciones e identificadores;
  • BreadcrumbList para jerarquía y rutas de categoría canónicas;
  • Organization, Person y LocalBusiness para metadatos de entidades;
  • VideoObject, Recipe, Event y otros tipos específicos de dominio.

El bloque de script no es un punto final privado. Es parte de la respuesta de la página y está destinado a máquinas como los rastreadores de búsqueda. Eso lo hace más duradero que una clase CSS generada, pero no automáticamente completo o correcto. Trátalo como una fuente para validar, no como un oráculo.

Instalar Beautiful Soup

Esta guía utiliza Python 3.10 o posterior, requests y beautifulsoup4 4.15.0:

bash Copy
python -m pip install "requests>=2.32,<3" "beautifulsoup4==4.15.0"

El API de búsqueda de árbol de Beautiful Soup puede filtrar etiquetas por atributo, lo cual es suficiente para recoger cada script que coincida. La decodificación JSON permanece en la biblioteca estándar de Python.

Obtener el HTML fuente

Comienza con una solicitud HTTP ordinaria. El objetivo utilizado aquí publica su JSON-LD en la respuesta inicial, por lo que la renderización de JavaScript añadiría costo sin cambiar el resultado:

python Copy
import requests

URL = "https://www.scrapeless.com/es/blog/what-is-web-scraping?utm_source=website&utm_medium=blog&utm_campaign=universalscrapingapi&utm_term=json-ld-structured-data-web-scraping"

response = requests.get(
    URL,
    headers={"User-Agent": "Mozilla/5.0"},
    timeout=30,
)
response.raise_for_status()
print("Bytes HTML:", len(response.content))
text Copy
Bytes HTML: 439487

El conteo de bytes puede cambiar cuando el paquete de la página cambia. La invariante útil es que la respuesta contiene al menos un script application/ld+json decodificable.

Analiza cada bloque JSON-LD

No uses soup.find(...) a menos que el contrato de la página prometa explícitamente un bloque. find_all preserva la posibilidad de que el artículo, la ruta de navegación y el editor vivan en scripts separados:

python Copy
import json
from bs4 import BeautifulSoup

soup = BeautifulSoup(response.text, "html.parser")
scripts = soup.find_all("script", type="application/ld+json")

parsed_blocks = []
for script in scripts:
    raw = script.get_text(strip=True)
    try:
        parsed_blocks.append(json.loads(raw))
    except json.JSONDecodeError:
        continue

print("Bloques JSON-LD:", len(scripts))
print("bloques decodificados:", len(parsed_blocks))

Saltar bloques malformados es seguro solo cuando también registras cuántos se omitieron. Un except silencioso puede convertir una regresión de metadatos en un conjunto de datos vacío que parece exitoso.

Normalizar Objetos, Arrays y @graph

Los autores de JSON-LD tienen varias maneras válidas de agrupar nodos. Este generador aplana las tres formas que un scraper encuentra con mayor frecuencia:

python Copy
def iter_nodes(value):
    if isinstance(value, list):
        for item in value:
            yield from iter_nodes(item)
    elif isinstance(value, dict):
        graph = value.get("@graph")
        if isinstance(graph, list):
            for item in graph:
                yield from iter_nodes(item)
        else:
            yield value

nodes = [node for block in parsed_blocks for node in iter_nodes(block)]
article = next(node for node in nodes if node.get("@type") == "Article")

print("nodos normalizados:", len(nodes))
print("tipo seleccionado:", article["@type"])

@type también puede ser un array. Si tu corpus incluye editores que emiten "@type": ["Article", "NewsArticle"], normaliza ese campo antes de probar la pertenencia.

Construir un Registro Nullable

Los objetos anidados necesitan el mismo cuidado que los campos de nivel superior. El autor puede ser un diccionario, una lista, una cadena o estar ausente. Este objetivo utiliza un diccionario, por lo que el ejemplo lo lee defensivamente y preserva los campos opcionales faltantes como None:

python Copy
visible_h1 = soup.find("h1").get_text(" ", strip=True)
author = article.get("author") or {}
publisher = article.get("publisher") or {}

record = {
    "type": article.get("@type"),
    "headline": article.get("headline"),
    "visible_h1": visible_h1,
    "author": author.get("name") if isinstance(author, dict) else None,
    "publisher": publisher.get("name") if isinstance(publisher, dict) else None,
    "published": article.get("datePublished"),
    "modified": article.get("dateModified"),
    "image": article.get("image"),
    "description": article.get("description"),
    "keywords": article.get("keywords"),
    "source_url": URL,
}

Mantener source_url en cada fila hace que las auditorías posteriores sean posibles. Sin procedencia, un analizador corregido no puede discernir qué registros necesitan ser reconstruidos.

Validar los Campos que tu Pipeline Requiere

La validación debe reflejar el contrato downstream, no todas las propiedades que Schema.org permite:

python Copy
required = ("headline", "author", "published", "source_url")
missing = [field for field in required if not record.get(field)]
if missing:
    raise ValueError(f"faltan campos requeridos: {missing}")

print("titular:", record["headline"])
print("H1 visible:", record["visible_h1"])
print("las cabeceras coinciden:", record["headline"] == record["visible_h1"])
print("autor:", record["author"])
print("editor:", record["publisher"])
print("publicado:", record["published"])
text Copy
titular: ¿Qué es el Web Scraping? Guía Definitiva 2025
H1 visible: ¿Qué es el Web Scraping? Guía Definitiva 2025
las cabeceras coinciden: True
autor: Emily Chen
editor: Scrapeless
publicado: 2025-09-17T08:35:31.224Z

El titular coincide en esta página. No generalices ese resultado: los editores a veces actualizan el H1 visible sin actualizar los metadatos estructurados, o usan un titular de búsqueda más corto en JSON-LD. Comparar ambas superficies es una útil verificación de calidad.

¿Listo para ejecutar el mismo analizador en una respuesta renderizada? Abre una cuenta en Scrapeless y mantiene el código de análisis sin cambios.

Extractor Runnable Completo

El script completo une descubrimiento, normalización, selección, mapeo nullable y validación:

python Copy
import json
import requests
from bs4 import BeautifulSoup

URL = "https://www.scrapeless.com/es/blog/what-is-web-scraping?utm_source=website&utm_medium=blog&utm_campaign=universalscrapingapi&utm_term=json-ld-structured-data-web-scraping"


def iter_nodes(value):
    if isinstance(value, list):
        for item in value:
            yield from iter_nodes(item)
    elif isinstance(value, dict):
        graph = value.get("@graph")
        if isinstance(graph, list):
            for item in graph:
                yield from iter_nodes(item)
        else:
            yield value


response = requests.get(
    URL,
    headers={"User-Agent": "Mozilla/5.0"},
    timeout=30,
)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")

nodes = []
invalid_blocks = 0
scripts = soup.find_all("script", type="application/ld+json")
for script in scripts:
    try:
        nodes.extend(iter_nodes(json.loads(script.get_text(strip=True))))
    except json.JSONDecodeError:
        invalid_blocks += 1

article = next(node for node in nodes if node.get("@type") == "Article")
author = article.get("author") or {}
publisher = article.get("publisher") or {}
visible_h1 = soup.find("h1").get_text(" ", strip=True)

record = {
    "type": article.get("@type"),
    "headline": article.get("headline"),
json Copy
"visible_h1": visible_h1,
    "author": author.get("name") if isinstance(author, dict) else None,
    "publisher": publisher.get("name") if isinstance(publisher, dict) else None,
    "published": article.get("datePublished"),
    "image": article.get("image"),
    "keywords": article.get("keywords"),
    "source_url": URL,
}

required = ("headline", "author", "published", "source_url")
missing = [field for field in required if not record.get(field)]
if missing:
    raise ValueError(f"falta campos requeridos: {missing}")

print(f"bytes HTML: {len(response.content)}")
print(f"bloques JSON-LD: {len(scripts)}")
print(f"nodos normalizados: {len(nodes)}")
print(f"bloques inválidos: {invalid_blocks}")
print(f"tipo: {record['type']}")
print(f"titular: {record['headline']}")
print(f"visible H1: {record['visible_h1']}")
print(f"coincidencia de encabezados: {record['headline'] == record['visible_h1']}")
print(f"autor: {record['author']}")
print(f"editor: {record['publisher']}")
print(f"publicado: {record['published']}")
print(f"caracteres de palabras clave: {len(record['keywords'] or '')}")

La ejecución en vivo devolvió un bloque válido, un nodo `Artículo` normalizado, ningún bloque mal formado, encabezados coincidentes, autor `Emily Chen`, editor `Scrapeless`, y 140 caracteres en la cadena de palabras clave.

## Cuando el JSON-LD Aparece Solo Después de Renderizar

Beautiful Soup analiza los bytes que recibe; no ejecuta el JavaScript de una página. Un diagnóstico rápido es comparar la respuesta en bruto con el DOM del navegador. Si el navegador muestra un script `application/ld+json` pero `requests` no encuentra ninguno, obtenga HTML renderizado antes de analizar.

> Nota: La solicitud a continuación requiere una cuenta de Scrapeless financiada. La cuenta de verificación devolvió una respuesta de saldo insuficiente durante la revisión final, por lo que esta llamada HTTP es una brecha previa al requisito; el analizador anterior se ejecutó completamente contra la página pública real.

```python
import os
import requests

rendered = requests.post(
    "https://api.scrapeless.com/api/v2/unlocker/request",
    headers={"x-api-token": os.environ["SCRAPELESS_API_KEY"]},
    json={
        "actor": "unlocker.webunlocker",
        "input": {
            "url": URL,
            "method": "GET",
            "js_render": True,
        },
    },
    timeout=90,
)
rendered.raise_for_status()
html = rendered.json()["data"]

Pase html a BeautifulSoup y reutilice el mismo normalizador. La API Universal de Scraping proporciona la respuesta renderizada; no cambia el contrato JSON-LD.

Problemas Comunes de Datos JSON-LD

La página tiene varios nodos coincidentes

Seleccione por tipo e identidad. Para las variantes de producto, use @id, URL, SKU u otro campo estable en lugar de tomar el primer nodo Producto.

@type es una matriz

Conviértalo en un conjunto antes de verificar la pertenencia. Una prueba de igualdad estricta contra una cadena pasará por alto un nodo de múltiples tipos válido.

El script contiene entidades HTML o comentarios

JSON-LD debe ser texto JSON válido. Si un editor lo envuelve en sintaxis inválida, registre ese bloque como mal formado y corríjalo para esa fuente conocida; no aplique reemplazos de cadena amplios que puedan corromper valores legítimos.

Los metadatos estructurados no coinciden con el texto visible

Almacene ambos valores y defina la precedencia para su caso de uso. La auditoría de búsqueda puede preferir el titular JSON-LD; el monitoreo de contenido puede preferir el H1 visible. Un desajuste es dato, no meramente un error.

Un campo cambia de objeto a lista

Normalize en el límite. Los autores y las imágenes suelen cambiar entre un objeto y una matriz a medida que evoluciona el CMS de un editor.

Conclusión

Un raspador JSON-LD confiable hace cuatro cosas: recopila cada script coincidente, decodifica sin ocultar bloques mal formados, normaliza diccionarios, listas y @graph, luego valida un pequeño contrato aguas abajo. Ese camino es más corto y generalmente más estable que reconstruir el mismo registro a partir de selectores de diseño de página. Mantenga el DOM visible como una verificación cruzada, retenga la procedencia de la fuente e introduzca la renderización solo cuando el HTML inicial demuestre que es necesario.

Comience con el plan gratuito de Scrapeless, revise la documentación para desarrolladores, y consulte los precios de Scrapeless antes de mover un corpus renderizado a producción.

FAQ

P: ¿Es más fácil raspar JSON-LD que HTML visible?

Copy
Sí, cuando el editor incluye los campos que necesitas. JSON-LD te proporciona propiedades y tipos con nombre, mientras que el HTML visible a menudo requiere selectores y limpieza de texto; aún deberías comparar campos críticos con la página renderizada.

**P: ¿Por qué debería analizar cada script `application/ld+json`?**

Una página puede colocar diferentes entidades en bloques separados. Leer solo el primer script puede devolver la organización o la miga de pan y perder el artículo o producto que querías.

**P: ¿Qué significa `@graph` para la extracción?**

`@graph` agrupa varios nodos JSON-LD dentro de un solo objeto. Aplana el grafo, luego selecciona nodos por `@type`, `@id`, URL u otro identificador estable.

**P: ¿Qué pasa si falta una propiedad JSON-LD?**

Mantén las propiedades opcionales como `None` y falla solo cuando falte un campo requerido por tu propio contrato posterior. Schema.org describe propiedades posibles; no obliga a los editores a completar todas ellas.

**P: ¿Puede JSON-LD diferir de la página visible?**

Sí. Los metadatos y el contenido visible pueden actualizarse en diferentes horarios u optimizarse para diferentes superficies. Almacena ambos valores cuando la diferencia importa y haz explícita la prioridad.

**P: ¿Necesito un navegador para extraer JSON-LD?**

No cuando el script está presente en el HTML inicial. Solo necesitas renderizar cuando JavaScript del lado del cliente inserta o modifica los datos estructurados después de que llega la respuesta en bruto.

**P: ¿Está siempre permitido extraer JSON-LD público?**

No hay una regla general que haga que toda colección sea lícita o permitida. Revisa los términos del sitio y <a href="https://datatracker.ietf.org/doc/html/rfc9309" rel="nofollow"><strong>directivas de robots</strong></a>, mantén el volumen de solicitudes dentro de límites, recoge solo los campos que necesitas y obtén asesoría legal para usos sensibles o comerciales.

En Scrapeless, solo accedemos a datos disponibles públicamente y cumplimos estrictamente con las leyes, regulaciones y políticas de privacidad del sitio web aplicables. El contenido de este blog es sólo para fines de demostración y no implica ninguna actividad ilegal o infractora. No ofrecemos garantías y renunciamos a toda responsabilidad por el uso de la información de este blog o enlaces de terceros. Antes de realizar cualquier actividad de scraping, consulte a su asesor legal y revise los términos de servicio del sitio web de destino u obtenga los permisos necesarios.

Artículos más populares

Catalogar