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

How to Scrape localStorage With Playwright and Scrapeless

Ava Wilson
Ava Wilson

Expert in Web Scraping Technologies

30-Jul-2026

A browser can hold useful page state outside the DOM. This guide uses Playwright to inspect localStorage on a public MDN demo, change three real preferences through the UI, capture the resulting storage events in a second tab, and prove the values survive a reload before moving the same logic to the Scrapeless Scraping Browser.

What Web Storage Adds to a Scraping Workflow

The Web Storage API gives an origin two string-keyed stores:

  • localStorage is shared by same-origin documents and survives ordinary page reloads and browser restarts when the profile is persistent.
  • sessionStorage is scoped to one top-level browsing context and lasts for that page session.

Neither store is a cookie jar. Web Storage values are not attached to HTTP requests automatically, and every key/value is a string. MDN's Web Storage API guide defines those scope and persistence boundaries.

For scraping, storage can contain UI preferences, cached identifiers, feature state, or public data the application has already loaded. Treat it as browser state to validate, not as a reason to collect authentication tokens or private user data.

Why Use Playwright With Scrapeless

Playwright can evaluate JavaScript in the page origin, open two pages in the same BrowserContext, and observe the browser's own storage event. The Scrapeless Scraping Browser provides a hosted Chromium session while keeping those Playwright APIs.

The browser connection uses connect_over_cdp. Once a page exists, storage inspection is the same whether Chromium is local or hosted.

Prerequisites

  • Python 3.10 or later.
  • playwright 1.59.0 or a current compatible version.
  • Local Chrome/Chromium for the complete runnable example.
  • A funded Scrapeless account and SCRAPELESS_API_KEY for the cloud connection. New browser sessions on the verification account returned code 14500, so the connection block is an explicit prerequisite gap.

Install

bash Copy
python -m pip install "playwright==1.59.0"

Connect to the Scrapeless Scraping Browser

Note: This block requires funded Scraping Browser balance. The final verification account returned insufficient balance, please recharge first. Run it with your own funded key; the complete storage workflow below ran in local Chrome against the same public page.

python Copy
import os
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright

params = urlencode({
    "token": os.environ["SCRAPELESS_API_KEY"],
    "sessionTTL": 120,
    "proxyCountry": "US",
})
cdp_url = f"wss://browser.scrapeless.com/api/v2/browser?{params}"

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(cdp_url)
    context = browser.contexts[0]
    page = context.pages[0]
    # Run the storage steps below with context and page.
    browser.close()

Use the Scrapeless developer docs for current connection parameters.

Inspect Every localStorage Key

MDN's public demo stores a background color, font, and image choice:

python Copy
TARGET_URL = "https://mdn.github.io/dom-examples/web-storage/"

page.goto(TARGET_URL, wait_until="domcontentloaded")
stored = page.evaluate(
    "Object.fromEntries(Object.entries(localStorage))"
)
print("initial storage:", stored)
print("session storage:", page.evaluate("sessionStorage.length"))
text Copy
initial storage: {'font': 'sans-serif', 'image': 'images/firefoxos.png', 'bgcolor': 'FF0000'}
session storage: 0

Reading all entries avoids tying the extractor to one key. Keep the origin in the output because the same key name can mean something different on another site.

Capture Changes in a Second Tab

The storage event fires in other same-origin documents that share the changed store. It does not fire in the tab that made the change.

python Copy
observer_page = context.new_page()
observer_page.goto(TARGET_URL, wait_until="domcontentloaded")
observer_page.evaluate("""
    window.capturedStorageEvents = [];
    addEventListener('storage', event => {
        capturedStorageEvents.push({
            key: event.key,
            oldValue: event.oldValue,
            newValue: event.newValue,
        });
    });
""")

Both pages must share one browser context and origin. A second context has separate storage and will not receive these events.

Change Real UI Controls

Change the demo's controls rather than writing keys directly:

python Copy
page.evaluate("""
    bgcolor.value = '00FF00';
    bgcolor.dispatchEvent(new Event('change', {bubbles: true}));
""")
page.select_option("#font", "serif")
page.select_option("#image", "images/crocodile.png")
page.wait_for_timeout(500)

print("updated storage:", page.evaluate(
    "Object.fromEntries(Object.entries(localStorage))"
))
print("events:", observer_page.evaluate("capturedStorageEvents"))
text Copy
updated storage: {'font': 'serif', 'image': 'images/crocodile.png', 'bgcolor': '00FF00'}
events: [{'key': 'bgcolor', 'oldValue': 'FF0000', 'newValue': '00FF00'}, {'key': 'font', 'oldValue': 'sans-serif', 'newValue': 'serif'}, {'key': 'image', 'oldValue': 'images/firefoxos.png', 'newValue': 'images/crocodile.png'}]

That output proves three things at once: the UI wrote storage, the second tab received exactly three changes, and each event preserved its previous value.

Ready to inspect state in a hosted session? Create a free Scrapeless account and replace only browser creation.

Prove the Values Survive Reload

