Skip to main content

graph-crawler

Web crawler that builds a graph of a site: pages are nodes, links are edges. You get a Graph object you can query, export, or feed into another tool.

Fetching goes through a chain of drivers with escalation: plain HTTP first, then TLS-fingerprint HTTP, then a real browser, then an "undetected" browser. The browser step accepts any engine (CloakBrowser, patchright, stock Playwright, or your own binary).

Install

pip install graph-crawler                 # HTTP only (aiohttp)
pip install "graph-crawler[browser]"      # + browser (stock Playwright)
pip install "graph-crawler[antidetect]"   # + TLS, patchright, CloakBrowser, nodriver
extra what it adds
browser driver="playwright" with bundled Chromium (playwright = alias)
tls curl_cffi — TLS fingerprint tier (driver="tls")
stealth patchright + playwright-stealth — engine "patchright"
cloak cloakbrowser — engine "cloakbrowser" (stealth in the binary)
undetected nodriver — last tier (driver="nodriver")
antidetect all four above
server FastAPI dashboard / REST / WebSocket (graph_crawler.api.server)
text tiktoken + stop-words — count_tokens, extract_keywords
markdown html2md-clean + markdown-it-py — MarkdownPlugin
mongodb, postgresql, celery, viz, articles, fuzzy, performance, system, all storage backends, distributed mode, extras

After installing a browser extra, run once:

playwright install chromium            # stock Playwright / patchright
python -m cloakbrowser install         # CloakBrowser binary (~200 MB)
# or point to an existing binary: export CLOAKBROWSER_BINARY=/opt/cloak/chrome

Python 3.11+.

Quickstart

import graph_crawler as gc

graph = gc.crawl("https://example.com", max_depth=2, max_pages=50)

print(len(graph.nodes), "pages")
for node in graph.nodes.values():
    print(node.depth, node.response_status, node.url)

hits = graph.search("pricing")
tree = graph.url_tree()
graph.export_nodes("nodes.json")
graph.export_edges("edges.json")

Async:

graph = await gc.async_crawl("https://example.com", driver="escalating")

Reuse one crawler for many runs (keeps the driver and storage open):

with gc.Crawler(driver="escalating", storage="sqlite", storage_config={"path": "site.db"}) as c:
    g1 = c.crawl("https://a.example")
    g2 = c.crawl("https://b.example")

Only some pages, with rules:

from graph_crawler import URLRule

graph = gc.crawl(
    "https://shop.example",
    url_rules=[URLRule(pattern=r"/product/", should_scan=True),
               URLRule(pattern=r"/cart", should_scan=False)],
)

Sitemap first:

graph = gc.crawl_sitemap("https://example.com/sitemap.xml", max_urls=500)

CLI:

graph-crawler crawl https://example.com --max-depth 2 --driver escalating --save example

Drivers

driver= accepts a name or a driver instance.

name tier needs notes
http / async http — aiohttp, default
tls / curl_cffi tls [tls] Chrome TLS fingerprint, no JS
playwright browser [playwright] real browser; engine chosen by browser_engine
nodriver undetected [undetected] CDP without webdriver, async
escalating all of them whatever is installed http → tls → browser → undetected

escalating builds tiers lazily: a browser is only started when a cheaper tier is blocked. Tiers whose dependency is missing are skipped.

Anti-detect: escalation and browser engines

How escalation decides

Every response goes through classify_block():

result when
none normal page
soft_block 429, plain 503, or 200 with an empty body
challenge Cloudflare "Just a moment", cf-mitigated: challenge, Incapsula, DDoS-Guard
hard_block 403 "Access Denied", Akamai _abck, DataDome headers
captcha PerimeterX, DataDome captcha, reCAPTCHA / hCaptcha / Turnstile markup

Anything other than none (and network errors) moves to the next tier.

The tier that worked is remembered per host, and the memory is adaptive:

  • it expires after GC_TIER_TTL seconds (default 1200 — about the lifetime of a cf_clearance cookie); after that the host is probed from the cheap tier again, so you do not stay on a browser once the protection is gone;
  • a block on the remembered tier drops the entry and escalates from the next tier;
  • only one request per host escalates at a time, the others wait for its result (no herd of browsers against one challenge);
  • the memory is per process. Workers do not share it.

Each response carries what happened in FetchResponse.extras:

resp.extras["bot_block_status"]  # "none" | "soft_block" | "challenge" | "hard_block" | "captcha"
resp.extras["channel"]           # "http" | "tls" | "browser" | "undetected"
resp.extras["driver"]            # "aiohttp" | "tls" | "playwright" | "nodriver"
resp.extras["engine"]            # "cloakbrowser" | "patchright" | "playwright" | None
resp.extras["escalations"]       # how many tiers were tried before this one

