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.
- wintergrab-0.2.0/.gitignore +31 -0
- wintergrab-0.2.0/CHANGELOG.md +61 -0
- wintergrab-0.2.0/LICENSE +21 -0
- wintergrab-0.2.0/PKG-INFO +303 -0
- wintergrab-0.2.0/README.md +254 -0
- wintergrab-0.2.0/benchmarks/README.md +233 -0
- wintergrab-0.2.0/docs/adaptive-selectors.md +112 -0
- wintergrab-0.2.0/docs/anti-blocking.md +160 -0
- wintergrab-0.2.0/docs/cli.md +137 -0
- wintergrab-0.2.0/docs/fetching.md +157 -0
- wintergrab-0.2.0/docs/getting-started.md +142 -0
- wintergrab-0.2.0/docs/parsing.md +155 -0
- wintergrab-0.2.0/docs/power-features.md +258 -0
- wintergrab-0.2.0/docs/spiders.md +273 -0
- wintergrab-0.2.0/examples/01_quickstart.py +47 -0
- wintergrab-0.2.0/examples/02_extract_to_csv.py +39 -0
- wintergrab-0.2.0/examples/03_adaptive_selectors.py +60 -0
- wintergrab-0.2.0/examples/04_async_many_pages.py +33 -0
- wintergrab-0.2.0/examples/05_quotes_spider.py +43 -0
- wintergrab-0.2.0/examples/06_books_resumable.py +72 -0
- wintergrab-0.2.0/examples/07_browser_rendering.py +31 -0
- wintergrab-0.2.0/examples/08_sessions_and_fallback.py +43 -0
- wintergrab-0.2.0/examples/09_zero_selector.py +39 -0
- wintergrab-0.2.0/examples/10_big_crawl.py +57 -0
- wintergrab-0.2.0/examples/README.md +26 -0
- wintergrab-0.2.0/pyproject.toml +107 -0
- wintergrab-0.2.0/src/wintergrab/__init__.py +91 -0
- wintergrab-0.2.0/src/wintergrab/__main__.py +5 -0
- wintergrab-0.2.0/src/wintergrab/adaptive/__init__.py +16 -0
- wintergrab-0.2.0/src/wintergrab/adaptive/fingerprint.py +286 -0
- wintergrab-0.2.0/src/wintergrab/adaptive/storage.py +142 -0
- wintergrab-0.2.0/src/wintergrab/cli.py +786 -0
- wintergrab-0.2.0/src/wintergrab/errors.py +77 -0
- wintergrab-0.2.0/src/wintergrab/fetchers/__init__.py +143 -0
- wintergrab-0.2.0/src/wintergrab/fetchers/blocking.py +63 -0
- wintergrab-0.2.0/src/wintergrab/fetchers/browser.py +897 -0
- wintergrab-0.2.0/src/wintergrab/fetchers/cache.py +407 -0
- wintergrab-0.2.0/src/wintergrab/fetchers/http.py +603 -0
- wintergrab-0.2.0/src/wintergrab/fetchers/response.py +404 -0
- wintergrab-0.2.0/src/wintergrab/parser/__init__.py +12 -0
- wintergrab-0.2.0/src/wintergrab/parser/autoextract.py +1440 -0
- wintergrab-0.2.0/src/wintergrab/parser/css.py +136 -0
- wintergrab-0.2.0/src/wintergrab/parser/extract.py +117 -0
- wintergrab-0.2.0/src/wintergrab/parser/selector.py +915 -0
- wintergrab-0.2.0/src/wintergrab/parser/structured.py +889 -0
- wintergrab-0.2.0/src/wintergrab/parser/text.py +331 -0
- wintergrab-0.2.0/src/wintergrab/proxy.py +200 -0
- wintergrab-0.2.0/src/wintergrab/py.typed +0 -0
- wintergrab-0.2.0/src/wintergrab/request.py +171 -0
- wintergrab-0.2.0/src/wintergrab/sitemaps.py +201 -0
- wintergrab-0.2.0/src/wintergrab/spider/__init__.py +8 -0
- wintergrab-0.2.0/src/wintergrab/spider/checkpoint.py +65 -0
- wintergrab-0.2.0/src/wintergrab/spider/engine.py +931 -0
- wintergrab-0.2.0/src/wintergrab/spider/exporters.py +376 -0
- wintergrab-0.2.0/src/wintergrab/spider/frontier.py +803 -0
- wintergrab-0.2.0/src/wintergrab/spider/progress.py +145 -0
- wintergrab-0.2.0/src/wintergrab/spider/robots.py +77 -0
- wintergrab-0.2.0/src/wintergrab/spider/scheduler.py +111 -0
- wintergrab-0.2.0/src/wintergrab/spider/sessions.py +73 -0
- wintergrab-0.2.0/src/wintergrab/spider/spider.py +448 -0
- wintergrab-0.2.0/src/wintergrab/spider/throttle.py +165 -0
- wintergrab-0.2.0/src/wintergrab/utils.py +187 -0
- wintergrab-0.2.0/tests/conftest.py +73 -0
- wintergrab-0.2.0/tests/test_adaptive.py +145 -0
- wintergrab-0.2.0/tests/test_autoextract.py +550 -0
- wintergrab-0.2.0/tests/test_browser.py +125 -0
- wintergrab-0.2.0/tests/test_cache.py +131 -0
- wintergrab-0.2.0/tests/test_cli.py +170 -0
- wintergrab-0.2.0/tests/test_crawl_tools.py +129 -0
- wintergrab-0.2.0/tests/test_disk_frontier.py +123 -0
- wintergrab-0.2.0/tests/test_examples.py +137 -0
- wintergrab-0.2.0/tests/test_fast_paths.py +140 -0
- wintergrab-0.2.0/tests/test_frontier.py +489 -0
- wintergrab-0.2.0/tests/test_http_fetcher.py +192 -0
- wintergrab-0.2.0/tests/test_live.py +107 -0
- wintergrab-0.2.0/tests/test_parser.py +201 -0
- wintergrab-0.2.0/tests/test_proxy.py +82 -0
- wintergrab-0.2.0/tests/test_regressions.py +518 -0
- wintergrab-0.2.0/tests/test_resume.py +146 -0
- wintergrab-0.2.0/tests/test_spider.py +397 -0
- wintergrab-0.2.0/tests/test_structured.py +595 -0
- wintergrab-0.2.0/tests/test_text.py +53 -0
- wintergrab-0.2.0/tests/test_throttle_scheduler.py +107 -0
- wintergrab-0.2.0/tests/test_zero_selector.py +104 -0
- 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`.
|
wintergrab-0.2.0/LICENSE
ADDED
|
@@ -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)
|