scrapeflow-py 0.1.0__py3-none-any.whl
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.
- examples/__init__.py +2 -0
- examples/advanced_example.py +80 -0
- examples/basic_usage.py +34 -0
- examples/workflow_example.py +86 -0
- scrapeflow/__init__.py +30 -0
- scrapeflow/anti_detection.py +140 -0
- scrapeflow/config.py +75 -0
- scrapeflow/engine.py +290 -0
- scrapeflow/exceptions.py +37 -0
- scrapeflow/extractors.py +236 -0
- scrapeflow/monitoring.py +157 -0
- scrapeflow/rate_limiter.py +77 -0
- scrapeflow/retry.py +132 -0
- scrapeflow/workflow.py +147 -0
- scrapeflow_py-0.1.0.dist-info/METADATA +370 -0
- scrapeflow_py-0.1.0.dist-info/RECORD +21 -0
- scrapeflow_py-0.1.0.dist-info/WHEEL +5 -0
- scrapeflow_py-0.1.0.dist-info/licenses/LICENSE +21 -0
- scrapeflow_py-0.1.0.dist-info/top_level.txt +3 -0
- tests/__init__.py +2 -0
- tests/test_config.py +65 -0
examples/__init__.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Advanced example showing anti-detection, rate limiting, and structured extraction."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from scrapeflow import ScrapeFlow
|
|
5
|
+
from scrapeflow.config import (
|
|
6
|
+
ScrapeFlowConfig,
|
|
7
|
+
AntiDetectionConfig,
|
|
8
|
+
RateLimitConfig,
|
|
9
|
+
RetryConfig,
|
|
10
|
+
)
|
|
11
|
+
from scrapeflow.extractors import StructuredExtractor
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def main():
|
|
15
|
+
"""Advanced scraping with all features enabled."""
|
|
16
|
+
# Configure anti-detection
|
|
17
|
+
anti_detection = AntiDetectionConfig(
|
|
18
|
+
rotate_user_agents=True,
|
|
19
|
+
stealth_mode=True,
|
|
20
|
+
viewport_width=1920,
|
|
21
|
+
viewport_height=1080,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Configure rate limiting
|
|
25
|
+
rate_limit = RateLimitConfig(
|
|
26
|
+
requests_per_second=1.0,
|
|
27
|
+
burst_size=3,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Configure retry behavior
|
|
31
|
+
retry = RetryConfig(
|
|
32
|
+
max_retries=5,
|
|
33
|
+
initial_delay=1.0,
|
|
34
|
+
max_delay=30.0,
|
|
35
|
+
exponential_base=2.0,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Create config
|
|
39
|
+
config = ScrapeFlowConfig(
|
|
40
|
+
anti_detection=anti_detection,
|
|
41
|
+
rate_limit=rate_limit,
|
|
42
|
+
retry=retry,
|
|
43
|
+
log_level="INFO",
|
|
44
|
+
debug=False,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
async with ScrapeFlow(config) as scraper:
|
|
48
|
+
# Navigate to page
|
|
49
|
+
await scraper.navigate("https://quotes.toscrape.com/")
|
|
50
|
+
|
|
51
|
+
# Use structured extractor
|
|
52
|
+
schema = {
|
|
53
|
+
"quotes": {
|
|
54
|
+
"items": ".quote",
|
|
55
|
+
"schema": {
|
|
56
|
+
"text": ".text",
|
|
57
|
+
"author": ".author",
|
|
58
|
+
"tags": ".tag",
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
extractor = StructuredExtractor(schema)
|
|
64
|
+
data = await extractor.extract(scraper.page)
|
|
65
|
+
|
|
66
|
+
print("Extracted quotes:")
|
|
67
|
+
for quote in data.get("quotes", []):
|
|
68
|
+
print(f" - {quote}")
|
|
69
|
+
|
|
70
|
+
# Get metrics
|
|
71
|
+
metrics = scraper.get_metrics()
|
|
72
|
+
print(f"\nScraping completed:")
|
|
73
|
+
print(f" Success rate: {metrics.get_success_rate():.2f}%")
|
|
74
|
+
print(f" Total requests: {metrics.total_requests}")
|
|
75
|
+
print(f" Retries: {metrics.retry_count}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
asyncio.run(main())
|
|
80
|
+
|
examples/basic_usage.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Basic usage example of ScrapeFlow."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from scrapeflow import ScrapeFlow
|
|
5
|
+
from scrapeflow.config import ScrapeFlowConfig
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def main():
|
|
9
|
+
"""Basic scraping example."""
|
|
10
|
+
# Create a simple configuration
|
|
11
|
+
config = ScrapeFlowConfig()
|
|
12
|
+
config.browser.headless = False # Set to True for headless mode
|
|
13
|
+
config.retry.max_retries = 3
|
|
14
|
+
|
|
15
|
+
# Use ScrapeFlow as a context manager
|
|
16
|
+
async with ScrapeFlow(config) as scraper:
|
|
17
|
+
# Navigate to a page
|
|
18
|
+
await scraper.navigate("https://example.com")
|
|
19
|
+
|
|
20
|
+
# Take a screenshot
|
|
21
|
+
await scraper.screenshot("example.png")
|
|
22
|
+
|
|
23
|
+
# Extract some data
|
|
24
|
+
title = await scraper.page.title()
|
|
25
|
+
print(f"Page title: {title}")
|
|
26
|
+
|
|
27
|
+
# Get metrics
|
|
28
|
+
metrics = scraper.get_metrics()
|
|
29
|
+
print(f"Success rate: {metrics.get_success_rate():.2f}%")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == "__main__":
|
|
33
|
+
asyncio.run(main())
|
|
34
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Workflow example showing how to use ScrapeFlow workflows."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from scrapeflow import ScrapeFlow, Workflow
|
|
5
|
+
from scrapeflow.config import ScrapeFlowConfig
|
|
6
|
+
from scrapeflow.extractors import Extractor
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def scrape_product_page(page, context):
|
|
10
|
+
"""Extract product information from a page."""
|
|
11
|
+
# Wait for product details to load
|
|
12
|
+
await page.wait_for_selector(".product-title", timeout=10000)
|
|
13
|
+
|
|
14
|
+
# Extract product data
|
|
15
|
+
product_data = {
|
|
16
|
+
"title": await Extractor.extract_text(page, ".product-title"),
|
|
17
|
+
"price": await Extractor.extract_text(page, ".price"),
|
|
18
|
+
"description": await Extractor.extract_text(page, ".description"),
|
|
19
|
+
"images": await Extractor.extract_images(page, ".product-image"),
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
context["product_data"] = product_data
|
|
23
|
+
return product_data
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def save_data(data, context):
|
|
27
|
+
"""Save extracted data (mock function)."""
|
|
28
|
+
print(f"Saving data: {data}")
|
|
29
|
+
# In real usage, you would save to database, file, etc.
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def handle_error(error, context):
|
|
34
|
+
"""Handle errors during scraping."""
|
|
35
|
+
print(f"Error occurred: {error}")
|
|
36
|
+
# You could log to file, send notification, etc.
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def main():
|
|
40
|
+
"""Workflow example."""
|
|
41
|
+
config = ScrapeFlowConfig()
|
|
42
|
+
config.retry.max_retries = 3
|
|
43
|
+
config.rate_limit.requests_per_second = 2.0
|
|
44
|
+
|
|
45
|
+
async with ScrapeFlow(config) as scraper:
|
|
46
|
+
# Create a workflow
|
|
47
|
+
workflow = Workflow(name="product_scraper")
|
|
48
|
+
|
|
49
|
+
# Add steps
|
|
50
|
+
async def navigate_to_product(page, context):
|
|
51
|
+
await scraper.navigate("https://example.com/products/1")
|
|
52
|
+
|
|
53
|
+
workflow.add_step(
|
|
54
|
+
name="navigate",
|
|
55
|
+
func=navigate_to_product,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
workflow.add_step(
|
|
59
|
+
name="extract_data",
|
|
60
|
+
func=scrape_product_page,
|
|
61
|
+
retryable=True,
|
|
62
|
+
required=True,
|
|
63
|
+
on_success=save_data,
|
|
64
|
+
on_error=handle_error,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Execute workflow
|
|
68
|
+
result = await scraper.run_workflow(workflow)
|
|
69
|
+
|
|
70
|
+
if result.success:
|
|
71
|
+
print("Workflow completed successfully!")
|
|
72
|
+
print(f"Extracted data: {result.final_data}")
|
|
73
|
+
else:
|
|
74
|
+
print(f"Workflow failed: {result.error}")
|
|
75
|
+
|
|
76
|
+
# Print metrics
|
|
77
|
+
metrics = scraper.get_metrics()
|
|
78
|
+
print(f"\nMetrics:")
|
|
79
|
+
print(f" Total requests: {metrics.total_requests}")
|
|
80
|
+
print(f" Success rate: {metrics.get_success_rate():.2f}%")
|
|
81
|
+
print(f" Average response time: {metrics.average_response_time:.2f}s")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
asyncio.run(main())
|
|
86
|
+
|
scrapeflow/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
|
|
8
|
+
from scrapeflow.engine import ScrapeFlow
|
|
9
|
+
from scrapeflow.workflow import Workflow, Step
|
|
10
|
+
from scrapeflow.extractors import Extractor, StructuredExtractor
|
|
11
|
+
from scrapeflow.exceptions import (
|
|
12
|
+
ScrapeFlowError,
|
|
13
|
+
ScrapeFlowRetryError,
|
|
14
|
+
ScrapeFlowTimeoutError,
|
|
15
|
+
ScrapeFlowBlockedError,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__version__ = "0.1.0"
|
|
19
|
+
__all__ = [
|
|
20
|
+
"ScrapeFlow",
|
|
21
|
+
"Workflow",
|
|
22
|
+
"Step",
|
|
23
|
+
"Extractor",
|
|
24
|
+
"StructuredExtractor",
|
|
25
|
+
"ScrapeFlowError",
|
|
26
|
+
"ScrapeFlowRetryError",
|
|
27
|
+
"ScrapeFlowTimeoutError",
|
|
28
|
+
"ScrapeFlowBlockedError",
|
|
29
|
+
]
|
|
30
|
+
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Anti-detection utilities for web scraping."""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
from typing import List, Optional, Dict
|
|
5
|
+
from scrapeflow.config import AntiDetectionConfig
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UserAgentRotator:
|
|
9
|
+
"""Manages user agent rotation."""
|
|
10
|
+
|
|
11
|
+
DEFAULT_USER_AGENTS = [
|
|
12
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
13
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
14
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
|
|
15
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15",
|
|
16
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0",
|
|
17
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:120.0) Gecko/20100101 Firefox/120.0",
|
|
18
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
def __init__(self, user_agents: Optional[List[str]] = None):
|
|
22
|
+
self.user_agents = user_agents or self.DEFAULT_USER_AGENTS
|
|
23
|
+
|
|
24
|
+
def get_random(self) -> str:
|
|
25
|
+
"""Get a random user agent."""
|
|
26
|
+
return random.choice(self.user_agents)
|
|
27
|
+
|
|
28
|
+
def get(self, index: int = None) -> str:
|
|
29
|
+
"""Get a user agent by index or random."""
|
|
30
|
+
if index is not None:
|
|
31
|
+
return self.user_agents[index % len(self.user_agents)]
|
|
32
|
+
return self.get_random()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ProxyRotator:
|
|
36
|
+
"""Manages proxy rotation."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, proxies: List[Dict[str, str]]):
|
|
39
|
+
self.proxies = proxies
|
|
40
|
+
self.current_index = 0
|
|
41
|
+
|
|
42
|
+
def get_next(self) -> Optional[Dict[str, str]]:
|
|
43
|
+
"""Get the next proxy in rotation."""
|
|
44
|
+
if not self.proxies:
|
|
45
|
+
return None
|
|
46
|
+
proxy = self.proxies[self.current_index % len(self.proxies)]
|
|
47
|
+
self.current_index += 1
|
|
48
|
+
return proxy
|
|
49
|
+
|
|
50
|
+
def get_random(self) -> Optional[Dict[str, str]]:
|
|
51
|
+
"""Get a random proxy."""
|
|
52
|
+
if not self.proxies:
|
|
53
|
+
return None
|
|
54
|
+
return random.choice(self.proxies)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class StealthMode:
|
|
58
|
+
"""Applies stealth techniques to avoid detection."""
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
async def apply_stealth(page):
|
|
62
|
+
"""Apply stealth techniques to a Playwright page."""
|
|
63
|
+
# Remove webdriver property
|
|
64
|
+
await page.add_init_script(
|
|
65
|
+
"""
|
|
66
|
+
Object.defineProperty(navigator, 'webdriver', {
|
|
67
|
+
get: () => undefined
|
|
68
|
+
});
|
|
69
|
+
"""
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Override permissions
|
|
73
|
+
await page.add_init_script(
|
|
74
|
+
"""
|
|
75
|
+
const originalQuery = window.navigator.permissions.query;
|
|
76
|
+
window.navigator.permissions.query = (parameters) => (
|
|
77
|
+
parameters.name === 'notifications' ?
|
|
78
|
+
Promise.resolve({ state: Notification.permission }) :
|
|
79
|
+
originalQuery(parameters)
|
|
80
|
+
);
|
|
81
|
+
"""
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Override plugins
|
|
85
|
+
await page.add_init_script(
|
|
86
|
+
"""
|
|
87
|
+
Object.defineProperty(navigator, 'plugins', {
|
|
88
|
+
get: () => [1, 2, 3, 4, 5]
|
|
89
|
+
});
|
|
90
|
+
"""
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Override languages
|
|
94
|
+
await page.add_init_script(
|
|
95
|
+
"""
|
|
96
|
+
Object.defineProperty(navigator, 'languages', {
|
|
97
|
+
get: () => ['en-US', 'en']
|
|
98
|
+
});
|
|
99
|
+
"""
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Mock chrome object
|
|
103
|
+
await page.add_init_script(
|
|
104
|
+
"""
|
|
105
|
+
window.chrome = {
|
|
106
|
+
runtime: {}
|
|
107
|
+
};
|
|
108
|
+
"""
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class AntiDetectionManager:
|
|
113
|
+
"""Manages all anti-detection features."""
|
|
114
|
+
|
|
115
|
+
def __init__(self, config: AntiDetectionConfig):
|
|
116
|
+
self.config = config
|
|
117
|
+
self.user_agent_rotator = (
|
|
118
|
+
UserAgentRotator(config.user_agents) if config.rotate_user_agents else None
|
|
119
|
+
)
|
|
120
|
+
self.proxy_rotator = (
|
|
121
|
+
ProxyRotator(config.proxies) if config.rotate_proxies else None
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def get_user_agent(self) -> Optional[str]:
|
|
125
|
+
"""Get a user agent if rotation is enabled."""
|
|
126
|
+
if self.user_agent_rotator:
|
|
127
|
+
return self.user_agent_rotator.get_random()
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
def get_proxy(self) -> Optional[Dict[str, str]]:
|
|
131
|
+
"""Get a proxy if rotation is enabled."""
|
|
132
|
+
if self.proxy_rotator:
|
|
133
|
+
return self.proxy_rotator.get_next()
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
async def apply_stealth(self, page):
|
|
137
|
+
"""Apply stealth mode to a page if enabled."""
|
|
138
|
+
if self.config.stealth_mode:
|
|
139
|
+
await StealthMode.apply_stealth(page)
|
|
140
|
+
|
scrapeflow/config.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Configuration and settings for ScrapeFlow."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Optional, List, Dict, Any
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BrowserType(str, Enum):
|
|
9
|
+
"""Supported browser types."""
|
|
10
|
+
|
|
11
|
+
CHROMIUM = "chromium"
|
|
12
|
+
FIREFOX = "firefox"
|
|
13
|
+
WEBKIT = "webkit"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class RetryConfig:
|
|
18
|
+
"""Configuration for retry behavior."""
|
|
19
|
+
|
|
20
|
+
max_retries: int = 3
|
|
21
|
+
initial_delay: float = 1.0
|
|
22
|
+
max_delay: float = 60.0
|
|
23
|
+
exponential_base: float = 2.0
|
|
24
|
+
jitter: bool = True
|
|
25
|
+
retryable_exceptions: tuple = (
|
|
26
|
+
Exception,
|
|
27
|
+
) # Will be customized per use case
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class RateLimitConfig:
|
|
32
|
+
"""Configuration for rate limiting."""
|
|
33
|
+
|
|
34
|
+
requests_per_second: float = 1.0
|
|
35
|
+
requests_per_minute: Optional[float] = None
|
|
36
|
+
burst_size: int = 5
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class AntiDetectionConfig:
|
|
41
|
+
"""Configuration for anti-detection features."""
|
|
42
|
+
|
|
43
|
+
rotate_user_agents: bool = True
|
|
44
|
+
rotate_proxies: bool = False
|
|
45
|
+
stealth_mode: bool = True
|
|
46
|
+
viewport_width: int = 1920
|
|
47
|
+
viewport_height: int = 1080
|
|
48
|
+
user_agents: List[str] = field(default_factory=list)
|
|
49
|
+
proxies: List[Dict[str, str]] = field(default_factory=list)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class BrowserConfig:
|
|
54
|
+
"""Configuration for browser settings."""
|
|
55
|
+
|
|
56
|
+
browser_type: BrowserType = BrowserType.CHROMIUM
|
|
57
|
+
headless: bool = True
|
|
58
|
+
timeout: int = 30000 # milliseconds
|
|
59
|
+
slow_mo: int = 0 # milliseconds
|
|
60
|
+
args: List[str] = field(default_factory=list)
|
|
61
|
+
proxy: Optional[Dict[str, str]] = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class ScrapeFlowConfig:
|
|
66
|
+
"""Main configuration for ScrapeFlow."""
|
|
67
|
+
|
|
68
|
+
browser: BrowserConfig = field(default_factory=BrowserConfig)
|
|
69
|
+
retry: RetryConfig = field(default_factory=RetryConfig)
|
|
70
|
+
rate_limit: RateLimitConfig = field(default_factory=RateLimitConfig)
|
|
71
|
+
anti_detection: AntiDetectionConfig = field(default_factory=AntiDetectionConfig)
|
|
72
|
+
log_level: str = "INFO"
|
|
73
|
+
log_file: Optional[str] = None
|
|
74
|
+
debug: bool = False
|
|
75
|
+
|