wintergrab 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. wintergrab-0.2.0/.gitignore +31 -0
  2. wintergrab-0.2.0/CHANGELOG.md +61 -0
  3. wintergrab-0.2.0/LICENSE +21 -0
  4. wintergrab-0.2.0/PKG-INFO +303 -0
  5. wintergrab-0.2.0/README.md +254 -0
  6. wintergrab-0.2.0/benchmarks/README.md +233 -0
  7. wintergrab-0.2.0/docs/adaptive-selectors.md +112 -0
  8. wintergrab-0.2.0/docs/anti-blocking.md +160 -0
  9. wintergrab-0.2.0/docs/cli.md +137 -0
  10. wintergrab-0.2.0/docs/fetching.md +157 -0
  11. wintergrab-0.2.0/docs/getting-started.md +142 -0
  12. wintergrab-0.2.0/docs/parsing.md +155 -0
  13. wintergrab-0.2.0/docs/power-features.md +258 -0
  14. wintergrab-0.2.0/docs/spiders.md +273 -0
  15. wintergrab-0.2.0/examples/01_quickstart.py +47 -0
  16. wintergrab-0.2.0/examples/02_extract_to_csv.py +39 -0
  17. wintergrab-0.2.0/examples/03_adaptive_selectors.py +60 -0
  18. wintergrab-0.2.0/examples/04_async_many_pages.py +33 -0
  19. wintergrab-0.2.0/examples/05_quotes_spider.py +43 -0
  20. wintergrab-0.2.0/examples/06_books_resumable.py +72 -0
  21. wintergrab-0.2.0/examples/07_browser_rendering.py +31 -0
  22. wintergrab-0.2.0/examples/08_sessions_and_fallback.py +43 -0
  23. wintergrab-0.2.0/examples/09_zero_selector.py +39 -0
  24. wintergrab-0.2.0/examples/10_big_crawl.py +57 -0
  25. wintergrab-0.2.0/examples/README.md +26 -0
  26. wintergrab-0.2.0/pyproject.toml +107 -0
  27. wintergrab-0.2.0/src/wintergrab/__init__.py +91 -0
  28. wintergrab-0.2.0/src/wintergrab/__main__.py +5 -0
  29. wintergrab-0.2.0/src/wintergrab/adaptive/__init__.py +16 -0
  30. wintergrab-0.2.0/src/wintergrab/adaptive/fingerprint.py +286 -0
  31. wintergrab-0.2.0/src/wintergrab/adaptive/storage.py +142 -0
  32. wintergrab-0.2.0/src/wintergrab/cli.py +786 -0
  33. wintergrab-0.2.0/src/wintergrab/errors.py +77 -0
  34. wintergrab-0.2.0/src/wintergrab/fetchers/__init__.py +143 -0
  35. wintergrab-0.2.0/src/wintergrab/fetchers/blocking.py +63 -0
  36. wintergrab-0.2.0/src/wintergrab/fetchers/browser.py +897 -0
  37. wintergrab-0.2.0/src/wintergrab/fetchers/cache.py +407 -0
  38. wintergrab-0.2.0/src/wintergrab/fetchers/http.py +603 -0
  39. wintergrab-0.2.0/src/wintergrab/fetchers/response.py +404 -0
  40. wintergrab-0.2.0/src/wintergrab/parser/__init__.py +12 -0
  41. wintergrab-0.2.0/src/wintergrab/parser/autoextract.py +1440 -0
  42. wintergrab-0.2.0/src/wintergrab/parser/css.py +136 -0
  43. wintergrab-0.2.0/src/wintergrab/parser/extract.py +117 -0
  44. wintergrab-0.2.0/src/wintergrab/parser/selector.py +915 -0
  45. wintergrab-0.2.0/src/wintergrab/parser/structured.py +889 -0
  46. wintergrab-0.2.0/src/wintergrab/parser/text.py +331 -0
  47. wintergrab-0.2.0/src/wintergrab/proxy.py +200 -0
  48. wintergrab-0.2.0/src/wintergrab/py.typed +0 -0
  49. wintergrab-0.2.0/src/wintergrab/request.py +171 -0
  50. wintergrab-0.2.0/src/wintergrab/sitemaps.py +201 -0
  51. wintergrab-0.2.0/src/wintergrab/spider/__init__.py +8 -0
  52. wintergrab-0.2.0/src/wintergrab/spider/checkpoint.py +65 -0
  53. wintergrab-0.2.0/src/wintergrab/spider/engine.py +931 -0
  54. wintergrab-0.2.0/src/wintergrab/spider/exporters.py +376 -0
  55. wintergrab-0.2.0/src/wintergrab/spider/frontier.py +803 -0
  56. wintergrab-0.2.0/src/wintergrab/spider/progress.py +145 -0
  57. wintergrab-0.2.0/src/wintergrab/spider/robots.py +77 -0
  58. wintergrab-0.2.0/src/wintergrab/spider/scheduler.py +111 -0
  59. wintergrab-0.2.0/src/wintergrab/spider/sessions.py +73 -0
  60. wintergrab-0.2.0/src/wintergrab/spider/spider.py +448 -0
  61. wintergrab-0.2.0/src/wintergrab/spider/throttle.py +165 -0
  62. wintergrab-0.2.0/src/wintergrab/utils.py +187 -0
  63. wintergrab-0.2.0/tests/conftest.py +73 -0
  64. wintergrab-0.2.0/tests/test_adaptive.py +145 -0
  65. wintergrab-0.2.0/tests/test_autoextract.py +550 -0
  66. wintergrab-0.2.0/tests/test_browser.py +125 -0
  67. wintergrab-0.2.0/tests/test_cache.py +131 -0
  68. wintergrab-0.2.0/tests/test_cli.py +170 -0
  69. wintergrab-0.2.0/tests/test_crawl_tools.py +129 -0
  70. wintergrab-0.2.0/tests/test_disk_frontier.py +123 -0
  71. wintergrab-0.2.0/tests/test_examples.py +137 -0
  72. wintergrab-0.2.0/tests/test_fast_paths.py +140 -0
  73. wintergrab-0.2.0/tests/test_frontier.py +489 -0
  74. wintergrab-0.2.0/tests/test_http_fetcher.py +192 -0
  75. wintergrab-0.2.0/tests/test_live.py +107 -0
  76. wintergrab-0.2.0/tests/test_parser.py +201 -0
  77. wintergrab-0.2.0/tests/test_proxy.py +82 -0
  78. wintergrab-0.2.0/tests/test_regressions.py +518 -0
  79. wintergrab-0.2.0/tests/test_resume.py +146 -0
  80. wintergrab-0.2.0/tests/test_spider.py +397 -0
  81. wintergrab-0.2.0/tests/test_structured.py +595 -0
  82. wintergrab-0.2.0/tests/test_text.py +53 -0
  83. wintergrab-0.2.0/tests/test_throttle_scheduler.py +107 -0
  84. wintergrab-0.2.0/tests/test_zero_selector.py +104 -0
  85. wintergrab-0.2.0/tests/testsite.py +532 -0
