You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: 0.3.0 - typed result objects, list/nested extraction, pooled browser
Picks up the three deferred capability items from 0.2.0.
Typed results (scrapo.results):
- scrape() returns ScrapeResult, crawl() returns CrawlResult, result.extraction
is an ExtractionView. Pydantic models: attribute access, validation,
model_dump(). They also keep dict-style read access (result["key"],
result.get(), "key" in result) so existing 0.1/0.2 code is unaffected;
isinstance(result, dict) is no longer true. MCP server serializes via
model_dump(mode="json").
List / nested extraction:
- schema fields typed list[BaseModel] are extracted as repeated DOM elements:
the LLM returns {"field": {"__list__": "<repeating el>", "<sub>": "<rel sel>"}},
verified against the live page, cached, and replayed with zero tokens like
scalar fields. New scrapo.extract.schema.list_fields() does the detection and
feeds a hint into the prompt.
Browser pooling (scrapo.access.browser_pool.BrowserPool):
- a TierRouter lazily launches one headless Chromium and reuses it across
fetches; proxy settings move to the context level so one browser serves
rotating proxies. crawl() shares one router across all pages instead of cold-
launching per page. TierRouter.aclose() tears it down; scrape() closes the
router it creates; scrape() gained a router= kwarg for explicit reuse.
- playwright-stealth is applied to the page before navigation (was racing a
context event) and tries both the old and new plugin APIs.
Tests: typed-result and crawl-result assertions, list extraction (LLM path +
cache replay), list_fields detection, browser-pool/router teardown (94 tests,
still fully offline). ruff + mypy --strict clean. Docs and CHANGELOG updated; no
em-dashes in Markdown. Bump to 0.3.0.
Copy file name to clipboardExpand all lines: CHANGELOG.md
+17-1Lines changed: 17 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
8
8
## [Unreleased]
9
9
10
+
## [0.3.0] - 2026-05-10
11
+
12
+
Capability release: typed results, list/nested extraction, and a reused browser.
13
+
14
+
### Added
15
+
16
+
-**Typed result objects** (`scrapo.results`): `scrape()` returns `ScrapeResult`, `crawl()` returns `CrawlResult`, and `extraction` on a result is an `ExtractionView`. They are Pydantic models, so you get attribute access (`result.markdown`), validation, and `result.model_dump()` for serialization. They also support `result["key"]`, `result.get("key", default)`, and `"key" in result` so code written against the 0.1/0.2 dict shape keeps working unchanged.
17
+
-**List / nested extraction**: schema fields typed `list[SomeBaseModel]` are now extracted as repeated DOM elements. The LLM returns a container selector plus per-subfield selectors (`{"products": {"__list__": "ul.grid > li", "name": "h3", "price": ".price"}}`), those are verified against the live page, cached, and replayed on later runs with zero LLM tokens, exactly like scalar fields. `scrapo.extract.schema.list_fields()` exposes the detection.
18
+
-**Browser-context pooling** (`scrapo.access.browser_pool.BrowserPool`): a `TierRouter` now lazily launches one Chromium and reuses it across fetches (proxy settings move to the context level so a single browser serves rotating proxies). A crawl no longer cold-launches a browser per page. `TierRouter.aclose()` tears it down; `scrape()` closes the router it creates, and `crawl()` shares one router across all pages. `scrape()` gained a `router=` keyword for callers that want to reuse one explicitly.
19
+
- The flaky `playwright-stealth` integration is applied to the page before navigation (instead of via a context event that raced the first page) and tries both the old and new plugin entry points.
20
+
21
+
### Changed
22
+
23
+
-`scrape()` / `crawl()` return Pydantic models instead of plain `dict`. Dict-style read access still works; `isinstance(result, dict)` does not. The MCP server serializes results with `model_dump(mode="json")`.
24
+
10
25
## [0.2.0] - 2026-05-10
11
26
12
27
Hardening release: makes the "cost-aware" and "production crawling" claims real,
@@ -73,6 +88,7 @@ Initial public release.
73
88
- The `robots.txt` gate is opt-in: set `SCRAPO_RESPECT_ROBOTS=1` (or `Config(respect_robots=True)`) to enable it. You are responsible for complying with each site's terms of use and applicable law.
74
89
- Alpha status: the public API and core subsystems are stable, but the T4 agent driver, full action caching, an S3 snapshot adapter, and a hosted control plane are intentionally lightweight or not yet implemented.
print(res.extraction.method) # 'llm' on the first run, 'selector' after
169
+
print(res.cost_usd)# 0.0 once selectors are cached
164
170
165
171
asyncio.run(main())
166
172
```
167
173
168
-
> First call uses the LLM and **caches the selectors it learns** (keyed by host + schema). Every subsequent call against that host + schema uses cached selectors and **zero LLM tokens**. When the layout drifts, validation fails, Scrapo falls back to the LLM, re-derives selectors, and self-heals; a cache entry that keeps failing is evicted automatically.
174
+
> First call uses the LLM and **caches the selectors it learns** (keyed by host + schema; for `list[Model]` fields it caches a container selector plus per-subfield selectors). Every subsequent call against that host + schema uses cached selectors and **zero LLM tokens**. When the layout drifts, validation fails, Scrapo falls back to the LLM, re-derives selectors, and self-heals; a cache entry that keeps failing is evicted automatically.
<summary><b>Hybrid selector + LLM extractor (scalar and list fields)</b></summary>
209
215
210
216
```
211
217
cache hit + validates -> return (method=selector, llm_calls=0, cost_usd=0)
212
218
miss / fail / over budget -> LLM with schema -> validate -> verify + persist selectors -> return (method=llm)
213
219
repeated cache failures -> evict the stale entry, re-derive next run
214
220
```
215
221
216
-
The LLM is asked to return both the JSON payload *and* CSS selectors per field. Returned selectors are verified against the live HTML before being cached, so a hallucinated selector never poisons the cache. The cache is keyed by host (not registered domain), so `blog.example.com` and `shop.example.com` never collide.
222
+
The LLM is asked to return both the JSON payload *and* CSS selectors per field. A scalar field gets a string selector; a `list[Model]` field gets `{"__list__": "<repeating element>", "<subfield>": "<selector relative to it>", ...}`, which Scrapo applies as `tree.css(container)` then per-subfield extraction inside each match. Returned selectors are verified against the live HTML before being cached, so a hallucinated selector never poisons the cache. The cache is keyed by host (not registered domain), so `blog.example.com` and `shop.example.com` never collide.
217
223
218
224
</details>
219
225
@@ -283,6 +289,7 @@ diff 9f3e1c... vs abc123...
283
289
-**SSRF guard.** Every fetch target is checked before a request goes out; loopback, link-local (including `169.254.169.254`), private RFC 1918 / ULA ranges, and well-known local hostnames are refused. Set `allow_private_hosts=True` (or `SCRAPO_ALLOW_PRIVATE_HOSTS=1`) for internal scraping. Crawl link discovery applies the same filter and skips obvious binary URLs.
284
290
-**Bounded HTTP retries.** Transient `429 / 5xx` and transport errors are retried with exponential backoff and jitter before the router escalates to a heavier tier (`SCRAPO_HTTP_RETRIES`, default `2`).
285
291
-**Concurrency-safe storage.** All SQLite stores (replay, selector cache, crawl queue) open in WAL mode with a busy timeout, so concurrent crawl workers do not trip over each other.
292
+
-**Browser reuse.** A `TierRouter` launches one headless Chromium lazily and reuses it across fetches (proxy applied per context), so a crawl is not paying a cold browser launch per page. `TierRouter.aclose()` tears it down; `scrape()` and `crawl()` handle that for you.
286
293
-**Cost accounting.** LLM cost is computed per call, recorded on the run, and enforceable via `Budget(max_llm_calls=..., max_cost_usd=...)`.
287
294
-**PII handling.** Flag PII in the audit log (`SCRAPO_PII_FILTER=1`), or redact it from the stored snapshot, markdown, and chunks (`SCRAPO_REDACT_SNAPSHOTS=1`).
288
295
-**Local UI hardening.**`scrapo serve` binds `127.0.0.1` by default, validates the `Host` header against an allowlist (anti DNS-rebinding), serializes scrapes, and warns loudly if you bind a public interface.
@@ -436,7 +443,7 @@ Issues and PRs welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the dev setup
436
443
437
444
## Project status
438
445
439
-
Alpha. The public API (`scrape`, `extract`, `crawl`) is stable; tier escalation, model pinning, replay schema, and the MCP tool surface are stable. Parts that are intentionally lightweight today and slated for hardening: T4 agent driver, list/nested extraction, browser-context pooling, full Stagehand-style action caching, S3 snapshot adapter, hosted control plane.
446
+
Alpha. The public API (`scrape`, `extract`, `crawl`) is stable; tier escalation, model pinning, replay schema, typed results, list extraction, and the MCP tool surface are stable. Parts that are intentionally lightweight today and slated for hardening: a batteries-included T4 agent driver, full Stagehand-style action caching, in-browser request interception, pagination/sitemap following, content-type routing (PDF/JSON/RSS), an S3 snapshot adapter, and a hosted control plane.
440
447
441
448
See [CHANGELOG.md](CHANGELOG.md) for release notes.
Copy file name to clipboardExpand all lines: layman.md
+2-3Lines changed: 2 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -22,7 +22,7 @@ If you have ever copied information off a web page by hand, or wished a program
22
22
## What's possible
23
23
24
24
- Get clean, readable text out of most public web pages.
25
-
- Get structured data (the specific fields you define) out of pages, reliably and repeatably.
25
+
- Get structured data (the specific fields you define) out of pages, reliably and repeatably, including lists (every product on a listing page, every row of a table) when you describe them as a list of records.
26
26
- Scrape JavaScript-heavy pages that do not work with a plain download.
27
27
- Get past light anti-bot defenses on your own; get past tougher ones by plugging in a commercial proxy service (Bright Data, Oxylabs, Scrapfly, and Zyte are supported out of the box).
28
28
- Crawl a whole site: follow links automatically, with limits on depth and page count, skipping duplicates.
@@ -40,9 +40,8 @@ If you have ever copied information off a web page by hand, or wished a program
40
40
-**It is not a no-code, point-and-click product.** You need to write a little Python or use the command line. The built-in web page is intentionally minimal; it is for trying things, not a polished app.
41
41
-**It cannot magically beat every site's defenses.** Aggressive bot protection and CAPTCHAs are genuinely hard. A proxy provider helps a lot, but nothing is guaranteed. The most advanced mode (an AI that drives a browser through logins and CAPTCHAs) exists but is lightweight and experimental today, and ships without a default driver.
42
42
-**It will not log into sites for you by default.** You can supply credentials or a saved login session, but automated login flows are still experimental.
43
-
-**There is no built-in list/table extraction yet.** Today it extracts one record per page well; pulling every row out of a listing page is on the roadmap, not in the box.
44
43
-**There is no hosted dashboard or scheduler.** Scrapo does not run your jobs in the cloud, send alerts, or give you a web console to manage everything. You run and schedule it yourself.
45
-
-**It is alpha software.** The core works and is stable, but expect rough edges. Some pieces (browser pooling, cloud snapshot storage, advanced action caching, a hosted control plane) are planned, not built.
44
+
-**It is alpha software.** The core works and is stable, but expect rough edges. Some pieces (a ready-made agent driver, in-browser request interception, following "next page" links and sitemaps, cloud snapshot storage, advanced action caching, a hosted control plane) are planned, not built.
46
45
-**AI extraction costs money.** The first run on a new site (or after a layout change) calls a paid AI model. Scrapo is designed to minimize this (most runs use the free cached recipe) but it is not literally free.
47
46
-**It is not legal advice or a compliance guarantee.** The robots rules, personal-data flagging, geo limits, and audit log are *tools* to help you scrape responsibly. You are still responsible for following each site's terms and the law. (Note: robots-rule enforcement is off by default; you have to turn it on.)
48
47
-**It is Python-only.** No JavaScript, Java, Go, etc. versions. Requires Python 3.11 or newer.
0 commit comments