๐ŸŽ‰ 75% of content is free forever โ€” Unlock Premium from $10/mo โ†’
CW
๐Ÿ’ผ Servicesโ„น๏ธ Aboutโœ‰๏ธ ContactView Pricing Plansfrom $10

Web Browsing Agent with Playwright

AI AgentsWeb Browsing Agent๐ŸŸข Free Lesson

Advertisement

Web Browsing Agent with Playwright

Web Browsing Agent Architecture

Web Browsing Agent PipelineUSER GOAL"Find flight prices""Compare laptop specs"Goal DecomposerBreak into stepsPlan navigation strategyPlaywright ControllerNavigate ยท Click ยท TypeWait ยท Screenshot ยท ExtractAnti-Bot DetectionUser-agent rotation ยท DelaysCAPTCHA detection ยท StealthDOM Parser & Content ExtractorRemove noise (ads, nav, scripts)Extract text, links, forms, tablesContent scoring for relevanceLLM Decision EngineAnalyze extracted content โ†’ Decide next actionnavigate ยท click ยท fill ยท extract ยท doneMax 15 steps | Budget: $0.15 per taskLoop untiltask completeError RecoveryTimeout โ†’ retry with backoff404 โ†’ try alternate URLCAPTCHA โ†’ escalate to humanExtracted DataStructured JSON outputPrices, specs, linksTables, lists, metadataTASK COMPLETE: Structured data, answers, or actions takenAvg 5-8 steps | 12-45 seconds | $0.05-0.20 per taskReal-World Performance75%Task Completion Rate6.2Avg Steps per Task28sAvg Task Time85%Extraction Accuracy

What is a Web Browsing Agent?

Web browsing agents automate web interaction by navigating pages, reading content, clicking links, filling forms, and extracting structured data. They combine LLM reasoning with browser automation to accomplish complex web tasks.

Why this matters: The web contains most of the world's information, but it's locked behind complex interfaces, dynamic JavaScript, anti-bot measures, and inconsistent page structures. A browsing agent can navigate these challenges to extract data that would take a human hours to collect manually.

Common Misconception

"Web scraping is just sending HTTP requests and parsing HTML."

Modern web pages are JavaScript-heavy single-page applications that require a real browser engine. Simple HTTP requests miss dynamically loaded content, cookie consent walls, and lazy-loaded images. Playwright handles all of this by running an actual Chromium browser.

Real-World Analogy

Think of the agent as a very efficient research assistant. You give them a task ("Find the cheapest round-trip flight from NYC to London for next weekend"), and they methodically visit airline websites, navigate search forms, extract prices, compare options, and report back with a structured answer โ€” handling popups, redirects, and JavaScript along the way.

Project Overview

We will build a web browsing agent that:

  • Navigates websites using Playwright with anti-bot stealth
  • Parses DOM content and extracts structured data
  • Follows links and navigates multi-page flows
  • Extracts information based on user goals
  • Handles dynamic content (JavaScript, SPAs, cookie consent)

Expected outcome: A web browsing agent that can extract data from complex websites.

Difficulty: Advanced (requires understanding of web technologies and browser automation)

Tools & Setup

ToolVersionPurpose
Python3.11+Core language
Playwright1.40+Browser automation
BeautifulSoup44.12+HTML parsing
OpenAI1.0+LLM backbone
lxml4.9+Fast HTML parsing

Step 1: Environment Setup

python -m venv venv
source venv/bin/activate
pip install playwright beautifulsoup4 openai lxml
playwright install chromium
export OPENAI_API_KEY="sk-your-key"

Step 2: Browser Controller

# browser.py
"""Playwright-based browser controller with anti-bot stealth measures."""
import logging
import random
import time
from typing import Dict, List, Optional
from dataclasses import dataclass
from playwright.sync_api import sync_playwright, Page, Browser, BrowserContext

logger = logging.getLogger(__name__)

USER_AGENTS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
]