@@ -0,0 +1,31 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ venv/
10
+ .python-version
11
+
12
+ # Tooling
13
+ .pytest_cache/
14
+ .mypy_cache/
15
+ .ruff_cache/
16
+ .coverage
17
+ htmlcov/
18
+
19
+ # Crawl output and state
20
+ .crawl/
21
+ crawls/
22
+ *.jsonl
23
+ !tests/data/*.jsonl
24
+
25
+ # Editors / OS
26
+ .idea/
27
+ .vscode/
28
+ .DS_Store
29
+
30
+ # Default HTTP cache directory
31
+ .wintergrab-cache/
@@ -0,0 +1,61 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ First release on PyPI.
6
+
7
+ - **Zero-selector extraction**: `structured_data()` (JSON-LD, microdata,
8
+ OpenGraph, Twitter, meta), `embedded_json()` (`__NEXT_DATA__`,
9
+ `window.__STATE__`, `JSON.parse` payloads), `find_json()`, `tables()`,
10
+ `next_page()`/`follow_next()`, `detect_records()`/`auto_extract()` and
11
+ learn-by-example `learn()` → reusable `LearnedSchema`.
12
+ - **HTTP cache** with `revalidate`/`prefer`/`offline`/`refresh` modes for all
13
+ fetchers and spiders (offline replay of whole crawls).
14
+ - **Disk frontier** (`frontier="disk"`): SQLite queue + scalable Bloom filter,
15
+ flat memory, at-least-once crash recovery.
16
+ - **Browser**: `capture=` records XHR/fetch API responses; `export_cookies()` /
17
+ `add_cookies()`; spiders share browser cookies with HTTP sessions.
18
+ - **Sitemaps**: `wg.sitemap()`, `Spider.sitemap_urls/rules/follow/since`.
19
+ - **Output**: SQLite exporter with upserts, `unique_key` de-duplication,
20
+ buffered writers, orjson.
21
+ - **Speed**: uvloop when installed (`[speed]` extra), leaner crawl loop, and
22
+ profile-guided hot-path shortcuts (URL joining and canonicalisation,
23
+ cached request host/fingerprint, compiled XPath cache, byte-level block
24
+ check). Each shortcut is tested to return exactly what the code it
25
+ bypasses returns.
26
+ - Live terminal progress line; `wintergrab doctor`; many new CLI flags.
27
+ - Engine robustness: no lost requests on cancel/force-stop/fatal errors,
28
+ crash-safe JSON output, correct Retry-After/429 pacing, signal handlers
29
+ restored, Ctrl+C in Jupyter.
30
+ - **Block detection** recognises Fastly's "Client Challenge" (served with
31
+ status 200) and DataDome, and a spider that gives up on a block page says
32
+ so instead of reporting a bare "HTTP 200".
33
+ - **Browser**: after a bot check passes, the capture waits until the real
34
+ page is fully parsed. Behind pypi.org's check, the browser used to return
35
+ a half-loaded page.
36
+ - **Security**: a page on an allowed domain that redirects elsewhere
37
+ (possibly to an internal address) is no longer passed to the callbacks
38
+ (`offsite_redirects` stat).
39
+ - The CLI writes UTF-8 when its output is redirected (Windows pipes default
40
+ to cp1252). Tested on Linux, macOS and Windows, Python 3.10-3.14.
41
+
42
+ ## 0.1.0
43
+
44
+ Internal milestone, never published to PyPI.
45
+
46
+ - **Fetching**: `get`/`post`/`aget`/`apost` shortcuts; `Fetcher` and `AsyncFetcher`
47
+ sessions with browser TLS/HTTP2 impersonation (curl_cffi), retries with
48
+ exponential backoff and `Retry-After`, proxy rotation, charset detection.
49
+ - **Browser**: `render`/`arender`, `BrowserFetcher` and `AsyncBrowserFetcher`
50
+ (Playwright) with stealth patches, resource blocking, `wait_for`, scrolling,
51
+ page actions, screenshots, challenge-page waiting and per-proxy contexts.
52
+ - **Parsing**: `Selector` with CSS (`::text`, `::attr()`) and XPath, extraction
53
+ schemas (`Field`), `find_by_text`, `find_by_regex`, `find_similar`, generated
54
+ selectors, links, text and Markdown conversion.
55
+ - **Adaptive selectors**: element fingerprints stored in SQLite; broken
56
+ selectors relocate their elements by similarity.
57
+ - **Spiders**: asyncio crawler with per-domain scheduling, AutoThrottle,
58
+ multiple sessions, session fallback for blocked pages, proxy rotation,
59
+ robots.txt, limits, JSON/JSONL/CSV output, streaming, and pause/resume with
60
+ checkpoints.
61
+ - **CLI**: `wintergrab get`, `wintergrab crawl`, `wintergrab shell`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 opensourcewinter and wintergrab contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,303 @@
1
+ Metadata-Version: 2.5
2
+ Name: wintergrab
3
+ Version: 0.2.0
4
+ Summary: Friendly web scraping that scales from one page to big crawls: browser-like fetching, adaptive selectors, resumable async spiders.
5
+ Project-URL: Homepage, https://github.com/opensourcewinter/wintergrab
6
+ Project-URL: Documentation, https://github.com/opensourcewinter/wintergrab/tree/main/docs
7
+ Project-URL: Changelog, https://github.com/opensourcewinter/wintergrab/blob/main/CHANGELOG.md
8
+ Project-URL: Source, https://github.com/opensourcewinter/wintergrab
9
+ Project-URL: Issues, https://github.com/opensourcewinter/wintergrab/issues
10
+ Author: wintergrab contributors
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: crawler,css-selectors,curl-cffi,playwright,scraping,spider,web-scraping,xpath
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Framework :: AsyncIO
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Internet :: WWW/HTTP
26
+ Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
27
+ Classifier: Topic :: Text Processing :: Markup :: HTML
28
+ Classifier: Typing :: Typed
29
+ Requires-Python: >=3.10
30
+ Requires-Dist: cssselect>=1.2
31
+ Requires-Dist: curl-cffi>=0.10
32
+ Requires-Dist: lxml>=5.0
33
+ Provides-Extra: all
34
+ Requires-Dist: orjson>=3.9; extra == 'all'
35
+ Requires-Dist: playwright>=1.45; extra == 'all'
36
+ Requires-Dist: uvloop>=0.19; (sys_platform != 'win32') and extra == 'all'
37
+ Provides-Extra: browser
38
+ Requires-Dist: playwright>=1.45; extra == 'browser'
39
+ Provides-Extra: dev
40
+ Requires-Dist: mypy>=1.10; extra == 'dev'
41
+ Requires-Dist: playwright>=1.45; extra == 'dev'
42
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
43
+ Requires-Dist: pytest>=8.0; extra == 'dev'
44
+ Requires-Dist: ruff>=0.6; extra == 'dev'
45
+ Provides-Extra: speed
46
+ Requires-Dist: orjson>=3.9; extra == 'speed'
47
+ Requires-Dist: uvloop>=0.19; (sys_platform != 'win32') and extra == 'speed'
48
+ Description-Content-Type: text/markdown
49
+
50
+ # wintergrab
51
+
52
+ **Friendly web scraping that scales from one page to big crawls.**
53
+
54
+ ```python
55
+ import wintergrab as wg
56
+
57
+ page = wg.get("https://quotes.toscrape.com/")
58
+ for quote in page.css(".quote"):
59
+ print(quote.css(".text::text").get(), "-", quote.css(".author::text").get())
60
+ ```
61
+
62
+ wintergrab is a Python toolkit for grabbing data from websites. Simple things
63
+ are one line. When a site is harder (JavaScript, bot checks, rate limits,
64
+ thousands of pages), the same API scales up.
65
+
66
+ - **Fetch like a real browser.** HTTP requests carry Chrome/Firefox/Safari
67
+ TLS and HTTP/2 fingerprints (via [curl_cffi](https://github.com/lexiforest/curl_cffi)).
68
+ A headless Chromium (via [Playwright](https://playwright.dev/python/)) is
69
+ one flag away for JavaScript pages. It hides common automation tells and
70
+ waits out "checking your browser" interstitials.
71
+ - **Parse with CSS or XPath.** Scrapy-style `::text` / `::attr(href)`,
72
+ extraction schemas, search by text, "find similar elements", and
73
+ HTML → Markdown/text conversion.
74
+ - **Selectors that adapt.** With `adaptive=True`, wintergrab remembers what a
75
+ selector matched. After a redesign that breaks it, it finds the most
76
+ similar elements on the new page.
77
+ - **Crawl at scale.** Async spiders with concurrency limits, multiple
78
+ sessions (HTTP + browser, several accounts…), proxy rotation with health
79
+ checks, **AutoThrottle** that backs off when a site pushes back,
80
+ robots.txt support, and **pause/resume** (Ctrl+C, then run again).
81
+ - **Scrape without selectors.** Pull JSON-LD/microdata/OpenGraph, the JSON
82
+ state that React/Next/Vue apps embed in their HTML, and every table.
83
+ `auto_extract()` finds a page's product grid or result list and names the
84
+ fields. `learn({"title": "…", "price": "…"})` writes the selectors for you
85
+ from values you can see on the page.
86
+ - **Built for big, long crawls.** An HTTP cache that revalidates with `304`s
87
+ and replays whole crawls offline. A disk-backed queue with a Bloom filter
88
+ that keeps memory flat at millions of URLs and survives `kill -9`. Sitemap
89
+ crawling, SQLite output with upserts, and a live progress line.
90
+ - **Fast.** In a [reproducible benchmark](https://github.com/opensourcewinter/wintergrab/blob/main/benchmarks/README.md) against a
91
+ local test shop, a wintergrab spider crawled about 1,000 pages/s on one
92
+ core. That is 1.8× Crawlee and 3.8× Scrapy at the same concurrency, with
93
+ under half their memory. On real sites, the site and your politeness
94
+ settings usually set the pace, not the crawler.
95
+ - **Browser superpowers.** Capture the JSON API calls a page makes while it
96
+ renders. Clear a login or JS check once in the browser, then continue over
97
+ fast HTTP with the same cookies.
98
+ - **A small CLI.** `wintergrab get` and `wintergrab crawl` cover the common
99
+ jobs with no code at all, including `--auto`, `--learn` and `--offline`.
100
+
101
+ ## Install
102
+
103
+ ```bash
104
+ pip install wintergrab # HTTP fetching, parsing, spiders, CLI
105
+ pip install "wintergrab[browser]" # + headless browser support
106
+ pip install "wintergrab[speed]" # + uvloop and orjson
107
+ playwright install chromium # one-time browser download (browser extra only)
108
+ wintergrab doctor # check what is installed
109
+ ```
110
+
111
+ Python 3.10+ on Linux, macOS and Windows. On a fresh Linux machine, use
112
+ `playwright install --with-deps chromium` to get the browser's system
113
+ libraries too. The development version installs straight from GitHub:
114
+ `pip install "wintergrab @ git+https://github.com/opensourcewinter/wintergrab"`.
115
+
116
+ ## A quick tour
117
+
118
+ ### Fetch and parse
119
+
120
+ ```python
121
+ import wintergrab as wg
122
+
123
+ page = wg.get("https://books.toscrape.com/") # looks like Chrome, retries hiccups
124
+ page.status, page.title # (200, 'All products | Books to Scrape')
125
+
126
+ page.css("h3 a::attr(title)").getall() # every title
127
+ page.css(".price_color::text").get() # first price: '£51.77'
128
+ page.xpath("//p[contains(@class, 'star-rating')]/@class").get()
129
+
130
+ for book in page.css("article.product_pod"): # loop and query inside
131
+ print(book.css("h3 a").attr("title"), book.css(".price_color").text)
132
+
133
+ page.links(".pager") # absolute URLs of links in the pager
134
+ page.find_by_text("Tipping the Velvet") # search by visible text
135
+ page.markdown(main_content=True) # the page as Markdown
136
+ ```
137
+
138
+ ### Pull out structured data
139
+
140
+ ```python
141
+ from wintergrab import Field
142
+
143
+ books = page.extract_all("article.product_pod", {
144
+ "title": "h3 a::attr(title)",
145
+ "price": Field(".price_color::text", transform=lambda p: float(p.lstrip("£"))),
146
+ "rating": Field("p.star-rating", attr="class", regex=r"star-rating (\w+)"),
147
+ })
148
+ page.extract({"titles": ["h3 a::attr(title)"]}) # a one-item list = all matches
149
+ ```
150
+
151
+ ### Survive layout changes
152
+
153
+ ```python
154
+ products = page.css(".product-card", adaptive=True)
155
+ ```
156
+
157
+ The first time, wintergrab saves a fingerprint of what matched: tag,
158
+ attributes, text, position, parent and neighbours. If the site later renames
159
+ `.product-card` or wraps it in new containers, the same call scores every
160
+ element on the new page and returns the closest matches. It logs a warning
161
+ so you know to update the selector. See [docs/adaptive-selectors.md](https://github.com/opensourcewinter/wintergrab/blob/main/docs/adaptive-selectors.md).
162
+
163
+ ### Scrape without writing selectors
164
+
165
+ ```python
166
+ page.auto_extract() # [{"title", "url", "image", "price", "rating"...}, ...] from the main record list
167
+ schema = page.learn({"title": "A Light in the Attic", "price": "£51.77"})
168
+ schema.extract(other_page) # the learned selectors work on every page of that template
169
+ page.structured_data() # JSON-LD, microdata, OpenGraph, meta tags
170
+ page.embedded_json() # __NEXT_DATA__, window.__INITIAL_STATE__, ... (SPAs without a browser)
171
+ page.tables() # every table as records
172
+ page.next_page() # pagination, auto-detected
173
+ ```
174
+
175
+ ### JavaScript pages
176
+
177
+ ```python
178
+ page = wg.render("https://quotes.toscrape.com/js/", wait_for=".quote")
179
+
180
+ with wg.BrowserFetcher(headless=True) as browser: # reuse one browser
181
+ page = browser.get(url, scroll=True, screenshot="page.png")
182
+ ```
183
+
184
+ ### Many pages at once
185
+
186
+ ```python
187
+ async with wg.AsyncFetcher() as fetcher:
188
+ pages = await fetcher.get_many(urls, concurrency=10)
189
+ ```
190
+
191
+ ### Crawl a site
192
+
193
+ ```python
194
+ from wintergrab import Spider
195
+
196
+ class BooksSpider(Spider):
197
+ start_urls = ["https://books.toscrape.com/"]
198
+ allowed_domains = ["books.toscrape.com"]
199
+ concurrency = 16 # AutoThrottle adapts the real speed per domain
200
+ crawl_dir = ".crawl/books" # makes it resumable: Ctrl+C pauses, re-run resumes
201
+ output = "books.jsonl" # items stream here (.jsonl / .json / .csv)
202
+
203
+ def parse(self, response):
204
+ for link in response.css("article.product_pod h3 a"):
205
+ yield response.follow(link, callback=self.parse_book)
206
+ yield from response.follow_all("li.next a")
207
+
208
+ def parse_book(self, response):
209
+ yield {
210
+ "title": response.css("h1::text").get(),
211
+ "price": response.css(".product_main .price_color::text").get(),
212
+ }
213
+
214
+ result = BooksSpider().run()
215
+ print(result.status, result.stats["pages"], result.stats["items"])
216
+ ```
217
+
218
+ Scaling up is a few attributes away:
219
+
220
+ ```python
221
+ class BigCrawl(Spider):
222
+ sitemap_urls = ["https://shop.example/robots.txt"] # discover pages from sitemaps
223
+ frontier = "disk" # flat memory for millions of URLs, crash-safe queue
224
+ crawl_dir = ".crawl/big"
225
+ cache = ".cache/big" # revalidating HTTP cache; cache_mode="offline" replays the crawl
226
+ output = "catalog.db" # SQLite...
227
+ unique_key = "url" # ...with upserts: re-crawls update rows in place
228
+ fallback_session = "browser" # blocked page? retry it in a headless browser, share its cookies
229
+ ```
230
+
231
+ Spiders also give you:
232
+
233
+ - **Sessions.** Route requests through different fetchers with
234
+ `Request(url, session="browser")`. Set `fallback_session="browser"` to
235
+ retry blocked pages in a headless browser automatically.
236
+ - **Proxy rotation.** `proxies = [...]` (or a `ProxyRotator`). Proxies that
237
+ keep failing are benched for a while.
238
+ - **Speed control.** Per-domain concurrency and delays that back off on
239
+ 429/503/block pages, honour `Retry-After` and robots.txt `Crawl-delay`,
240
+ and recover gradually.
241
+ - **Limits and hooks.** `max_pages`, `max_items`, `max_depth`,
242
+ `process_item()`, `on_error()`, `on_start()` / `on_close()`, and
243
+ `async for item in spider.stream()`.
244
+
245
+ ### Command line
246
+
247
+ ```bash
248
+ wintergrab get https://quotes.toscrape.com # page as Markdown
249
+ wintergrab get https://quotes.toscrape.com --css ".quote .text::text"
250
+ wintergrab get https://books.toscrape.com --each article.product_pod \
251
+ --field title="h3 a::attr(title)" --field price=.price_color::text -o books.csv
252
+ wintergrab get https://quotes.toscrape.com/js/ --browser --wait-for .quote
253
+
254
+ wintergrab get https://books.toscrape.com --auto # records, no selectors
255
+ wintergrab get https://books.toscrape.com --learn "title=A Light in the Attic" --save-schema books.json
256
+ wintergrab crawl https://books.toscrape.com --schema books.json --paginate -o books.jsonl
257
+ wintergrab get https://shop.example/p/1 --structured # JSON-LD, OpenGraph...
258
+
259
+ wintergrab crawl my_spider.py -o items.jsonl --crawl-dir .crawl/mine # run a spider file
260
+ wintergrab crawl https://books.toscrape.com --follow "li.next a" --follow "h3 a" \
261
+ --each ".product_main" --field title=h1::text --max-pages 50 -o books.jsonl
262
+
263
+ wintergrab shell https://quotes.toscrape.com # explore interactively
264
+ ```
265
+
266
+ ## Documentation
267
+
268
+ | Guide | What's inside |
269
+ |---|---|
270
+ | [Getting started](https://github.com/opensourcewinter/wintergrab/blob/main/docs/getting-started.md) | Install, first scrape, first spider, in 10 minutes |
271
+ | [Fetching](https://github.com/opensourcewinter/wintergrab/blob/main/docs/fetching.md) | `get`/`Fetcher`/`AsyncFetcher`/`BrowserFetcher`, options, errors |
272
+ | [Parsing](https://github.com/opensourcewinter/wintergrab/blob/main/docs/parsing.md) | Selectors, extraction schemas, text search, Markdown |
273
+ | [Adaptive selectors](https://github.com/opensourcewinter/wintergrab/blob/main/docs/adaptive-selectors.md) | How relocation works and how to tune it |
274
+ | [Spiders](https://github.com/opensourcewinter/wintergrab/blob/main/docs/spiders.md) | Crawling, sessions, pause/resume, output, every setting |
275
+ | [Power features](https://github.com/opensourcewinter/wintergrab/blob/main/docs/power-features.md) | Zero-selector extraction, cache & offline replay, API capture, cookie handoff, sitemaps, disk frontier, SQLite |
276
+ | [Tough sites](https://github.com/opensourcewinter/wintergrab/blob/main/docs/anti-blocking.md) | Impersonation, browsers, proxies, AutoThrottle, etiquette |
277
+ | [CLI](https://github.com/opensourcewinter/wintergrab/blob/main/docs/cli.md) | `get`, `crawl` and `shell` reference |
278
+ | [Examples](https://github.com/opensourcewinter/wintergrab/tree/main/examples/) | Runnable scripts for every feature |
279
+
280
+ ## Scrape responsibly
281
+
282
+ wintergrab makes polite crawling the default. Spiders obey robots.txt,
283
+ adapt their speed to each site, and back off when asked. Stealth features
284
+ exist so legitimate automation isn't misclassified. They don't make it OK
285
+ to ignore a site's terms, hammer servers, or collect personal data you
286
+ have no right to. Check the rules of each site you scrape.
287
+
288
+ ## Development
289
+
290
+ ```bash
291
+ python -m venv .venv && . .venv/bin/activate
292
+ pip install -e ".[dev]"
293
+ playwright install chromium # for the browser tests (skipped otherwise)
294
+ pytest # runs against a local test site; no internet needed
295
+ ruff check . && ruff format --check .
296
+ ```
297
+
298
+ See [CONTRIBUTING.md](https://github.com/opensourcewinter/wintergrab/blob/main/CONTRIBUTING.md) for the live tests and the release
299
+ process, and [SECURITY.md](https://github.com/opensourcewinter/wintergrab/blob/main/SECURITY.md) to report a vulnerability.
300
+
301
+ ## License
302
+
303
+ [MIT](https://github.com/opensourcewinter/wintergrab/blob/main/LICENSE)
@@ -0,0 +1,254 @@
1
+ # wintergrab
2
+
3
+ **Friendly web scraping that scales from one page to big crawls.**
4
+
5
+ ```python
6
+ import wintergrab as wg
7
+
8
+ page = wg.get("https://quotes.toscrape.com/")
9
+ for quote in page.css(".quote"):
10
+ print(quote.css(".text::text").get(), "-", quote.css(".author::text").get())
11
+ ```
12
+
13
+ wintergrab is a Python toolkit for grabbing data from websites. Simple things
14
+ are one line. When a site is harder (JavaScript, bot checks, rate limits,
15
+ thousands of pages), the same API scales up.
16
+
17
+ - **Fetch like a real browser.** HTTP requests carry Chrome/Firefox/Safari
18
+ TLS and HTTP/2 fingerprints (via [curl_cffi](https://github.com/lexiforest/curl_cffi)).
19
+ A headless Chromium (via [Playwright](https://playwright.dev/python/)) is
20
+ one flag away for JavaScript pages. It hides common automation tells and
21
+ waits out "checking your browser" interstitials.
22
+ - **Parse with CSS or XPath.** Scrapy-style `::text` / `::attr(href)`,
23
+ extraction schemas, search by text, "find similar elements", and
24
+ HTML → Markdown/text conversion.
25
+ - **Selectors that adapt.** With `adaptive=True`, wintergrab remembers what a
26
+ selector matched. After a redesign that breaks it, it finds the most
27
+ similar elements on the new page.
28
+ - **Crawl at scale.** Async spiders with concurrency limits, multiple
29
+ sessions (HTTP + browser, several accounts…), proxy rotation with health
30
+ checks, **AutoThrottle** that backs off when a site pushes back,
31
+ robots.txt support, and **pause/resume** (Ctrl+C, then run again).
32
+ - **Scrape without selectors.** Pull JSON-LD/microdata/OpenGraph, the JSON
33
+ state that React/Next/Vue apps embed in their HTML, and every table.
34
+ `auto_extract()` finds a page's product grid or result list and names the
35
+ fields. `learn({"title": "…", "price": "…"})` writes the selectors for you
36
+ from values you can see on the page.
37
+ - **Built for big, long crawls.** An HTTP cache that revalidates with `304`s
38
+ and replays whole crawls offline. A disk-backed queue with a Bloom filter
39
+ that keeps memory flat at millions of URLs and survives `kill -9`. Sitemap
40
+ crawling, SQLite output with upserts, and a live progress line.
41
+ - **Fast.** In a [reproducible benchmark](https://github.com/opensourcewinter/wintergrab/blob/main/benchmarks/README.md) against a
42
+ local test shop, a wintergrab spider crawled about 1,000 pages/s on one
43
+ core. That is 1.8× Crawlee and 3.8× Scrapy at the same concurrency, with
44
+ under half their memory. On real sites, the site and your politeness
45
+ settings usually set the pace, not the crawler.
46
+ - **Browser superpowers.** Capture the JSON API calls a page makes while it
47
+ renders. Clear a login or JS check once in the browser, then continue over
48
+ fast HTTP with the same cookies.
49
+ - **A small CLI.** `wintergrab get` and `wintergrab crawl` cover the common
50
+ jobs with no code at all, including `--auto`, `--learn` and `--offline`.
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ pip install wintergrab # HTTP fetching, parsing, spiders, CLI
56
+ pip install "wintergrab[browser]" # + headless browser support
57
+ pip install "wintergrab[speed]" # + uvloop and orjson
58
+ playwright install chromium # one-time browser download (browser extra only)
59
+ wintergrab doctor # check what is installed
60
+ ```
61
+
62
+ Python 3.10+ on Linux, macOS and Windows. On a fresh Linux machine, use
63
+ `playwright install --with-deps chromium` to get the browser's system
64
+ libraries too. The development version installs straight from GitHub:
65
+ `pip install "wintergrab @ git+https://github.com/opensourcewinter/wintergrab"`.
66
+
67
+ ## A quick tour
68
+
69
+ ### Fetch and parse
70
+
71
+ ```python
72
+ import wintergrab as wg
73
+
74
+ page = wg.get("https://books.toscrape.com/") # looks like Chrome, retries hiccups
75
+ page.status, page.title # (200, 'All products | Books to Scrape')
76
+
77
+ page.css("h3 a::attr(title)").getall() # every title
78
+ page.css(".price_color::text").get() # first price: '£51.77'
79
+ page.xpath("//p[contains(@class, 'star-rating')]/@class").get()
80
+
81
+ for book in page.css("article.product_pod"): # loop and query inside
82
+ print(book.css("h3 a").attr("title"), book.css(".price_color").text)
83
+
84
+ page.links(".pager") # absolute URLs of links in the pager
85
+ page.find_by_text("Tipping the Velvet") # search by visible text
86
+ page.markdown(main_content=True) # the page as Markdown
87
+ ```
88
+
89
+ ### Pull out structured data
90
+
91
+ ```python
92
+ from wintergrab import Field
93
+
94
+ books = page.extract_all("article.product_pod", {
95
+ "title": "h3 a::attr(title)",
96
+ "price": Field(".price_color::text", transform=lambda p: float(p.lstrip("£"))),
97
+ "rating": Field("p.star-rating", attr="class", regex=r"star-rating (\w+)"),
98
+ })
99
+ page.extract({"titles": ["h3 a::attr(title)"]}) # a one-item list = all matches
100
+ ```
101
+
102
+ ### Survive layout changes
103
+
104
+ ```python
105
+ products = page.css(".product-card", adaptive=True)
106
+ ```
107
+
108
+ The first time, wintergrab saves a fingerprint of what matched: tag,
109
+ attributes, text, position, parent and neighbours. If the site later renames
110
+ `.product-card` or wraps it in new containers, the same call scores every
111
+ element on the new page and returns the closest matches. It logs a warning
112
+ so you know to update the selector. See [docs/adaptive-selectors.md](https://github.com/opensourcewinter/wintergrab/blob/main/docs/adaptive-selectors.md).
113
+
114
+ ### Scrape without writing selectors
115
+
116
+ ```python
117
+ page.auto_extract() # [{"title", "url", "image", "price", "rating"...}, ...] from the main record list
118
+ schema = page.learn({"title": "A Light in the Attic", "price": "£51.77"})
119
+ schema.extract(other_page) # the learned selectors work on every page of that template
120
+ page.structured_data() # JSON-LD, microdata, OpenGraph, meta tags
121
+ page.embedded_json() # __NEXT_DATA__, window.__INITIAL_STATE__, ... (SPAs without a browser)
122
+ page.tables() # every table as records
123
+ page.next_page() # pagination, auto-detected
124
+ ```
125
+
126
+ ### JavaScript pages
127
+
128
+ ```python
129
+ page = wg.render("https://quotes.toscrape.com/js/", wait_for=".quote")
130
+
131
+ with wg.BrowserFetcher(headless=True) as browser: # reuse one browser
132
+ page = browser.get(url, scroll=True, screenshot="page.png")
133
+ ```
134
+
135
+ ### Many pages at once
136
+
137
+ ```python
138
+ async with wg.AsyncFetcher() as fetcher:
139
+ pages = await fetcher.get_many(urls, concurrency=10)
140
+ ```
141
+
142
+ ### Crawl a site
143
+
144
+ ```python
145
+ from wintergrab import Spider
146
+
147
+ class BooksSpider(Spider):
148
+ start_urls = ["https://books.toscrape.com/"]
149
+ allowed_domains = ["books.toscrape.com"]
150
+ concurrency = 16 # AutoThrottle adapts the real speed per domain
151
+ crawl_dir = ".crawl/books" # makes it resumable: Ctrl+C pauses, re-run resumes
152
+ output = "books.jsonl" # items stream here (.jsonl / .json / .csv)
153
+
154
+ def parse(self, response):
155
+ for link in response.css("article.product_pod h3 a"):
156
+ yield response.follow(link, callback=self.parse_book)
157
+ yield from response.follow_all("li.next a")
158
+
159
+ def parse_book(self, response):
160
+ yield {
161
+ "title": response.css("h1::text").get(),
162
+ "price": response.css(".product_main .price_color::text").get(),
163
+ }
164
+
165
+ result = BooksSpider().run()
166
+ print(result.status, result.stats["pages"], result.stats["items"])
167
+ ```
168
+
169
+ Scaling up is a few attributes away:
170
+
171
+ ```python
172
+ class BigCrawl(Spider):
173
+ sitemap_urls = ["https://shop.example/robots.txt"] # discover pages from sitemaps
174
+ frontier = "disk" # flat memory for millions of URLs, crash-safe queue
175
+ crawl_dir = ".crawl/big"
176
+ cache = ".cache/big" # revalidating HTTP cache; cache_mode="offline" replays the crawl
177
+ output = "catalog.db" # SQLite...
178
+ unique_key = "url" # ...with upserts: re-crawls update rows in place
179
+ fallback_session = "browser" # blocked page? retry it in a headless browser, share its cookies
180
+ ```
181
+
182
+ Spiders also give you:
183
+
184
+ - **Sessions.** Route requests through different fetchers with
185
+ `Request(url, session="browser")`. Set `fallback_session="browser"` to
186
+ retry blocked pages in a headless browser automatically.
187
+ - **Proxy rotation.** `proxies = [...]` (or a `ProxyRotator`). Proxies that
188
+ keep failing are benched for a while.
189
+ - **Speed control.** Per-domain concurrency and delays that back off on
190
+ 429/503/block pages, honour `Retry-After` and robots.txt `Crawl-delay`,
191
+ and recover gradually.
192
+ - **Limits and hooks.** `max_pages`, `max_items`, `max_depth`,
193
+ `process_item()`, `on_error()`, `on_start()` / `on_close()`, and
194
+ `async for item in spider.stream()`.
195
+
196
+ ### Command line
197
+
198
+ ```bash
199
+ wintergrab get https://quotes.toscrape.com # page as Markdown
200
+ wintergrab get https://quotes.toscrape.com --css ".quote .text::text"
201
+ wintergrab get https://books.toscrape.com --each article.product_pod \
202
+ --field title="h3 a::attr(title)" --field price=.price_color::text -o books.csv
203
+ wintergrab get https://quotes.toscrape.com/js/ --browser --wait-for .quote
204
+
205
+ wintergrab get https://books.toscrape.com --auto # records, no selectors
206
+ wintergrab get https://books.toscrape.com --learn "title=A Light in the Attic" --save-schema books.json
207
+ wintergrab crawl https://books.toscrape.com --schema books.json --paginate -o books.jsonl
208
+ wintergrab get https://shop.example/p/1 --structured # JSON-LD, OpenGraph...
209
+
210
+ wintergrab crawl my_spider.py -o items.jsonl --crawl-dir .crawl/mine # run a spider file
211
+ wintergrab crawl https://books.toscrape.com --follow "li.next a" --follow "h3 a" \
212
+ --each ".product_main" --field title=h1::text --max-pages 50 -o books.jsonl
213
+
214
+ wintergrab shell https://quotes.toscrape.com # explore interactively
215
+ ```
216
+
217
+ ## Documentation
218
+
219
+ | Guide | What's inside |
220
+ |---|---|
221
+ | [Getting started](https://github.com/opensourcewinter/wintergrab/blob/main/docs/getting-started.md) | Install, first scrape, first spider, in 10 minutes |
222
+ | [Fetching](https://github.com/opensourcewinter/wintergrab/blob/main/docs/fetching.md) | `get`/`Fetcher`/`AsyncFetcher`/`BrowserFetcher`, options, errors |
223
+ | [Parsing](https://github.com/opensourcewinter/wintergrab/blob/main/docs/parsing.md) | Selectors, extraction schemas, text search, Markdown |
224
+ | [Adaptive selectors](https://github.com/opensourcewinter/wintergrab/blob/main/docs/adaptive-selectors.md) | How relocation works and how to tune it |
225
+ | [Spiders](https://github.com/opensourcewinter/wintergrab/blob/main/docs/spiders.md) | Crawling, sessions, pause/resume, output, every setting |
226
+ | [Power features](https://github.com/opensourcewinter/wintergrab/blob/main/docs/power-features.md) | Zero-selector extraction, cache & offline replay, API capture, cookie handoff, sitemaps, disk frontier, SQLite |
227
+ | [Tough sites](https://github.com/opensourcewinter/wintergrab/blob/main/docs/anti-blocking.md) | Impersonation, browsers, proxies, AutoThrottle, etiquette |
228
+ | [CLI](https://github.com/opensourcewinter/wintergrab/blob/main/docs/cli.md) | `get`, `crawl` and `shell` reference |
229
+ | [Examples](https://github.com/opensourcewinter/wintergrab/tree/main/examples/) | Runnable scripts for every feature |
230
+
231
+ ## Scrape responsibly
232
+
233
+ wintergrab makes polite crawling the default. Spiders obey robots.txt,
234
+ adapt their speed to each site, and back off when asked. Stealth features
235
+ exist so legitimate automation isn't misclassified. They don't make it OK
236
+ to ignore a site's terms, hammer servers, or collect personal data you
237
+ have no right to. Check the rules of each site you scrape.
238
+
239
+ ## Development
240
+
241
+ ```bash
242
+ python -m venv .venv && . .venv/bin/activate
243
+ pip install -e ".[dev]"
244
+ playwright install chromium # for the browser tests (skipped otherwise)
245
+ pytest # runs against a local test site; no internet needed
246
+ ruff check . && ruff format --check .
247
+ ```
248
+
249
+ See [CONTRIBUTING.md](https://github.com/opensourcewinter/wintergrab/blob/main/CONTRIBUTING.md) for the live tests and the release
250
+ process, and [SECURITY.md](https://github.com/opensourcewinter/wintergrab/blob/main/SECURITY.md) to report a vulnerability.
251
+
252
+ ## License
253
+
254
+ [MIT](https://github.com/opensourcewinter/wintergrab/blob/main/LICENSE)