Techniques 12 min read

Headless Browser Scraper in Python: Complete Guide

Build a headless browser scraper in Python and deploy it on PythonAnywhere: setup, working code, and anti-block tips. Follow along and start scraping.

ST
Scraping.Pro Team
Data collection for business needs
Published: 31 December 2025

Headless Browser Scraper in Python: Complete Guide

When a page hands you its data in the raw HTML, a plain HTTP request and a parser are all you need. But a growing share of the web renders in the browser: React, Vue, and Angular front ends fetch content over AJAX after load, prices hide behind infinite scroll, and anti-bot layers actively check whether the client can run JavaScript. For those sites you have to drive a real browser — with no window on screen. That's a headless browser scraper: a full browser engine, running without a graphical interface, controlled entirely from Python.

This guide builds one from scratch, compares the tools worth using in 2026, walks through deploying it on a platform like PythonAnywhere, and covers the anti-block measures that decide whether it keeps working past the first hundred requests.

What "headless" means now

A headless browser is a normal browser — Chrome, Firefox, WebKit — with its GUI switched off. It still parses HTML, runs JavaScript, applies CSS, builds the DOM, and fires events; it just draws to an in-memory surface instead of a screen. Your code navigates, waits for elements, clicks, and reads the rendered DOM exactly as a user's browser would.

This is worth flagging because the old way of doing it is dead. Years ago, "headless" meant tricking a full browser into running without a display: you'd start a virtual X server (Xvfb) and let Firefox draw into that invisible framebuffer via a wrapper like pyvirtualdisplay. It worked, but it was fragile and heavy. Modern Chrome and Firefox ship a native headless mode — no Xvfb, no framebuffer juggling. You pass a flag (--headless=new for Chrome) and the browser runs headless directly. Xvfb still has one honest use today — watching a headed browser over VNC while you debug in a container — but you no longer need it just to scrape.

When you actually need a browser

A browser is the most expensive tool in the scraping kit — hundreds of megabytes of RAM per instance and far slower than an HTTP request. Only reach for headless browser web scraping when you genuinely need it:

  • content appears in the DOM only after JavaScript runs (SPAs, lazy loading, client-side rendering);
  • you need to interact: click, scroll, fill forms, step through multi-page flows;
  • the site checks JS execution, browser fingerprint, or behavior to filter bots;
  • you need a screenshot or PDF of the rendered page.

If the data sits in the raw HTML — or arrives as JSON through an internal API you can call directly — skip the browser. An honest HTTP request with httpx or requests will be ten to fifty times faster and far more stable. Check the page source and the Network tab before committing to headless.

The tools worth using

Tool Engine(s) Best for Notes
Playwright Chromium, Firefox, WebKit New projects, reliability Auto-waiting, one API for 3 engines, async-native
Selenium 4 Chrome, Firefox, Edge, Safari Cross-language teams, existing suites Selenium Manager auto-installs drivers
Pyppeteer Chromium Legacy CDP code Largely superseded by Playwright
Splash WebKit (older) Lightweight render service Aging; less capable than modern engines

For a new Python headless browser scraper in 2026, Playwright is the default recommendation. It auto-waits for elements (killing most flaky-timing bugs), drives Chromium, Firefox, and WebKit through one API, and installs its own browser binaries. Selenium 4 remains an excellent choice — especially if your team already uses it or works across languages — and Selenium Manager now resolves the right driver automatically, so the old ChromeDriver-version headaches are gone.

Building the scraper: Playwright

Install the package and its browser binaries:

bash
pip install playwright
playwright install chromium

A complete headless scrape — launch, navigate, wait for real content, extract:

python
from playwright.sync_api import sync_playwright

