scrapeflow-py 0.1.2__tar.gz → 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.
- {scrapeflow_py-0.1.2/scrapeflow_py.egg-info → scrapeflow_py-0.2.0}/PKG-INFO +83 -15
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/README.md +81 -14
- scrapeflow_py-0.2.0/examples/authenticated_quotes_example.py +77 -0
- scrapeflow_py-0.2.0/examples/specification_driven_example.py +80 -0
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/requirements.txt +1 -0
- scrapeflow_py-0.2.0/scrapeflow/__init__.py +71 -0
- scrapeflow_py-0.2.0/scrapeflow/browser_runtime.py +96 -0
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow/config.py +21 -0
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow/engine.py +106 -98
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow/exceptions.py +6 -0
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow/extractors.py +13 -10
- scrapeflow_py-0.2.0/scrapeflow/mcp_backend.py +144 -0
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow/monitoring.py +54 -3
- scrapeflow_py-0.2.0/scrapeflow/ports.py +71 -0
- scrapeflow_py-0.2.0/scrapeflow/registry.py +189 -0
- scrapeflow_py-0.2.0/scrapeflow/robots.py +113 -0
- scrapeflow_py-0.2.0/scrapeflow/schema_library.py +49 -0
- scrapeflow_py-0.2.0/scrapeflow/specifications.py +218 -0
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow/workflow.py +5 -49
- scrapeflow_py-0.2.0/scrapeflow/workflow_executor.py +49 -0
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0/scrapeflow_py.egg-info}/PKG-INFO +95 -15
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow_py.egg-info/SOURCES.txt +16 -1
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow_py.egg-info/requires.txt +1 -0
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/setup.py +2 -1
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/tests/test_config.py +13 -0
- scrapeflow_py-0.2.0/tests/test_engine_orchestration.py +125 -0
- scrapeflow_py-0.2.0/tests/test_public_api_compat.py +18 -0
- scrapeflow_py-0.2.0/tests/test_registry.py +16 -0
- scrapeflow_py-0.2.0/tests/test_robots.py +20 -0
- scrapeflow_py-0.2.0/tests/test_specifications.py +48 -0
- scrapeflow_py-0.1.2/scrapeflow/__init__.py +0 -30
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/LICENSE +0 -0
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/MANIFEST.in +0 -0
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/examples/__init__.py +0 -0
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/examples/advanced_example.py +0 -0
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/examples/basic_usage.py +0 -0
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/examples/workflow_example.py +0 -0
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/pyproject.toml +0 -0
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow/anti_detection.py +0 -0
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow/rate_limiter.py +0 -0
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow/retry.py +0 -0
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow_py.egg-info/dependency_links.txt +0 -0
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/scrapeflow_py.egg-info/top_level.txt +0 -0
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/setup.cfg +0 -0
- {scrapeflow_py-0.1.2 → scrapeflow_py-0.2.0}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scrapeflow-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: An opinionated scraping workflow engine built on Playwright
|
|
5
5
|
Home-page: https://github.com/irfanalidv/scrapeflow-py
|
|
6
6
|
Author: Irfan Ali
|
|
@@ -20,6 +20,7 @@ License-File: LICENSE
|
|
|
20
20
|
Requires-Dist: playwright>=1.40.0
|
|
21
21
|
Requires-Dist: aiohttp>=3.9.0
|
|
22
22
|
Requires-Dist: tenacity>=8.2.0
|
|
23
|
+
Requires-Dist: pydantic>=2.0.0
|
|
23
24
|
Dynamic: author
|
|
24
25
|
Dynamic: classifier
|
|
25
26
|
Dynamic: description
|
|
@@ -35,17 +36,7 @@ Dynamic: summary
|
|
|
35
36
|
|
|
36
37
|
**An opinionated scraping workflow engine built on Playwright**
|
|
37
38
|
|
|
38
|
-
[](https://github.com/irfanalidv/scrapeflow-py/blob/main/LICENSE)
|
|
39
|
-
|
|
40
|
-
[](https://pypi.org/project/scrapeflow-py/)
|
|
41
|
-
|
|
42
|
-
[](https://pepy.tech/projects/scrapeflow-py)
|
|
43
|
-
|
|
44
|
-
[](https://www.python.org/)
|
|
45
|
-
|
|
46
|
-
[](https://playwright.dev/)
|
|
47
|
-
|
|
48
|
-
[](https://github.com/irfanalidv/scrapeflow-py)
|
|
39
|
+
[](https://github.com/irfanalidv/scrapeflow-py/blob/main/LICENSE) [](https://pypi.org/project/scrapeflow-py/) [](https://pepy.tech/projects/scrapeflow-py) [](https://www.python.org/) [](https://playwright.dev/) [](https://github.com/irfanalidv/scrapeflow-py)
|
|
49
40
|
|
|
50
41
|
---
|
|
51
42
|
|
|
@@ -53,6 +44,12 @@ ScrapeFlow is a production-ready Python library that transforms Playwright into
|
|
|
53
44
|
|
|
54
45
|
## 🚀 Features
|
|
55
46
|
|
|
47
|
+
- **📋 Specification-Driven Extraction**: Declarative Pydantic models define fields, types, and validation—decouple field definitions from page structure
|
|
48
|
+
- **🤖 robots.txt Compliance**: Built-in robots.txt parsing and enforcement; ethical crawling by design
|
|
49
|
+
- **⚖️ Ethical Crawling (GDPR/CCPA)**: Configurable data retention, anonymization, and consent options in the specification layer
|
|
50
|
+
- **📦 Component Registry**: Shared, versioned selectors, pagination handlers, and login flows—platform thinking over one-off scrapers
|
|
51
|
+
- **🔔 Monitoring & Alerting**: Alert callbacks on failure thresholds; rollback hooks for failed extraction runs
|
|
52
|
+
- **🔌 MCP Extensibility**: Pluggable backends for Scrapy MCP Server, Playwright MCP, or LLM-based semantic extraction
|
|
56
53
|
- **🔄 Intelligent Retry Logic**: Automatic retries with exponential backoff and jitter
|
|
57
54
|
- **⚡ Rate Limiting**: Token bucket algorithm to respect server limits
|
|
58
55
|
- **🕵️ Anti-Detection**: Stealth mode, user agent rotation, and proxy support
|
|
@@ -62,6 +59,10 @@ ScrapeFlow is a production-ready Python library that transforms Playwright into
|
|
|
62
59
|
- **🔧 Error Handling**: Comprehensive error classification and recovery
|
|
63
60
|
- **📝 Type Hints**: Full type support for better IDE experience
|
|
64
61
|
|
|
62
|
+
<p align="center">
|
|
63
|
+
<img src="docs/scrapeflow-architecture.png" alt="ScrapeFlow Architecture" width="800">
|
|
64
|
+
</p>
|
|
65
|
+
|
|
65
66
|
## 📦 Installation
|
|
66
67
|
|
|
67
68
|
```bash
|
|
@@ -414,6 +415,63 @@ config = ScrapeFlowConfig(
|
|
|
414
415
|
)
|
|
415
416
|
```
|
|
416
417
|
|
|
418
|
+
### Specification-Driven Extraction (Pydantic)
|
|
419
|
+
|
|
420
|
+
**Use Case:** Declarative extraction with validation—fields, types, and rules in specs, not fragile XPaths.
|
|
421
|
+
|
|
422
|
+
```python
|
|
423
|
+
from pydantic import BaseModel
|
|
424
|
+
from scrapeflow import ScrapeFlow, SpecificationExtractor
|
|
425
|
+
from scrapeflow.specifications import FieldSpec, ItemSpec, ProductPriceSpec
|
|
426
|
+
|
|
427
|
+
# Model for list of products
|
|
428
|
+
class BookListing(BaseModel):
|
|
429
|
+
books: list[ProductPriceSpec]
|
|
430
|
+
|
|
431
|
+
# Schema maps fields to selectors
|
|
432
|
+
schema = {
|
|
433
|
+
"books": ItemSpec(
|
|
434
|
+
items_selector="article.product_pod",
|
|
435
|
+
fields={
|
|
436
|
+
"title": FieldSpec(selector="h3 a"),
|
|
437
|
+
"price": FieldSpec(selector=".price_color"),
|
|
438
|
+
"availability": FieldSpec(selector=".instock.availability", default=""),
|
|
439
|
+
"url": FieldSpec(selector="h3 a", type="attribute", attribute="href"),
|
|
440
|
+
},
|
|
441
|
+
)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async with ScrapeFlow() as scraper:
|
|
445
|
+
await scraper.navigate("https://books.toscrape.com/")
|
|
446
|
+
extractor = SpecificationExtractor(BookListing, schema=schema)
|
|
447
|
+
# Extract and validate in one step
|
|
448
|
+
data = await extractor.extract(scraper.page)
|
|
449
|
+
for book in data.books:
|
|
450
|
+
print(f"{book.title}: {book.price}")
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### Ethical Crawling & robots.txt
|
|
454
|
+
|
|
455
|
+
**Use Case:** GDPR/CCPA compliance and robots.txt respect built into the specification layer.
|
|
456
|
+
|
|
457
|
+
```python
|
|
458
|
+
from scrapeflow import ScrapeFlow
|
|
459
|
+
from scrapeflow.config import ScrapeFlowConfig, EthicalCrawlingConfig
|
|
460
|
+
|
|
461
|
+
config = ScrapeFlowConfig(
|
|
462
|
+
ethical_crawling=EthicalCrawlingConfig(
|
|
463
|
+
respect_robots_txt=True, # Check robots.txt before each request
|
|
464
|
+
user_agent_for_robots="ScrapeFlow",
|
|
465
|
+
anonymize_ip=True, # GDPR: minimize personal data
|
|
466
|
+
data_retention_days=30, # Document retention policy
|
|
467
|
+
)
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
async with ScrapeFlow(config) as scraper:
|
|
471
|
+
# navigate() automatically checks robots.txt
|
|
472
|
+
await scraper.navigate("https://example.com/page")
|
|
473
|
+
```
|
|
474
|
+
|
|
417
475
|
### Anti-Detection
|
|
418
476
|
|
|
419
477
|
**Use Case:** Scraping protected e-commerce sites that block automated access.
|
|
@@ -839,16 +897,26 @@ ScrapeFlow is built with a modular architecture:
|
|
|
839
897
|
```
|
|
840
898
|
scrapeflow/
|
|
841
899
|
├── engine.py # Main ScrapeFlow engine
|
|
842
|
-
├──
|
|
843
|
-
├──
|
|
900
|
+
├── ports.py # Protocols for dependency inversion
|
|
901
|
+
├── browser_runtime.py # Playwright runtime adapter
|
|
902
|
+
├── workflow.py # Workflow definition entities
|
|
903
|
+
├── workflow_executor.py # Workflow execution service
|
|
904
|
+
├── config.py # Configuration classes (incl. EthicalCrawlingConfig)
|
|
905
|
+
├── specifications.py # Pydantic specification-driven extraction
|
|
906
|
+
├── schema_library.py # Reusable schema definitions
|
|
907
|
+
├── robots.py # robots.txt parsing and enforcement
|
|
908
|
+
├── registry.py # Shared selector/component registry
|
|
909
|
+
├── mcp_backend.py # MCP integration extensibility
|
|
844
910
|
├── anti_detection.py # Anti-detection utilities
|
|
845
911
|
├── rate_limiter.py # Rate limiting implementation
|
|
846
912
|
├── retry.py # Retry logic and error classification
|
|
847
|
-
├── monitoring.py # Metrics
|
|
913
|
+
├── monitoring.py # Metrics, logging, alerting
|
|
848
914
|
├── extractors.py # Data extraction utilities
|
|
849
915
|
└── exceptions.py # Custom exceptions
|
|
850
916
|
```
|
|
851
917
|
|
|
918
|
+
For deeper design details, see [`ARCHITECTURE.md`](ARCHITECTURE.md).
|
|
919
|
+
|
|
852
920
|
## 🤝 Contributing
|
|
853
921
|
|
|
854
922
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
@@ -2,17 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**An opinionated scraping workflow engine built on Playwright**
|
|
4
4
|
|
|
5
|
-
[](https://github.com/irfanalidv/scrapeflow-py/blob/main/LICENSE)
|
|
6
|
-
|
|
7
|
-
[](https://pypi.org/project/scrapeflow-py/)
|
|
8
|
-
|
|
9
|
-
[](https://pepy.tech/projects/scrapeflow-py)
|
|
10
|
-
|
|
11
|
-
[](https://www.python.org/)
|
|
12
|
-
|
|
13
|
-
[](https://playwright.dev/)
|
|
14
|
-
|
|
15
|
-
[](https://github.com/irfanalidv/scrapeflow-py)
|
|
5
|
+
[](https://github.com/irfanalidv/scrapeflow-py/blob/main/LICENSE) [](https://pypi.org/project/scrapeflow-py/) [](https://pepy.tech/projects/scrapeflow-py) [](https://www.python.org/) [](https://playwright.dev/) [](https://github.com/irfanalidv/scrapeflow-py)
|
|
16
6
|
|
|
17
7
|
---
|
|
18
8
|
|
|
@@ -20,6 +10,12 @@ ScrapeFlow is a production-ready Python library that transforms Playwright into
|
|
|
20
10
|
|
|
21
11
|
## 🚀 Features
|
|
22
12
|
|
|
13
|
+
- **📋 Specification-Driven Extraction**: Declarative Pydantic models define fields, types, and validation—decouple field definitions from page structure
|
|
14
|
+
- **🤖 robots.txt Compliance**: Built-in robots.txt parsing and enforcement; ethical crawling by design
|
|
15
|
+
- **⚖️ Ethical Crawling (GDPR/CCPA)**: Configurable data retention, anonymization, and consent options in the specification layer
|
|
16
|
+
- **📦 Component Registry**: Shared, versioned selectors, pagination handlers, and login flows—platform thinking over one-off scrapers
|
|
17
|
+
- **🔔 Monitoring & Alerting**: Alert callbacks on failure thresholds; rollback hooks for failed extraction runs
|
|
18
|
+
- **🔌 MCP Extensibility**: Pluggable backends for Scrapy MCP Server, Playwright MCP, or LLM-based semantic extraction
|
|
23
19
|
- **🔄 Intelligent Retry Logic**: Automatic retries with exponential backoff and jitter
|
|
24
20
|
- **⚡ Rate Limiting**: Token bucket algorithm to respect server limits
|
|
25
21
|
- **🕵️ Anti-Detection**: Stealth mode, user agent rotation, and proxy support
|
|
@@ -29,6 +25,10 @@ ScrapeFlow is a production-ready Python library that transforms Playwright into
|
|
|
29
25
|
- **🔧 Error Handling**: Comprehensive error classification and recovery
|
|
30
26
|
- **📝 Type Hints**: Full type support for better IDE experience
|
|
31
27
|
|
|
28
|
+
<p align="center">
|
|
29
|
+
<img src="docs/scrapeflow-architecture.png" alt="ScrapeFlow Architecture" width="800">
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
32
|
## 📦 Installation
|
|
33
33
|
|
|
34
34
|
```bash
|
|
@@ -381,6 +381,63 @@ config = ScrapeFlowConfig(
|
|
|
381
381
|
)
|
|
382
382
|
```
|
|
383
383
|
|
|
384
|
+
### Specification-Driven Extraction (Pydantic)
|
|
385
|
+
|
|
386
|
+
**Use Case:** Declarative extraction with validation—fields, types, and rules in specs, not fragile XPaths.
|
|
387
|
+
|
|
388
|
+
```python
|
|
389
|
+
from pydantic import BaseModel
|
|
390
|
+
from scrapeflow import ScrapeFlow, SpecificationExtractor
|
|
391
|
+
from scrapeflow.specifications import FieldSpec, ItemSpec, ProductPriceSpec
|
|
392
|
+
|
|
393
|
+
# Model for list of products
|
|
394
|
+
class BookListing(BaseModel):
|
|
395
|
+
books: list[ProductPriceSpec]
|
|
396
|
+
|
|
397
|
+
# Schema maps fields to selectors
|
|
398
|
+
schema = {
|
|
399
|
+
"books": ItemSpec(
|
|
400
|
+
items_selector="article.product_pod",
|
|
401
|
+
fields={
|
|
402
|
+
"title": FieldSpec(selector="h3 a"),
|
|
403
|
+
"price": FieldSpec(selector=".price_color"),
|
|
404
|
+
"availability": FieldSpec(selector=".instock.availability", default=""),
|
|
405
|
+
"url": FieldSpec(selector="h3 a", type="attribute", attribute="href"),
|
|
406
|
+
},
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async with ScrapeFlow() as scraper:
|
|
411
|
+
await scraper.navigate("https://books.toscrape.com/")
|
|
412
|
+
extractor = SpecificationExtractor(BookListing, schema=schema)
|
|
413
|
+
# Extract and validate in one step
|
|
414
|
+
data = await extractor.extract(scraper.page)
|
|
415
|
+
for book in data.books:
|
|
416
|
+
print(f"{book.title}: {book.price}")
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Ethical Crawling & robots.txt
|
|
420
|
+
|
|
421
|
+
**Use Case:** GDPR/CCPA compliance and robots.txt respect built into the specification layer.
|
|
422
|
+
|
|
423
|
+
```python
|
|
424
|
+
from scrapeflow import ScrapeFlow
|
|
425
|
+
from scrapeflow.config import ScrapeFlowConfig, EthicalCrawlingConfig
|
|
426
|
+
|
|
427
|
+
config = ScrapeFlowConfig(
|
|
428
|
+
ethical_crawling=EthicalCrawlingConfig(
|
|
429
|
+
respect_robots_txt=True, # Check robots.txt before each request
|
|
430
|
+
user_agent_for_robots="ScrapeFlow",
|
|
431
|
+
anonymize_ip=True, # GDPR: minimize personal data
|
|
432
|
+
data_retention_days=30, # Document retention policy
|
|
433
|
+
)
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
async with ScrapeFlow(config) as scraper:
|
|
437
|
+
# navigate() automatically checks robots.txt
|
|
438
|
+
await scraper.navigate("https://example.com/page")
|
|
439
|
+
```
|
|
440
|
+
|
|
384
441
|
### Anti-Detection
|
|
385
442
|
|
|
386
443
|
**Use Case:** Scraping protected e-commerce sites that block automated access.
|
|
@@ -806,16 +863,26 @@ ScrapeFlow is built with a modular architecture:
|
|
|
806
863
|
```
|
|
807
864
|
scrapeflow/
|
|
808
865
|
├── engine.py # Main ScrapeFlow engine
|
|
809
|
-
├──
|
|
810
|
-
├──
|
|
866
|
+
├── ports.py # Protocols for dependency inversion
|
|
867
|
+
├── browser_runtime.py # Playwright runtime adapter
|
|
868
|
+
├── workflow.py # Workflow definition entities
|
|
869
|
+
├── workflow_executor.py # Workflow execution service
|
|
870
|
+
├── config.py # Configuration classes (incl. EthicalCrawlingConfig)
|
|
871
|
+
├── specifications.py # Pydantic specification-driven extraction
|
|
872
|
+
├── schema_library.py # Reusable schema definitions
|
|
873
|
+
├── robots.py # robots.txt parsing and enforcement
|
|
874
|
+
├── registry.py # Shared selector/component registry
|
|
875
|
+
├── mcp_backend.py # MCP integration extensibility
|
|
811
876
|
├── anti_detection.py # Anti-detection utilities
|
|
812
877
|
├── rate_limiter.py # Rate limiting implementation
|
|
813
878
|
├── retry.py # Retry logic and error classification
|
|
814
|
-
├── monitoring.py # Metrics
|
|
879
|
+
├── monitoring.py # Metrics, logging, alerting
|
|
815
880
|
├── extractors.py # Data extraction utilities
|
|
816
881
|
└── exceptions.py # Custom exceptions
|
|
817
882
|
```
|
|
818
883
|
|
|
884
|
+
For deeper design details, see [`ARCHITECTURE.md`](ARCHITECTURE.md).
|
|
885
|
+
|
|
819
886
|
## 🤝 Contributing
|
|
820
887
|
|
|
821
888
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authenticated extraction example using quotes.toscrape.com.
|
|
3
|
+
|
|
4
|
+
Demonstrates:
|
|
5
|
+
- login workflow with username/password
|
|
6
|
+
- authenticated data extraction using SpecificationExtractor
|
|
7
|
+
- robots.txt-aware navigation
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from scrapeflow import (
|
|
14
|
+
ScrapeFlow,
|
|
15
|
+
SpecificationExtractor,
|
|
16
|
+
get_registry,
|
|
17
|
+
register_quotes_login_handler,
|
|
18
|
+
)
|
|
19
|
+
from scrapeflow.config import ScrapeFlowConfig, EthicalCrawlingConfig
|
|
20
|
+
from scrapeflow.specifications import FieldSpec, ItemSpec
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class QuoteItem(BaseModel):
|
|
24
|
+
text: str
|
|
25
|
+
author: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class QuotesPage(BaseModel):
|
|
29
|
+
quotes: list[QuoteItem]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def run_authenticated_scrape(username: str, password: str) -> None:
|
|
33
|
+
schema = {
|
|
34
|
+
"quotes": ItemSpec(
|
|
35
|
+
items_selector=".quote",
|
|
36
|
+
fields={
|
|
37
|
+
"text": FieldSpec(selector=".text"),
|
|
38
|
+
"author": FieldSpec(selector=".author"),
|
|
39
|
+
},
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
config = ScrapeFlowConfig(
|
|
44
|
+
ethical_crawling=EthicalCrawlingConfig(
|
|
45
|
+
respect_robots_txt=True,
|
|
46
|
+
user_agent_for_robots="ScrapeFlow",
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
async with ScrapeFlow(config) as scraper:
|
|
51
|
+
register_quotes_login_handler(get_registry())
|
|
52
|
+
handler = get_registry().get_login("quotes_login")
|
|
53
|
+
if handler is None:
|
|
54
|
+
raise RuntimeError("Missing quotes_login handler in registry")
|
|
55
|
+
|
|
56
|
+
is_logged_in = await scraper.login_with_handler(
|
|
57
|
+
login_url="https://quotes.toscrape.com/login",
|
|
58
|
+
username=username,
|
|
59
|
+
password=password,
|
|
60
|
+
handler=handler,
|
|
61
|
+
timeout=10000,
|
|
62
|
+
)
|
|
63
|
+
if not is_logged_in:
|
|
64
|
+
error_text = await scraper.page.locator(".error").first.text_content()
|
|
65
|
+
raise RuntimeError(f"Login failed: {(error_text or '').strip()}")
|
|
66
|
+
|
|
67
|
+
extractor = SpecificationExtractor(QuotesPage, schema=schema)
|
|
68
|
+
data = await extractor.extract(scraper.page)
|
|
69
|
+
|
|
70
|
+
print(f"Authenticated login: {is_logged_in}")
|
|
71
|
+
print(f"Quotes extracted: {len(data.quotes)}")
|
|
72
|
+
for idx, quote in enumerate(data.quotes[:3], start=1):
|
|
73
|
+
print(f"{idx}. {quote.author}: {quote.text[:80]}...")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
asyncio.run(run_authenticated_scrape(username="admin", password="amdin"))
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Specification-driven extraction example - aligns with PST.AG job requirements.
|
|
3
|
+
|
|
4
|
+
Demonstrates:
|
|
5
|
+
- Pydantic specification-driven extraction
|
|
6
|
+
- robots.txt compliance
|
|
7
|
+
- Ethical crawling (GDPR/CCPA config)
|
|
8
|
+
- Shared selector registry
|
|
9
|
+
- Monitoring with alerting
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
from scrapeflow import ScrapeFlow, SpecificationExtractor
|
|
16
|
+
from scrapeflow.config import (
|
|
17
|
+
ScrapeFlowConfig,
|
|
18
|
+
EthicalCrawlingConfig,
|
|
19
|
+
RateLimitConfig,
|
|
20
|
+
)
|
|
21
|
+
from scrapeflow.specifications import FieldSpec, ItemSpec, JobListingSpec
|
|
22
|
+
from scrapeflow.registry import get_registry, register_job_listing_schema
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class JobListingsPage(BaseModel):
|
|
26
|
+
"""Output model for job listings extraction."""
|
|
27
|
+
|
|
28
|
+
jobs: list[JobListingSpec]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def scrape_job_listings():
|
|
32
|
+
"""Scrape job listings using specification-driven extraction."""
|
|
33
|
+
# Ethical crawling: robots.txt, rate limiting
|
|
34
|
+
config = ScrapeFlowConfig(
|
|
35
|
+
ethical_crawling=EthicalCrawlingConfig(
|
|
36
|
+
respect_robots_txt=True,
|
|
37
|
+
user_agent_for_robots="ScrapeFlow",
|
|
38
|
+
),
|
|
39
|
+
rate_limit=RateLimitConfig(requests_per_second=1.0),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
schema = {
|
|
43
|
+
"jobs": ItemSpec(
|
|
44
|
+
items_selector=".job-card, .job-listing, article.job, .job-item, .quote",
|
|
45
|
+
fields={
|
|
46
|
+
"title": FieldSpec(selector="h2 a, .job-title, .title a, .text"),
|
|
47
|
+
"company": FieldSpec(
|
|
48
|
+
selector=".company, .employer, .author",
|
|
49
|
+
default="",
|
|
50
|
+
),
|
|
51
|
+
"location": FieldSpec(
|
|
52
|
+
selector=".location, .job-location",
|
|
53
|
+
default="",
|
|
54
|
+
),
|
|
55
|
+
"url": FieldSpec(
|
|
56
|
+
selector="h2 a, .job-title a, a",
|
|
57
|
+
type="attribute",
|
|
58
|
+
attribute="href",
|
|
59
|
+
),
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async with ScrapeFlow(config) as scraper:
|
|
65
|
+
# navigate() checks robots.txt automatically
|
|
66
|
+
await scraper.navigate("https://quotes.toscrape.com/")
|
|
67
|
+
|
|
68
|
+
extractor = SpecificationExtractor(JobListingsPage, schema=schema)
|
|
69
|
+
data = await extractor.extract(scraper.page)
|
|
70
|
+
|
|
71
|
+
print(f"Extracted {len(data.jobs)} items (validated via Pydantic)")
|
|
72
|
+
for i, job in enumerate(data.jobs[:3]):
|
|
73
|
+
print(f" {i+1}. {job.title[:50]}... | {job.company or 'N/A'}")
|
|
74
|
+
|
|
75
|
+
metrics = scraper.get_metrics()
|
|
76
|
+
print(f"\nMetrics: {metrics.total_requests} requests, {metrics.get_success_rate():.1f}% success")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
asyncio.run(scrape_job_listings())
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ScrapeFlow - An opinionated scraping workflow engine built on Playwright.
|
|
3
|
+
|
|
4
|
+
ScrapeFlow provides a robust, production-ready framework for web scraping
|
|
5
|
+
with built-in error handling, retry logic, anti-detection, and workflow management.
|
|
6
|
+
|
|
7
|
+
Specification-driven extraction, robots.txt compliance, and ethical crawling
|
|
8
|
+
(GDPR/CCPA) are built into the design.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from scrapeflow.engine import ScrapeFlow
|
|
12
|
+
from scrapeflow.workflow import Workflow, Step
|
|
13
|
+
from scrapeflow.workflow_executor import WorkflowExecutor
|
|
14
|
+
from scrapeflow.extractors import Extractor, StructuredExtractor
|
|
15
|
+
from scrapeflow.specifications import (
|
|
16
|
+
FieldSpec,
|
|
17
|
+
ItemSpec,
|
|
18
|
+
SpecificationExtractor,
|
|
19
|
+
ProductPriceSpec,
|
|
20
|
+
JobListingSpec,
|
|
21
|
+
TariffCodeSpec,
|
|
22
|
+
)
|
|
23
|
+
from scrapeflow.robots import RobotsChecker
|
|
24
|
+
from scrapeflow.registry import (
|
|
25
|
+
SelectorRegistry,
|
|
26
|
+
LoginHandler,
|
|
27
|
+
get_registry,
|
|
28
|
+
register_quotes_login_handler,
|
|
29
|
+
)
|
|
30
|
+
from scrapeflow.browser_runtime import PlaywrightBrowserRuntime
|
|
31
|
+
from scrapeflow.mcp_backend import MCPBackend, PlaceholderMCPBackend, create_mcp_backend
|
|
32
|
+
from scrapeflow.exceptions import (
|
|
33
|
+
ScrapeFlowError,
|
|
34
|
+
ScrapeFlowRetryError,
|
|
35
|
+
ScrapeFlowTimeoutError,
|
|
36
|
+
ScrapeFlowBlockedError,
|
|
37
|
+
ScrapeFlowValidationError,
|
|
38
|
+
ScrapeFlowRobotsDisallowedError,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__version__ = "0.2.0"
|
|
42
|
+
__all__ = [
|
|
43
|
+
"ScrapeFlow",
|
|
44
|
+
"Workflow",
|
|
45
|
+
"Step",
|
|
46
|
+
"WorkflowExecutor",
|
|
47
|
+
"Extractor",
|
|
48
|
+
"StructuredExtractor",
|
|
49
|
+
"FieldSpec",
|
|
50
|
+
"ItemSpec",
|
|
51
|
+
"SpecificationExtractor",
|
|
52
|
+
"ProductPriceSpec",
|
|
53
|
+
"JobListingSpec",
|
|
54
|
+
"TariffCodeSpec",
|
|
55
|
+
"RobotsChecker",
|
|
56
|
+
"SelectorRegistry",
|
|
57
|
+
"LoginHandler",
|
|
58
|
+
"get_registry",
|
|
59
|
+
"register_quotes_login_handler",
|
|
60
|
+
"PlaywrightBrowserRuntime",
|
|
61
|
+
"MCPBackend",
|
|
62
|
+
"PlaceholderMCPBackend",
|
|
63
|
+
"create_mcp_backend",
|
|
64
|
+
"ScrapeFlowError",
|
|
65
|
+
"ScrapeFlowRetryError",
|
|
66
|
+
"ScrapeFlowTimeoutError",
|
|
67
|
+
"ScrapeFlowBlockedError",
|
|
68
|
+
"ScrapeFlowValidationError",
|
|
69
|
+
"ScrapeFlowRobotsDisallowedError",
|
|
70
|
+
]
|
|
71
|
+
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Playwright browser runtime adapter."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from playwright.async_api import (
|
|
5
|
+
async_playwright,
|
|
6
|
+
Playwright,
|
|
7
|
+
Browser,
|
|
8
|
+
BrowserContext,
|
|
9
|
+
Page,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from scrapeflow.config import ScrapeFlowConfig, BrowserType
|
|
13
|
+
from scrapeflow.anti_detection import AntiDetectionManager
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PlaywrightBrowserRuntime:
|
|
17
|
+
"""Infrastructure adapter for Playwright lifecycle and navigation."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, config: ScrapeFlowConfig, anti_detection: AntiDetectionManager):
|
|
20
|
+
self.config = config
|
|
21
|
+
self.anti_detection = anti_detection
|
|
22
|
+
self.playwright: Optional[Playwright] = None
|
|
23
|
+
self.browser: Optional[Browser] = None
|
|
24
|
+
self.context: Optional[BrowserContext] = None
|
|
25
|
+
self._page: Optional[Page] = None
|
|
26
|
+
self._is_running = False
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def page(self) -> Optional[Page]:
|
|
30
|
+
return self._page
|
|
31
|
+
|
|
32
|
+
async def start(self) -> None:
|
|
33
|
+
if self._is_running:
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
self.playwright = await async_playwright().start()
|
|
37
|
+
browser_type_map = {
|
|
38
|
+
BrowserType.CHROMIUM: self.playwright.chromium,
|
|
39
|
+
BrowserType.FIREFOX: self.playwright.firefox,
|
|
40
|
+
BrowserType.WEBKIT: self.playwright.webkit,
|
|
41
|
+
}
|
|
42
|
+
browser_launcher = browser_type_map[self.config.browser.browser_type]
|
|
43
|
+
|
|
44
|
+
proxy = self.config.browser.proxy or self.anti_detection.get_proxy()
|
|
45
|
+
browser_args = self.config.browser.args.copy()
|
|
46
|
+
if self.config.anti_detection.stealth_mode:
|
|
47
|
+
browser_args.extend(
|
|
48
|
+
[
|
|
49
|
+
"--disable-blink-features=AutomationControlled",
|
|
50
|
+
"--disable-dev-shm-usage",
|
|
51
|
+
"--no-sandbox",
|
|
52
|
+
]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
self.browser = await browser_launcher.launch(
|
|
56
|
+
headless=self.config.browser.headless,
|
|
57
|
+
slow_mo=self.config.browser.slow_mo,
|
|
58
|
+
args=browser_args,
|
|
59
|
+
proxy=proxy,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
context_options = {
|
|
63
|
+
"viewport": {
|
|
64
|
+
"width": self.config.anti_detection.viewport_width,
|
|
65
|
+
"height": self.config.anti_detection.viewport_height,
|
|
66
|
+
},
|
|
67
|
+
"user_agent": self.anti_detection.get_user_agent(),
|
|
68
|
+
}
|
|
69
|
+
self.context = await self.browser.new_context(**context_options)
|
|
70
|
+
self._page = await self.context.new_page()
|
|
71
|
+
await self.anti_detection.apply_stealth(self._page)
|
|
72
|
+
self._page.set_default_timeout(self.config.browser.timeout)
|
|
73
|
+
self._is_running = True
|
|
74
|
+
|
|
75
|
+
async def close(self) -> None:
|
|
76
|
+
if not self._is_running:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
if self._page:
|
|
80
|
+
await self._page.close()
|
|
81
|
+
self._page = None
|
|
82
|
+
if self.context:
|
|
83
|
+
await self.context.close()
|
|
84
|
+
self.context = None
|
|
85
|
+
if self.browser:
|
|
86
|
+
await self.browser.close()
|
|
87
|
+
self.browser = None
|
|
88
|
+
if self.playwright:
|
|
89
|
+
await self.playwright.stop()
|
|
90
|
+
self.playwright = None
|
|
91
|
+
self._is_running = False
|
|
92
|
+
|
|
93
|
+
async def goto(self, url: str, wait_until: str, timeout: int) -> None:
|
|
94
|
+
if not self._page:
|
|
95
|
+
raise RuntimeError("Browser runtime is not started.")
|
|
96
|
+
await self._page.goto(url, wait_until=wait_until, timeout=timeout)
|
|
@@ -61,6 +61,24 @@ class BrowserConfig:
|
|
|
61
61
|
proxy: Optional[Dict[str, str]] = None
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
@dataclass
|
|
65
|
+
class EthicalCrawlingConfig:
|
|
66
|
+
"""
|
|
67
|
+
Ethical crawling configuration—GDPR/CCPA baked into specification design.
|
|
68
|
+
|
|
69
|
+
Rate limiting, robots.txt respect, and compliance are built into the
|
|
70
|
+
specification layer, not retrofitted.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
respect_robots_txt: bool = True
|
|
74
|
+
user_agent_for_robots: str = "ScrapeFlow"
|
|
75
|
+
max_pages_per_domain: Optional[int] = None
|
|
76
|
+
honor_noindex: bool = True
|
|
77
|
+
data_retention_days: Optional[int] = None # GDPR: document retention
|
|
78
|
+
anonymize_ip: bool = False # GDPR: minimize personal data
|
|
79
|
+
consent_required: bool = False # GDPR: require consent for tracking
|
|
80
|
+
|
|
81
|
+
|
|
64
82
|
@dataclass
|
|
65
83
|
class ScrapeFlowConfig:
|
|
66
84
|
"""Main configuration for ScrapeFlow."""
|
|
@@ -69,6 +87,9 @@ class ScrapeFlowConfig:
|
|
|
69
87
|
retry: RetryConfig = field(default_factory=RetryConfig)
|
|
70
88
|
rate_limit: RateLimitConfig = field(default_factory=RateLimitConfig)
|
|
71
89
|
anti_detection: AntiDetectionConfig = field(default_factory=AntiDetectionConfig)
|
|
90
|
+
ethical_crawling: EthicalCrawlingConfig = field(
|
|
91
|
+
default_factory=EthicalCrawlingConfig
|
|
92
|
+
)
|
|
72
93
|
log_level: str = "INFO"
|
|
73
94
|
log_file: Optional[str] = None
|
|
74
95
|
debug: bool = False
|