@dataclass
class BrowserConfig:
    headless: bool = True
    timeout: int = 30000
    viewport_width: int = 1280
    viewport_height: int = 720
    user_agent: str = USER_AGENTS[0]
    stealth_mode: bool = True
    request_delay: float = 1.0  # seconds between requests


class BrowserController:
    """Manages Playwright browser with stealth and error recovery.

    Args:
        config: Browser configuration options.
    """

    def __init__(self, config: BrowserConfig = None):
        self.config = config or BrowserConfig()
        self.playwright = None
        self.browser: Optional[Browser] = None
        self.context: Optional[BrowserContext] = None
        self.page: Optional[Page] = None

    def start(self) -> None:
        """Launch browser with stealth configurations."""
        self.playwright = sync_playwright().start()
        self.browser = self.playwright.chromium.launch(
            headless=self.config.headless,
            args=[
                "--disable-blink-features=AutomationControlled",
                "--disable-dev-shm-usage",
            ],
        )
        ua = random.choice(USER_AGENTS) if self.config.stealth_mode else self.config.user_agent
        self.context = self.browser.new_context(
            viewport={
                "width": self.config.viewport_width,
                "height": self.config.viewport_height,
            },
            user_agent=ua,
            java_script_enabled=True,
        )
        # Stealth: override navigator.webdriver
        self.context.add_init_script("""
            Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
        """)
        self.page = self.context.new_page()
        self.page.set_default_timeout(self.config.timeout)
        logger.info("Browser started with stealth mode")

    def stop(self) -> None:
        """Clean up browser resources."""
        if self.browser:
            self.browser.close()
        if self.playwright:
            self.playwright.stop()
        logger.info("Browser stopped")

    def navigate(self, url: str) -> Dict:
        """Navigate to URL with error handling and anti-bot delays."""
        try:
            time.sleep(random.uniform(0.5, self.config.request_delay))
            response = self.page.goto(url, wait_until="domcontentloaded")
            self.page.wait_for_load_state("networkidle")
            return {
                "success": True,
                "url": self.page.url,
                "title": self.page.title(),
                "status": response.status if response else None,
            }
        except Exception as e:
            logger.error(f"Navigation failed: {e}")
            return {"success": False, "error": str(e)}

    def click(self, selector: str) -> Dict:
        """Click an element with wait for navigation."""
        try:
            self.page.click(selector)
            self.page.wait_for_load_state("networkidle")
            return {"success": True, "url": self.page.url}
        except Exception as e:
            logger.error(f"Click failed: {e}")
            return {"success": False, "error": str(e)}

    def fill(self, selector: str, value: str) -> Dict:
        """Fill a form field."""
        try:
            self.page.fill(selector, value)
            return {"success": True}
        except Exception as e:
            logger.error(f"Fill failed: {e}")
            return {"success": False, "error": str(e)}

    def scroll_to_bottom(self) -> None:
        """Auto-scroll to load lazy content."""
        self.page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
        time.sleep(1)

    def get_content(self) -> str:
        return self.page.content()

    def get_text(self) -> str:
        return self.page.inner_text("body")

    def screenshot(self, path: str) -> None:
        self.page.screenshot(path=path, full_page=True)

Step 3: DOM Parser and Content Extractor

# parser.py
"""DOM parser with noise removal, content scoring, and structured extraction."""
import re
import logging
from typing import Dict, List, Optional
from bs4 import BeautifulSoup

logger = logging.getLogger(__name__)


