Fügen Sie eine Produkt-URL in ChatGPT ein und Sie erhalten oft einen Preis. Öffnen Sie die Live-Seite: die Zahl ist falsch, veraltet oder erfunden. Es handelt sich nicht um einen unzuverlässigen Scraper. Das Modell hat die URL nie geöffnet. Es hat den Wert aus Trainingsdaten abgeleitet und selbstsicher formuliert.
Behandeln Sie ChatGPT als Parser, nicht als Browser. Piloterr lädt die Seite. Versuchen Sie zuerst JSON-LD oder einen CSS-Selektor. Nur der verbleibende Text geht an ein OpenAI-Modell. Pydantic verwirft jedes Objekt, das nicht zum Schema passt.
Die Demonstration nutzt books.toscrape.com, einen öffentlichen Katalog zu Übungszwecken. Bleiben Sie auf öffentlichen Seiten, respektieren Sie robots.txt und lesen Sie Ist Web Scraping legal?, bevor Sie eine kommerzielle Website ansteuern.
In diesem Tutorial:
- Eine Produktseite mit Website Crawler laden (1 Credit).
- Titel, Preis, Bestand und UPC per CSS lesen, wenn das Markup stabil ist.
- Auf Structured Outputs von
gpt-5.6-lunawechseln, wenn nicht. - Eine Kategorie auf
article.product_podzerlegen, nicht mitten im Wort.
Zwei Aufgaben, zwei Werkzeuge
Ein Sprachmodell liest Text. Es rendert kein JavaScript, rotiert keine IP-Adressen und löst keinen Cloudflare-Challenge. Bitten Sie es, eine URL abzurufen, und Sie erhalten eine blockierte Seite, eine leere SPA oder ein erfundenes Payload.
Halten Sie die Trennung explizit:
- Eine vollständige Seite laden (Piloterr, oder ein Library-Endpoint, wenn es einen gibt).
- Felder extrahieren (CSS, JSON-LD oder ein Modell).
ChatGPT eignet sich, wenn das Markup unregelmäßig ist oder sich so oft ändert, dass die Pflege einer großen Selektor-Menge teurer wird als ein Schema. Amazon, LinkedIn und ähnliche Ziele verfügen bereits über gepflegte APIs. Nutzen Sie diese.
Das DOM parsen, bevor Sie das Modell aufrufen
Beginnen Sie mit einem CSS-Extractor. Auf diesem Katalog ist der Titel h1, der Preis p.price_color, die UPC eine Tabellenzeile. Dieser Weg verbraucht weniger Tokens als gpt-5.6-luna und liefert am nächsten Tag dieselbe Zeichenkette.
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 bleibt None: diese Seite hat keine ISBN. Bitten Sie das Modell nicht, eine zu erfinden.
Suchen Sie in dieser Reihenfolge:
- Eine Library-API (Amazon Product und so weiter)
application/ld+jsonauf der Seite- Einen stabilen CSS- oder XPath-Knoten
- Ein Modell für den Rest: unstrukturierte Spezifikationen, FAQ-Blöcke, Texte, die Einheiten und Hinweise mischen
Klare Preisstrings gehen durch Price-Parser ohne 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
Fetch-Modus wählen, bevor Sie den Prompt schreiben
Ein ungeeigneter Modus liefert leeres HTML. Das Modell «extrahiert» dann von einer Anmeldeseite.
| Modus | Credits | Wann |
|---|---|---|
| Website Crawler | 1 | Statisches HTML oder JSON-APIs. Am schnellsten. Kein JS. |
| Website Rendering | 2 | Inhalt nach JavaScript. Braucht wait_in_seconds oder wait_for. |
| Website WebUnlocker | 3 | Allowlist-Domains hinter Cloudflare, DataDome, PerimeterX, Akamai. Kein JS. |
Vergleich: Crawler vs Rendering vs WebUnlocker. Dieses Tutorial verwendet Crawler, weil books.toscrape.com serverseitig gerendert ist.
HTML laden
Python 3.10+, ein Piloterr-API-Key und ein OpenAI-Key. Beide in der Umgebung. Niemals im 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"
Crawler ist ein GET. query ist die Ziel-URL. Der JSON-Body ist der HTML-String.
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
Einige hundert Kilobyte sind erwartet. Einige hundert Zeichen sind oft eine Challenge-Seite. Wechseln Sie den Modus. Bitten Sie das Modell nicht, unvollständiges HTML zu kompensieren.
Für eine stark JavaScript-lastige Produktseite gilt dasselbe Anfragemuster für Rendering (2 Credits):
RENDER = "https://api.piloterr.com/v2/website/rendering"
def fetch_rendered(url: str) -> str:
return fetch_html(url, endpoint=RENDER, wait_in_seconds=4)
Dokumentation: Website Crawler und Website Rendering. Ein erfolgreicher Aufruf kostet 1 oder 2 Credits. Client-4xx und die meisten Plattform-5xx nicht. Siehe Pricing und die Markdown-Version /pricing.md.
HTML reduzieren, bevor Sie das Modell aufrufen
Scripts, SVG-Pfade und Cookie-Banner verbrauchen Tokens und verdecken die benötigten Felder.
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, dann Validierung
Wenn CSS fehlschlägt, wird der Prompt zum Extractor. Benennen Sie jedes Feld. Verwenden Sie null, wenn der Text den Wert nicht enthält. client.responses.parse bindet die Antwort an die Pydantic-Klasse. Ein fehlerhaftes Objekt wird nicht als vertrauenswürdiges Dict behandelt.
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 reicht zum Kopieren von Feldern. Wechseln Sie zu gpt-5.6 nur, wenn zwei Preise widersprechen oder eine Tabelle falsch gelesen wird. Führen Sie eine Stichprobe von 20 Seiten aus, messen Sie die Fehler, dann wählen Sie das Modell. client.chat.completions.parse funktioniert weiterhin, wenn Sie bei Chat Completions bleiben. Bevorzugen Sie responses.parse mit openai 2.x.
Ein 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))
Erwartete Form auf der Produktseite „A Light in the Attic“ (UPC steht auf der Seite, ISBN nicht):
{
"title": "A Light in the Attic",
"price": "£51.77",
"availability": "In stock (22 available)",
"upc": "a897fe39b1053632",
"isbn": null
}
Eine halluzinierte ISBN kann in einer Tabelle stehen, ohne Alarm auszulösen, bis eine Bestellung darauf aufsetzt.
Lange Seiten: an Produktkarten schneiden
Eine gesamte Kategorie in einen Prompt zu senden überschreitet das Kontextfenster. Schneiden Sie nicht bei 12 000 Zeichen mitten im Wort. Parsen Sie die Liste, dann eine Karte.
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
Listing-Karten haben keine UPC-Tabelle. CSS liefert None, das Modell füllt Titel und Preis aus dem Kartentext. Führen Sie über den Titel zusammen, oder behalten Sie den href, wenn Sie ihn gespeichert haben.
print(extract_catalog(fetch_html(CATALOG)))
Den Agenten laden lassen, dieselbe Trennung beibehalten
HTML in die ChatGPT-Oberfläche einzufügen eignet sich für eine Demonstration. Tägliche Ausführungen erfolgen in Python, oder in einem Tool, das der Agent aufruft.
Piloterr MCP stellt Crawler und Rendering unter https://mcp.piloterr.com/ mit demselben x-api-key bereit. Claude und Cursor können eine Seite laden. Sie validieren das JSON weiterhin in Ihrem Code. Spezifikationshinweise: Piloterr MCP am 2026-07-28.
Praktiken, die Sie vermeiden sollten
- ChatGPT nach einer Live-URL fragen und der Antwort vertrauen.
- Rohes HTML mit Werbung und Scripts mitsenden.
- Modell-Output ohne Pydantic-Prüfung (oder gleichwertig) speichern.
- Rendering auf einem statischen Katalog nutzen und 2 Credits ohne Bedarf abrechnen.
- Hinter einer Anmeldung scrapen oder personenbezogene Daten sammeln, mit der Begründung, das Modell «brauche Kontext».
FAQ
Kann ChatGPT eine Website allein scrapen?
Nein. Es gibt keinen Browser, den Sie steuern, und keine zuverlässige Möglichkeit, die URL zu laden, die Sie gerade eingegeben haben. Das Modell kann Text umschreiben, den Sie bereits abgerufen haben. Laden Sie mit Website Crawler oder Website Rendering, dann extrahieren Sie.
Warum nicht direkt requests.get aufrufen?
Auf books.toscrape.com können Sie das. Auf einem JavaScript-Shop oder einer Cloudflare-geschützten Seite erhalten Sie einen leeren Root oder eine Challenge. Das Modell schreibt trotzdem Felder daraus. Piloterr liefert die Seite, die ein Browser anzeigen würde, und Sie zahlen nur bei erfolgreichem Fetch.
Wann ist Rendering der passende Fetch?
Wenn Preis oder Liste in «Quelltext anzeigen» fehlen, in den DevTools nach der Hydration aber vorhanden sind. Warten Sie mit wait_in_seconds oder wait_for. Steht das HTML bereits in der ersten Antwort, bleiben Sie bei Crawler.
Wie verhindern Sie erfundene Preise?
Zuerst CSS oder JSON-LD. Dann wortgetreue Anweisung, reasoning={"effort": "none"}, Structured Outputs und Pydantic. Vergleichen Sie den Preisstring mit Price-Parser. Schlägt die Validierung fehl: einmal erneut versuchen oder ein Selektor. Füllen Sie keinen fehlenden Preis.
ChatGPT oder eine Library-API?
Library-API, wenn die Site bereits abgedeckt ist. ChatGPT bei einer Ad-hoc-Seite oder einem instabilen Template. Beides kombinieren ist sinnvoll: Fetch mit Crawler, CSS parsen, Modell nur auf den Feldern, die fehlgeschlagen sind.
Was, wenn die Liste für einen Modell-Aufruf zu lang ist?
Pro article oder pro Zeile extrahieren. Auf Titel oder URL deduplizieren. Nicht am Zeichenindex schneiden, außer die Seite hat kein Listen-Markup.
Ist dieses Vorgehen zulässig?
Öffentliche Seiten, angemessene Rate, keine Anmeldung, keine personenbezogenen Daten, und die AGB der Site gelten trotzdem. Beginnen Sie mit Ist Web Scraping legal?. Für kommerzielle Weiterverwendung: Lizenz oder offizieller Feed, wenn es einen gibt.