Saltar al contenido principal
Piloterr
Volver al blog
18 de agosto de 2026

Extraer datos estructurados con ChatGPT después de cargar la página

Pegue una URL de producto en ChatGPT y a menudo obtendrá un precio. Abra la página en producción: el número es incorrecto, desactualizado o inventado. No se trata de un extractor defectuoso. El modelo nunca abrió la URL. Infirió el valor a partir de los datos de entrenamiento y lo formuló con seguridad.

Trate a ChatGPT como un parser, no como un navegador. Piloterr carga la página. Pruebe primero JSON-LD o un selector CSS. Solo el texto restante se envía a un modelo OpenAI. Pydantic descarta cualquier objeto que no cumpla el esquema.

La demostración utiliza books.toscrape.com, un catálogo público destinado a la práctica. Limítese a páginas públicas, respete el robots.txt y consulte ¿El scraping es legal? antes de apuntar a un sitio comercial.

En este tutorial:

  1. Cargar una ficha con Website Crawler (1 crédito).
  2. Leer título, precio, stock y UPC con CSS cuando el marcado sea estable.
  3. Recurrir a structured outputs de gpt-5.6-luna cuando no lo sea.
  4. Dividir una categoría en article.product_pod, no a mitad de una palabra.

Dos tareas, dos herramientas

Un modelo de lenguaje lee texto. No ejecuta JavaScript, no rota direcciones IP y no supera un challenge de Cloudflare. Si le pide que recupere una URL, obtiene una página bloqueada, una SPA vacía o un payload inventado.

Conserve una separación explícita:

  1. Cargar una página completa (Piloterr, o un endpoint de la biblioteca si existe).
  2. Extraer campos (CSS, JSON-LD o un modelo).

ChatGPT es pertinente cuando el marcado es irregular, o cambia con tanta frecuencia que mantener un conjunto amplio de selectores cuesta más que un esquema. Amazon, LinkedIn y objetivos similares ya disponen de APIs mantenidas. Úselas.

Analizar el DOM antes de llamar al modelo

Empiece por un extractor CSS. En este catálogo el título es h1, el precio p.price_color y el UPC una fila de tabla. Esta vía consume menos tokens que gpt-5.6-luna y devuelve la misma cadena al día siguiente.

Python
from bs4 import BeautifulSoup
from pydantic import BaseModel


class Book(BaseModel):
    title: str
    price: str
    availability: str
    upc: str | None = None
    isbn: str | None = None


def extract_book_css(html: str) -> Book | None:
    soup = BeautifulSoup(html, "html.parser")
    title = soup.select_one("article.product_page h1")
    price = soup.select_one("article.product_page p.price_color")
    stock = soup.select_one("article.product_page p.instock.availability")
    if not (title and price and stock):
        return None

    upc = None
    for row in soup.select("article.product_page table tr"):
        label = row.select_one("th")
        value = row.select_one("td")
        if label and value and label.get_text(strip=True) == "UPC":
            upc = value.get_text(strip=True)
            break

    return Book(
        title=title.get_text(strip=True),
        price=price.get_text(strip=True),
        availability=" ".join(stock.get_text().split()),
        upc=upc,
        isbn=None,
    )

isbn permanece en None: esta página no tiene ISBN. No pida al modelo que invente uno.

Busque, en este orden:

  • Una API de la biblioteca (Amazon Product y similares)
  • application/ld+json en la página
  • Un nodo CSS o XPath estable
  • Un modelo para lo que quede: especificaciones no estructuradas, FAQ, textos que mezclan unidades y avisos

Los precios ya en claro pasan por Price-Parser sin LLM.

Python
import json


def json_ld_blocks(html: str) -> list[dict]:
    soup = BeautifulSoup(html, "html.parser")
    blocks = []
    for node in soup.select('script[type="application/ld+json"]'):
        if not node.string:
            continue
        try:
            parsed = json.loads(node.string)
        except json.JSONDecodeError:
            continue
        blocks.extend(parsed if isinstance(parsed, list) else [parsed])
    return blocks

Elija el modo de recuperación antes del prompt

Un modo inadecuado produce HTML vacío. El modelo «extrae» entonces desde una página de autenticación.

ModoCréditosCuándo
Website Crawler1HTML estático o APIs JSON. El más rápido. Sin JS.
Website Rendering2Contenido tras JavaScript. Necesita wait_in_seconds o wait_for.
Website WebUnlocker3Dominios en allowlist detrás de Cloudflare, DataDome, PerimeterX, Akamai. Sin JS.

Comparativa: Crawler vs Rendering vs WebUnlocker. Este tutorial utiliza Crawler, porque books.toscrape.com se renderiza en servidor.

Cargar el HTML

Python 3.10+, una clave API de Piloterr y una clave OpenAI. Las dos en el entorno. Nunca en el script.