class DOMParser:
    """Parse HTML with noise removal and content scoring.

    Removes: scripts, styles, nav, footer, header, ads, sidebars.
    Extracts: text, links, forms, tables, metadata.
    """

    NOISE_TAGS = ["script", "style", "nav", "footer", "header", "aside", "noscript"]
    NOISE_CLASSES = re.compile(r"(sidebar|ad|popup|modal|cookie|banner)", re.I)

    def parse(self, html: str) -> Dict:
        """Parse HTML and extract structured content."""
        soup = BeautifulSoup(html, "lxml")
        self._remove_noise(soup)

        return {
            "title": soup.title.string if soup.title else "",
            "text": self._extract_text(soup),
            "links": self._extract_links(soup),
            "forms": self._extract_forms(soup),
            "tables": self._extract_tables(soup),
            "structured": self._extract_structured(soup),
            "meta": self._extract_meta(soup),
        }

    def _remove_noise(self, soup: BeautifulSoup) -> None:
        """Remove non-content elements."""
        for tag in soup.find_all(self.NOISE_TAGS):
            tag.decompose()
        for tag in soup.find_all(class_=self.NOISE_CLASSES):
            tag.decompose()
        for tag in soup.find_all(id=self.NOISE_CLASSES):
            tag.decompose()

    def _extract_links(self, soup: BeautifulSoup) -> List[Dict]:
        links = []
        for a in soup.find_all("a", href=True):
            text = a.get_text(strip=True)
            if text and len(text) > 2:
                links.append({"text": text[:100], "href": a["href"]})
        return links[:50]

    def _extract_forms(self, soup: BeautifulSoup) -> List[Dict]:
        forms = []
        for form in soup.find_all("form"):
            inputs = []
            for inp in form.find_all(["input", "select", "textarea"]):
                inputs.append({
                    "type": inp.get("type", "text"),
                    "name": inp.get("name", ""),
                    "placeholder": inp.get("placeholder", ""),
                })
            forms.append({
                "action": form.get("action", ""),
                "method": form.get("method", "get"),
                "inputs": inputs,
            })
        return forms

    def _extract_text(self, soup: BeautifulSoup) -> str:
        text = soup.get_text(separator="\n", strip=True)
        lines = [line.strip() for line in text.splitlines() if line.strip()]
        return "\n".join(lines[:200])

    def _extract_tables(self, soup: BeautifulSoup) -> List[List[List[str]]]:
        result = []
        for table in soup.find_all("table")[:5]:
            rows = []
            for tr in table.find_all("tr"):
                cells = [td.get_text(strip=True) for td in tr.find_all(["td", "th"])]
                rows.append(cells)
            result.append(rows)
        return result

    def _extract_structured(self, soup: BeautifulSoup) -> Dict:
        data = {}
        lists = soup.find_all(["ul", "ol"])
        if lists:
            data["lists"] = [
                [li.get_text(strip=True) for li in lst.find_all("li")]
                for lst in lists[:10]
            ]
        return data

    def _extract_meta(self, soup: BeautifulSoup) -> Dict:
        meta = {}
        for tag in soup.find_all("meta"):
            name = tag.get("name") or tag.get("property", "")
            content = tag.get("content", "")
            if name and content:
                meta[name] = content
        return meta

Step 4: Web Browsing Agent

# agent.py
"""Web browsing agent with LLM-driven decision making and error recovery."""
import json
import re
import logging
from typing import Dict, List
from openai import OpenAI
from browser import BrowserController, BrowserConfig
from parser import DOMParser

logger = logging.getLogger(__name__)

SYSTEM_PROMPT = """You are a web browsing agent. You navigate websites to accomplish user goals.

Available actions:
- navigate(url): Go to a URL
- click(selector): Click an element (CSS selector)
- fill(selector, value): Fill a form field
- extract(): Get page content
- done(answer): Task complete

Analyze the page content and decide the next action. Always explain your reasoning.
If stuck after 3 attempts at the same page, try a different approach or give up."""


