jsEasy
jsEasy (jseasy) is a lightweight Python runtime for scraping and testing HTML pages that need JavaScript execution, but not a full browser.
It gives Python code a browser-like DOM, executes page scripts with QuickJS, supports common network APIs such as fetch() and XMLHttpRequest, and returns the final DOM for extraction.
requests + BeautifulSoup
< jsEasy
< Playwright / Selenium / real browsers
jsEasy is designed for the middle ground: pages where JavaScript mutates the DOM, loads JSON, runs timers, or executes small modules, but where layout, pixels, GPU APIs, and browser fingerprint parity are unnecessary.
Highlights
- No browser process: no Chromium download, no WebDriver, no browser startup cost.
- Python-first API:
Page.open(),Page.from_html(),select(),select_all(),eval(),html(). - Browser-like runtime: DOM, events, timers, storage, history, location, CSSOM,
fetch, XHR, module scripts. - Scraper-friendly behavior: failed third-party scripts are collected in diagnostics instead of aborting the whole page.
- Typed package: ships
py.typed. - PyPI-ready: wheel/sdist build, docs, examples, tests, and release checklist.
Installation
pip install jseasy
Python 3.10+ is supported.
Quick Start
from jseasy import Page
page = Page.from_html("""
<main id="app"></main>
<script>
const title = document.createElement("h1");
title.textContent = "Loaded without Chrome";
document.querySelector("#app").appendChild(title);
</script>
""")
print(page.select("#app h1").text)
Output:
Loaded without Chrome
Loading A Real Page
from jseasy import Page
with Page.open("https://example.com") as page:
print(page.select("h1").text)
print(len(page.html()))
Page.open() fetches the URL, parses HTML, loads stylesheets, executes scripts, drains pending work, and gives you the current DOM.
Fetch And XHR
import httpx
from jseasy import Page
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(200, json={"name": "Ada"})
client = httpx.Client(transport=httpx.MockTransport(handler))
page = Page.from_html(
"""
<div id="name"></div>
<script>
fetch("/api")
.then((response) => response.json())
.then((data) => {
document.querySelector("#name").textContent = data.name;
});
</script>
""",
url="https://example.test",
client=client,
)
print(page.select("#name").text)
Module Scripts
import httpx
from jseasy import Page
def handler(request: httpx.Request) -> httpx.Response:
if request.url.path == "/app.js":
return httpx.Response(
200,
text="""
import { label } from "./labels.js";
document.querySelector("#app").textContent = label;
""",
)
return httpx.Response(200, text='export const label = "module loaded";')
client = httpx.Client(transport=httpx.MockTransport(handler))
page = Page.from_html(
'<div id="app"></div><script type="module" src="/app.js"></script>',
url="https://example.test",
client=client,
)
print(page.select("#app").text)
Diagnostics
jsEasy is intentionally tolerant by default. A tracking script, analytics widget, or unsupported browser feature should not necessarily prevent scraping the rest of the DOM.
with Page.open("https://example.com") as page:
print(page.logs) # console.log/warn/error output
print(page.script_errors) # script exceptions collected during load
print(page.resource_errors) # stylesheet/resource failures
Set raise_script_errors=True during development when you want the first script failure to raise immediately.
page = Page.from_html(html, raise_script_errors=True)
Browser API Coverage
jsEasy implements a pragmatic subset of browser APIs:
| Area | Supported |
|---|---|
| DOM | Document, Element, Node, Text, DocumentFragment |
| Selection | querySelector, querySelectorAll, matches, closest |
| Mutation | appendChild, removeChild, insertBefore, innerHTML, textContent, basic MutationObserver |
| Events | Event, CustomEvent, MouseEvent, KeyboardEvent, addEventListener, dispatchEvent |
| Runtime | setTimeout, setInterval, requestAnimationFrame, Promise draining, performance.now |
| Network | fetch, XMLHttpRequest, Request, Response, Headers, navigator.sendBeacon |
| State | localStorage, sessionStorage, document.cookie, history, location |
| CSSOM | document.styleSheets, CSSStyleSheet, CSSStyleRule, CSSStyleDeclaration, getComputedStyle |
| Modules | classic scripts, type="module", simple static local imports |
| Utilities | atob, btoa, console |
See docs/api.md for details.
When To Use jsEasy
Use jsEasy when:
- you need DOM extraction after simple or moderate JavaScript execution;
- content is loaded via
fetch()or XHR; - scripts manipulate the DOM but do not require layout;
- you want fast startup in CLI jobs, tests, CI, workers, or small containers;
- Playwright/Selenium feels too heavy for the target page.
Use a real browser when:
- the site depends on layout metrics, canvas, WebGL, media, Shadow DOM, or complex framework hydration;
- the target is fingerprint-sensitive;
- you need browser DevTools, screenshots, or user interaction fidelity;
- the page presents CAPTCHAs, payment walls, login walls, or other access controls.
Security And Ethics
jsEasy is intended for legitimate scraping, testing, research, and data extraction where you are allowed to access the content. Respect site terms, robots policies, rate limits, copyright, privacy, and access controls.
Do not use jsEasy to bypass CAPTCHAs, authentication, payment walls, or other security mechanisms. For security challenge pages, use a human-in-the-loop checkpoint.
Examples
See examples/:
Documentation
Development
python -m venv .venv
. .venv/bin/activate
pip install -e ".[dev]"
pytest
python -m build
twine check dist/*
Status
Alpha. jsEasy is useful today for small to medium JavaScript-enhanced scraping workflows, but the API may change before 1.0.
License
MIT
Metadata
Release files for jseasy 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| jseasy-0.1.0.tar.gz | 24.6 kB | Without provenance | Details for jseasy-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| jseasy-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for jseasy-0.1.0-py3-none-any.whl |
Total release size: 41.7 kB
Release files / jseasy-0.1.0.tar.gz
| Download URL | jseasy-0.1.0.tar.gz |
|---|---|
| Size | 24.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b14f0d7369c586e92dde768675d8a63d29fdf9641850185d8947d51fc25fa0dd
|
|
BLAKE2b-256 checksum How to use checksums |
6891a0877760bdf2738b59b3825750e4bef0f84280533682d576b85f21c30428
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.2
|
Release files / jseasy-0.1.0-py3-none-any.whl
| Download URL | jseasy-0.1.0-py3-none-any.whl |
|---|---|
| Size | 17.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2cb6d5293e6ce6fc5fcf1e0a00ebe8723cca30066bb665e678b3fbc104021f9c
|
|
BLAKE2b-256 checksum How to use checksums |
ce1a4e2f6aadc5d179b0db7efc70aee91d3ece6f6463faabec8d4fc362fbb2f5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.2
|
Security for jseasy 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.