Bash
python --version
mkdir fetch-then-extract && cd fetch-then-extract
pip install requests beautifulsoup4 "openai>=2" pydantic
export PILOTERR_API_KEY="your-x-api-key"
export OPENAI_API_KEY="your-openai-key"

Crawler es un GET. query es la URL objetivo. El cuerpo JSON es la cadena HTML.

Python
import os
import requests

API = "https://api.piloterr.com/v2/website/crawler"
KEY = os.environ["PILOTERR_API_KEY"]
PAGE = "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"
CATALOG = "https://books.toscrape.com/"


def fetch_html(url: str, endpoint: str = API, **extra: object) -> str:
    response = requests.get(
        endpoint,
        headers={"x-api-key": KEY},
        params={"query": url, "allow_redirects": "true", **extra},
        timeout=60,
    )
    response.raise_for_status()
    html = response.json()
    if not isinstance(html, str) or len(html) < 200:
        raise RuntimeError("Fetch returned too little HTML. Try Rendering or WebUnlocker.")
    return html

Unos cientos de kilobytes es el comportamiento esperado. Unos cientos de caracteres suelen corresponder a una página de challenge. Cambie de modo. No pida al modelo que compense un HTML incompleto.

Para una ficha con un uso intensivo de JavaScript, el mismo modelo de solicitud se aplica a Rendering (2 créditos):

Python
RENDER = "https://api.piloterr.com/v2/website/rendering"

def fetch_rendered(url: str) -> str:
    return fetch_html(url, endpoint=RENDER, wait_in_seconds=4)

Documentación: Website Crawler y Website Rendering. Una llamada correcta cuesta 1 o 2 créditos. Los 4xx de cliente y la mayoría de 5xx de plataforma no. Consulte precios y la versión Markdown /pricing.md.

Reducir el HTML antes de llamar al modelo

Los scripts, los SVG y el banner de cookies consumen tokens y ocultan los campos útiles.

Python
def readable_text(html: str) -> str:
    soup = BeautifulSoup(html, "html.parser")
    for tag in soup(["script", "style", "noscript", "svg", "iframe"]):
        tag.decompose()
    main = soup.select_one("article.product_page") or soup.body or soup
    return main.get_text("\n", strip=True)

Structured outputs, luego validación

Cuando el CSS falla, el prompt se convierte en el extractor. Nombre cada campo. Use null si el texto no indica el valor. client.responses.parse vincula la respuesta a la clase Pydantic: un objeto mal formado no se trata como un diccionario fiable.

Python
from openai import OpenAI

client = OpenAI()

SYSTEM = (
    "Read the page text and fill the schema. "
    "Copy strings as they appear. "
    "Use null when a key is absent. "
    "Do not invent a title, price, UPC, or ISBN."
)


def extract_book_llm(text: str) -> Book:
    response = client.responses.parse(
        model="gpt-5.6-luna",
        input=[
            {"role": "system", "content": SYSTEM},
            {"role": "user", "content": f"Fill the Book schema from this page text.\n\n{text}"},
        ],
        text_format=Book,
        reasoning={"effort": "none"},
    )
    book = response.output_parsed
    if book is None:
        raise ValueError("Model returned no parsed object")
    return book

gpt-5.6-luna basta para copiar campos. Pase a gpt-5.6 solo si dos precios se contradicen o una tabla se interpreta mal. Ejecute una muestra de 20 páginas, mida los fallos y elija el modelo. client.chat.completions.parse sigue funcionando si se queda en Chat Completions. Prefiera responses.parse con openai 2.x.

Un solo script

Python
if __name__ == "__main__":
    html = fetch_html(PAGE)
    book = extract_book_css(html)
    if book is None:
        book = extract_book_llm(readable_text(html))
    print(book.model_dump_json(indent=2))

Forma esperada en la ficha «A Light in the Attic» (el UPC está en la página, el ISBN no):

JSON
{
  "title": "A Light in the Attic",
  "price": "£51.77",
  "availability": "In stock (22 available)",
  "upc": "a897fe39b1053632",
  "isbn": null
}

Un ISBN alucinado puede insertarse en una hoja de cálculo sin alerta, hasta que alguien realice un pedido.

Páginas largas: divida en las tarjetas de producto

Enviar una categoría entera en un solo prompt supera la ventana de contexto. No divida a 12 000 caracteres a mitad de una palabra. Analice la lista, luego extraiga una tarjeta.

Python
def product_cards(html: str) -> list[str]:
    soup = BeautifulSoup(html, "html.parser")
    return [str(card) for card in soup.select("article.product_pod")]


def extract_catalog(html: str) -> list[Book]:
    books: list[Book] = []
    seen: set[str] = set()
    for card_html in product_cards(html):
        book = extract_book_css(card_html) or extract_book_llm(readable_text(card_html))
        key = book.title.casefold()
        if key in seen:
            continue
        seen.add(key)
        books.append(book)
    return books

