Configuration¶
scraper.ScraperConfig is a dataclass and every field has a working default. The docstrings on
it are the authoritative reference — this page is the map, and points at the source for values
so the two cannot disagree.
The shape is deliberate. What is configurable is the set of capabilities available and how patient the run is allowed to be. Which capability gets used, and when, is decided from evidence at runtime. The previous generation of this library had thirty-odd settings and most of them were levers on layers that were rarely the binding constraint — cipher rotation, header randomisation, per-request User-Agent choice. Tuning those is exactly the activity the bound says is wasted.
Two settings change what this library can do; the rest adjust how it does it.
from scraper import CdpSolver, ExitKind, ExitSpec, Scraper, ScraperConfig
config = ScraperConfig(
exits=[ExitSpec(url="http://user:pw@residential.test:8000", kind=ExitKind.RESIDENTIAL)],
browser=CdpSolver(),
)
scraper = Scraper(origin="https://example.com", config=config)
exits is the only thing that moves layer 1, because reputation is not something a client
emits. browser is the only thing that reaches the challenge layers. Everything else is
tuning.
CdpSolver() takes no path above because it finds a browser itself — inside macOS
application bundles, under the Windows program directories, and across PATH,
distribution and flatpak locations on Linux. Chromium-family builds count under six
brands, since Brave and Edge answer CDP exactly as Chrome does; BidiSolver() looks for
Firefox, ESR and LibreWolf included. Pass executable= to name one, and use
find_chromium() or find_firefox() to see everything installed.
mode= decides whether a window may appear. headless never shows one, which is the
setting for a server or a container; headed always does; auto starts hidden and opens a
window only once the unattended attempt has failed. Hiding costs nothing on its own — a
corrected headless browser clears every challenged host a headed one does — so a window is
worth only the person it brings, and auto asks for one only when the solver has already
run out of ideas. Both solvers accept the older headless= boolean, where False now means
auto.
One browser of each engine runs at a time, since exceeding what the platform allows fails in
a way that looks like the site refusing the request rather than a local limit. Firefox and
Chrome do not contend with each other; set_browser_slots(count, engine) raises either.
Transport¶
| Field | Notes |
|---|---|
impersonate |
curl-impersonate target. Keep the family alias — see below. |
prefer_http3 |
Offer HTTP/3 where the origin advertises it. Off by default. |
verify_tls |
A debugging aid, not a bypass. Nothing in the detection stack cares. |
transport |
Inject your own Transport. The seam tests use. |
Prefer "chrome" over "chrome136". A pinned profile ages into a signal on its own: no real
user runs a two-year-old browser, and the older profile predates the post-quantum key share
current builds all send, so a client claiming to be current Chrome without one contradicts its
own User-Agent. scraper.transport.stale_profile_warning checks at construction and logs if you
pinned something older than the installed build offers.
prefer_http3 is off by default because HTTP/3 through some proxies is worse than the mild
mismatch of never offering it.
Addresses¶
| Field | Notes |
|---|---|
exits |
ExitSpec / TorPoolSpec list. Sorted by kind; declare the kind honestly. |
max_sessions_per_exit |
Concurrent requests per address. Clamped to the low single digits. |
allow_rotation |
Whether a spent address may be replaced at all. |
retire_exit_for |
Seconds a blamed address stays out of the pool. |
ExitKind is not decoration — ExitKind.reach is what the planner consults before
recommending a rotation as a cure for layer 1. Claiming MOBILE for a datacenter range
does not change what the reputation database thinks; it only stops this library from
telling you that layer 1 is why nothing works. A kind other than DIRECT with no url
raises, since the address would be the local one either way.
See layers.md.
Behaviour¶
| Field | Notes |
|---|---|
pacing |
A PacingPolicy. Defaults live on that dataclass. |
remember |
Persist what is learned. On by default; see below. |
data_dir |
Where learned state and browser profiles live. |
remember=False is right for tests and one-off scripts and costs more than it looks like:
every run then rediscovers the binding layer with the same number of failed requests, and those
failures are themselves what the behavioural layer counts.
data_dir defaults to scraper.default_data_dir(), which honours SCRAPER_DATA_DIR first so a
deployment can place it on a volume. See behaviour.md.
Capabilities¶
| Field | Adds |
|---|---|
browser |
The clearance tier: layers 6, 7, 9, 10, 13. |
archive / archive_max_age |
The archive tier. Set a max age. |
managed |
The managed tier. A provider callable. |
botauth |
Signs every request. Layer 18, and the cheapest tier there is. |
tiers |
Rungs of your own. Each declares its cost and reach; see tiers.md. |
See tiers.md.
Patience¶
| Field | Notes |
|---|---|
max_attempts |
Attempts for one retrieval, across all tiers. |
max_rotations |
Addresses to spend on one retrieval. Deliberately small. |
promote_after |
Failures at a covered emit layer before re-attributing to the composite. |
solve_timeout |
How long a browser may work. |
retry_backoff |
Base seconds for the retry wait, doubled per attempt. |
max_retry_wait |
Ceiling on that wait. |
max_rotations being small is a design position, not caution: burning through a pool one
request at a time is the signature of a misdiagnosis, not of an unlucky exit.
retry_backoff only applies when the server named no delay. A Retry-After header always
wins, and 408, 502, 504 and the 52x family never send one — so those retries were
previously issued back-to-back against a site already in trouble.
Content safety¶
| Field | Notes |
|---|---|
guard_topic |
Watch for decoy content. On by default. |
on_decoy |
"warn" (default), "raise", or "ignore". |
"raise" is right for anything that trains on or republishes what it collects. See
decoy-content.md.
Request defaults¶
| Field | Notes |
|---|---|
timeout |
(connect, read) or a single number. |
parser |
BeautifulSoup feature name for PageSoup. |
raise_for_status |
Raise requests.HTTPError on a non-2xx that survived the ladder. |
raise_for_status only decides whether a 404 arrives as a return value or an exception; it
reaches you either way, because a 404 is the site's answer about a path and is never
attributed to a layer.
Extras¶
pip install lncrawl-scraper # the baseline: impersonated HTTP
pip install "lncrawl-scraper[cdp]" # the clearance tier
pip install "lncrawl-scraper[botauth]" # signed requests
pip install "lncrawl-scraper[image]" # get_image
pip install "lncrawl-scraper[all]"
Each extra adds reach rather than convenience. Impersonation is not an extra: layers 2–5 are one barrier and an ordinary Python client fails all four in the first round trip, so a build without it would not be a degraded scraper but one that cannot reach a protected page at all.