🎯 A customizable, anti-detection cloud browser powered by self-developed Chromium designed for web crawlers and AI Agents.πŸ‘‰Try Now
Back to Blog

ChatGPT Web Scraping: A Practical Guide

Alex Johnson
Alex Johnson

Senior Web Scraping Engineer

17-Jul-2026

TL;DR:

  • ChatGPT does not fetch web pages β€” it parses the text you hand it. The model has no browser, no JavaScript engine, and no way past an access challenge, so every working "ChatGPT web scraping" setup is two layers: something fetches the page, the model extracts the fields.
  • The extraction layer is genuinely good. With the OpenAI Python SDK's structured outputs, you define a Pydantic schema and chat.completions.parse returns validated objects β€” no selector maintenance when the page layout shifts.
  • The limit is measurable. A plain HTTP GET on a JavaScript-rendered demo page returns 0 quote elements; the same URL fetched through the Scrapeless Universal Scraping API with js_render enabled returns all 10. That gap is exactly what the model cannot cross on its own.
  • Schema design beats prompt cleverness. Nullable fields, explicit types, and one page per call produce extractions you can trust; vague prompts produce confident inventions.
  • Know which tool you are holding. "ChatGPT web scraping" (this guide β€” the model as parser) is the opposite of a ChatGPT scraper, which captures ChatGPT's own answers as data.
  • Free to start. The fetch layer in this guide runs on the free plan β€” create your API key at app.scrapeless.com.

Can ChatGPT scrape websites?

Not by itself. ChatGPT is a language model: it reads and produces text, and it does none of the things a scraper's fetch layer does β€” issue requests, execute JavaScript, hold sessions, or answer anti-bot challenges. Paste page text into it and it extracts fields impressively well; point it at a URL and you are relying on whatever fetch tool wraps the model that day, which fails quietly on rendered or protected pages.

So the practical architecture of ChatGPT web scraping is always the same two layers. A fetch layer retrieves a faithful copy of the page. An extraction layer β€” the OpenAI API with a schema β€” turns that copy into structured records. This guide builds the extraction layer first, because that is where ChatGPT earns its place, then demonstrates the fetch layer's failure mode and its fix with a live, reproducible example.

One disambiguation before the code, because the keyword is genuinely ambiguous: this guide uses ChatGPT as the scraper's brain. The reverse job β€” capturing ChatGPT's own answers and their citations as data β€” is a different tool entirely, covered by the ChatGPT Scraper API guide and the broader LLM scraper explainer.

Install

Two packages cover both layers β€” the OpenAI SDK for extraction and requests for HTTP. The versions here are the ones this guide was written against (openai 2.34.0):

bash Copy
pip install "openai==2.34.0" requests

Configure

Both layers authenticate from the environment, so no key ever lives in source code:

bash Copy
export OPENAI_API_KEY="sk-your_openai_key"
export SCRAPELESS_API_KEY="sk_your_scrapeless_key"

Basic implementation: schema-first extraction

The current OpenAI SDK does structured extraction in one call: define the record as a Pydantic model, pass it as response_format, and chat.completions.parse returns instances of it. The schema carries the reliability. The model is constrained to the field names and types you declared β€” the same contract style the JSON Schema specification formalizes β€” so output quality stops depending on how carefully you begged in the prompt.

Note: This block needs an OPENAI_API_KEY with API credit β€” the one prerequisite this guide does not assume, so the extraction calls are shown without captured output and their result shape is described below. The fetch layer that follows runs for real with only a Scrapeless key.

python Copy
# extract.py β€” ChatGPT as the extraction layer (requires OPENAI_API_KEY)
from openai import OpenAI
from pydantic import BaseModel


class Quote(BaseModel):
    text: str
    author: str
    tags: list[str]


class QuotePage(BaseModel):
    quotes: list[Quote]


client = OpenAI()  # reads OPENAI_API_KEY from the environment
page_html = open("page.html", encoding="utf-8").read()

completion = client.chat.completions.parse(
    model="gpt-4.1-mini-2025-04-14",
    messages=[
        {
            "role": "system",
            "content": "Extract every quote from the page. "
            "Use null for nothing β€” omit a quote entirely rather than inventing fields.",
        },
        {"role": "user", "content": page_html},
    ],
    response_format=QuotePage,
)

page = completion.choices[0].message.parsed
print(f"extracted {len(page.quotes)} quotes")

On a parsed page this returns a QuotePage whose .quotes list holds one validated Quote per record β€” typed Python objects, not a string you still have to json.loads and defend. The current parameters and schema rules are in the OpenAI structured outputs guide.

Advanced patterns

Three habits separate a dependable extractor from a demo:

  • Make absence representable. If a field can be missing on the real page, type it str | None and say so in the system message. A schema with only required strings forces the model to fill gaps, and it will.
  • Feed the model less page. HTML chrome costs tokens and invites distraction. If you can isolate the content container β€” or fetch the page as markdown instead of HTML β€” the extraction gets cheaper and more accurate at the same time.
  • Keep one page per call. Batching ten pages into one prompt saves requests and costs correctness: records bleed across page boundaries. The loop belongs in Python, not in the prompt.

There is also a second, older way to use ChatGPT for scraping: ask it to write a selector-based script for you, then run that script on every page. It remains the right call for high-volume work on a stable layout β€” the generated code runs for free after the first page β€” but you review it like any code you did not write, and it inherits every fetch-layer limit in the next section.

The honest limit: ChatGPT cannot fetch the page