def scrape(url):
    with sync_playwright() as p:
        browser = p.chromium.launch(
            headless=True,
            args=["--no-sandbox", "--disable-dev-shm-usage"],
        )
        page = browser.new_page(
            user_agent=("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                        "AppleWebKit/537.36 (KHTML, like Gecko) "
                        "Chrome/124.0 Safari/537.36")
        )
        page.goto(url, wait_until="domcontentloaded")

        # wait for the JS-rendered element, not a blind sleep
        page.wait_for_selector(".product-card", timeout=15000)

        cards = page.query_selector_all(".product-card")
        results = []
        for card in cards:
            title = card.query_selector("h2")
            price = card.query_selector(".price")
            results.append({
                "title": title.inner_text().strip() if title else None,
                "price": price.inner_text().strip() if price else None,
            })

        browser.close()
        return results

if __name__ == "__main__":
    for row in scrape("https://example.com/catalog"):
        print(row)

The key habit is wait_for_selector — wait for the specific element you need rather than a fixed sleep(). Fixed sleeps are either too short (you scrape an empty page) or wastefully long. Playwright also exposes page.wait_for_load_state("networkidle") for pages that trickle content in.

Building the scraper: Selenium 4

If you prefer Selenium, the modern setup is much cleaner than the Firefox-plus-Xvfb era. No manual driver download — Selenium Manager handles it:

bash
pip install selenium beautifulsoup4
python
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from bs4 import BeautifulSoup

options = Options()
options.add_argument("--headless=new")          # native headless, no Xvfb
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")  # critical inside containers
options.add_argument("--window-size=1920,1080")

driver = webdriver.Chrome(options=options)       # driver auto-resolved
try:
    driver.get("https://example.com/catalog")
    WebDriverWait(driver, 15).until(
        EC.presence_of_element_located((By.CSS_SELECTOR, ".product-card"))
    )
    soup = BeautifulSoup(driver.page_source, "html.parser")
    for card in soup.select(".product-card"):
        title = card.select_one("h2")
        price = card.select_one(".price")
        print(title.get_text(strip=True) if title else None,
              price.get_text(strip=True) if price else None)
finally:
    driver.quit()   # always in finally, or the process lingers

Two details that matter on a server: --headless=new is Chrome's current headless implementation (the earlier one was more detectable and less faithful), and driver.quit() belongs in a finally block — a browser process that isn't closed keeps running and eating memory, and on a busy scraper these orphans pile up fast. If you want to render with Selenium but parse with something you already know, feeding driver.page_source into BeautifulSoup, as above, is a common and comfortable pattern; see our Selenium web scraping walkthrough for more.

Deploying on PythonAnywhere (and why some hosts fight you)

The original motivation for this article was hosting a scraper on PythonAnywhere — and it's an instructive case, because managed Python hosts often restrict exactly what a headless browser needs. Historically, WebKit-based tools failed there because the platform's virtualization didn't support them, which is why people fell back to Firefox inside Xvfb.

The lesson generalizes. On a sandboxed PaaS you'll typically hit:

  • No system browser or no permission to install one. You may not be able to apt install chromium. Playwright's playwright install downloads browsers into your home directory, which sidesteps this on some hosts — but not all allow the launch.
  • The sandbox itself. Chrome's sandbox needs kernel features containers restrict, hence --no-sandbox. Skipping the sandbox is acceptable for scraping in an already-isolated environment.
  • /dev/shm too small. Chrome uses shared memory and crashes when it fills; --disable-dev-shm-usage routes it to disk and fixes the classic "tab crashed" error in constrained environments.
  • Memory ceilings and no long-running processes. Free and low tiers cap RAM and may forbid always-on tasks. A single Chromium can exceed a small memory allowance on its own.
  • Outbound network allow-lists. Some sandboxes only permit HTTP(S) to whitelisted domains — fine for their proxy but a wall for scraping arbitrary sites.

Practical guidance: for anything beyond light use, deploy the scraper on a VPS or a Docker container you control, where you can install the browser, set launch flags, and manage the process lifecycle. Docker is the cleanest option — Playwright and Selenium both publish images with the browsers baked in, and a fresh container per batch means orphaned browser processes have nowhere to accumulate. Reserve restricted PaaS hosts for scheduling and orchestration, not for running the browser itself.

The best headless browser API for web scraping — when to offload

