Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors
Filter by Categories
About Article
Analyze Data
Archive
Best Practices
Better Outputs
Blog
Code Optimization
Code Quality
Command Line
Course
Daily tips
Dashboard
Data Analysis & Manipulation
Data Engineer
Data Visualization
DataFrame
Delta Lake
DevOps
DuckDB
Environment Management
Feature Engineer
Git
Jupyter Notebook
LLM
LLM Tools
Machine Learning
Machine Learning & AI
Machine Learning Tools
Manage Data
MLOps
Natural Language Processing
Newsletter Archive
NumPy
Pandas
Polars
PySpark
Python Helpers
Python Tips
Python Utilities
Scrape Data
SQL
Testing
Time Series
Tools
Visualization
Visualization & Reporting
Workflow & Automation
Workflow Automation

web-scraping

Auto-created tag for web-scraping

browser-use: Turn Plain English Prompts into Working Browser Automation

Table of Contents

Introduction
What is browser-use?
Synthesizing Hacker News Themes
Working with the Output
What This Run Cost
A Second Experiment: Scraping Newegg
Trade-offs
When to Use Each Tool
Conclusion

Introduction
Traditional browser automation tools like Playwright require you to write CSS selectors for every element you want to extract:
# Extract a laptop's price from a product card
price = await card.locator("h4.price").text_content()

This approach works, but it tightly couples your scraper to the site’s HTML structure. If a class name changes, your scraper breaks. You then have to inspect the updated HTML and rewrite your selectors from scratch.
What if you could just describe what you want in plain English?
# Tell an AI agent what to extract
agent = Agent(
task="Find gaming laptops under $1500 and extract the name, price, and GPU",
llm=ChatOpenAI(model="gpt-4o"),
)

That is what browser-use does. Instead of describing the steps, you describe the goal, and an LLM works out the steps for you.

💻 Get the Code: Open the notebook in Google Colab to run it in your browser, or grab the source from GitHub.

Stay Current with CodeCut
Easy-to-digest articles on Python, AI, and open-source tools. Delivered twice a week.

.codecut-subscribe-form {
max-width: 650px;
display: flex;
flex-direction: column;
gap: 8px;
}

.codecut-input {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: #FFFFFF;
border-radius: 8px !important;
padding: 8px 12px;
font-family: ‘Comfortaa’, sans-serif !important;
font-size: 14px !important;
color: #333333;
border: none !important;
outline: none;
width: 100%;
box-sizing: border-box;
}

input[type=”email”].codecut-input {
border-radius: 8px !important;
}

.codecut-input::placeholder {
color: #666666;
}

.codecut-email-row {
display: flex;
align-items: stretch;
height: 36px;
gap: 8px;
}

.codecut-email-row .codecut-input {
flex: 1;
}

.codecut-subscribe-btn {
background: #72BEFA;
color: #2F2D2E;
border: none;
border-radius: 8px;
padding: 8px 14px;
font-family: ‘Comfortaa’, sans-serif;
font-size: 14px;
font-weight: 500;
cursor: pointer;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.3s ease;
}

.codecut-subscribe-btn:hover {
background: #5aa8e8;
}

.codecut-subscribe-btn:disabled {
background: #999;
cursor: not-allowed;
}

.codecut-message {
font-family: ‘Comfortaa’, sans-serif;
font-size: 12px;
padding: 8px;
border-radius: 6px;
display: none;
}

.codecut-message.success {
background: #d4edda;
color: #155724;
display: block;
}

/* WordPress dark-theme overrides */
.codecut-subscribe-form .codecut-input {
background: #2F2D2E !important;
border: 1px solid #72BEFA !important;
color: #FFFFFF !important;
}

.codecut-subscribe-form .codecut-input::placeholder {
color: #999999 !important;
}

.codecut-subscribe-form .codecut-subscribe-btn {
background: #72BEFA !important;
color: #2F2D2E !important;
}

.codecut-subscribe-form .codecut-subscribe-btn:hover {
background: #5aa8e8 !important;
}

/* Mobile responsive */
@media (max-width: 480px) {
.codecut-email-row {
flex-direction: column;
height: auto;
gap: 8px;
}

.codecut-input {
border-radius: 8px;
height: 36px;
}

.codecut-subscribe-btn {
width: 100%;
text-align: center;
border-radius: 8px;
height: 36px;
}
}

Subscribe

What is browser-use?
browser-use is a Python library that gives an LLM a working browser. Under the hood it uses Playwright to drive the browser, but the LLM reads each page and decides what to click, type, and extract. You write the task in plain English, and the agent figures out the rest.
To install browser-use:
pip install browser-use
playwright install chromium

This article uses browser-use v0.12.5.
Since this tutorial uses OpenAI’s GPT-4o as the agent’s model, you will need an OpenAI API key. Store it in a .env file:
OPENAI_API_KEY=your-key-here