Everything above assumed page.html exists and is faithful. That assumption is where ChatGPT-only scraping breaks, and the break is reproducible. A plain HTTP GET returns the bytes the server sends β€” per the HTTP semantics specification, a representation of the resource, not the page a browser would build from it. On a JavaScript-rendered page those are very different documents:

python Copy
# plain_fetch.py β€” what a plain GET actually sees on a JS-rendered page
import requests

url = "https://quotes.toscrape.com/js/"
resp = requests.get(url, timeout=60)
html = resp.text
print(f"status {resp.status_code} | {len(html):,} characters")
print("quote elements in the HTML:", html.count('<span class="text"'))

The run prints status 200 β€” a perfectly successful request β€” and 0 quote elements, because on this demo page the quotes exist only inside a script variable until a JavaScript engine builds the DOM. Hand this HTML to the extractor above and the best possible model extracts nothing, or worse, guesses.

The fix is to render before you extract. The Universal Scraping API takes the same URL in one POST, executes the page server-side with js_render enabled, and returns the DOM a reader would see β€” unlocking and proxy routing included, nothing to run locally:

python Copy
# rendered_fetch.py β€” the same URL, rendered server-side before extraction
import os

import requests

resp = requests.post(
    "https://api.scrapeless.com/api/v1/unlocker/request",
    headers={
        "Content-Type": "application/json",
        "x-api-token": os.environ["SCRAPELESS_API_KEY"],
    },
    json={
        "actor": "unlocker.webunlocker",
        "input": {"url": "https://quotes.toscrape.com/js/", "method": "GET", "js_render": True},
    },
    timeout=120,
)
resp.raise_for_status()
html = resp.json().get("data", "")
print(f"rendered page: {len(html):,} characters")
print("quote elements in the HTML:", html.count('<span class="text"'))

with open("page.html", "w", encoding="utf-8") as f:
    f.write(html)

Same URL, and the run prints 10 quote elements in 8,940 characters of rendered HTML β€” the file the extraction layer needed all along. The two prints side by side are the whole argument: 0 quote elements in the plain fetch, 10 in the rendered one. Chain the two scripts and you have the working pattern: render through Scrapeless, extract with ChatGPT.

Get your API key on the free plan: app.scrapeless.com

Troubleshooting

  • The model returns prose instead of JSON. You are on the plain create call β€” switch to chat.completions.parse with a response_format model, which constrains the output at the API level instead of asking nicely.
  • Fields come back filled that should be empty. Make the field optional in the schema and state the null rule in the system message. The failure is in the contract, not the model's mood.
  • Extraction is right on some pages, empty on others. Compare what you actually fetched, not what the browser shows you β€” count a known element in the fetched HTML the way the plain-fetch script does. Zero matches means a rendering or access problem, and you fix it at the fetch layer (js_render, or a different egress country) rather than in the prompt.
  • Token costs climb with volume. Strip the page to its content container before the call, or extract from markdown rather than raw HTML. For stable, high-volume layouts, let the model write a selector script once and spend tokens only when the layout changes.

Conclusion

ChatGPT changed which half of scraping is hard. Extraction β€” the part that used to mean brittle selectors β€” is now a schema and one SDK call. Fetching β€” the part the model cannot do β€” still decides whether there is anything real to extract, and the 0-versus-10 demonstration above is what that boundary looks like in practice. Build the two layers separately, verify the fetch before you pay for the extraction, and the combination is a scraper that survives redesigns.

Ready to Feed Your Extractor Real Pages?

The fetch layer in this guide is one POST per page on the Universal Scraping API β€” plans and request volumes are on the pricing page, and the developer docs cover every parameter unlocker.webunlocker accepts. Create a key on the free plan at app.scrapeless.com and re-run the two fetch scripts yourself.

FAQ

Q: Can ChatGPT scrape websites directly?

No. The model cannot issue HTTP requests, execute JavaScript, or pass anti-bot checks β€” it extracts from text that something else fetched. Consumer chat products sometimes wrap the model with a browsing tool, but that tool is small-scale and blocked by many sites, which is why production setups pair the model with a dedicated fetch layer.

Q: Is scraping with ChatGPT legal?

The extraction layer does not change the rules of the collection layer. Scrape public pages only, respect site terms and the robots directives defined by the Robots Exclusion Protocol, keep volumes bounded, and treat any personal data under the privacy laws that apply to you β€” the same obligations as for any scraper, plus your model provider's usage terms.

Q: When is a traditional selector scraper still the better choice?

At high volume on stable layouts. An LLM call costs tokens on every page; a selector script costs nothing after it is written. The practical split: use the model where layouts vary or change often, and generated-then-reviewed selector code where one layout repeats thousands of times.

Q: How is this different from a "ChatGPT scraper"?

Direction. This guide points the model at the web β€” ChatGPT is the parser. A ChatGPT scraper points a scraper at ChatGPT β€” it captures the assistant's answers, citations, and product results as structured data. Scrapeless ships that as an actor; the ChatGPT Scraper API guide linked in the introduction covers it.

Q: Which OpenAI model should I use for extraction?

Start with a small, current model and only move up if extraction quality demands it. Structured outputs constrain the response shape regardless of model size, so the smaller tiers handle well-defined schemas on clean input β€” spend the savings on fetching more pages instead.

At Scrapeless, we only access publicly available data while strictly complying with applicable laws, regulations, and website privacy policies. The content in this blog is for demonstration purposes only and does not involve any illegal or infringing activities. We make no guarantees and disclaim all liability for the use of information from this blog or third-party links. Before engaging in any scraping activities, consult your legal advisor and review the target website's terms of service or obtain the necessary permissions.

Most Popular Articles

Catalogue