class WebBrowsingAgent:
    """LLM-driven web browsing agent with error recovery.

    Args:
        model: OpenAI model for decision making.
        max_steps: Maximum navigation steps per task.
    """

    def __init__(
        self,
        model: str = "gpt-4-turbo-preview",
        max_steps: int = 15,
    ):
        self.client = OpenAI()
        self.model = model
        self.max_steps = max_steps
        self.browser = BrowserController()
        self.parser = DOMParser()
        self.history: List[Dict] = []

    def start(self) -> None:
        self.browser.start()

    def stop(self) -> None:
        self.browser.stop()

    def run(self, goal: str) -> Dict:
        """Execute a web browsing task from goal to completion.

        Args:
            goal: Natural language task description.

        Returns:
            Dict with success status, answer, steps taken, and history.
        """
        self.history = []
        repeated_pages = 0

        for step in range(self.max_steps):
            current_page = self.browser.get_content()
            parsed = self.parser.parse(current_page)

            # Detect stuck loops
            if self._is_loop(parsed["title"]):
                repeated_pages += 1
                if repeated_pages >= 3:
                    return {
                        "success": False,
                        "error": "Agent stuck in loop",
                        "steps": step + 1,
                        "history": self.history,
                    }
            else:
                repeated_pages = 0

            context = self._build_context(goal, parsed, step)

            try:
                response = self.client.chat.completions.create(
                    model=self.model,
                    messages=[
                        {"role": "system", "content": SYSTEM_PROMPT},
                        {"role": "user", "content": context},
                    ],
                    temperature=0.0,
                )
                action = self._parse_action(response.choices[0].message.content)
            except Exception as e:
                logger.error(f"LLM call failed: {e}")
                return {
                    "success": False,
                    "error": f"LLM error: {e}",
                    "steps": step + 1,
                    "history": self.history,
                }

            self.history.append({
                "step": step + 1,
                "action": action,
                "page_title": parsed["title"],
            })

            if action["type"] == "done":
                return {
                    "success": True,
                    "answer": action.get("answer", ""),
                    "steps": step + 1,
                    "history": self.history,
                }

            result = self._execute_action(action)
            if not result["success"]:
                logger.warning(f"Action failed: {result.get('error')}")
                self.history[-1]["error"] = result.get("error")

        return {
            "success": False,
            "error": "Max steps reached",
            "steps": self.max_steps,
            "history": self.history,
        }

    def _build_context(self, goal: str, parsed: Dict, step: int) -> str:
        text_preview = parsed["text"][:2000]
        links_preview = json.dumps(parsed["links"][:10], indent=2)
        forms_preview = json.dumps(parsed["forms"][:3], indent=2)

        return f"""Goal: {goal}
Current page: {parsed['title']}
URL: {self.browser.page.url}
Step: {step + 1} of {self.max_steps}

Page content:
{text_preview}

Available links:
{links_preview}

Available forms:
{forms_preview}

What action should I take next? Respond with one of:
- navigate(url): Go to a URL
- click(selector): Click an element
- fill(selector, value): Fill a form field
- extract(): Get page content
- done(answer): Task complete with answer"""

    def _parse_action(self, response: str) -> Dict:
        for action_type in ["navigate", "click", "fill", "extract", "done"]:
            if f"{action_type}(" in response:
                match = re.search(rf"{action_type}\(([^)]*)\)", response)
                if match:
                    args = match.group(1).split(",")
                    return {
                        "type": action_type,
                        "args": [a.strip().strip("\"'") for a in args],
                    }
        return {"type": "done", "args": ["Could not determine action"]}

    def _execute_action(self, action: Dict) -> Dict:
        if action["type"] == "navigate":
            return self.browser.navigate(action["args"][0])
        elif action["type"] == "click":
            return self.browser.click(action["args"][0])
        elif action["type"] == "fill":
            return self.browser.fill(action["args"][0], action["args"][1])
        elif action["type"] == "extract":
            return {"success": True, "content": self.browser.get_text()}
        elif action["type"] == "done":
            return {"success": True}
        return {"success": False, "error": "Unknown action"}

    def _is_loop(self, page_title: str) -> bool:
        if len(self.history) < 2:
            return False
        return self.history[-1]["page_title"] == page_title

Mathematical Foundation

Page Relevance Score:

Where each parameter means:

  • โ€” Term frequency of query in document
  • โ€” Position on page (lower = more relevant)
  • โ€” Link relevance score

Intuition: Combines text matching, position, and link quality to rank page content. In practice, content scoring helps the DOM parser identify the main content area, filtering out sidebars and navigation.