Then load it with python-dotenv:
import nest_asyncio

nest_asyncio.apply()

from dotenv import load_dotenv
load_dotenv()

Synthesizing Hacker News Themes
We will point browser-use at Hacker News and ask it to:
Find the top AI-related stories on the front page and identify the common themes.
This task is a good fit for browser-use because it mixes extraction with judgment: the agent has to classify each story and then reason across all of them to pull out themes.
Let’s set it up. First, define the output schema using Pydantic. This tells browser-use what structure to return:
import asyncio
from pydantic import BaseModel
from browser_use import Agent, ChatOpenAI

class HNResults(BaseModel):
titles: list[str]
points: list[int]
comments: list[int]
urls: list[str]
themes: list[str]
summary: str

Now define the agent. It takes four parameters:

task: the natural language instructions for what you want the agent to do.
llm: the model that drives the agent. Here we use GPT-4o.
output_model_schema: the Pydantic schema that tells the agent how to structure its final result.
calculate_cost: when set to True, browser-use tracks token usage and dollar cost so you can inspect them later.

async def find_ai_stories():
# Configure the agent with task, model, schema, and cost tracking
agent = Agent(
task=(
"Go to https://news.ycombinator.com/ and find "
"all stories on the front page that are "
"about AI, LLMs, or AI agents. "
"For each story, extract the title, points, "
"comment count, and URL. "
"Then identify 2-3 common themes across these "
"stories and write a short summary of what the "
"Hacker News community is currently excited or "
"concerned about regarding AI."
),
llm=ChatOpenAI(model="gpt-4o"),
output_model_schema=HNResults,
calculate_cost=True,
)

# Run the agent and get the structured result
history = await agent.run()
result = history.final_result()

# Parse into HNResults, falling back to an empty result if nothing came back
parsed = HNResults.model_validate_json(result) if result else HNResults(
titles=[], points=[], comments=[], urls=[], themes=[], summary="",
)
return parsed, history

Run the agent:
results, history = asyncio.run(find_ai_stories())
print(f"Found {len(results.titles)} AI-related stories")

📍 Step 1:
👍 Eval: Successfully navigated to the Hacker News front page
and identified several stories that may relate to AI.
🧠 Memory: On the Hacker News front page. Identified potential
AI-related stories by their titles. Need to extract
details for analysis.
🎯 Next goal: Extract details from the identified AI-related
stories on the front page.
▶️ extract: query: AI|LLM|AI agent, extract_links: True

📍 Step 2:
👍 Eval: Successfully extracted details of AI-related stories
from Hacker News.
🧠 Memory: Extracted details of AI-related stories. Ready to
analyze for common themes and summarize.
🎯 Next goal: Analyze the extracted stories to identify 2-3
common themes and write a summary.
▶️ done: 8 stories extracted, 3 themes identified

✅ Task completed successfully

⚠️ Agent reported success but judge thinks task failed
⚖️ Judge Verdict: ❌ FAIL
Failure Reason: The agent included a non-AI related story
('Solod – A subset of Go that translates to C') in its results.

Found 8 AI-related stories

Each step in the log shows the agent’s internal reasoning loop. There are four fields, and they work together to drive the next action:

Eval checks whether the last action worked. This makes the agent self-correcting, so failures get retried instead of silently propagating.
Memory tracks what has been done so far. This stops the agent from repeating expensive actions and is what makes multi-step tasks possible.
Next goal plans the next action based on eval and memory. The LLM decides what to do next, so you do not have to write a state machine.
Action executes the plan from a built-in toolkit of navigate, click, extract, and scroll. The agent picks the right tool on its own, so the same code works on a new site.

%%{init: {“theme”: “dark”}}%%
flowchart TD
Task([Task prompt]) –> Eval
Eval[Eval: check last action] –> Memory[Memory: track progress]
Memory –> NextGoal[Next goal: plan next step]
NextGoal –> Action[Action: execute from toolkit]
Action –>|Not done| Eval
Action –>|Done| Result([Worker result])
Once the worker declares success, a second LLM steps in as a judge.
This is a clever setup. Workers rarely catch their own mistakes without an independent perspective. Since the judge only sees the prompt and final result, it can objectively evaluate the output.
Here, it correctly identified that Solod is not an AI story.
%%{init: {“theme”: “dark”}}%%
flowchart TD
Result([Worker result]) –> Review[Judge: review against original task]
Review –> Verdict{Pass or fail?}
Verdict –>|Pass| Output([Trusted output])
Verdict –>|Fail| Retry([Retry or alert])
Let’s look at the results:
for title, points, comments, url in zip(
results.titles, results.points, results.comments, results.urls
):
print(f" {title}")
print(f" {points} points | {comments} comments")
print(f" {url}")
print()

print("Themes:")
for theme in results.themes:
print(f" – {theme}")