Las tarjetas del listado no tienen tabla UPC. El CSS devuelve None y el modelo rellena título y precio a partir del texto de la tarjeta. Fusione por título, o conserve el href si lo almacenó.

Python
print(extract_catalog(fetch_html(CATALOG)))

Deje que el agente cargue la página, conserve la misma separación

Pegar HTML en la interfaz de ChatGPT es adecuado para una demostración. El procesamiento diario se ejecuta en Python, o en una herramienta que el agente llama.

El MCP de Piloterr expone Crawler y Rendering en https://mcp.piloterr.com/ con el mismo x-api-key. Claude y Cursor pueden cargar una página. Valide el JSON en su código. Notas de especificación: MCP de Piloterr el 2026-07-28.

Prácticas a evitar

  • Pedir a ChatGPT una URL en producción y confiar en la respuesta.
  • Enviar HTML en bruto, con anuncios y scripts incluidos.
  • Guardar la salida del modelo sin validación Pydantic (o equivalente).
  • Usar Rendering en un catálogo estático y facturar 2 créditos sin necesidad.
  • Extraer páginas autenticadas, o recopilar datos personales, con el argumento de que el modelo «necesitaba contexto».

FAQ

¿Puede ChatGPT extraer un sitio por sí solo?

No. No dispone de un navegador que usted controle ni de un método fiable para cargar la URL que acaba de pegar. Puede reformular un texto ya recuperado. Cargue con Website Crawler o Website Rendering, luego extraiga.

¿Por qué no llamar a requests.get directamente?

Puede, en books.toscrape.com. En un escaparate JavaScript o un sitio protegido por Cloudflare obtiene un nodo raíz vacío o una página de challenge. El modelo emitirá campos a partir de esa respuesta. Piloterr devuelve la página que mostraría un navegador, y solo se factura si la carga tiene éxito.

¿Cuándo es Rendering el modo adecuado?

Cuando el precio o la lista no aparecen en «Ver código fuente», pero sí en DevTools después de la hidratación. Espere con wait_in_seconds o wait_for. Si el HTML ya viene en la primera respuesta, permanezca en Crawler.

¿Cómo evitar precios inventados?

CSS o JSON-LD primero. Luego instrucción de copiar tal cual, reasoning={"effort": "none"}, structured outputs y Pydantic. Compare la cadena de precio con Price-Parser. Si falla la validación, un nuevo intento o un selector. No complete un precio ausente.

¿ChatGPT o una API de la biblioteca?

La API cuando el sitio ya está cubierto. ChatGPT para un caso puntual o una plantilla inestable. Combinar ambos es adecuado: carga con Crawler, análisis CSS, modelo solo en los campos que fallaron.

¿Y si el listado es demasiado largo para una llamada?

Extraiga por article o por fila. Deduplique por título o URL. No corte por índice de carácter salvo que la página no tenga marcado de lista.

Páginas públicas, ritmo razonable, sin autenticación, sin datos personales, y los términos del sitio se aplican. Empiece por ¿El scraping es legal?. Para reutilización comercial, licencia o feed oficial si existe.

Más para leer

Guías y noticias sobre web scraping, proxies y extracción de datos.

Noticias

El MCP de Piloterr ya usa la spec 2026-07-28

Nuestro MCP alojado en mcp.piloterr.com sigue MCP 2026-07-28: núcleo stateless request/response, misma URL Streamable HTTP y misma x-api-key.

Josselin Liebe
Josselin Liebe
Leer
Web Scraping

Web scraping para automoción: seguir precios de coches y piezas en AutoScout24, Mobile.de, Leboncoin y Autodoc

Cómo las plataformas de pricing automovilístico usan APIs de scraping para vigilar anuncios de coches de ocasión en AutoScout24, Mobile.de y Leboncoin, y precios de piezas en las más de 30 tiendas nacionales de Autodoc. Diseño del pipeline, ejemplos de API y un script en Python.

Josselin Liebe
Josselin Liebe
Leer
Web Scraping

Web scraping para brand protection: cazar falsificaciones en AliExpress, Alibaba y Amazon

Cómo las plataformas de brand protection usan APIs de scraping para detectar anuncios falsificados, capturar evidencias del vendedor y vigilar las republicaciones en AliExpress, Alibaba, Amazon y otros marketplaces. Diseño del pipeline, ejemplos de API y un script en Python.

Josselin Liebe
Josselin Liebe
Leer

¿Listo para empezar?

Tu API de web scraping está a un clic. Comienza con +500 créditos, sin infraestructura que configurar, sin proxies que gestionar y sin necesidad de tarjeta de crédito.

  • +500 créditos
  • Sin tarjeta de crédito
  • Todos los endpoints incluidos