scrapeflow-py 0.2.0__tar.gz → 0.4.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.
- scrapeflow_py-0.4.0/CHANGELOG.md +53 -0
- scrapeflow_py-0.4.0/MANIFEST.in +5 -0
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/PKG-INFO +427 -53
- scrapeflow_py-0.2.0/scrapeflow_py.egg-info/PKG-INFO → scrapeflow_py-0.4.0/README.md +395 -67
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/examples/__init__.py +0 -1
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/examples/advanced_example.py +5 -6
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/examples/authenticated_quotes_example.py +2 -1
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/examples/basic_usage.py +2 -1
- scrapeflow_py-0.4.0/examples/hybrid_extraction_example.py +71 -0
- scrapeflow_py-0.4.0/examples/llm_extraction_example.py +70 -0
- scrapeflow_py-0.4.0/examples/session_persistence_example.py +79 -0
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/examples/specification_driven_example.py +6 -4
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/examples/workflow_example.py +4 -4
- scrapeflow_py-0.4.0/pyproject.toml +107 -0
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/__init__.py +44 -22
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/anti_detection.py +18 -31
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/browser_runtime.py +19 -9
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/config.py +25 -16
- scrapeflow_py-0.4.0/scrapeflow/content_utils.py +48 -0
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/engine.py +47 -40
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/exceptions.py +0 -1
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/extractors.py +19 -29
- scrapeflow_py-0.4.0/scrapeflow/llm_extract.py +181 -0
- scrapeflow_py-0.4.0/scrapeflow/mcp_backend.py +240 -0
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/monitoring.py +13 -14
- scrapeflow_py-0.4.0/scrapeflow/pagination.py +86 -0
- scrapeflow_py-0.4.0/scrapeflow/ports.py +57 -0
- scrapeflow_py-0.4.0/scrapeflow/py.typed +0 -0
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/rate_limiter.py +4 -9
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/registry.py +20 -20
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/retry.py +9 -19
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/robots.py +1 -4
- scrapeflow_py-0.4.0/scrapeflow/specifications.py +321 -0
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/workflow.py +18 -18
- scrapeflow_py-0.2.0/README.md → scrapeflow_py-0.4.0/scrapeflow_py.egg-info/PKG-INFO +442 -34
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow_py.egg-info/SOURCES.txt +11 -3
- scrapeflow_py-0.4.0/scrapeflow_py.egg-info/requires.txt +15 -0
- scrapeflow_py-0.4.0/scrapeflow_py.egg-info/top_level.txt +1 -0
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/tests/test_config.py +5 -7
- scrapeflow_py-0.4.0/tests/test_content_utils.py +36 -0
- scrapeflow_py-0.4.0/tests/test_hybrid_extractor.py +47 -0
- scrapeflow_py-0.4.0/tests/test_release.py +207 -0
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/tests/test_robots.py +1 -0
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/tests/test_specifications.py +7 -5
- scrapeflow_py-0.2.0/MANIFEST.in +0 -4
- scrapeflow_py-0.2.0/pyproject.toml +0 -16
- scrapeflow_py-0.2.0/requirements.txt +0 -5
- scrapeflow_py-0.2.0/scrapeflow/mcp_backend.py +0 -144
- scrapeflow_py-0.2.0/scrapeflow/ports.py +0 -71
- scrapeflow_py-0.2.0/scrapeflow/specifications.py +0 -218
- scrapeflow_py-0.2.0/scrapeflow_py.egg-info/requires.txt +0 -4
- scrapeflow_py-0.2.0/scrapeflow_py.egg-info/top_level.txt +0 -3
- scrapeflow_py-0.2.0/setup.py +0 -36
- scrapeflow_py-0.2.0/tests/__init__.py +0 -2
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/LICENSE +0 -0
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/schema_library.py +0 -0
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow/workflow_executor.py +0 -0
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/scrapeflow_py.egg-info/dependency_links.txt +0 -0
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/setup.cfg +0 -0
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/tests/test_engine_orchestration.py +0 -0
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/tests/test_public_api_compat.py +0 -0
- {scrapeflow_py-0.2.0 → scrapeflow_py-0.4.0}/tests/test_registry.py +0 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.4.0] - 2026-10-31
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- LLM extraction works with current `mistralai` releases (2.x/3.x moved the client to
|
|
14
|
+
`mistralai.client.Mistral`). Previously every LLM call failed with a misleading
|
|
15
|
+
"mistralai is required" `ImportError` even though it was installed.
|
|
16
|
+
- The wheel no longer installs stray top-level `tests` and `examples` packages into
|
|
17
|
+
`site-packages`.
|
|
18
|
+
- `create_mcp_backend()` no longer silently returns a do-nothing backend: unknown backend names
|
|
19
|
+
raise `ValueError`, and the not-yet-implemented `scrapy_mcp` / `playwright_mcp` backends emit a
|
|
20
|
+
`UserWarning`.
|
|
21
|
+
- Step timeouts chain the original `asyncio.TimeoutError` (`raise ... from`).
|
|
22
|
+
- Dockerfile: browsers are installed where `PLAYWRIGHT_BROWSERS_PATH` points, and `pytest` is
|
|
23
|
+
available for the default `CMD` (the image previously could not run its own default command).
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- `py.typed` marker (PEP 561) and `Typing :: Typed` classifier.
|
|
27
|
+
- `__version__` is read from installed package metadata (`importlib.metadata`).
|
|
28
|
+
- Tests: mocked Mistral schema generation and extraction (code-fence handling, bad JSON, async,
|
|
29
|
+
new SDK layout), MCP backend factory, and real-browser tests against a local HTTP server
|
|
30
|
+
(spec-driven extraction and robots.txt enforcement). 36 tests total.
|
|
31
|
+
- CHANGELOG, SECURITY.md, CODE_OF_CONDUCT.md, issue/PR templates, Dependabot, CITATION.cff.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- Requires Python 3.10+ (3.8 and 3.9 are end-of-life); classifiers for 3.10 to 3.13.
|
|
35
|
+
- Packaging is `pyproject.toml`-only (removed `setup.py`, `pytest.ini`, `requirements*.txt`),
|
|
36
|
+
with PEP 639 `license = "MIT"` and a `dev` extra (pytest, black, ruff, mypy, build, twine).
|
|
37
|
+
- CI: lint (ruff + black) is enforced instead of `|| true`, tests run on Python 3.10 to 3.13 with
|
|
38
|
+
Chromium installed, and the Docker job now runs the test suite inside the image.
|
|
39
|
+
- Codebase formatted with black and cleaned with ruff (imports, modern type hints).
|
|
40
|
+
|
|
41
|
+
## [0.3.0] - 2026-03-09
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
- Hybrid extraction, Mistral LLM extraction, session persistence, content cleaning.
|
|
45
|
+
|
|
46
|
+
## [0.2.0] - 2026-03-09
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
- Specification-driven extraction, robots.txt compliance, ethical crawling, architecture refactor.
|
|
50
|
+
|
|
51
|
+
## [0.1.2] - 2025-11-27
|
|
52
|
+
|
|
53
|
+
- Initial public releases (0.1.0 to 0.1.2).
|
|
@@ -1,42 +1,67 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scrapeflow-py
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
License: MIT
|
|
8
|
-
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Opinionated Playwright scraping workflow engine: retries, rate limiting, anti-detection, robots.txt compliance, Pydantic spec-driven and LLM-powered extraction, sessions, and metrics.
|
|
5
|
+
Author-email: Irfan Ali <irfanali29@hotmail.com>
|
|
6
|
+
Maintainer-email: Irfan Ali <irfanali29@hotmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/irfanalidv/scrapeflow-py
|
|
9
|
+
Project-URL: Documentation, https://github.com/irfanalidv/scrapeflow-py#readme
|
|
10
|
+
Project-URL: Repository, https://github.com/irfanalidv/scrapeflow-py
|
|
11
|
+
Project-URL: Issues, https://github.com/irfanalidv/scrapeflow-py/issues
|
|
12
|
+
Project-URL: Changelog, https://github.com/irfanalidv/scrapeflow-py/blob/main/CHANGELOG.md
|
|
13
|
+
Keywords: web-scraping,scraping,scraper,crawler,playwright,browser-automation,workflow,data-extraction,llm-extraction,mistral,pydantic,rate-limiting,retry,robots-txt,anti-detection,asyncio
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
15
|
Classifier: Intended Audience :: Developers
|
|
10
|
-
Classifier:
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Framework :: AsyncIO
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
|
|
19
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
11
21
|
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
14
22
|
Classifier: Programming Language :: Python :: 3.10
|
|
15
23
|
Classifier: Programming Language :: Python :: 3.11
|
|
16
24
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
-
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Requires-Python: >=3.10
|
|
18
29
|
Description-Content-Type: text/markdown
|
|
19
30
|
License-File: LICENSE
|
|
20
31
|
Requires-Dist: playwright>=1.40.0
|
|
21
32
|
Requires-Dist: aiohttp>=3.9.0
|
|
22
33
|
Requires-Dist: tenacity>=8.2.0
|
|
23
34
|
Requires-Dist: pydantic>=2.0.0
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
Requires-Dist: mistralai>=1.0.0
|
|
36
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
37
|
+
Provides-Extra: dev
|
|
38
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
39
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
40
|
+
Requires-Dist: black>=24.0.0; extra == "dev"
|
|
41
|
+
Requires-Dist: ruff>=0.6.0; extra == "dev"
|
|
42
|
+
Requires-Dist: mypy>=1.7.0; extra == "dev"
|
|
43
|
+
Requires-Dist: build>=1.0.0; extra == "dev"
|
|
44
|
+
Requires-Dist: twine>=5.0.0; extra == "dev"
|
|
30
45
|
Dynamic: license-file
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Dynamic: summary
|
|
46
|
+
|
|
47
|
+
<div align="center">
|
|
34
48
|
|
|
35
49
|
# ScrapeFlow
|
|
36
50
|
|
|
37
|
-
**An opinionated scraping workflow engine built on Playwright
|
|
51
|
+
**An opinionated scraping workflow engine built on Playwright.**
|
|
52
|
+
|
|
53
|
+
Retries, rate limiting, anti-detection, robots.txt compliance, Pydantic spec-driven and
|
|
54
|
+
Mistral LLM-powered extraction, login sessions, workflows, and metrics in one async API.
|
|
55
|
+
|
|
56
|
+
[](https://pypi.org/project/scrapeflow-py/)
|
|
57
|
+
[](https://pypi.org/project/scrapeflow-py/)
|
|
58
|
+
[](https://github.com/irfanalidv/scrapeflow-py/actions/workflows/ci.yml)
|
|
59
|
+
[](https://pepy.tech/projects/scrapeflow-py)
|
|
60
|
+
[](https://github.com/irfanalidv/scrapeflow-py/blob/main/LICENSE)
|
|
61
|
+
[](https://github.com/psf/black)
|
|
62
|
+
[](https://playwright.dev/)
|
|
38
63
|
|
|
39
|
-
|
|
64
|
+
</div>
|
|
40
65
|
|
|
41
66
|
---
|
|
42
67
|
|
|
@@ -49,7 +74,12 @@ ScrapeFlow is a production-ready Python library that transforms Playwright into
|
|
|
49
74
|
- **⚖️ Ethical Crawling (GDPR/CCPA)**: Configurable data retention, anonymization, and consent options in the specification layer
|
|
50
75
|
- **📦 Component Registry**: Shared, versioned selectors, pagination handlers, and login flows—platform thinking over one-off scrapers
|
|
51
76
|
- **🔔 Monitoring & Alerting**: Alert callbacks on failure thresholds; rollback hooks for failed extraction runs
|
|
52
|
-
- **🔌
|
|
77
|
+
- **🔌 Pluggable Extraction Backends**: `MCPBackend` interface with a built-in Mistral LLM backend; subclass it to plug in an MCP server (Scrapy/Playwright MCP adapters are planned, not yet implemented)
|
|
78
|
+
- **🤖 Mistral LLM Extraction**: Generate schemas from natural language, extract without selectors—uses `MISTRAL_API_KEY`
|
|
79
|
+
- **🔄 Hybrid Extraction**: Selector-first, LLM fallback when validation fails—self-healing spiders
|
|
80
|
+
- **🛡️ Selector Fallback Chain**: Try multiple selectors per field (`[".price", ".cost"]`) for resilience
|
|
81
|
+
- **🫙 Session Persistence**: Save/load cookies and localStorage via `save_storage_state()` and `storage_state_path`
|
|
82
|
+
- **🧹 Content Cleaning**: Strip scripts/styles before LLM extraction for better accuracy and token usage
|
|
53
83
|
- **🔄 Intelligent Retry Logic**: Automatic retries with exponential backoff and jitter
|
|
54
84
|
- **⚡ Rate Limiting**: Token bucket algorithm to respect server limits
|
|
55
85
|
- **🕵️ Anti-Detection**: Stealth mode, user agent rotation, and proxy support
|
|
@@ -60,13 +90,16 @@ ScrapeFlow is a production-ready Python library that transforms Playwright into
|
|
|
60
90
|
- **📝 Type Hints**: Full type support for better IDE experience
|
|
61
91
|
|
|
62
92
|
<p align="center">
|
|
63
|
-
<img src="docs/scrapeflow-architecture.png" alt="ScrapeFlow Architecture" width="800">
|
|
93
|
+
<img src="https://raw.githubusercontent.com/irfanalidv/scrapeflow-py/main/docs/scrapeflow-architecture.png" alt="ScrapeFlow Architecture" width="800">
|
|
64
94
|
</p>
|
|
65
95
|
|
|
66
96
|
## 📦 Installation
|
|
67
97
|
|
|
98
|
+
Requires Python 3.10+.
|
|
99
|
+
|
|
68
100
|
```bash
|
|
69
101
|
pip install scrapeflow-py
|
|
102
|
+
playwright install chromium # download the browser Playwright drives
|
|
70
103
|
```
|
|
71
104
|
|
|
72
105
|
Or install from source:
|
|
@@ -75,17 +108,24 @@ Or install from source:
|
|
|
75
108
|
git clone https://github.com/irfanalidv/scrapeflow-py.git
|
|
76
109
|
cd scrapeflow-py
|
|
77
110
|
pip install -e .
|
|
111
|
+
playwright install chromium
|
|
78
112
|
```
|
|
79
113
|
|
|
80
|
-
|
|
114
|
+
On a fresh Linux machine or container, `playwright install --with-deps chromium` also installs
|
|
115
|
+
the system libraries Chromium needs. A `Dockerfile` is included.
|
|
81
116
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
117
|
+
## ⚙️ Configuration (environment variables)
|
|
118
|
+
|
|
119
|
+
| Variable | Purpose |
|
|
120
|
+
| --- | --- |
|
|
121
|
+
| `MISTRAL_API_KEY` | Required only for LLM features (`generate_schema_from_prompt`, `extract_with_schema`, `MistralLLMBackend`, `HybridExtractor` LLM fallback). Read from the environment or a `.env` file. |
|
|
122
|
+
|
|
123
|
+
Everything else (rate limits, retries, browser, anti-detection, robots.txt, ethical crawling) is
|
|
124
|
+
configured in code via `ScrapeFlowConfig`; see [Configuration](#configuration) below.
|
|
85
125
|
|
|
86
126
|
## 🎯 Real-World Use Cases
|
|
87
127
|
|
|
88
|
-
|
|
128
|
+
Typical use cases:
|
|
89
129
|
|
|
90
130
|
- **💰 E-commerce Price Monitoring** - Track competitor prices, monitor deals, and optimize pricing strategies
|
|
91
131
|
- **📰 News & Content Aggregation** - Collect articles from multiple sources for content platforms
|
|
@@ -371,6 +411,132 @@ print(f"💼 Found {len(quotes)} quotes across pages")
|
|
|
371
411
|
💼 Found 20 quotes across pages
|
|
372
412
|
```
|
|
373
413
|
|
|
414
|
+
### Use Case 5: LLM-Powered Extraction (No Selectors)
|
|
415
|
+
|
|
416
|
+
**Real-world scenario:** Extract structured data using Mistral AI—no CSS selectors, just natural language field descriptions. Ideal for pages with inconsistent markup or when you want semantic understanding.
|
|
417
|
+
|
|
418
|
+
**Requires:** `MISTRAL_API_KEY` in `.env` or environment.
|
|
419
|
+
|
|
420
|
+
```python
|
|
421
|
+
import asyncio
|
|
422
|
+
import os
|
|
423
|
+
|
|
424
|
+
try:
|
|
425
|
+
from dotenv import load_dotenv
|
|
426
|
+
load_dotenv()
|
|
427
|
+
except ImportError:
|
|
428
|
+
pass
|
|
429
|
+
|
|
430
|
+
from scrapeflow import ScrapeFlow, create_mcp_backend
|
|
431
|
+
from scrapeflow.config import ScrapeFlowConfig, EthicalCrawlingConfig
|
|
432
|
+
from scrapeflow.llm_extract import generate_schema_from_prompt, extract_with_schema
|
|
433
|
+
|
|
434
|
+
async def run_llm_extraction():
|
|
435
|
+
if not os.environ.get("MISTRAL_API_KEY"):
|
|
436
|
+
print("Set MISTRAL_API_KEY in .env to run this example")
|
|
437
|
+
return
|
|
438
|
+
|
|
439
|
+
config = ScrapeFlowConfig(
|
|
440
|
+
ethical_crawling=EthicalCrawlingConfig(respect_robots_txt=True),
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
async with ScrapeFlow(config) as scraper:
|
|
444
|
+
await scraper.navigate("https://quotes.toscrape.com/")
|
|
445
|
+
|
|
446
|
+
# Option 1: Generate schema from natural language
|
|
447
|
+
schema = generate_schema_from_prompt(
|
|
448
|
+
"Extract quote text, author name, and list of tags"
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# Option 2: Extract using schema + page content (no selectors!)
|
|
452
|
+
content = await scraper.page.evaluate("() => document.body.innerText")
|
|
453
|
+
data = extract_with_schema(
|
|
454
|
+
content,
|
|
455
|
+
schema,
|
|
456
|
+
prompt="Extract all quotes from this page with text, author, and tags",
|
|
457
|
+
)
|
|
458
|
+
print("LLM extracted:", data)
|
|
459
|
+
|
|
460
|
+
# Option 3: MistralLLMBackend—field descriptions only
|
|
461
|
+
backend = create_mcp_backend("mistral")
|
|
462
|
+
semantic_data = await backend.extract_with_semantics(
|
|
463
|
+
scraper.page,
|
|
464
|
+
field_descriptions={
|
|
465
|
+
"quote": "The quote text",
|
|
466
|
+
"author": "Author name",
|
|
467
|
+
"tags": "Comma-separated tags",
|
|
468
|
+
},
|
|
469
|
+
context="Extract the first quote on the page",
|
|
470
|
+
)
|
|
471
|
+
print("Semantic extraction:", semantic_data)
|
|
472
|
+
|
|
473
|
+
asyncio.run(run_llm_extraction())
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
**Real Output:**
|
|
477
|
+
|
|
478
|
+
```
|
|
479
|
+
Generated schema: {'type': 'object', 'properties': {'quote_text': {...}, 'author_name': {...}, 'tags': {...}}, 'required': ['quote_text', 'author_name', 'tags']}
|
|
480
|
+
|
|
481
|
+
LLM extracted: [{'quote_text': 'The world as we have created it is a process of our thinking...', 'author_name': 'Albert Einstein', 'tags': ['change', 'deep-thoughts', 'thinking', 'world']}, ...]
|
|
482
|
+
|
|
483
|
+
Semantic extraction: {'quote': 'The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.', 'author': 'Albert Einstein', 'tags': 'change, deep-thoughts, thinking, world'}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
**What you get:** Schema generation from prompts, extraction without brittle selectors, and semantic understanding of page content—powerful for dynamic or inconsistent sites.
|
|
487
|
+
|
|
488
|
+
### Use Case 6: Login + Session Persistence
|
|
489
|
+
|
|
490
|
+
**Real-world scenario:** Log in once, save cookies, then reuse the session on future runs—no re-login needed.
|
|
491
|
+
|
|
492
|
+
```python
|
|
493
|
+
import asyncio
|
|
494
|
+
import os
|
|
495
|
+
from scrapeflow import ScrapeFlow, SpecificationExtractor, get_registry, register_quotes_login_handler
|
|
496
|
+
from scrapeflow.config import ScrapeFlowConfig, EthicalCrawlingConfig, BrowserConfig
|
|
497
|
+
from scrapeflow.specifications import FieldSpec, ItemSpec
|
|
498
|
+
from pydantic import BaseModel
|
|
499
|
+
|
|
500
|
+
class QuoteItem(BaseModel):
|
|
501
|
+
text: str
|
|
502
|
+
author: str
|
|
503
|
+
|
|
504
|
+
class QuotesPage(BaseModel):
|
|
505
|
+
quotes: list[QuoteItem]
|
|
506
|
+
|
|
507
|
+
async def run():
|
|
508
|
+
state_file = "/tmp/scrapeflow_session.json"
|
|
509
|
+
schema = {"quotes": ItemSpec(items_selector=".quote", fields={"text": ".text", "author": ".author"})}
|
|
510
|
+
|
|
511
|
+
# Step 1: Login and save (if first run)
|
|
512
|
+
if not os.path.exists(state_file):
|
|
513
|
+
async with ScrapeFlow(ScrapeFlowConfig(ethical_crawling=EthicalCrawlingConfig(respect_robots_txt=True))) as scraper:
|
|
514
|
+
register_quotes_login_handler(get_registry())
|
|
515
|
+
handler = get_registry().get_login("quotes_login")
|
|
516
|
+
await scraper.login_with_handler("https://quotes.toscrape.com/login", "admin", "amdin", handler)
|
|
517
|
+
await scraper.save_storage_state(state_file)
|
|
518
|
+
|
|
519
|
+
# Step 2: Use saved session (no login)
|
|
520
|
+
config = ScrapeFlowConfig(ethical_crawling=EthicalCrawlingConfig(respect_robots_txt=True),
|
|
521
|
+
browser=BrowserConfig(storage_state_path=state_file))
|
|
522
|
+
async with ScrapeFlow(config) as scraper:
|
|
523
|
+
await scraper.navigate("https://quotes.toscrape.com/")
|
|
524
|
+
data = await SpecificationExtractor(QuotesPage, schema=schema).extract(scraper.page)
|
|
525
|
+
print(f"Extracted {len(data.quotes)} quotes (authenticated)")
|
|
526
|
+
|
|
527
|
+
asyncio.run(run())
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
**Real Output:**
|
|
531
|
+
|
|
532
|
+
```
|
|
533
|
+
Extracted 10 quotes (authenticated)
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
*(On first run: login + save; on subsequent runs: load session and extract without re-login.)*
|
|
537
|
+
|
|
538
|
+
**What you get:** `login_with_handler`, `save_storage_state`, `storage_state_path`—reusable authenticated sessions.
|
|
539
|
+
|
|
374
540
|
## 📚 Documentation
|
|
375
541
|
|
|
376
542
|
### Configuration
|
|
@@ -568,6 +734,40 @@ async with ScrapeFlow(config) as scraper:
|
|
|
568
734
|
# - Temporary blocks
|
|
569
735
|
```
|
|
570
736
|
|
|
737
|
+
### Pagination
|
|
738
|
+
|
|
739
|
+
**Use Case:** Scrape multiple pages (e.g. search results, product listings) with limits.
|
|
740
|
+
|
|
741
|
+
```python
|
|
742
|
+
import asyncio
|
|
743
|
+
from scrapeflow import ScrapeFlow
|
|
744
|
+
from scrapeflow.config import ScrapeFlowConfig, PaginationConfig
|
|
745
|
+
from scrapeflow.pagination import paginate
|
|
746
|
+
from scrapeflow.registry import get_registry
|
|
747
|
+
|
|
748
|
+
async def extract_quotes(page, context):
|
|
749
|
+
quotes = []
|
|
750
|
+
for i in range(await page.locator(".quote").count()):
|
|
751
|
+
q = page.locator(".quote").nth(i)
|
|
752
|
+
text = await q.locator(".text").text_content()
|
|
753
|
+
author = await q.locator(".author").text_content()
|
|
754
|
+
quotes.append({"text": text or "", "author": author or ""})
|
|
755
|
+
return quotes
|
|
756
|
+
|
|
757
|
+
async def main():
|
|
758
|
+
get_registry().register_pagination("quotes", next_selector="li.next a", has_next="li.next")
|
|
759
|
+
handler = get_registry().get_pagination("quotes")
|
|
760
|
+
config = PaginationConfig(max_pages=3, max_results=25)
|
|
761
|
+
|
|
762
|
+
async with ScrapeFlow(ScrapeFlowConfig()) as scraper:
|
|
763
|
+
async for page_data in paginate(scraper, "https://quotes.toscrape.com/", handler, extract_quotes, config):
|
|
764
|
+
print(f"Page: {len(page_data)} quotes")
|
|
765
|
+
for q in page_data[:2]:
|
|
766
|
+
print(f" - {q['author']}: {q['text'][:40]}...")
|
|
767
|
+
|
|
768
|
+
asyncio.run(main())
|
|
769
|
+
```
|
|
770
|
+
|
|
571
771
|
### Data Extraction
|
|
572
772
|
|
|
573
773
|
**Use Case:** Extracting structured data from [quotes.toscrape.com](https://quotes.toscrape.com/) and [books.toscrape.com](https://books.toscrape.com/).
|
|
@@ -798,13 +998,175 @@ asyncio.run(scrape_with_error_handling())
|
|
|
798
998
|
✅ Successfully scraped: Quotes to Scrape
|
|
799
999
|
```
|
|
800
1000
|
|
|
1001
|
+
## 📋 Feature Coverage—Nothing Hidden
|
|
1002
|
+
|
|
1003
|
+
Every ScrapeFlow feature is demonstrated somewhere. Quick reference:
|
|
1004
|
+
|
|
1005
|
+
| Feature | Where to See It |
|
|
1006
|
+
|---------|-----------------|
|
|
1007
|
+
| **Extractors** | Use Case 1, `basic_usage.py`, `advanced_example.py` |
|
|
1008
|
+
| **StructuredExtractor** | Use Case 2, `workflow_example.py`, `advanced_example.py` |
|
|
1009
|
+
| **SpecificationExtractor** | Use Case 6, Doc section, `specification_driven_example.py` |
|
|
1010
|
+
| **HybridExtractor** | `hybrid_extraction_example.py` |
|
|
1011
|
+
| **FieldSpec, ItemSpec** | Doc section, `specification_driven_example.py`, `hybrid_extraction_example.py` |
|
|
1012
|
+
| **Selector fallback** `[".a", ".b"]` | `hybrid_extraction_example.py` |
|
|
1013
|
+
| **LLM extraction** | Use Case 5, `llm_extraction_example.py` |
|
|
1014
|
+
| **Login** | Use Case 6, `authenticated_quotes_example.py`, `session_persistence_example.py` |
|
|
1015
|
+
| **Session persistence** | Use Case 6, `session_persistence_example.py` |
|
|
1016
|
+
| **Registry, LoginHandler** | `authenticated_quotes_example.py`, `specification_driven_example.py` |
|
|
1017
|
+
| **Workflow** | Use Case 2, `workflow_example.py` |
|
|
1018
|
+
| **Rate limit, Retry** | Use Cases 1, 4, `basic_usage.py`, `advanced_example.py` |
|
|
1019
|
+
| **Anti-detection** | Use Case 3, `advanced_example.py` |
|
|
1020
|
+
| **Ethical crawling, robots.txt** | `specification_driven_example.py`, `authenticated_quotes_example.py` |
|
|
1021
|
+
| **Pagination** | Doc section, `scrapeflow.pagination.paginate`, `PaginationConfig` |
|
|
1022
|
+
| **Content cleaning** | Used internally by MistralLLMBackend; `scrapeflow.content_utils` |
|
|
1023
|
+
|
|
801
1024
|
## 🎨 Complete Examples
|
|
802
1025
|
|
|
803
|
-
Check out the `examples/` directory
|
|
1026
|
+
Check out the `examples/` directory—each example showcases specific features:
|
|
1027
|
+
|
|
1028
|
+
| Example | Features Demonstrated |
|
|
1029
|
+
|---------|------------------------|
|
|
1030
|
+
| `basic_usage.py` | ScrapeFlow, Extractor, rate limit, retry, metrics |
|
|
1031
|
+
| `workflow_example.py` | Workflow, steps, on_success/on_error, StructuredExtractor |
|
|
1032
|
+
| `advanced_example.py` | Anti-detection, rate limit, retry, StructuredExtractor |
|
|
1033
|
+
| `specification_driven_example.py` | SpecificationExtractor, FieldSpec, ItemSpec, ethical crawling, registry |
|
|
1034
|
+
| `authenticated_quotes_example.py` | Login, `login_with_handler`, registry, LoginHandler |
|
|
1035
|
+
| `llm_extraction_example.py` | Mistral LLM, `generate_schema_from_prompt`, `extract_with_schema`, MistralLLMBackend *(requires `MISTRAL_API_KEY`)* |
|
|
1036
|
+
| `hybrid_extraction_example.py` | HybridExtractor, selector fallback chain `[".a", ".b"]`, LLM fallback |
|
|
1037
|
+
| `session_persistence_example.py` | `save_storage_state`, `storage_state_path`, skip login on reuse |
|
|
1038
|
+
|
|
1039
|
+
### Real Outputs from Examples
|
|
1040
|
+
|
|
1041
|
+
Outputs below are from running each example against live sites (quotes.toscrape.com, books.toscrape.com).
|
|
1042
|
+
|
|
1043
|
+
**`basic_usage.py`**
|
|
1044
|
+
|
|
1045
|
+
```
|
|
1046
|
+
Page title: Quotes to Scrape
|
|
1047
|
+
|
|
1048
|
+
Extracted 5 quotes:
|
|
1049
|
+
|
|
1050
|
+
1. "The world as we have created it is a process of our thinkin...
|
|
1051
|
+
Author: Albert Einstein
|
|
1052
|
+
Tags: change, deep-thoughts, thinking, world
|
|
1053
|
+
|
|
1054
|
+
2. "It is our choices, Harry, that show what we truly are, far ...
|
|
1055
|
+
Author: J.K. Rowling
|
|
1056
|
+
Tags: abilities, choices
|
|
1057
|
+
|
|
1058
|
+
3. "There are only two ways to live your life. One is as though...
|
|
1059
|
+
Author: Albert Einstein
|
|
1060
|
+
Tags: inspirational, life, live, miracle, miracles
|
|
1061
|
+
|
|
1062
|
+
4. "The person, be it gentleman or lady, who has not pleasure i...
|
|
1063
|
+
Author: Jane Austen
|
|
1064
|
+
Tags: aliteracy, books, classic, humor
|
|
1065
|
+
|
|
1066
|
+
5. "Imperfection is beauty, madness is genius and it's better t...
|
|
1067
|
+
Author: Marilyn Monroe
|
|
1068
|
+
Tags: be-yourself, inspirational
|
|
1069
|
+
|
|
1070
|
+
📊 Metrics:
|
|
1071
|
+
Success rate: 100.00%
|
|
1072
|
+
Total requests: 1
|
|
1073
|
+
Average response time: 1.72s
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
**`advanced_example.py`**
|
|
1077
|
+
|
|
1078
|
+
```
|
|
1079
|
+
📝 Extracted quotes:
|
|
1080
|
+
|
|
1081
|
+
1. "The world as we have created it is a process of our thinkin...
|
|
1082
|
+
Author: Albert Einstein
|
|
1083
|
+
Tags: change, deep-thoughts, thinking, world
|
|
1084
|
+
|
|
1085
|
+
2. "It is our choices, Harry, that show what we truly are, far ...
|
|
1086
|
+
Author: J.K. Rowling
|
|
1087
|
+
Tags: abilities, choices
|
|
1088
|
+
|
|
1089
|
+
3. "There are only two ways to live your life. One is as though...
|
|
1090
|
+
Author: Albert Einstein
|
|
1091
|
+
Tags: inspirational, life, live, miracle, miracles
|
|
1092
|
+
|
|
1093
|
+
4. "The person, be it gentleman or lady, who has not pleasure i...
|
|
1094
|
+
Author: Jane Austen
|
|
1095
|
+
Tags: aliteracy, books, classic, humor
|
|
1096
|
+
|
|
1097
|
+
5. "Imperfection is beauty, madness is genius and it's better t...
|
|
1098
|
+
Author: Marilyn Monroe
|
|
1099
|
+
Tags: be-yourself, inspirational
|
|
1100
|
+
|
|
1101
|
+
Scraping completed:
|
|
1102
|
+
Success rate: 100.00%
|
|
1103
|
+
Total requests: 1
|
|
1104
|
+
Retries: 0
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
**`workflow_example.py`**
|
|
804
1108
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
-
|
|
1109
|
+
```
|
|
1110
|
+
💾 Saving 20 books to database...
|
|
1111
|
+
- A Light in the ...... - £51.77
|
|
1112
|
+
- Tipping the Velvet... - £53.74
|
|
1113
|
+
- Soumission... - £50.10
|
|
1114
|
+
|
|
1115
|
+
✅ Workflow completed successfully!
|
|
1116
|
+
📚 Extracted 20 books
|
|
1117
|
+
|
|
1118
|
+
📊 Metrics:
|
|
1119
|
+
Total requests: 3
|
|
1120
|
+
Success rate: 100.00%
|
|
1121
|
+
Average response time: 5.85s
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
**`specification_driven_example.py`**
|
|
1125
|
+
|
|
1126
|
+
```
|
|
1127
|
+
Extracted 10 items (validated via Pydantic)
|
|
1128
|
+
1. "The world as we have created it is a process of o... | Albert Einstein
|
|
1129
|
+
2. "It is our choices, Harry, that show what we truly... | J.K. Rowling
|
|
1130
|
+
3. "There are only two ways to live your life. One is... | Albert Einstein
|
|
1131
|
+
|
|
1132
|
+
Metrics: 1 requests, 100.0% success
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1135
|
+
**`authenticated_quotes_example.py`**
|
|
1136
|
+
|
|
1137
|
+
```
|
|
1138
|
+
Authenticated login: True
|
|
1139
|
+
Quotes extracted: 10
|
|
1140
|
+
1. Albert Einstein: "The world as we have created it is a process of our thinking. It cannot be chan...
|
|
1141
|
+
2. J.K. Rowling: "It is our choices, Harry, that show what we truly are, far more than our abilit...
|
|
1142
|
+
3. Albert Einstein: "There are only two ways to live your life. One is as though nothing is a miracl...
|
|
1143
|
+
```
|
|
1144
|
+
|
|
1145
|
+
**`hybrid_extraction_example.py`**
|
|
1146
|
+
|
|
1147
|
+
```
|
|
1148
|
+
Extracted (selector or LLM):
|
|
1149
|
+
Title: A Light in the Attic
|
|
1150
|
+
Price: £51.77
|
|
1151
|
+
Availability: In stock (22 available)
|
|
1152
|
+
```
|
|
1153
|
+
|
|
1154
|
+
**`llm_extraction_example.py`** *(requires MISTRAL_API_KEY)*
|
|
1155
|
+
|
|
1156
|
+
```
|
|
1157
|
+
Generated schema: {'type': 'object', 'properties': {'quote_text': {'type': 'string', 'description': 'The text of the quote'}, 'author_name': {'type': 'string', 'description': 'The name of the author of the quote'}, 'tags': {'type': 'array', 'description': 'A list of tags associated with the quote'}}, 'required': ['quote_text', 'author_name', 'tags']}
|
|
1158
|
+
|
|
1159
|
+
LLM extracted: [{'quote_text': 'The world as we have created it is a process of our thinking...', 'author_name': 'Albert Einstein', 'tags': ['change', 'deep-thoughts', 'thinking', 'world']}, ...]
|
|
1160
|
+
|
|
1161
|
+
Semantic extraction: {'quote': 'The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.', 'author': 'Albert Einstein', 'tags': 'change, deep-thoughts, thinking, world'}
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
**`session_persistence_example.py`**
|
|
1165
|
+
|
|
1166
|
+
```
|
|
1167
|
+
Session saved to /tmp/scrapeflow_quotes_session.json
|
|
1168
|
+
Loaded session: extracted 10 quotes (authenticated)
|
|
1169
|
+
```
|
|
808
1170
|
|
|
809
1171
|
### Example: Complete Book Scraper with All Features
|
|
810
1172
|
|
|
@@ -896,30 +1258,42 @@ ScrapeFlow is built with a modular architecture:
|
|
|
896
1258
|
|
|
897
1259
|
```
|
|
898
1260
|
scrapeflow/
|
|
899
|
-
├── engine.py
|
|
900
|
-
├── ports.py
|
|
901
|
-
├── browser_runtime.py
|
|
902
|
-
├── workflow.py
|
|
1261
|
+
├── engine.py # Main ScrapeFlow engine
|
|
1262
|
+
├── ports.py # Protocols for dependency inversion
|
|
1263
|
+
├── browser_runtime.py # Playwright runtime adapter
|
|
1264
|
+
├── workflow.py # Workflow definition entities
|
|
903
1265
|
├── workflow_executor.py # Workflow execution service
|
|
904
|
-
├── config.py
|
|
905
|
-
├── specifications.py
|
|
906
|
-
├── schema_library.py
|
|
907
|
-
├──
|
|
908
|
-
├──
|
|
909
|
-
├──
|
|
910
|
-
├──
|
|
911
|
-
├──
|
|
912
|
-
├──
|
|
913
|
-
├──
|
|
914
|
-
├──
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1266
|
+
├── config.py # Configuration (EthicalCrawling, Pagination, etc.)
|
|
1267
|
+
├── specifications.py # SpecificationExtractor, HybridExtractor, FieldSpec
|
|
1268
|
+
├── schema_library.py # Reusable schema definitions
|
|
1269
|
+
├── extractors.py # Extractor, StructuredExtractor
|
|
1270
|
+
├── llm_extract.py # Mistral LLM schema + extraction
|
|
1271
|
+
├── content_utils.py # HTML cleaning for LLM
|
|
1272
|
+
├── mcp_backend.py # MCPBackend, MistralLLMBackend
|
|
1273
|
+
├── pagination.py # paginate() helper
|
|
1274
|
+
├── robots.py # robots.txt parsing and enforcement
|
|
1275
|
+
├── registry.py # Selectors, login handlers, pagination
|
|
1276
|
+
├── anti_detection.py # Stealth mode, user agent rotation
|
|
1277
|
+
├── rate_limiter.py # Rate limiting implementation
|
|
1278
|
+
├── retry.py # Retry logic and error classification
|
|
1279
|
+
├── monitoring.py # Metrics, logging, alerting
|
|
1280
|
+
└── exceptions.py # Custom exceptions
|
|
1281
|
+
```
|
|
1282
|
+
|
|
1283
|
+
For deeper design details, see [`ARCHITECTURE.md`](https://github.com/irfanalidv/scrapeflow-py/blob/main/ARCHITECTURE.md). Release notes are in [`CHANGELOG.md`](https://github.com/irfanalidv/scrapeflow-py/blob/main/CHANGELOG.md).
|
|
919
1284
|
|
|
920
1285
|
## 🤝 Contributing
|
|
921
1286
|
|
|
922
|
-
Contributions are welcome!
|
|
1287
|
+
Contributions are welcome! See [CONTRIBUTING.md](https://github.com/irfanalidv/scrapeflow-py/blob/main/CONTRIBUTING.md)
|
|
1288
|
+
and the [Code of Conduct](https://github.com/irfanalidv/scrapeflow-py/blob/main/CODE_OF_CONDUCT.md).
|
|
1289
|
+
|
|
1290
|
+
```bash
|
|
1291
|
+
git clone https://github.com/irfanalidv/scrapeflow-py.git && cd scrapeflow-py
|
|
1292
|
+
pip install -e ".[dev]" && playwright install chromium
|
|
1293
|
+
ruff check . && black --check . && pytest
|
|
1294
|
+
```
|
|
1295
|
+
|
|
1296
|
+
Security issues: please follow [SECURITY.md](https://github.com/irfanalidv/scrapeflow-py/blob/main/SECURITY.md).
|
|
923
1297
|
|
|
924
1298
|
1. Fork the repository
|
|
925
1299
|
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
|
@@ -929,7 +1303,7 @@ Contributions are welcome! Please feel free to submit a Pull Request.
|
|
|
929
1303
|
|
|
930
1304
|
## 📝 License
|
|
931
1305
|
|
|
932
|
-
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
1306
|
+
This project is licensed under the MIT License - see the [LICENSE](https://github.com/irfanalidv/scrapeflow-py/blob/main/LICENSE) file for details.
|
|
933
1307
|
|
|
934
1308
|
## 🙏 Acknowledgments
|
|
935
1309
|
|