print(f"\nSummary: {results.summary}")

Show HN: Ghost Pepper – Local hold-to-talk speech-to-text for macOS
363 points | 170 comments
https://github.com/matthartman/ghost-pepper

Solod – A subset of Go that translates to C
122 points | 30 comments
https://github.com/solod-dev/solod

Issue: Claude Code is unusable for complex engineering tasks with Feb updates
1047 points | 587 comments
https://github.com/anthropics/claude-code/issues/42796

Sam Altman may control our future – can he be trusted?
1340 points | 542 comments
https://www.newyorker.com/magazine/2026/04/13/sam-altman-may-control-our-future-can-he-be-trusted

Launch HN: Freestyle – Sandboxes for Coding Agents
265 points | 145 comments
https://www.freestyle.sh/

A cryptography engineer's perspective on quantum computing timelines
455 points | 186 comments
https://words.filippo.io/crqc-timeline/

AI singer now occupies eleven spots on iTunes singles chart
166 points | 257 comments
https://www.showbiz411.com/2026/04/05/itunes-takeover-by-fake-ai-singer…

Show HN: Hippo, biologically inspired memory for AI agents
90 points | 17 comments
https://github.com/kitfunso/hippo-memory

Themes:
– AI in media and entertainment
– Ethical concerns about AI leadership
– Technical challenges in AI development

Summary: The Hacker News community is currently excited about advancements in AI tools like speech-to-text applications and biologically inspired memory systems. There are also significant discussions around ethical concerns regarding influential figures in AI like Sam Altman. Additionally, there are technical challenges being highlighted in the development of complex AI systems.

Most of the output is solid:

Structured fields are accurate. Titles, points, comments, and URLs are correct for every row.
Themes map to real stories. “AI in media and entertainment” lines up with the AI singer post, “Ethical concerns about AI leadership” with the Sam Altman piece, and “Technical challenges in AI development” with the Claude Code issue.
The summary reads like a take, not a list. Instead of restating each story, it picks out what the community is excited about (new AI tools), worried about (Sam Altman), and struggling with (complex AI systems).

However, the classifications are wrong in two places. Solod (a Go-to-C transpiler) and the quantum computing post both made the list even though neither is about AI.
Working with the Output
Once you have the structured output, you can do something with it. The simplest first step is to load it into a pandas DataFrame:
import pandas as pd

df = pd.DataFrame({
"title": results.titles,
"points": results.points,
"comments": results.comments,
"url": results.urls,
})
df

title
points
comments
url

0
Show HN: Ghost Pepper – Local hold-to-talk spe…
363
170
https://github.com/matthartman/ghost-pepper

1
Solod – A subset of Go that translates to C
122
30
https://github.com/solod-dev/solod

2
Issue: Claude Code is unusable for complex eng…
1047
587
https://github.com/anthropics/claude-code/issu…

3
Sam Altman may control our future – can he be …
1340
542
https://www.newyorker.com/magazine/2026/04/13/…

4
Launch HN: Freestyle – Sandboxes for Coding Ag…
265
145
https://www.freestyle.sh/

5
A cryptography engineer’s perspective on quant…
455
186
https://words.filippo.io/crqc-timeline/

6
AI singer now occupies eleven spots on iTunes …
166
257
https://www.showbiz411.com/2026/04/05/itunes-t…

7
Show HN: Hippo, biologically inspired memory f…
90
17
https://github.com/kitfunso/hippo-memory

Now you can drop the misclassified rows the judge flagged earlier and keep only the true AI stories:
misclassified_rows = [1, 5]
ai_only = df.drop(misclassified_rows).reset_index(drop=True)
ai_only

title
points
comments
url

0
Show HN: Ghost Pepper – Local hold-to-talk spe…
363
170
https://github.com/matthartman/ghost-pepper

1
Issue: Claude Code is unusable for complex eng…
1047
587
https://github.com/anthropics/claude-code/issu…

2
Sam Altman may control our future – can he be …
1340
542
https://www.newyorker.com/magazine/2026/04/13/…

3
Launch HN: Freestyle – Sandboxes for Coding Ag…
265
145
https://www.freestyle.sh/

4
AI singer now occupies eleven spots on iTunes …
166
257
https://www.showbiz411.com/2026/04/05/itunes-t…

5
Show HN: Hippo, biologically inspired memory f…
90
17
https://github.com/kitfunso/hippo-memory

What This Run Cost
Now let’s check what this synthesis cost in tokens, dollars, and time.
usage = history.usage
print(f"Total tokens: {usage.total_tokens:,}")
print(f"Total cost: ${usage.total_cost:.4f}")
print(f"Steps: {len(history.history)}")
print(f"Duration: {history.total_duration_seconds():.1f}s")

Total tokens: 42,339
Total cost: $0.1157
Steps: 3
Duration: 53.2s

