Cole uma URL de produto no ChatGPT e muitas vezes você receberá um preço. Abra a página em produção: o número está errado, desatualizado ou inventado. Não se trata de um extrator defeituoso. O modelo nunca abriu a URL. Inferiu o valor a partir dos dados de treino e formulou-o com confiança.
Trate o ChatGPT como um parser, não como um navegador. A Piloterr carrega a página. Tente primeiro JSON-LD ou um seletor CSS. Apenas o texto restante é enviado a um modelo OpenAI. O Pydantic descarta qualquer objeto que não cumpra o schema.
A demonstração utiliza books.toscrape.com, um catálogo público destinado à prática. Limite-se a páginas públicas, respeite o robots.txt e consulte O scraping é legal? antes de visar um site comercial.
Neste tutorial:
- Carregar uma ficha com Website Crawler (1 crédito).
- Ler título, preço, estoque e UPC em CSS quando o markup for estável.
- Recorrer a structured outputs do
gpt-5.6-lunaquando não for. - Dividir uma categoria em
article.product_pod, e não no meio de uma palavra.
Duas tarefas, duas ferramentas
Um modelo de linguagem lê texto. Ele não executa JavaScript, não rotaciona endereços IP e não ultrapassa um challenge da Cloudflare. Se você pedir que recupere uma URL, obtém uma página bloqueada, uma SPA vazia ou um payload inventado.
Mantenha uma separação explícita:
- Carregar uma página completa (Piloterr, ou um endpoint da biblioteca se existir).
- Extrair campos (CSS, JSON-LD ou um modelo).
O ChatGPT é pertinente quando o markup é irregular, ou muda com frequência suficiente para que a manutenção de um conjunto amplo de seletores custe mais do que um schema. Amazon, LinkedIn e alvos semelhantes já dispõem de APIs mantidas. Use-as.
Analisar o DOM antes de chamar o modelo
Comece por um extrator CSS. Neste catálogo o título é h1, o preço p.price_color e o UPC uma linha de tabela. Esse caminho consome menos tokens do que gpt-5.6-luna e devolve a mesma string no dia seguinte.
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 None: esta página não tem ISBN. Não peça ao modelo para inventar um.
Procure, nesta ordem:
- Uma API da biblioteca (Amazon Product e assim por diante)
application/ld+jsonna página- Um nó CSS ou XPath estável
- Um modelo para o restante: especificações não estruturadas, FAQ, textos que misturam unidades e avisos
Preços já em texto claro passam pelo Price-Parser sem LLM.
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
Escolha o modo de recuperação antes do prompt
Um modo inadequado produz HTML vazio. O modelo «extrai» então a partir de uma página de autenticação.
| Modo | Créditos | Quando |
|---|---|---|
| Website Crawler | 1 | HTML estático ou APIs JSON. O mais rápido. Sem JS. |
| Website Rendering | 2 | Conteúdo depois do JavaScript. Precisa de wait_in_seconds ou wait_for. |
| Website WebUnlocker | 3 | Domínios na allowlist atrás de Cloudflare, DataDome, PerimeterX, Akamai. Sem JS. |
Comparativo: Crawler vs Rendering vs WebUnlocker. Este tutorial utiliza Crawler, porque books.toscrape.com é renderizado no servidor.
Carregar o HTML
Python 3.10+, uma chave API da Piloterr e uma chave OpenAI. As duas no ambiente. Nunca no script.
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"
O Crawler é um GET. query é a URL alvo. O corpo JSON é a string HTML.
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
Algumas centenas de kilobytes é o comportamento esperado. Algumas centenas de caracteres correspondem em geral a uma página de challenge. Troque de modo. Não peça ao modelo para compensar um HTML incompleto.
Para uma ficha com uso intensivo de JavaScript, o mesmo modelo de solicitação aplica-se ao Rendering (2 créditos):
RENDER = "https://api.piloterr.com/v2/website/rendering"
def fetch_rendered(url: str) -> str:
return fetch_html(url, endpoint=RENDER, wait_in_seconds=4)
Documentação: Website Crawler e Website Rendering. Uma chamada bem-sucedida custa 1 ou 2 créditos. 4xx do cliente e a maior parte dos 5xx da plataforma não. Consulte preços e a versão Markdown /pricing.md.
Reduzir o HTML antes de chamar o modelo
Scripts, SVG e o banner de cookies consomem tokens e ocultam os campos úteis.
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, depois validação
Quando o CSS falha, o prompt torna-se o extrator. Nomeie cada campo. Use null se o texto não indicar o valor. client.responses.parse vincula a resposta à classe Pydantic: um objeto malformado não é tratado como um dicionário confiável.
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. Passe para gpt-5.6 apenas se dois preços se contradisserem ou uma tabela for interpretada incorretamente. Execute uma amostra de 20 páginas, meça as falhas e escolha o modelo. client.chat.completions.parse ainda funciona se ficar no Chat Completions. Prefira responses.parse com openai 2.x.
Um único script
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 na ficha «A Light in the Attic» (o UPC está na página, o ISBN não):
{
"title": "A Light in the Attic",
"price": "£51.77",
"availability": "In stock (22 available)",
"upc": "a897fe39b1053632",
"isbn": null
}
Um ISBN alucinado pode inserir-se numa planilha sem alerta, até que um pedido seja feito com base nele.
Páginas longas: divida nas fichas de produto
Enviar uma categoria inteira num único prompt ultrapassa a janela de contexto. Não recorte em 12 000 caracteres no meio de uma palavra. Analise a lista, depois extraia uma ficha.
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
As fichas da listagem não têm tabela UPC. O CSS devolve None e o modelo preenche título e preço a partir do texto da ficha. Una pelo título, ou conserve o href se o tiver armazenado.
print(extract_catalog(fetch_html(CATALOG)))
Deixe o agente carregar a página, mantenha a mesma separação
Colar HTML na interface do ChatGPT é adequado para uma demonstração. O processamento diário executa-se em Python, ou numa ferramenta chamada pelo agente.
O MCP da Piloterr expõe Crawler e Rendering em https://mcp.piloterr.com/ com o mesmo x-api-key. Claude e Cursor podem carregar uma página. Você valida o JSON no seu código. Notas de especificação: MCP da Piloterr em 2026-07-28.
Práticas a evitar
- Pedir ao ChatGPT uma URL em produção e confiar na resposta.
- Enviar HTML bruto, com anúncios e scripts incluídos.
- Guardar a saída do modelo sem validação Pydantic (ou equivalente).
- Usar Rendering num catálogo estático e faturar 2 créditos sem necessidade.
- Extrair páginas autenticadas, ou coletar dados pessoais, com o argumento de que o modelo «precisava de contexto».
FAQ
O ChatGPT consegue extrair um site sozinho?
Não. Não dispõe de um navegador que você controle nem de um método confiável para carregar a URL que você acabou de colar. Pode reformular um texto já recuperado. Carregue com Website Crawler ou Website Rendering, depois extraia.
Por que não chamar requests.get diretamente?
Você pode, no books.toscrape.com. Numa vitrine JavaScript ou num site protegido pela Cloudflare, você obtém um nó raiz vazio ou uma página de challenge. O modelo emitirá campos a partir dessa resposta. A Piloterr devolve a página que um navegador exibiria, e a cobrança ocorre apenas se o carregamento for bem-sucedido.
Quando o Rendering é o modo adequado?
Quando o preço ou a lista não aparecem em «Ver código-fonte», mas estão visíveis no DevTools depois da hidratação. Espere com wait_in_seconds ou wait_for. Se o HTML já veio na primeira resposta, permaneça no Crawler.
Como impedir preços inventados?
CSS ou JSON-LD primeiro. Depois instrução de copiar à letra, reasoning={"effort": "none"}, structured outputs e Pydantic. Compare a string de preço com o Price-Parser. Se a validação falhar, uma nova tentativa ou um seletor. Não complete um preço ausente.
ChatGPT ou uma API da biblioteca?
A API quando o site já está coberto. ChatGPT para um caso pontual ou um template instável. Combinar os dois é adequado: carregamento com Crawler, análise CSS, modelo apenas nos campos que falharam.
E se a lista for longa demais para uma chamada?
Extraia por article ou por linha. Deduplique por título ou URL. Não recorte por índice de caractere, salvo se a página não tiver markup de lista.
Esta prática é legal?
Páginas públicas, ritmo razoável, sem autenticação, sem dados pessoais, e os termos do site aplicam-se. Comece por O scraping é legal?. Para reutilização comercial, licença ou feed oficial se existir.