python Copy
page.reload(wait_until="domcontentloaded")
reloaded = page.evaluate(
    "Object.fromEntries(Object.entries(localStorage))"
)
controls = {
    "bgcolor": page.locator("#bgcolor").input_value(),
    "font": page.locator("#font").input_value(),
    "image": page.locator("#image").input_value(),
}

print("reloaded storage:", reloaded)
print("control values:", controls)
text Copy
reloaded storage: {'font': 'serif', 'image': 'images/crocodile.png', 'bgcolor': '00FF00'}
control values: {'bgcolor': '00FF00', 'font': 'serif', 'image': 'images/crocodile.png'}

Matching the visible controls with stored state is stronger than checking storage alone. It proves the application consumed the saved values after navigation.

Complete Runnable Script

python Copy
from playwright.sync_api import sync_playwright

TARGET_URL = "https://mdn.github.io/dom-examples/web-storage/"

with sync_playwright() as p:
    browser = p.chromium.launch(
        executable_path="/usr/bin/google-chrome",
        headless=True,
    )
    context = browser.new_context()
    page = context.new_page()
    observer_page = context.new_page()

    observer_page.goto(TARGET_URL, wait_until="domcontentloaded")
    observer_page.evaluate("""
        window.capturedStorageEvents = [];
        addEventListener('storage', event => {
            capturedStorageEvents.push({
                key: event.key,
                oldValue: event.oldValue,
                newValue: event.newValue,
            });
        });
    """)
    page.goto(TARGET_URL, wait_until="domcontentloaded")

    initial = page.evaluate(
        "Object.fromEntries(Object.entries(localStorage))"
    )
    page.evaluate("""
        bgcolor.value = '00FF00';
        bgcolor.dispatchEvent(new Event('change', {bubbles: true}));
    """)
    page.select_option("#font", "serif")
    page.select_option("#image", "images/crocodile.png")
    page.wait_for_timeout(500)

    updated = page.evaluate(
        "Object.fromEntries(Object.entries(localStorage))"
    )
    events = observer_page.evaluate("capturedStorageEvents")
    page.reload(wait_until="domcontentloaded")
    reloaded = page.evaluate(
        "Object.fromEntries(Object.entries(localStorage))"
    )
    controls = {
        "bgcolor": page.locator("#bgcolor").input_value(),
        "font": page.locator("#font").input_value(),
        "image": page.locator("#image").input_value(),
    }

    assert len(events) == 3
    assert updated == reloaded == controls
    assert page.evaluate("sessionStorage.length") == 0

    print("title:", page.title())
    print("initial storage:", initial)
    print("updated storage:", updated)
    print("event keys:", [event["key"] for event in events])
    print("event count:", len(events))
    print("reloaded storage:", reloaded)
    print("control values:", controls)
    print("session storage entries:", page.evaluate("sessionStorage.length"))
    browser.close()

The run returns three initial keys, three updated keys, three cross-tab events, matching reloaded controls, and zero sessionStorage entries.

localStorage, sessionStorage, and Cookies

localStorage

Shared by same-origin documents, string-only, and persistent until cleared. Use it when the application deliberately keeps state across visits.

sessionStorage

Separate for each top-level page session. Duplicating or opening a tab may create a distinct lifecycle even on the same origin. Inspect it from the exact page whose state matters.

Cookies

Sent with matching HTTP requests according to cookie rules. Use Playwright's context cookie APIs rather than confusing them with Web Storage.

Safe Extraction Boundaries

Storage may contain credentials, user identifiers, or private state. Restrict examples to public demo pages, collect only fields required for the stated task, and never publish tokens or session material. Origin access through a browser does not grant permission to reuse every value you can read.

Conclusion

Web Storage extraction is useful when the browser has state that the visible DOM alone cannot explain. Read the complete map, keep the origin with the record, observe cross-tab changes when you need provenance, and verify that the application consumes stored values after navigation. Moving the workflow to Scrapeless changes the browser connection, not the storage logic.

Start with Scrapeless, review Scrapeless pricing, and read what CDP exposes before moving stateful sessions to the cloud.

FAQ

Q: Can Playwright read localStorage?

Yes. Evaluate Object.fromEntries(Object.entries(localStorage)) inside a page on the target origin.

Q: Why not use context.storage_state() for everything?

Storage state is useful for exporting supported authentication state, but direct page evaluation lets you inspect the live key map and correlate it with UI changes and storage events.

Q: Does localStorage survive a reload?

Yes, unless the application or browser clears it. The MDN demo's three changed values survived the tested reload and repopulated the controls.

Q: Does the tab that changes localStorage receive a storage event?

No. The event is delivered to other same-origin documents that share the store, which is why the example listens in a second page.

Q: Is sessionStorage shared across tabs?

No. It is scoped to a top-level browsing context and its same-origin child contexts, so inspect it from the specific page session.

Q: Are Web Storage values always JSON?

No. Every value is stored as a string. Parse JSON only when the application's format is confirmed, and handle invalid JSON explicitly.

Q: Is it safe to scrape browser storage?

Only within the target's permissions and your data-handling obligations. Avoid credentials and private user state, keep collection bounded, and review the site's terms and robots policy where applicable.

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