Saltar al contenido
Empezar gratis
Volver al blog

Cómo analizar JSON en Python: desde respuestas de API hasta JSON oculto en páginas web

Suciu DanÚltima actualización el 13 min read
Cómo analizar JSON en Python: desde respuestas de API hasta JSON oculto en páginas web
En resumen: Para analizar JSON en Python, usa el módulo json integrado: json.loads() para una cadena o bytes, json.load() para un archivo y response.json() de Requests para una respuesta HTTP. Comprueba por separado el estado HTTP y la validez del JSON, porque una respuesta 200 puede seguir siendo HTML. Esta guía también cubre errores de decodificación, acceso anidado, validación, archivos grandes y JSON oculto en páginas web.

El módulo integrado de Python json analiza JSON. Utiliza json.loads() para una cadena o bytes, json.load() para un archivo y el método response.json() para una respuesta HTTP. Esta guía aborda los errores de decodificación, el acceso anidado seguro, la validación, los archivos de gran tamaño, la precisión numérica y el JSON oculto en páginas web.

Puntos clave

  • Es preferible utilizar json.loads para cadenas/bytes y json.load para archivos; abre los archivos en UTF-8 (o utf-8-sig cuando haya un BOM).
  • Trata el estado HTTP y la validez de JSON como comprobaciones independientes: una respuesta 200 puede seguir siendo HTML.
  • Navega por datos anidados y desordenados con .get() cadenas o un pequeño dig() ayuda; valida con Pydantic o JSON Schema tras el análisis.
  • Transmite matrices enormes con ijson, añade las ejecuciones de rastreo como líneas JSON y evalúa orjson en tus propias cargas útiles.
  • Da prioridad al JSON incrustado en la página (JSON-LD, __NEXT_DATA__) o los campos estructurados de una API de extracción frente a los frágiles selectores CSS cuando el diseño HTML cambia con frecuencia.

¿Cómo se analiza una cadena JSON en Python?

Llama a json.loads() a un str o bytes valor. Devuelve un objeto de Python —normalmente un dict o list—que refleja la estructura JSON.

import json

raw = """
{
  "sku": "WSA-BOTTLE-750",
  "title": "Insulated Steel Water Bottle 750ml",
  "price": 19.99,
  "currency": "USD",
  "in_stock": true,
  "variants": [
    {"color": "matte black", "capacity_ml": 750},
    {"color": "ocean blue", "capacity_ml": 750}
  ],
  "seller": {
    "name": "TrailGear Co",
    "rating": 4.8,
    "country": "US"
  }
}
"""

product = json.loads(raw)
print(product["sku"], product["variants"][0]["color"])
# WSA-BOTTLE-750 matte black

# bytes work the same way
product_from_bytes = json.loads(raw.encode("utf-8"))
assert product_from_bytes["title"] == product["title"]

En todos los ejemplos aparece la misma ficha de producto, por lo que cada ejemplo se basa en un único registro de tipo «scraped».

¿Cómo se lee un archivo JSON en Python?

Abre el archivo con codificación UTF-8 y pasa el objeto de archivo a json.load().

import json
from pathlib import Path

path = Path("product.json")
with path.open(encoding="utf-8") as f:
    product = json.load(f)

print(product["currency"])  # USD

Qué hacer cuando un archivo JSON comienza con un BOM