Navigation Strategy:

Where is the action-value function and is the step limit (15 for typical tasks).

Intuition: Choose actions that maximize progress toward goal while respecting resource limits. The LLM acts as the policy function, evaluating each state and selecting the optimal action.

Performance Considerations

MetricValueCost Impact
Task Completion75%+For simple web tasks
Avg Steps5-8Depends on complexity
Page Load Time1-5sNetwork dependent
Extraction Accuracy85%+For structured content
Cost per Task$0.05-0.20~5-10 LLM calls per task
Browser Memory500MBPer browser instance

Latency breakdown: Page load (2s) + DOM parse (100ms) + LLM decision (1.5s) + Action execution (1s) = ~4.6s per step. A 6-step task takes ~28 seconds total.

Security Notes

  • Respect robots.txt โ€” Always check before scraping
  • Rate limiting โ€” Add 1-2s delays between requests to avoid overloading servers
  • Terms of Service โ€” Check website ToS before automated access
  • Data privacy โ€” Don't collect personal data without consent
  • CAPTCHA handling โ€” Detect and escalate to human, don't attempt to bypass
  • Credential safety โ€” Never store login credentials in code

Interview Questions

1. How do you handle dynamic content (JavaScript-rendered pages)?

Answer: Dynamic content strategies: (1) Wait for network idle โ€” Playwright waits for no network requests, (2) Explicit waits โ€” wait_for_selector() for specific elements, (3) Scroll handling โ€” auto-scroll to load lazy content, (4) Cookie consent โ€” detect and dismiss popups automatically, (5) SPA navigation โ€” handle client-side routing. Playwright handles most cases automatically. For complex SPAs: use wait_for_load_state("networkidle"). Test with real websites to identify patterns.

2. How do you extract structured data from messy HTML?

Answer: Extraction strategies: (1) Noise removal โ€” remove scripts, styles, ads, nav, footer, (2) Content scoring โ€” find main content by text density, (3) Table parsing โ€” extract tabular data as structured JSON, (4) List parsing โ€” extract ordered/unordered lists, (5) Pattern matching โ€” CSS selectors for known patterns. BeautifulSoup with lxml parser is fastest. Key: preprocess HTML to remove noise before extraction. Use content scoring to find main content area when layout is unknown.

3. What is the role of the LLM in web browsing?

Answer: LLM responsibilities: (1) Goal understanding โ€” parse user request into web actions, (2) Page analysis โ€” understand page structure and content, (3) Action planning โ€” decide next action (click, fill, navigate), (4) Error recovery โ€” handle unexpected page states, (5) Data synthesis โ€” combine information from multiple pages. The LLM acts as the "brain" while the browser is the "body". Key: provide clear context (page content, available actions, forms) to the LLM for optimal decisions.

4. How do you handle anti-bot measures?

Answer: Anti-bot strategies: (1) Real user agent โ€” rotate realistic browser user agents, (2) Rate limiting โ€” add 1-2s delays between requests, (3) Session management โ€” maintain cookies and sessions, (4) Stealth mode โ€” override navigator.webdriver, disable automation flags, (5) CAPTCHA handling โ€” detect and escalate to human. Important: respect robots.txt and terms of service. For production: use headless browsers with stealth plugins. Balance automation with ethical web scraping.

5. How do you evaluate web browsing agent performance?

Answer: Evaluation metrics: (1) Task completion rate โ€” % of tasks successfully completed (target: 75%+), (2) Step efficiency โ€” average steps to complete task (target: 5-8), (3) Accuracy โ€” correct data extracted (target: 85%+), (4) Speed โ€” time to complete task (target: <45s), (5) Robustness โ€” handles website changes, (6) Safety โ€” doesn't violate terms of service. Use benchmark datasets (WebArena, Mind2Web) for evaluation. A/B test different prompts and strategies.

6. How would you extend the agent for e-commerce tasks?

