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 ADDED
@@ -0,0 +1,2 @@
1
+ """Examples for ScrapeFlow usage."""
2
+
@@ -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
+
@@ -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
+