Algunos editores guardan el UTF-8 con una marca de orden de bytes (EF BB BF). Ese carácter invisible se sitúa antes del primer { e impide que la decodificación se realice correctamente. Abre el archivo con encoding="utf-8-sig" para que Python elimine la BOM antes de json.load se ejecute. Si ya tienes los bytes, descodifícalos con utf-8-sig y, a continuación, llama a json.loads.

import json
from pathlib import Path

bom_path = Path("product_bom.json")
with bom_path.open(encoding="utf-8-sig") as f:
    product = json.load(f)

# Or from bytes:
product = json.loads(bom_path.read_bytes().decode("utf-8-sig"))

Cómo se asignan los tipos JSON a los tipos de Python

Los tipos JSON se corresponden con los tipos de Python tal y como se definen en la gramática JSON de json.org y en la biblioteca estándar:

JSON

Python (json.loads)

Python → JSON (json.dumps)

objeto

dict

dict

matriz

list

list / tuple

cadena

str

str

número (entero)

int

int

número (real)

float

float / int

true / false

True / False

bool

null

None

None

Por qué cambian tus claves y tuplas tras un «round trip»

Los nombres de los objetos JSON son siempre cadenas. Las claves enteras del diccionario se convierten en cadenas tras dumps → loads. Las tuplas se convierten en listas porque JSON solo admite matrices. Los conjuntos no son serializables y generan un TypeError.

import json

print(json.loads(json.dumps({1: "a", 2: "b"})))
# {'1': 'a', '2': 'b'}

print(json.loads(json.dumps({"coords": (10, 20)})))
# {'coords': [10, 20]}

try:
    json.dumps({"tags": {"refillable", "bpa-free"}})
except TypeError as e:
    print(e)
# Object of type set is not JSON serializable

¿Cómo se analiza una respuesta JSON de una API?

Llama a response.raise_for_status() para detectar errores HTTP y, a continuación, response.json() para decodificar el cuerpo. Los códigos de estado y la validez del JSON son independientes: un código de estado de éxito no garantiza que el cuerpo sea JSON.

import requests

resp = requests.get("https://httpbin.org/json", timeout=15)
resp.raise_for_status()
data = resp.json()
print(type(data), list(data)[:3])

Cuando una respuesta 200 no es JSON

Las páginas de bloqueo y de desafío suelen devolver HTML con un código HTTP 200. Comprueba Content-Type, detecta los errores de decodificación y muestra una vista previa breve y segura del cuerpo:

import json
from typing import Any

import requests


def get_json(url: str, timeout: float = 15.0) -> Any:
    resp = requests.get(url, timeout=timeout)
    resp.raise_for_status()
    content_type = resp.headers.get("Content-Type", "")
    if "json" not in content_type.lower() and "javascript" not in content_type.lower():
        preview = resp.text[:120].replace("\n", " ")
        raise ValueError(
            f"Expected JSON Content-Type, got {content_type!r}. "
            f"Body starts: {preview!r}"
        )
    try:
        return resp.json()
    except json.JSONDecodeError as e:
        preview = resp.text[:120].replace("\n", " ")
        raise ValueError(
            f"JSON decode failed at pos {e.pos}: {e.msg}. "
            f"Body starts: {preview!r}"
        ) from e


product_feed = get_json("https://httpbin.org/json")
# get_json("https://httpbin.org/html")  # raises ValueError

Una página HTML con un estado 200 suele ser una página de bloqueo; en nuestra guía sobre cómo extraer datos sin que te bloqueen se explica cómo evitarlo.

¿Cómo se soluciona un JSONDecodeError?

JSONDecodeError significa que la entrada no es JSON válido: cuerpo vacío, carga truncada, HTML, comillas simples, una coma al final o algo similar. Lee e.msg, e.lineno, e.colnoy e.pos, y luego imprime el texto que rodea pos para que puedas ver dónde se produce el error. Los detalles completos sobre los atributos y los hooks se encuentran en la documentación del módulo Python json.

import json


def locate_decode_error(text: str) -> None:
    try:
        json.loads(text)
    except json.JSONDecodeError as e:
        start = max(0, e.pos - 20)
        end = min(len(text), e.pos + 20)
        print(f"{e.msg} at line {e.lineno}, col {e.colno}, pos {e.pos}")
        print(f"Around error: {text[start:end]!r}")


locate_decode_error('{"sku": "WSA-BOTTLE-750", "price":')
# Expecting value at line 1, col 36, pos 35

Causas comunes y cómo se manifiesta cada una de ellas

Síntoma (fragmento del mensaje)

Causa probable

Solución

Expecting value en la posición 0

Cuerpo vacío o HTML/undefined

Comprueba el transporte; asegúrate de que has obtenido JSON

Expecting value en medio de la cadena

Respuesta truncada

Vuelve a intentarlo, aumenta los tiempos de espera o utiliza un flujo con comprobaciones de longitud

Expecting property name enclosed in double quotes

Claves entre comillas simples o coma final

Corrige el generador; no lo «repares» con eval

Extra data

Dos valores JSON en una sola cadena

Dividir las líneas JSON o analizar un valor cada vez

Etiquetas HTML en la vista previa

Bloqueo leve o página de inicio de sesión

Corrige los encabezados, las cookies o la representación; consulta la guía de resolución de bloqueos anterior

¿Cómo se accede de forma segura a un JSON anidado?

Indexación rígida (data["seller"]["rating"]) genera un KeyError cuando falta una clave. .get() Las cadenas devuelven un valor por defecto, pero se vuelven confusas en rutas profundas. Un pequeño dig() recorre las claves de los diccionarios y los índices de las listas, y trata una ruta que falta de forma diferente a una null.

from typing import Any


def dig(data: Any, *path: str | int, default: Any = None) -> Any:
    cur: Any = data
    for key in path:
        if isinstance(cur, dict):
            if key not in cur:
                return default
            cur = cur[key]
        elif isinstance(cur, list) and isinstance(key, int):
            if key < 0 or key >= len(cur):
                return default
            cur = cur[key]
        else:
            return default
    return cur


product = {
    "variants": [{"color": "matte black", "capacity_ml": 750}],
    "seller": {"name": "TrailGear Co", "rating": 4.8},
}

print(dig(product, "seller", "rating"))                 # 4.8
print(dig(product, "variants", 0, "color"))             # matte black
print(dig(product, "variants", 9, "color", default="?"))  # ?
print(dig({"seller": {"rating": None}}, "seller", "rating", default="missing"))
# None  — key exists, value is null
print(dig({"seller": {}}, "seller", "rating", default="missing"))
# missing — key absent

¿Cómo se validan los datos JSON en Python?

El análisis sintáctico solo comprueba la sintaxis. Después de loads, comprueba los tipos y los campos obligatorios con Pydantic o JSON Schema.

from decimal import Decimal

from pydantic import BaseModel, ValidationError


class Variant(BaseModel):
    color: str
    capacity_ml: int


class Seller(BaseModel):
    name: str
    rating: float
    country: str


class Product(BaseModel):
    sku: str
    title: str
    price: Decimal
    currency: str
    in_stock: bool
    variants: list[Variant]
    seller: Seller
    notes: str | None = None


product = Product.model_validate(
    {
        "sku": "WSA-BOTTLE-750",
        "title": "Insulated Steel Water Bottle 750ml",
        "price": 19.99,
        "currency": "USD",
        "in_stock": True,
        "variants": [{"color": "matte black", "capacity_ml": 750}],
        "seller": {"name": "TrailGear Co", "rating": 4.8, "country": "US"},
    }
)
print(product.price)  # 19.99

try:
    Product.model_validate(
        {
            "sku": "WSA-BOTTLE-750",
            "title": "Insulated Steel Water Bottle 750ml",
            "price": "cheap",
            "currency": "USD",
            "in_stock": True,
            "variants": [],
            "seller": {"name": "TrailGear Co", "rating": 4.8, "country": "US"},
        }
    )
except ValidationError as e:
    print(e.error_count(), "validation error(s)")

La misma estructura que un JSON Schema, validada con el jsonschema paquete (véase también «Comprender JSON Schema»):

import jsonschema

schema = {
    "type": "object",
    "required": ["sku", "title", "price", "currency", "in_stock", "variants", "seller"],
    "properties": {
        "sku": {"type": "string"},
        "title": {"type": "string"},
        "price": {"type": "number"},
        "currency": {"type": "string", "minLength": 3, "maxLength": 3},
        "in_stock": {"type": "boolean"},
        "variants": {
            "type": "array",
            "items": {
                "type": "object",
                "required": ["color", "capacity_ml"],
                "properties": {
                    "color": {"type": "string"},
                    "capacity_ml": {"type": "integer"},
                },
            },
        },
        "seller": {
            "type": "object",
            "required": ["name", "rating", "country"],
            "properties": {
                "name": {"type": "string"},
                "rating": {"type": "number"},
                "country": {"type": "string"},
            },
        },
    },
}

jsonschema.validate(
    {
        "sku": "WSA-BOTTLE-750",
        "title": "Insulated Steel Water Bottle 750ml",
        "price": 19.99,
        "currency": "USD",
        "in_stock": True,
        "variants": [{"color": "matte black", "capacity_ml": 750}],
        "seller": {"name": "TrailGear Co", "rating": 4.8, "country": "US"},
    },
    schema,
)

¿Cómo se escribe JSON en una cadena o en un archivo?

Utiliza json.dumps para una cadena y json.dump para un archivo. indent formatea de forma legible, sort_keys=True estabiliza las diferencias y separators=(",", ":") reduce el tamaño de los datos.

import json
from pathlib import Path

product = {
    "sku": "WSA-BOTTLE-750",
    "title": "Insulated Steel Water Bottle 750ml",
    "price": 19.99,
    "currency": "USD",
    "in_stock": True,
}

print(json.dumps(product, indent=2, sort_keys=True))
print(json.dumps(product, separators=(",", ":")))

with Path("product_out.json").open("w", encoding="utf-8") as f:
    json.dump(product, f, ensure_ascii=False, indent=2)

Escribir texto que no esté en inglés sin secuencias de escape

Por defecto dumps escapa los caracteres no ASCII como \uXXXX. Utiliza ensure_ascii=False cuando desees que el UTF-8 sea legible en archivos y registros.

import json

title = {"title": "Bouteille d'eau 750ml — acier"}
print(json.dumps(title))
# {"title": "Bouteille d'eau 750ml \u2014 acier"}
print(json.dumps(title, ensure_ascii=False))
# {"title": "Bouteille d'eau 750ml — acier"}

Serialización de datetime, Decimal y clases de datos

Pasa una default= función para los tipos que el codificador no reconozca. También puedes crear una subclase json.JSONEncoder cuando necesites una clase de codificador reutilizable.

import json
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
from decimal import Decimal
from typing import Any


def default_encoder(obj: Any) -> Any:
    if isinstance(obj, datetime):
        return obj.isoformat()
    if isinstance(obj, Decimal):
        return str(obj)
    if hasattr(obj, "__dataclass_fields__"):
        return asdict(obj)
    raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")


@dataclass
class Offer:
    sku: str
    price: Decimal
    listed_at: datetime


offer = Offer(
    sku="WSA-BOTTLE-750",
    price=Decimal("19.99"),
    listed_at=datetime(2026, 10, 6, 12, 0, tzinfo=timezone.utc),
)
print(json.dumps(offer, default=default_encoder))
# {"sku": "WSA-BOTTLE-750", "price": "19.99", "listed_at": "2026-10-06T12:00:00+00:00"}

¿Cómo se mantiene la precisión en los precios y los números grandes?

Utiliza parse_float=Decimal para que valores como 19.99 se mantengan exactos como decimales en lugar de números flotantes binarios. La RFC 8259, el estándar JSON, no permite NaN ni Infinity. Tras el análisis sintáctico, el módulo `decimal` de Python es el lugar habitual para obtener valores seguros en materia monetaria.

import json
from decimal import Decimal

data = json.loads('{"price": 19.99}', parse_float=Decimal)
print(data["price"], type(data["price"]))
# 19.99 <class 'decimal.Decimal'>

# Reject NaN / Infinity when encoding
try:
    json.dumps({"price": float("nan")}, allow_nan=False)
except ValueError as e:
    print(e)

El decodificador de Python acepta NaN y Infinity por defecto como una extensión. Pasa un parse_constant que genere una excepción si quieres rechazarlos en la entrada.

¿Cómo se detectan las claves duplicadas en JSON?

La RFC 8259, el estándar JSON, establece que los nombres de los objetos deben ser únicos, pero json.loads conserva el último valor sin avisar. En su lugar, object_pairs_hook puede lanzar una excepción en su lugar.

import json
from typing import Any


def detect_duplicates(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
    seen: set[str] = set()
    out: dict[str, Any] = {}
    for key, value in pairs:
        if key in seen:
            raise ValueError(f"Duplicate key: {key!r}")
        seen.add(key)
        out[key] = value
    return out


text = '{"sku": "A", "sku": "B", "price": 1}'
print(json.loads(text)["sku"])  # B — last value wins

try:
    json.loads(text, object_pairs_hook=detect_duplicates)
except ValueError as e:
    print(e)  # Duplicate key: 'sku'

¿Cómo se trabaja con archivos JSON Lines?

El formato JSON Lines almacena un valor JSON por línea. Añade un registro tras cada extracción, luego lee línea por línea, omite los espacios en blanco e indica el número de línea cuando un registro no se pueda decodificar.

import json
from pathlib import Path

path = Path("products.jsonl")

# Append during a run
with path.open("a", encoding="utf-8") as f:
    for rec in (
        {"sku": "WSA-BOTTLE-750", "price": 19.99},
        {"sku": "WSA-LID-01", "price": 4.5},
    ):
        f.write(json.dumps(rec, ensure_ascii=False) + "\n")

# Read with line numbers
records = []
with path.open(encoding="utf-8") as f:
    for lineno, line in enumerate(f, start=1):
        line = line.strip()
        if not line:
            continue
        try:
            records.append(json.loads(line))
        except json.JSONDecodeError as e:
            raise ValueError(f"Invalid JSON on line {lineno}: {e.msg}") from e

print(len(records), "records")

¿Cómo se analiza un archivo JSON que es demasiado grande para la memoria?

Utiliza «ijson» en PyPI: itera por los elementos de una ruta en lugar de cargar todo el documento. Utiliza use_float=True solo cuando la precisión de los números flotantes binarios sea aceptable.

import json
from decimal import Decimal
from pathlib import Path

import ijson

# Build a sample file so the example runs anywhere
large = Path("large_products.json")
items = [{"sku": f"SKU-{i}", "price": i * 1.5} for i in range(5_000)]
large.write_text(json.dumps({"items": items}), encoding="utf-8")

count = 0
total = Decimal("0")
with large.open("rb") as f:
    for item in ijson.items(f, "items.item"):
        count += 1
        total += Decimal(str(item["price"]))

print(count, "items; sum", total)

¿Es orjson más rápido que el módulo json?

A menudo sí, pero compruébalo con tus propios datos. La documentación de orjson en PyPI señala las diferencias en la API que importan en la práctica: dumps devuelve bytes, las opciones son indicadores en lugar de argumentos clave, y la sangría utiliza OPT_INDENT_2 en lugar de un indent entero.

import json
import timeit

import orjson

payload = {
    "sku": "WSA-BOTTLE-750",
    "title": "Insulated Steel Water Bottle 750ml",
    "price": 19.99,
    "variants": [{"color": "matte black", "capacity_ml": 750}],
}

encoded = orjson.dumps(payload)
assert isinstance(encoded, bytes)
decoded = orjson.loads(encoded)
pretty = orjson.dumps(payload, option=orjson.OPT_INDENT_2)

def harness() -> None:
    orjson.dumps(payload)
    json.dumps(payload)

print(timeit.timeit(harness, number=5_000))
# Run locally and compare; numbers vary by machine and payload.

¿Cómo se extrae el JSON oculto dentro de una página web?

Muchas páginas incluyen sus datos como JSON en <script> , lo que suele ser más estable que los selectores CSS en el HTML renderizado. Analiza ese JSON y, a continuación, recórrelo con las mismas herramientas que utilizas para las respuestas de las API.

Lectura de datos estructurados en JSON-LD

El JSON-LD se encuentra en script[type="application/ld+json"] bloques. Una página puede generar un único objeto, una lista o un @graph envoltura. Filtra por @type == "Product" (o cualquier otro tipo que necesites):

import json
from typing import Any

from bs4 import BeautifulSoup

html = """
<!DOCTYPE html>
<html><head>
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Insulated Steel Water Bottle 750ml",
  "sku": "WSA-BOTTLE-750",
  "offers": {"@type": "Offer", "price": "19.99", "priceCurrency": "USD"}
}
</script>
<script type="application/ld+json">
[
  {"@type": "BreadcrumbList", "itemListElement": []},
  {"@type": "Product", "name": "Spare Lid", "sku": "WSA-LID-01"}
]
</script>
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@graph": [
    {"@type": "Organization", "name": "TrailGear Co"},
    {"@type": "Product", "name": "Bottle Bundle", "sku": "WSA-BUNDLE"}
  ]
}
</script>
</head><body></body></html>
"""


def extract_json_ld_products(page_html: str) -> list[dict[str, Any]]:
    soup = BeautifulSoup(page_html, "html.parser")
    products: list[dict[str, Any]] = []

    def collect(node: Any) -> None:
        if isinstance(node, list):
            for item in node:
                collect(item)
            return
        if not isinstance(node, dict):
            return
        if node.get("@type") == "Product":
            products.append(node)
        graph = node.get("@graph")
        if isinstance(graph, list):
            collect(graph)

    for tag in soup.select('script[type="application/ld+json"]'):
        if not tag.string:
            continue
        collect(json.loads(tag.string))
    return products


for product in extract_json_ld_products(html):
    print(product["sku"], product["name"])

Cuando una página inserta su JSON-LD mediante JavaScript, un navegador alojado que representa la página antes de devolverla te proporciona el HTML definitivo.

La lectura __NEXT_DATA__ y el estado del framework

__NEXT_DATA__ y el estado del framework

Next.js y otros marcos similares incrustan las propiedades de la página en una script#__NEXT_DATA__ etiqueta. Analiza el texto de la etiqueta y, a continuación, recórrelo con dig(). Para ver un tutorial completo sobre el sector minorista, consulta nuestra guía de extracción de datos de Walmart, que analiza el registro del producto de __NEXT_DATA__.

import json
from typing import Any

from bs4 import BeautifulSoup


def dig(data: Any, *path: str | int, default: Any = None) -> Any:
    cur: Any = data
    for key in path:
        if isinstance(cur, dict):
            if key not in cur:
                return default
            cur = cur[key]
        elif isinstance(cur, list) and isinstance(key, int):
            if key < 0 or key >= len(cur):
                return default
            cur = cur[key]
        else:
            return default
    return cur


html = """
<!DOCTYPE html>
<html><body>
<script id="__NEXT_DATA__" type="application/json">
{
  "props": {
    "pageProps": {
      "product": {
        "sku": "WSA-BOTTLE-750",
        "title": "Insulated Steel Water Bottle 750ml",
        "price": 19.99
      }
    }
  }
}
</script>
</body></html>
"""

soup = BeautifulSoup(html, "html.parser")
tag = soup.find("script", id="__NEXT_DATA__")
next_data = json.loads(tag.string)
print(dig(next_data, "props", "pageProps", "product", "sku"))
# WSA-BOTTLE-750

Llamar al punto final JSON que la página carga en segundo plano

Abre DevTools → Red, recarga la página y filtra por Fetch/XHR. Muchas interfaces de usuario de productos y listados cargan una URL JSON a la que el navegador ya realiza una llamada. Si se puede acceder públicamente a ese punto final según los términos del sitio, puedes solicitarlo directamente con los mismos encabezados que utiliza la página y analizarlo con response.json(). Da prioridad a las API documentadas o claramente públicas cuando existan. Consulta las condiciones del sitio y nuestros principios para el uso responsable de los datos web antes de llamar a puntos finales internos.

¿Cómo se analiza el JSON de WebScrapingAPI?

La API de Scraper puede devolver campos seleccionados como JSON en lugar de HTML sin procesar. Utiliza extract_rules para los campos con nombre, json_response=1 para el contenedor completo (incluidos los metadatos JSON-LD), o decodifica un objetivo JSON a partir de envelope["body"]. Mantén la clave en WSA_API_KEY.

Obtener campos con nombre mediante extract_rules

Pasa un objeto extract_rules a /v2. La respuesta contiene una propiedad por cada clave de regla. Las reglas anidadas recorren las tarjetas repetidas; table_json convierte las tablas HTML en objetos de fila.

import json
import os

import requests

api_key = os.environ["WSA_API_KEY"]

extract_rules = {
    "title": {"selector": "h1", "output": "text"},
    "results": {
        "selector": "article.product_pod",
        "all": 1,
        "output": {
            "name": {"selector": "h3 a", "output": "@title"},
            "url": {"selector": "h3 a", "output": "@href"},
            "price": {"selector": "p.price_color", "output": "text"},
        },
    },
}

resp = requests.get(
    "https://api.webscrapingapi.com/v2",
    params={
        "api_key": api_key,
        "url": "https://books.toscrape.com/",
        "extract_rules": json.dumps(extract_rules),
    },
    timeout=60,
)
resp.raise_for_status()
fields = resp.json()
print(fields["title"])
# All products
print(fields["results"][:2])
# [{'name': 'A Light in the Attic', 'url': 'catalogue/a-light-in-the-attic_1000/index.html', 'price': '£51.77'}, {'name': 'Tipping the Velvet', 'url': 'catalogue/tipping-the-velvet_999/index.html', 'price': '£53.74'}]

La lista de referencia de reglas de extracción enumera todos los tipos de salida, incluyendo table_json.

Lectura de la respuesta JSON completa

Configurar json_response=1 para envolver el rasgado en un objeto JSON con headers, cost, initial-status-code, body, type, y metadata. El metadata objeto incluye los microdatos extraídos y el JSON-LD, por lo que metadata["json-ld"] es un atajo cuando solo necesitas datos estructurados de la página.

import os

import requests

api_key = os.environ["WSA_API_KEY"]

resp = requests.get(
    "https://api.webscrapingapi.com/v2",
    params={
        "api_key": api_key,
        "url": "https://example.com",
        "json_response": 1,
    },
    timeout=60,
)
resp.raise_for_status()
envelope = resp.json()

print(envelope["initial-status-code"], envelope["type"], envelope["cost"])
body = envelope["body"]          # page content as a string
json_ld = envelope.get("metadata", {}).get("json-ld", [])
print(type(body), len(json_ld))

La documentación completa sobre la respuesta JSON describe cada campo de la envoltura.

Análisis de un cuerpo JSON dentro de la envoltura

Cuando la propia URL de destino devuelve JSON, la envoltura sigue colocando esa carga útil body como una cadena. Consulta type (por ejemplo "json") y descódalo con json.loads(envelope["body"]) antes de recorrer los campos.

import json
import os

import requests

api_key = os.environ["WSA_API_KEY"]

resp = requests.get(
    "https://api.webscrapingapi.com/v2",
    params={
        "api_key": api_key,
        "url": "https://httpbin.org/json",
        "json_response": 1,
    },
    timeout=60,
)
resp.raise_for_status()
envelope = resp.json()

if envelope.get("type") == "json":
    payload = json.loads(envelope["body"])
else:
    # HTML or other types still arrive as a string in body
    payload = envelope["body"]

print(type(payload))

En el caso de los resultados de búsqueda, una API SERP que devuelva los resultados como JSON estructurado evita por completo el análisis de HTML.

¿Qué método de análisis de JSON deberías utilizar?

Adapta la entrada a la herramienta: la biblioteca estándar para cadenas y archivos, ijson para matrices enormes, un script de análisis para datos incrustados en HTML y una API de scraping cuando quieras campos o un «envelope» en lugar de HTML sin procesar.

Lo que tienes

Qué utilizar

Cadena JSON o bytes

json.loads

Archivo JSON (UTF-8)

json.load con encoding="utf-8"

Archivo que comienza con un BOM

encoding="utf-8-sig" y, a continuación, json.load / json.loads

Solicitudes / Respuesta HTTP

raise_for_status() + Comprobación del tipo de contenido + response.json()

Se necesitan valores por defecto para las rutas anidadas

.get() cadena o dig()

Se necesitan tipos / campos obligatorios

Modelo Pydantic o esquema JSON tras el análisis

Moneda o decimales precisos

parse_float=Decimal

Las claves duplicadas deben dar error

object_pairs_hook detector

Un registro por línea de rastreo

Líneas JSON (*.jsonl)

Matriz demasiado grande para la RAM

Transmisión de JSON

Codificación/decodificación con requisitos críticos de rendimiento

orjson (mide en tu carga útil)

Datos incrustados en HTML

JSON-LD / __NEXT_DATA__ / otros script JSON

Campos seleccionados de una página a través de la API

WebScrapingAPI extract_rules

Se necesitan encabezados, coste y metadatos JSON-LD

json_response=1 sobre

La URL de destino devuelve JSON a través de la API

json.loads(envelope["body"]) cuando type es json

Preguntas frecuentes

¿Cuál es la diferencia entre json.load y json.loads?

json.loads analiza una cadena JSON o bytes que ya están en memoria. json.load lee desde un objeto similar a un archivo y analiza ese flujo. Utiliza loads para los cuerpos de las API y el texto en memoria; utiliza load para archivos abiertos con la codificación adecuada. Ambos devuelven los mismos tipos de Python para el mismo JSON.

¿Cómo analizo una respuesta JSON de una API en Python?

Envía la solicitud con Requests (u otro cliente HTTP), llama a raise_for_status(), comprueba que el Content-Type parece JSON y, a continuación, llama a response.json() o json.loads(response.text). Considera que una página HTML en blanco con código 200 es un problema de transporte, no un problema de sintaxis JSON, e inspecciona una vista previa breve del cuerpo cuando falle la decodificación.

¿Por qué aparece el mensaje «JSONDecodeError: Expecting value»?

El decodificador ha llegado a un punto donde debería comenzar un valor JSON y no ha encontrado nada válido: cuerpo vacío, JSON truncado, HTML o un token como undefined. Muestra e.pos y los caracteres que lo rodean. Corrige la respuesta de origen o el transporte; no lo pases por alto con un except.

¿Cómo puedo obtener de forma segura un valor anidado de un JSON en Python?

Evita el uso encadenado de data["a"]["b"] cuando puedan faltar claves. Utiliza .get("a", {}).get("b") para rutas poco profundas, o una pequeña dig(data, "a", "b", default=None) ayudante que también acepte índices de lista. Recuerda que una clave que falte y un JSON explícito null son resultados diferentes.

¿Cómo se validan los datos JSON en Python?

El análisis sintáctico solo comprueba la sintaxis. Después loads, valida los datos con un modelo de Pydantic (tipos, elementos opcionales, modelos anidados) o un documento de JSON Schema mediante el jsonschema paquete. Mantén los campos monetarios como Decimal en el modelo cuando el redondeo de los números flotantes sea incorrecto, y muestra los errores de validación a quien realiza la llamada.

¿Cómo mantengo la precisión de los precios decimales al analizar JSON?

Pasa parse_float=Decimal a json.loads o json.load para que los precios numéricos se conviertan en decimal.Decimal en lugar de float. Al codificar, serializa los decimales como cadenas (o números enteros de unidades menores) con un default= gancho, y mantén allow_nan=False de modo que las constantes numéricas que no sean JSON no puedan colarse en la salida.

¿Cómo puedo leer un archivo JSON muy grande en Python?

No utilices json.load en matrices de varios gigabytes. Utiliza ijson para iterar sobre los elementos de una ruta, como "items.item" mientras el archivo permanezca en el disco. Opta por JSON Lines cuando controles el generador, ya que cada línea es un registro independiente que puedes leer en flujo y recuperar línea por línea.

¿Qué es JSON Lines y cuándo debo utilizarlo?

JSON Lines (application/jsonl) almacena un valor JSON por cada fila delimitada por un salto de línea. Es ideal para flujos de trabajo de extracción de datos y registro en los que se añaden registros a medida que llegan y se leen más tarde sin necesidad de cargar el archivo completo. Omite las líneas en blanco e indica el número de la línea en la que se ha producido un error cuando una fila no se puede descodificar.

¿Es orjson un sustituto directo de json?

No del todo. orjson suele ser más rápido, pero dumps devuelve bytes, los indicadores de opción sustituyen a muchos argumentos clave y la sangría utiliza OPT_INDENT_2. Si una biblioteca espera un str de dumps, añade .decode(). Mide ambos aspectos en tu carga útil antes de cambiar el código de producción.

¿Cómo puedo extraer JSON-LD o __NEXT_DATA__ de una página web con Python?

__NEXT_DATA__ de una página web con Python?

Analiza el HTML con BeautifulSoup, selecciona script[type="application/ld+json"] o script#__NEXT_DATA__, y ejecuta json.loads sobre el texto de la etiqueta. Trata el JSON-LD como un objeto, una lista o una @graph lista y, a continuación, filtra por @type. Recorre las propiedades anidadas con dig(). Para las etiquetas renderizadas por el cliente, recupera el HTML renderizado antes de analizarlo.

¿Cómo puedo formatear JSON de forma legible en Python?

Llama a json.dumps(data, indent=2, sort_keys=True) para obtener una cadena estable y legible, o json.dump(..., indent=2) al escribir un archivo en el disco. Añade ensure_ascii=False para texto que no esté en inglés, de modo que los caracteres no se escapen. Con orjson, pasa option=orjson.OPT_INDENT_2 y decodifica los bytes devueltos si necesitas una cadena.

Obtén JSON estructurado de cualquier página

Si prefieres obtener campos con nombre en lugar de gestionar selectores CSS, la API de Scraper extract_rules y json_response opciones de la API de Scraper devuelven JSON que puedes introducir directamente en json.loads y a los ayudantes mencionados anteriormente. Configura WSA_API_KEY, empieza por la página del producto de la API de Scraper y mantén la validación junto a cada análisis.

Acerca del autor

Suciu Dan, Cofundador @ WebScrapingAPI

Suciu Dan

Cofundador

Suciu Dan es cofundador de WebScrapingAPI y escribe guías prácticas dirigidas a desarrolladores sobre el scraping web con Python, el scraping web con Ruby y las infraestructuras de proxy.

Extracción de datos web con AWS Lambda: guía para Python y Java 2026
Guías

Extracción de datos web con AWS Lambda: guía para Python y Java 2026

TL;DR: En resumen: el web scraping con AWS Lambda funciona mejor cuando cada invocación es breve, está delimitada y se puede reintentar de forma independiente. Empieza con HTTP directo, AWS SAM y S3, y añade SQS, contenedores, renderización en navegador, proxies o una capa de recuperación gestionada solo cuando la carga de trabajo demuestre que los necesita.

Suciu Dan33 min read
Leer artículo
Cómo utilizar GoSpider: rastrear, depurar URL y extraer datos
Guías

Cómo utilizar GoSpider: rastrear, depurar URL y extraer datos

TL;DR: En resumen: GoSpider es un rastreador de línea de comandos diseñado para descubrir URL, no un extractor completo de datos estructurados. Esta guía sobre cómo utilizar GoSpider muestra cómo realizar un rastreo limitado, gestionar correctamente los resultados, pasar los datos de Colly a CSV y seguir una ruta de diagnóstico para respuestas 403 o páginas que requieren renderización con JavaScript.

Suciu Dan24 min read
Leer artículo
Cómo raspar Redfin: Guía Python de Datos Inmobiliarios
Guías

Cómo raspar Redfin: Guía Python de Datos Inmobiliarios

TL;DR: Redfin expone puntos finales de API ocultos que devuelven JSON estructurado para los listados de propiedades, lo que permite omitir por completo el frágil análisis HTML. Esta guía te guía a través de la construcción de un raspador de Python que extrae datos de alquiler y venta, busca por ubicación, supervisa los nuevos listados a través de mapas de sitio XML y exporta resultados limpios a CSV o JSON.

Suciu Dan14 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.