Using it

import graph_crawler as gc

# default chain, browser tier uses CloakBrowser if its binary is present
graph = gc.crawl("https://protected.example", driver="escalating")

# pick the browser engine, stop after the browser tier (0=http 1=tls 2=browser 3=undetected)
graph = gc.crawl(
    "https://protected.example",
    driver="escalating",
    driver_config={"browser_engine": "patchright", "max_tier": 2},
)

Directly as a driver:

from graph_crawler.infrastructure.transport.escalation import EscalatingDriver, EscalationPolicy

async with EscalatingDriver(policy=EscalationPolicy.default(browser_engine="cloakbrowser")) as d:
    resp = await d.fetch("https://protected.example")
    print(resp.status_code, resp.extras["channel"], resp.extras["engine"])

Your own chain:

from graph_crawler.infrastructure.transport import DriverType
from graph_crawler.infrastructure.transport.escalation import EscalationPolicy, EscalationTier

policy = EscalationPolicy(
    tiers=[
        EscalationTier(channel="tls", driver=DriverType.TLS, config={"impersonate": "chrome"}),
        EscalationTier(channel="browser", driver=DriverType.PLAYWRIGHT, engine="cloakbrowser"),
    ],
    remember_per_host=False,
)

Browser engines

The browser tier is stock PlaywrightDriver; the engine decides which Playwright module and which Chromium binary it launches.

engine what it is falls back to
cloakbrowser Chromium with fingerprint patches compiled into the binary patchright
patchright Playwright fork without automation leaks (Runtime.enable …) playwright
playwright stock —

Fallback is automatic (one INFO log line with the reason). A silent downgrade to a weaker stealth is dangerous for anti-detect profiles, so there is a strict mode: GC_ENGINE_STRICT=1 (or {"browser_engine": {"strict": True}}, driver_config={"engine_strict": True} for escalating) raises ImportError instead of falling back. Choose the engine:

gc.crawl(url, driver="playwright", driver_config={"browser_engine": {"type": "cloakbrowser"}})

or via env GC_BROWSER_ENGINE=cloakbrowser (applies when no engine is set in config).

Plug in your own binary or Playwright fork without touching the library:

from graph_crawler.infrastructure.transport.playwright.engine import register_engine

register_engine(
    "my-chrome",
    executable_resolver=lambda: "/opt/my-chrome/chrome",   # None → fall back
    launch_hook=lambda args: ([*args, "--my-flag"], {"ignore_default_args": ["--enable-automation"]}),
    fallback="playwright",
)

gc.crawl(url, driver="escalating", driver_config={"browser_engine": "my-chrome"})

playwright_module="patchright.async_api" can be passed to register_engine if your engine needs a different Playwright implementation.

Stealth lives in the engine (CloakBrowser / patchright). The library only decides when to escalate and which engine to launch.

Storage

storage="memory" (default), "json", "sqlite", "postgresql", "mongodb". Large crawls: use sqlite or a server backend — nodes are evicted from RAM and loaded back on access.

Tests

pip install -e ".[browser,tls,dev]"
pytest                       # offline, no browser needed
pytest -m manual tests/manual   # real sites, network required

Metadata

Release files for graph-crawler 5.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for graph-crawler 5.2.0
File Size Uploaded Provenance
graph_crawler-5.2.0.tar.gz 1.0 MB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for graph-crawler 5.2.0
File Interpreter ABI Platform
graph_crawler-5.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 2.3 MB

Release files / graph_crawler-5.2.0.tar.gz

Download URL graph_crawler-5.2.0.tar.gz
Size 1.0 MB
Tags Source
SHA-256 checksum
How to use checksums
983815c1eea9835cdc29624a814272018c62fd4c761cb699f2def5f3809c8322
BLAKE2b-256 checksum
How to use checksums
85dc812bd3c2590a30c82ea7af3ccb044d651a6cc3195f529b14204482d3e2e8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / graph_crawler-5.2.0-py3-none-any.whl

Download URL graph_crawler-5.2.0-py3-none-any.whl
Size 1.3 MB
Tags Python 3
SHA-256 checksum
How to use checksums
50fb8123492ec894d6ecd45314436af1af5c480d73ebdcb49be1e6b6b241476f
BLAKE2b-256 checksum
How to use checksums
dcc35a720cbaaeca0a389937a164e468cad867925f5e28a5e97b800aefb7128c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Security for graph-crawler 5.2.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.

Release history Release notifications | RSS feed

This release

5.2.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page