Running headless browsers at scale is its own operations problem: memory leaks over long sessions, zombie processes after crashes, blocks, and CAPTCHAs. That's why a whole category of hosted headless browser APIs for web scraping exists — you send a URL (and optional JS/wait instructions) to an endpoint, and it returns the rendered HTML or a screenshot from a managed, fingerprinted, proxy-backed browser pool.

The trade-off is straightforward:

  • Self-hosted (Playwright/Selenium on your box): full control, no per-request fee, but you own the infrastructure, the anti-bot arms race, and the on-call pager.
  • Hosted browser/scraping API: rendering, proxy rotation, and often CAPTCHA handling are somebody else's problem, billed per request. Faster to ship, pricier at volume, less low-level control.

A pragmatic split: prototype and run modest jobs on self-hosted Playwright; move to a hosted API — or a fully managed data extraction service — when block rates climb or you don't want to babysit a browser farm. There's no single "best" API; the right one depends on your target sites, volume, and how much of the anti-bot fight you want to outsource.

Anti-block tips

A headless browser gets you rendered HTML; staying unblocked is a separate discipline. The essentials:

  • Set a realistic User-Agent and viewport. Default automation UAs and tiny windows are giveaways. Match a current desktop Chrome, as in the examples above.
  • Rotate IPs. The single biggest factor at volume. Route through rotating proxies — residential or mobile for the toughest targets — so requests don't all originate from one datacenter address.
  • Throttle and randomize. Add jittered delays between actions and requests; human traffic isn't metronomic. Respect the site and don't hammer it.
  • Reduce your automation fingerprint. Headless Chrome leaks tells (navigator.webdriver, missing plugins, odd WebGL). Stealth plugins and undetected-chromedriver patch the obvious ones, though determined anti-bot vendors keep pace.
  • Handle CAPTCHAs. When a challenge appears, integrate a CAPTCHA solving service rather than giving up the session.
  • Block unneeded resources. Skipping images, fonts, and analytics (page.route in Playwright) cuts memory and speeds every page — useful density on a constrained host.

For the full playbook, our guides on anti-scraping protection and how sites detect scraping cover what you're up against.

FAQ

What is a headless browser scraper? A scraper that drives a full browser engine (Chrome, Firefox, WebKit) with its GUI disabled, so it can render JavaScript and interact with a page while running on a server with no screen. In Python, Playwright and Selenium are the common tools.

Do I still need Xvfb for headless scraping? No. Modern Chrome and Firefox have native headless modes (--headless=new), so Xvfb is no longer required to run without a display. Its only real use now is watching a headed browser over VNC while debugging in a container.

Playwright or Selenium for headless browser automation in Python? Playwright for new projects — auto-waiting, three engines behind one API, and self-installing browsers make it the most reliable default. Selenium 4 is the pick if you have an existing suite or need cross-language parity; Selenium Manager now handles drivers automatically.

Can I run a headless browser on PythonAnywhere or similar hosts? Sometimes, but sandbox limits (no system browser, small /dev/shm, memory caps, no long-running processes) make it fragile. For anything beyond light use, deploy on a VPS or a Docker container you control, and keep the PaaS for scheduling.

Why does my headless Chrome crash on a server? Usually shared-memory exhaustion. Add --disable-dev-shm-usage and --no-sandbox, give the container enough RAM, and always close()/quit() the browser so processes don't leak.

Wrapping up

A headless browser scraper is the right tool when — and only when — a page needs JavaScript to reveal its data. Build it with Playwright (or Selenium 4), wait on real elements instead of blind sleeps, and always close the browser. Deploy where you control the environment: a Docker container or VPS beats a restrictive PaaS for running the browser itself. And treat staying unblocked as its own project — proxies, throttling, fingerprint hygiene, CAPTCHA handling. When the browser farm and the anti-bot arms race stop being a good use of your time, scraping.pro runs JavaScript-heavy scraping as a managed data extraction service: you get clean, structured data and we own the headless infrastructure.