Plenty of modern sites keep data you care about in the browser's local storage rather than in the HTML — auth tokens, feature flags, cached API responses, cart contents, user preferences. Because that data never appears in the page source, a plain requests.get() will never see it. So the question comes up a lot: how to scrape local storage with Python?
There are two honest answers, and this guide covers both. The clean, supported way is to drive a real browser (Selenium or Playwright) and read localStorage through JavaScript. The other way — when you already have a Chrome/Chromium profile on disk and want the data offline — is to read the underlying LevelDB files directly. We will walk through working code for each.
What local storage actually is
localStorage is part of the Web Storage API. A site stores string key/value pairs on your machine that persist across sessions (unlike sessionStorage, which clears when the tab closes, and unlike cookies, which are sent to the server on every request).
Three properties matter for scraping:
- It is client-side only. As the spec puts it, web storage "falls exclusively under the purview of client-side scripting." The server never receives it automatically, so you cannot fetch it with an HTTP request — you have to be in the browser context, or read the files the browser wrote.
- It is scoped per origin.
https://example.comandhttps://sub.example.comhave separate stores. - Values are strings. Objects are usually stored as JSON, so you will often
json.loads()what you read back.
That first point is the whole reason an HTTP client cannot help: requests, httpx, and urllib download bytes; they do not run JavaScript and they do not maintain a DOM with a window.localStorage object. To read it live you need a JavaScript engine — i.e., a browser. (This is also why the old advice to use PhantomJS is obsolete: PhantomJS has been unmaintained since 2018. Use Playwright or Selenium instead.)
Approach 1: Read local storage with Selenium
Selenium drives a real browser and lets you run arbitrary JavaScript in the page via execute_script(). The simplest, most reliable pattern is to run a tiny JS snippet that dumps every entry.
# pip install selenium (Selenium 4.6+ auto-manages drivers)
from selenium import webdriver
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
driver = webdriver.Chrome(options=options)
driver.get("https://example.com/")
# Dump the entire localStorage as a Python dict:
storage = driver.execute_script(
"return Object.fromEntries(Object.entries(window.localStorage));"
)
for key, value in storage.items():
print(key, "=>", value)
driver.quit()
Object.fromEntries(Object.entries(localStorage)) hands you back a clean object that Selenium deserializes into a Python dict — much tidier than the old for-loop-with-localStorage.key(i) approach, though that still works if you prefer it:
storage = driver.execute_script("""
const out = {};
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i);
out[k] = localStorage.getItem(k);
}
return out;
""")
You can also read, write, or clear individual keys:
token = driver.execute_script("return window.localStorage.getItem('auth_token');")
driver.execute_script("window.localStorage.setItem('lang', 'en');")
driver.execute_script("window.localStorage.removeItem('lang');")
If a value is JSON, parse it in Python:
import json
raw = driver.execute_script("return window.localStorage.getItem('cart');")
cart = json.loads(raw) if raw else None
Important: local storage is populated by the page's JavaScript after load. Don't read it the instant get() returns — wait for the app to initialize (a WebDriverWait for a known element, or for the key itself to appear):
from selenium.webdriver.support.ui import WebDriverWait
WebDriverWait(driver, 15).until(
lambda d: d.execute_script("return window.localStorage.getItem('auth_token');")
)
For login flows, headless setup, and anti-bot notes, see our full Selenium web scraping guide.
Approach 2: Read local storage with Playwright (recommended)
Playwright is the modern default — faster startup, better waiting, and a built-in way to export storage. Reading localStorage is a one-liner via evaluate():
# pip install playwright && playwright install chromium
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://example.com/", wait_until="networkidle")
storage = page.evaluate(
"() => Object.fromEntries(Object.entries(window.localStorage))"
)
print(storage)
browser.close()
Playwright also exposes the entire browser state — cookies and per-origin localStorage — through storage_state(), which is perfect when you want to snapshot everything or reuse a logged-in session later:
context = browser.new_context()
page = context.new_page()
page.goto("https://example.com/")
page.wait_for_load_state("networkidle")
state = context.storage_state() # dict with "cookies" and "origins"
for origin in state["origins"]:
print(origin["origin"])
for item in origin["localStorage"]:
print(" ", item["name"], "=>", item["value"])
# Or persist it and restore later:
context.storage_state(path="state.json")
# new_context(storage_state="state.json") to reload cookies + localStorage
That storage_state round-trip is the cleanest way to grab a token once, save it, and skip logging in on every run.
Approach 3: Read local storage straight from LevelDB files
Sometimes you don't want to launch a browser at all — you already have a Chrome/Chromium/Edge profile on disk (yours, or one your automation created) and just want the stored values offline. Chromium keeps localStorage in a LevelDB database inside the profile folder:
- Windows:
%LocalAppData%\Google\Chrome\User Data\Default\Local Storage\leveldb\ - macOS:
~/Library/Application Support/Google/Chrome/Default/Local Storage/leveldb/ - Linux:
~/.config/google-chrome/Default/Local Storage/leveldb/
(Firefox is different — it stores DOM storage in a SQLite file, webappsstore.sqlite, plus per-site ls folders; you can read those with the sqlite3 module.)
You can read the Chromium LevelDB with the plyvel binding:
# pip install plyvel (needs the LevelDB C library installed)
import plyvel
path = "/home/user/.config/google-chrome/Default/Local Storage/leveldb"
db = plyvel.DB(path, create_if_missing=False)
for key, value in db:
# Keys look like: b"_https://example.com\x00\x01theKey"
# A leading 0x01 byte on the value marks UTF-16; 0x00 marks Latin-1.
if key.startswith(b"_https://example.com"):
try:
k = key.split(b"\x00", 1)[1].lstrip(b"\x01")
v = value.lstrip(b"\x00\x01")
print(k.decode("utf-8", "replace"), "=>",
v.decode("utf-16-le", "replace"))
except Exception as e:
print("skip", e)
db.close()
Two things to know before you rely on this:
- Lock the profile out first. LevelDB does not allow two writers. If Chrome is running against that profile,
plyvel.DB()will fail to open it — close the browser, or copy theleveldbfolder somewhere and open the copy. - The encoding is fiddly. Keys are prefixed with the origin and a separator byte; values carry a one-byte marker indicating Latin-1 (
0x00) or UTF-16LE (0x01). The snippet above handles the common cases, but for a robust parser consider a dedicated tool such as the communityccl_chromium_reader/dumpzilla-style scripts used in digital forensics, which decode these records correctly.
Reading files directly is powerful but brittle across Chrome versions. If you can afford to launch a browser, Approach 1 or 2 is more portable.
Which approach should you use?
| Scenario | Best approach |
|---|---|
| Scraping a live site as part of a crawl | Playwright evaluate / storage_state |
| Existing Selenium project | Selenium execute_script |
| Reuse a logged-in session across runs | Playwright storage_state(path=...) |
| Data already sits in a Chrome profile on disk | Read LevelDB with plyvel (browser closed) |
| Firefox profile on disk | Read webappsstore.sqlite with sqlite3 |
For almost every scraping task, drive the browser — it is version-independent and handles the encoding for you. Save the LevelDB route for offline/forensic situations where launching Chrome is not an option.
Gotchas and good practice
- Wait for population. The values you want appear after the app's JavaScript runs. Wait for the key or a dependent element before reading.
sessionStorageworks the same way. SwaplocalStorageforsessionStoragein any of the JS snippets — but remember it dies with the tab.- Tokens are sensitive. Auth tokens pulled from local storage grant access; store and transmit them carefully, and honor the target's Terms of Service and privacy law before harvesting anything tied to a person.
- Don't fight the storage — find the source. Local storage is often a cache of an API response. Check the DevTools Network tab: if you can call that underlying JSON API directly, you may not need the browser at all.
FAQ
Can I scrape local storage without a browser? Only by reading the browser's LevelDB/SQLite files on disk, and only when the browser isn't holding them open. To read it from a live site you need a JavaScript context — Selenium or Playwright.
Why can't requests read localStorage?
requests performs an HTTP request and returns the raw body. It never executes JavaScript and has no window object, so localStorage simply doesn't exist in that context. See scraping dynamic content.
Where does Chrome store localStorage?
In a LevelDB database at .../User Data/Default/Local Storage/leveldb/ within the profile directory. Values are keyed by origin with an encoding marker byte.
Playwright or Selenium for this?
Playwright — its evaluate() and especially storage_state() make reading and persisting storage cleaner. Selenium's execute_script() is perfectly fine if that's your existing stack.
Bottom line
Scraping local storage comes down to getting into the right context. The robust, portable way is to run the page in Selenium or Playwright and read window.localStorage through JavaScript — Playwright's storage_state() even lets you snapshot cookies and storage together and replay a session. When you instead have a browser profile sitting on disk, you can decode the LevelDB records with plyvel, mindful of the file lock and the UTF-16 markers.
If wrangling headless browsers, sessions, and rotating proxies isn't how you want to spend your week, scraping.pro runs this as a managed web scraping service and hands back clean, structured data.