12 cents and 53 seconds is what this run cost. Here is what the same money buys you elsewhere:

GPT-4o. A single long-prompt API call. Same dollar cost, but no browser, no scraping, no real page involved.
Your own time. Opening Hacker News, skimming 30 stories, copying the AI ones into a doc, and writing a summary by hand. Probably 10 to 20 minutes of focused work.
Playwright + LLM. A scraper to grab the page, one LLM call to classify, another to synthesize, and the code to glue them together. More code, more failure points, and likely more total tokens.

If 12 cents per run is too much for your use case, there are three ways to bring it down:

Use a cheaper model. Swap gpt-4o for gpt-4o-mini or claude-haiku. The same task often runs for under 2 cents, with some loss of reasoning quality.
Run a local model. browser-use works with Ollama and LM Studio, so the dollar cost drops to zero. The trade-off is that local models need to be 30B+ to handle structured output reliably, and each step is slower.
Tighten the prompt. Shorter tasks mean fewer steps, and each step carries the full conversation history forward, so cutting one step can save thousands of tokens.

For a full walkthrough on setting up local LLMs for workflows like this, see our LangChain and Ollama guide.

A Second Experiment: Scraping Newegg
browser-use doesn’t always work this well. To show where it breaks, I ran it on a harder task: scraping Newegg for gaming laptops. Here is the prompt:
task=(
"Go to https://www.newegg.com/Gaming-Laptops"
"/SubCategory/ID-3365 and find gaming laptops "
"matching these criteria:\n"
"- Price: $0-$1500\n"
"- GPU: NVIDIA GeForce RTX 50 Series\n"
"- RAM: 32GB\n"
"For each laptop, extract the name, "
"price, GPU, CPU, RAM, and storage. "
"Then pick the best value and explain why."
)

This run didn’t go well. Here are the issues:

Price filter: The agent typed the values but skipped APPLY, so the listings never refreshed.
32GB RAM constraint: Silently dropped. The final results all had 16GB.
Pagination: Stopped at the first page instead of collecting results from all pages.

Trade-offs
browser-use is powerful, but it comes with real trade-offs:

Speed: The agent took 30–60 seconds for the Hacker News task. Each step requires LLM reasoning, while a Playwright script would finish in ~5 seconds.
Cost: A single run is cheap (~$0.12), but costs grow quickly. Since each step carries full context, doubling steps can cost more than 2x.
Non-determinism: Results vary between runs. The agent may take different actions, and the judge may reach different conclusions.
Task fit: Results depend heavily on the page and what you ask for. The same tool can work well on one site and fail on another.

When to Use Each Tool
Knowing the trade-offs, here is how to decide which tool to reach for:

Metric
Playwright
browser-use

Speed
Fast
Slower

Cost per run
Free
Paid per LLM call

Deterministic
Yes
No

Works on a new site
Needs new selectors
Change the URL

Handles reasoning tasks
No (hardcoded rules)
Yes (LLM reasons)

Exact constraint satisfaction
Yes
No (silently relaxes hard constraints)

Choose Playwright when:

You need identical results every run
Speed matters (high-volume or frequent runs)
You need exact constraint satisfaction (e.g., “every result must have 32GB RAM”)

Choose browser-use when:

The task requires judgment or classification (e.g., “which of these stories are about AI”) rather than pattern matching
The task requires synthesis across multiple items (e.g., “what themes connect these”)
The page or task may change over time and you do not want to maintain selectors
You want scraping, classification, and reasoning in a single prompt instead of three separate pipelines

Conclusion
The best way to understand browser-use is to try it on a small project you actually care about. Here are a few ideas:

Summarize a Reddit thread. Provide a URL and ask for the top 3 arguments. Then schedule it to run daily across selected subreddits and post summaries to Slack.
Pull today’s top stories from a news site. Extract titles, sources, and short summaries, then schedule a daily digest sent to your inbox.
Watch the price of a single product. Monitor a product page and get notified when the price drops below a set threshold.

Each of these starts with a single prompt, costs just a few cents to test, and can quickly become something you use every day.
Related Tutorials

From CSS Selectors to Natural Language: Web Scraping with ScrapeGraphAI: Another natural-language scraping tool with a different architectural approach, built around graph-based extraction pipelines.

Stay Current with CodeCut
Easy-to-digest articles on Python, AI, and open-source tools. Delivered twice a week.

.codecut-subscribe-form {
max-width: 650px;
display: flex;
flex-direction: column;
gap: 8px;
}

.codecut-input {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: #FFFFFF;
border-radius: 8px !important;
padding: 8px 12px;
font-family: ‘Comfortaa’, sans-serif !important;
font-size: 14px !important;
color: #333333;
border: none !important;
outline: none;
width: 100%;
box-sizing: border-box;
}

input[type=”email”].codecut-input {
border-radius: 8px !important;
}