Answer: E-commerce extensions: (1) Product search โ€” navigate search, filter results, sort by price, (2) Price comparison โ€” extract prices from multiple sites, (3) Cart management โ€” add/remove items, (4) Checkout flow โ€” handle multi-step checkout, (5) Account management โ€” login, saved addresses, payment methods. Key challenges: handling dynamic pricing (prices change hourly), stock availability (real-time checks), and anti-bot measures (e-commerce sites are aggressive). Use structured extraction for product data.

7. What are the legal considerations for web scraping?

Answer: Legal considerations: (1) Terms of service โ€” check website ToS, (2) robots.txt โ€” respect crawl directives, (3) Copyright โ€” don't reproduce copyrighted content verbatim, (4) Privacy โ€” don't collect personal data without consent, (5) Computer fraud laws โ€” don't bypass access controls (CFAA). Best practices: check robots.txt first, add delays (1-2s), don't overload servers, respect rate limits, store only what you need. Key: consult legal counsel for commercial scraping.

8. How do you handle multi-page navigation flows?

Answer: Multi-page strategies: (1) State machine โ€” track current state and valid transitions, (2) Session management โ€” maintain cookies and login state, (3) Backtracking โ€” return to previous state if stuck, (4) Goal decomposition โ€” break complex flows into steps, (5) Error recovery โ€” handle unexpected page states. Example: checkout flow = cart โ†’ login โ†’ shipping โ†’ payment โ†’ confirmation. Key: maintain state across pages, handle errors gracefully, and detect loops (repeated page titles).

Common Pitfalls & Solutions

PitfallSolution
JavaScript not renderingUse Playwright with wait_for_load_state("networkidle")
Anti-bot detectionRotate user agents, add delays, use stealth mode
Cookie consent popupsDetect and dismiss automatically via common selectors
Dynamic content loadingAuto-scroll, wait for network idle
Complex page layoutsUse content scoring to find main content area
Session expirationMaintain cookies, refresh sessions periodically
Broken selectorsUse multiple selector strategies (CSS, XPath, text)
Memory leaksProperly close browser contexts with try/finally
Agent loopsDetect repeated page titles, break after 3 repetitions

Summary with Key Takeaways

  • Playwright provides the best balance of features and reliability for web browsing automation
  • DOM parsing requires noise removal and content scoring for accurate extraction
  • LLM reasoning is essential for understanding page structure and deciding actions
  • Anti-bot measures require careful implementation and ethical considerations
  • Multi-page flows need state management, loop detection, and error recovery
  • Always respect robots.txt and terms of service โ€” check before scraping
  • Test with real websites to identify patterns and edge cases
  • Cost per task ($0.05-0.20) must be justified by the value of extracted data

KnowledgeCheck

  1. Why is Playwright preferred over Selenium for modern web browsing agents?

    • a) It's faster
    • b) It handles JavaScript rendering and SPAs better with auto-waiting
    • c) It's simpler
    • d) It's free
  2. What is the first step in DOM parsing for accurate extraction?

    • a) Extract links
    • b) Remove noise (scripts, styles, ads, nav, footer)
    • c) Parse tables
    • d) Extract text
  3. How does the agent detect it's stuck in a navigation loop?

    • a) It counts total steps
    • b) It detects repeated page titles across consecutive steps
    • c) It checks the URL
    • d) It monitors CPU usage
  4. What is the recommended delay between requests for ethical scraping?

    • a) 0ms (as fast as possible)
    • b) 1-2 seconds
    • c) 30 seconds
    • d) 5 minutes
  5. What is the purpose of content scoring in DOM parsing?

    • a) Rank search results
    • b) Find main content area by text density and structure
    • c) Count words
    • d) Measure page load time
  6. What should you check before scraping any website?

    • a) Page speed
    • b) Terms of service and robots.txt
    • c) Page rank
    • d) Number of visitors

Answers: 1-b, 2-b, 3-b, 4-b, 5-b, 6-b

Need Expert AI Agents Help?

Get personalized tutoring, project support, or professional consulting.

Advertisement