.codecut-input::placeholder {
color: #666666;
}

.codecut-email-row {
display: flex;
align-items: stretch;
height: 36px;
gap: 8px;
}

.codecut-email-row .codecut-input {
flex: 1;
}

.codecut-subscribe-btn {
background: #72BEFA;
color: #2F2D2E;
border: none;
border-radius: 8px;
padding: 8px 14px;
font-family: ‘Comfortaa’, sans-serif;
font-size: 14px;
font-weight: 500;
cursor: pointer;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.3s ease;
}

.codecut-subscribe-btn:hover {
background: #5aa8e8;
}

.codecut-subscribe-btn:disabled {
background: #999;
cursor: not-allowed;
}

.codecut-message {
font-family: ‘Comfortaa’, sans-serif;
font-size: 12px;
padding: 8px;
border-radius: 6px;
display: none;
}

.codecut-message.success {
background: #d4edda;
color: #155724;
display: block;
}

/* WordPress dark-theme overrides */
.codecut-subscribe-form .codecut-input {
background: #2F2D2E !important;
border: 1px solid #72BEFA !important;
color: #FFFFFF !important;
}

.codecut-subscribe-form .codecut-input::placeholder {
color: #999999 !important;
}

.codecut-subscribe-form .codecut-subscribe-btn {
background: #72BEFA !important;
color: #2F2D2E !important;
}

.codecut-subscribe-form .codecut-subscribe-btn:hover {
background: #5aa8e8 !important;
}

/* Mobile responsive */
@media (max-width: 480px) {
.codecut-email-row {
flex-direction: column;
height: auto;
gap: 8px;
}

.codecut-input {
border-radius: 8px;
height: 36px;
}

.codecut-subscribe-btn {
width: 100%;
text-align: center;
border-radius: 8px;
height: 36px;
}
}

Subscribe

pre.mermaid { background: transparent !important; padding: 0 !important; } pre.mermaid svg { background: transparent !important; } pre.mermaid .cluster rect { fill: transparent !important; stroke: #555 !important; }

browser-use: Turn Plain English Prompts into Working Browser Automation Read More »

From CSS Selectors to Natural Language: Web Scraping with ScrapeGraphAI

Table of Contents

Introduction
What is ScrapeGraphAI?
Setup
Installation
OpenAI Configuration
Local Models with Ollama

Natural Language Prompts
Structured Output with Pydantic
JavaScript Content
Multi-Page Scraping
Key Takeaways

Introduction
BeautifulSoup is the go-to library for web scraping thanks to its simple API and flexible parsing. The workflow is straightforward: fetch HTML, inspect elements in DevTools, and write selectors to extract data:
from pprint import pprint

from bs4 import BeautifulSoup
import requests

url = "https://books.toscrape.com"
response = requests.get(url)
soup = BeautifulSoup(response.content, "html.parser")

books = []
for article in soup.select("article.product_pod"):
title = article.select_one("h3 a")["title"]
price = article.select_one("p.price_color").text
books.append({"title": title, "price": price})

pprint(books[:3])

Output:
[{'price': '£51.77', 'title': 'A Light in the Attic'},
{'price': '£53.74', 'title': 'Tipping the Velvet'},
{'price': '£50.10', 'title': 'Soumission'}]

The output is correct, but selectors are tightly coupled to the HTML structure. This means when the site redesigns, everything breaks, so you spend more time maintaining selectors than extracting data:
# Before: <article class="product_pod">
# After: <div class="book-card">
soup.select("article.product_pod") # Now returns []

# Before: <p class="price_color">£51.77</p>
# After: <span class="price">£51.77</span>
soup.select_one("p.price_color") # Returns None, crashes on .text

What if you could just describe the data you want and let an LLM figure out the extraction? That’s where ScrapeGraphAI comes in.

💻 Get the Code: The complete source code for this tutorial are available on GitHub. Clone it to follow along!

What is ScrapeGraphAI?
ScrapeGraphAI is an open-source Python library for LLM-powered web scraping. Rather than writing CSS selectors, you describe the data you want in plain English.
Key benefits:

No selector maintenance: Describe what data you want, not where it lives in the HTML
Self-healing scrapers: The LLM adjusts automatically when websites redesign
Structured output: Define Pydantic schemas for type-safe extraction
JavaScript support: Built-in rendering for React, Vue, and Angular sites
Multi-provider: Use OpenAI, Anthropic, or local models via Ollama

Stay Current with CodeCut
Easy-to-digest articles on Python, AI, and open-source tools. Delivered twice a week.

.codecut-subscribe-form {
max-width: 650px;
display: flex;
flex-direction: column;
gap: 8px;
}

.codecut-input {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: #FFFFFF;
border-radius: 8px !important;
padding: 8px 12px;
font-family: ‘Comfortaa’, sans-serif !important;
font-size: 14px !important;
color: #333333;
border: none !important;
outline: none;
width: 100%;
box-sizing: border-box;
}

input[type=”email”].codecut-input {
border-radius: 8px !important;
}

.codecut-input::placeholder {
color: #666666;
}

.codecut-email-row {
display: flex;
align-items: stretch;
height: 36px;
gap: 8px;
}

.codecut-email-row .codecut-input {
flex: 1;
}

.codecut-subscribe-btn {
background: #72BEFA;
color: #2F2D2E;
border: none;
border-radius: 8px;
padding: 8px 14px;
font-family: ‘Comfortaa’, sans-serif;
font-size: 14px;
font-weight: 500;
cursor: pointer;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.3s ease;
}

.codecut-subscribe-btn:hover {
background: #5aa8e8;
}

.codecut-subscribe-btn:disabled {
background: #999;
cursor: not-allowed;
}

.codecut-message {
font-family: ‘Comfortaa’, sans-serif;
font-size: 12px;
padding: 8px;
border-radius: 6px;
display: none;
}

.codecut-message.success {
background: #d4edda;
color: #155724;
display: block;
}

/* WordPress dark-theme overrides */
.codecut-subscribe-form .codecut-input {
background: #2F2D2E !important;
border: 1px solid #72BEFA !important;
color: #FFFFFF !important;
}

.codecut-subscribe-form .codecut-input::placeholder {
color: #999999 !important;
}

.codecut-subscribe-form .codecut-subscribe-btn {
background: #72BEFA !important;
color: #2F2D2E !important;
}

.codecut-subscribe-form .codecut-subscribe-btn:hover {
background: #5aa8e8 !important;
}

/* Mobile responsive */
@media (max-width: 480px) {
.codecut-email-row {
flex-direction: column;
height: auto;
gap: 8px;
}

.codecut-input {
border-radius: 8px;
height: 36px;
}

.codecut-subscribe-btn {
width: 100%;
text-align: center;
border-radius: 8px;
height: 36px;
}
}

Subscribe

Setup
Installation
Install ScrapeGraphAI and Playwright for browser automation:
pip install scrapegraphai playwright
playwright install

OpenAI Configuration
For cloud-based extraction, you’ll need an OpenAI API key. Store it in a .env file:
OPENAI_API_KEY=your-api-key-here

Then load it in your script:
from dotenv import load_dotenv
import os

load_dotenv()

graph_config = {
"llm": {
"api_key": os.getenv("OPENAI_API_KEY"),
"model": "openai/gpt-4o-mini",
},
"verbose": False,
"headless": True,
}

Local Models with Ollama
For zero API costs, use local models via Ollama. ScrapeGraphAI requires two models:

LLM (llama3.2): Interprets your prompts and extracts data
Embedding model (nomic-embed-text): Converts page content into a format the LLM can search

📖 New to Ollama? See our complete guide to running local LLMs with Ollama.

Install Ollama and pull both:
# Install Ollama from https://ollama.ai
ollama pull llama3.2
ollama pull nomic-embed-text

Then configure ScrapeGraphAI to use local inference:
graph_config_local = {
"llm": {
"model": "ollama/llama3.2",
"temperature": 0,
"format": "json",
"base_url": "http://localhost:11434",
},
"embeddings": {
"model": "ollama/nomic-embed-text",
"base_url": "http://localhost:11434",
},
"verbose": False,
"headless": True,
}

The same extraction code works with both configurations. Switch between cloud and local by changing the config.
Natural Language Prompts
ScrapeGraphAI extraction works in three steps:

Prompt: Describe the data you want in plain English
Source: Provide the URL to scrape
Config: Set your LLM provider and credentials

Pass these to SmartScraperGraph and call run():
from dotenv import load_dotenv
from scrapegraphai.graphs import SmartScraperGraph
import os

load_dotenv()

graph_config = {
"llm": {
"api_key": os.getenv("OPENAI_API_KEY"),
"model": "openai/gpt-4o-mini",
},
"verbose": False,
"headless": True,
}

smart_scraper = SmartScraperGraph(
prompt="Extract the first 5 book titles and their prices",
source="https://books.toscrape.com",
config=graph_config,
)

result = smart_scraper.run()

Output:
{'content': [{'price': '£51.77', 'title': 'A Light in the Attic'},
{'price': '£53.74', 'title': 'Tipping the Velvet'},
{'price': '£50.10', 'title': 'Soumission'},
{'price': '£47.82', 'title': 'Sharp Objects'},
{'price': '£54.23', 'title': 'Sapiens: A Brief History of Humankind'}]}

The LLM understood “first 5 book titles and their prices” without any knowledge of the page’s HTML structure.
Structured Output with Pydantic
Raw scraped data often needs cleaning and validation. With ScrapeGraphAI, you can define a Pydantic schema to get type-safe, validated output directly from extraction.
from dotenv import load_dotenv
from scrapegraphai.graphs import SmartScraperGraph
from pydantic import BaseModel, Field
from typing import List
import os

load_dotenv()

class Book(BaseModel):
title: str = Field(description="The title of the book")
price: float = Field(description="Price in GBP as a number")
rating: int = Field(description="Star rating from 1 to 5")

class BookCatalog(BaseModel):
books: List[Book]

graph_config = {
"llm": {
"api_key": os.getenv("OPENAI_API_KEY"),
"model": "openai/gpt-4o-mini",
},
"verbose": False,
"headless": True,
}

smart_scraper = SmartScraperGraph(
prompt="Extract the first 3 books with their titles, prices, and star ratings",
source="https://books.toscrape.com",
schema=BookCatalog,
config=graph_config,
)

result = smart_scraper.run()

Output:
{'books': [{'price': 51.77, 'rating': 5, 'title': 'A Light in the Attic'},
{'price': 53.74, 'rating': 5, 'title': 'Tipping the Velvet'},
{'price': 50.1, 'rating': 5, 'title': 'Soumission'}]}

The output matches the Pydantic schema:

price: Converted from '£51.77' string to 51.77 float
rating: Extracted from star icons as integer 5
title: Captured as string

The schema ensures:

price is extracted as a float, not a string like "£51.77"
rating is converted to an int from the star display
Missing or invalid fields raise validation errors

The data is analysis-ready, so you don’t need any post-processing in pandas.
For more advanced LLM output validation patterns, see our PydanticAI guide.
JavaScript Content
Modern websites built with React, Vue, or Angular render content dynamically. BeautifulSoup only parses the initial HTML before JavaScript runs, so it misses the actual content.
To demonstrate this, let’s fetch a JavaScript-rendered page with BeautifulSoup:
from bs4 import BeautifulSoup
import requests

soup = BeautifulSoup(requests.get("https://quotes.toscrape.com/js/").content, "html.parser")
print(soup.select(".quote"))

Output:
[]

The result is an empty list because the content loads via JavaScript after the initial HTML is served.
Selenium can handle JavaScript, but requires explicit waits and complex timing logic.
ScrapeGraphAI uses Playwright to handle JavaScript rendering automatically. The headless parameter controls whether the browser runs visibly or in the background:
from scrapegraphai.graphs import SmartScraperGraph
from dotenv import load_dotenv
import os

load_dotenv()

graph_config = {
"llm": {
"api_key": os.getenv("OPENAI_API_KEY"),
"model": "openai/gpt-4o-mini",
},
"verbose": False,
"headless": True, # Browser runs in background
}

# quotes.toscrape.com/js loads content via JavaScript
smart_scraper = SmartScraperGraph(
prompt="Extract the first 3 quotes with their text and authors",
source="https://quotes.toscrape.com/js/",
config=graph_config,
)

result = smart_scraper.run()

Output:
{'content': [{'author': 'Albert Einstein',
'quote': 'The world as we have created it is a process of our '
'thinking. It cannot be changed without changing our '
'thinking.'},
{'author': 'J.K. Rowling',
'quote': 'It is our choices, Harry, that show what we truly are, '
'far more than our abilities.'},
{'author': 'Albert Einstein',
'quote': 'There are only two ways to live your life. One is as '
'though nothing is a miracle. The other is as though '
'everything is a miracle.'}]}

Unlike the empty BeautifulSoup result, ScrapeGraphAI successfully extracted all three quotes from the JavaScript-rendered page. The LLM chose sensible field names (author, quote) based solely on our natural language prompt.
Multi-Page Scraping
Research tasks often require data from multiple sources. Scraping multiple sites usually requires building individual scrapers for each layout, then manually combining the results into a unified format.
SearchGraph automates this workflow. It searches the web, scrapes relevant pages, and returns aggregated results:
from scrapegraphai.graphs import SearchGraph
import os
from dotenv import load_dotenv

load_dotenv()

graph_config = {
"llm": {
"api_key": os.getenv("OPENAI_API_KEY"),
"model": "openai/gpt-4o-mini",
},
"max_results": 3,
"verbose": False,
}

search_graph = SearchGraph(
prompt="Find the top 3 Python web scraping libraries and their GitHub stars",
config=graph_config,
)

result = search_graph.run()

Output:
{'sources': ['https://github.com/luminati-io/Python-scraping-libraries',
'https://brightdata.com/blog/web-data/python-web-scraping-libraries',
'https://www.geeksforgeeks.org/python/python-web-scraping-tutorial/',
'https://www.projectpro.io/article/python-libraries-for-web-scraping/625'],
'top_libraries': [{'github_stars': '~52.3k', 'name': 'Requests'},
{'github_stars': '~53.7k', 'name': 'Scrapy'},
{'github_stars': '~31.2k', 'name': 'Selenium'},
{'github_stars': 1800, 'name': 'BeautifulSoup'}]}

For scraping multiple known URLs with the same prompt, use SmartScraperMultiGraph:
from scrapegraphai.graphs import SmartScraperMultiGraph
import os
from dotenv import load_dotenv

load_dotenv()

graph_config = {
"llm": {
"api_key": os.getenv("OPENAI_API_KEY"),
"model": "openai/gpt-4o-mini",
},
"verbose": False,
"headless": True,
}

multi_scraper = SmartScraperMultiGraph(
prompt="Extract the page title and main heading",
source=[
"https://books.toscrape.com",
"https://quotes.toscrape.com",
],
config=graph_config,
)

result = multi_scraper.run()

Output:
{'main_headings': ['All products', 'Quotes to Scrape'],
'page_titles': ['Books to Scrape', 'Quotes to Scrape'],
'sources': ['https://books.toscrape.com', 'https://quotes.toscrape.com']}

Both approaches return consistent, structured output regardless of the underlying HTML differences between sites.
Key Takeaways
ScrapeGraphAI shifts web scraping from writing CSS selectors to describing the data you want:

Natural language prompts replace hard-coded CSS selectors and XPath expressions
Pydantic schemas provide type-safe, validated output ready for analysis
Built-in JavaScript rendering handles React, Vue, and Angular sites automatically
Multi-provider support lets you choose between cloud APIs and local models
SearchGraph automates multi-source research with a single prompt

The library is best suited for:

Exploratory data collection where site structures vary
Research tasks requiring data from multiple sources
Projects where scraper maintenance costs exceed development time
Extracting structured data from JavaScript-heavy applications

For high-volume production workloads on sites with stable HTML, Scrapy remains the faster choice. ScrapeGraphAI pays off when the time saved on selector updates outweighs the per-request LLM cost.
Related Tutorials

Turn Receipt Images into Spreadsheets with LlamaIndex: Extract structured data from images and PDFs instead of web pages
Transform Any PDF into Searchable AI Data with Docling: Convert PDF documents into RAG-ready structured data

Stay Current with CodeCut
Easy-to-digest articles on Python, AI, and open-source tools. Delivered twice a week.

.codecut-subscribe-form {
max-width: 650px;
display: flex;
flex-direction: column;
gap: 8px;
}

.codecut-input {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: #FFFFFF;
border-radius: 8px !important;
padding: 8px 12px;
font-family: ‘Comfortaa’, sans-serif !important;
font-size: 14px !important;
color: #333333;
border: none !important;
outline: none;
width: 100%;
box-sizing: border-box;
}

input[type=”email”].codecut-input {
border-radius: 8px !important;
}

.codecut-input::placeholder {
color: #666666;
}

.codecut-email-row {
display: flex;
align-items: stretch;
height: 36px;
gap: 8px;
}

.codecut-email-row .codecut-input {
flex: 1;
}

.codecut-subscribe-btn {
background: #72BEFA;
color: #2F2D2E;
border: none;
border-radius: 8px;
padding: 8px 14px;
font-family: ‘Comfortaa’, sans-serif;
font-size: 14px;
font-weight: 500;
cursor: pointer;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.3s ease;
}

.codecut-subscribe-btn:hover {
background: #5aa8e8;
}

.codecut-subscribe-btn:disabled {
background: #999;
cursor: not-allowed;
}

.codecut-message {
font-family: ‘Comfortaa’, sans-serif;
font-size: 12px;
padding: 8px;
border-radius: 6px;
display: none;
}

.codecut-message.success {
background: #d4edda;
color: #155724;
display: block;
}

/* WordPress dark-theme overrides */
.codecut-subscribe-form .codecut-input {
background: #2F2D2E !important;
border: 1px solid #72BEFA !important;
color: #FFFFFF !important;
}

.codecut-subscribe-form .codecut-input::placeholder {
color: #999999 !important;
}

.codecut-subscribe-form .codecut-subscribe-btn {
background: #72BEFA !important;
color: #2F2D2E !important;
}

.codecut-subscribe-form .codecut-subscribe-btn:hover {
background: #5aa8e8 !important;
}

/* Mobile responsive */
@media (max-width: 480px) {
.codecut-email-row {
flex-direction: column;
height: auto;
gap: 8px;
}

.codecut-input {
border-radius: 8px;
height: 36px;
}

.codecut-subscribe-btn {
width: 100%;
text-align: center;
border-radius: 8px;
height: 36px;
}
}

Subscribe

📚 Want to go deeper? Learning new techniques is the easy part. Knowing how to structure, test, and deploy them is what separates side projects from real work. My book shows you how to build data science projects that actually make it to production. Get the book →

From CSS Selectors to Natural Language: Web Scraping with ScrapeGraphAI Read More »

Scroll to Top

Work with Khuyen Tran

Work with Khuyen Tran