scraprime 0.1.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.
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: scraprime
3
+ Version: 0.1.0
4
+ Summary: Prime infrastructure for undetectable scraping. 3-tier WAF bypass & auto-healing parsers.
5
+ Home-page: https://github.com/yourusername/scraprime
6
+ Author: Anees Ur Rahman
7
+ Author-email: anisshewa@gmail.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: curl_cffi>=0.7.0
13
+ Requires-Dist: scrapling>=0.2.0
14
+ Requires-Dist: camoufox[geoip]>=0.4.0
15
+ Requires-Dist: aiohttp>=3.9.0
16
+ Requires-Dist: pydantic>=2.0.0
17
+ Requires-Dist: selectolax>=0.3.21
18
+ Dynamic: author
19
+ Dynamic: author-email
20
+ Dynamic: classifier
21
+ Dynamic: home-page
22
+ Dynamic: requires-dist
23
+ Dynamic: requires-python
24
+ Dynamic: summary
@@ -0,0 +1,16 @@
1
+ from .fetcher import StealthFetcher
2
+ from .parser import AdaptiveParser
3
+ from .webhook import send_to_n8n, send_to_n8n_sync
4
+ from .models import ProxyConfig, FetchRequest, ScrapedItem
5
+
6
+ __all__ = [
7
+ "StealthFetcher",
8
+ "AdaptiveParser",
9
+ "send_to_n8n",
10
+ "send_to_n8n_sync",
11
+ "ProxyConfig",
12
+ "FetchRequest",
13
+ "ScrapedItem"
14
+ ]
15
+
16
+ __version__ = "0.1.0"
@@ -0,0 +1,162 @@
1
+ import asyncio
2
+ from typing import Optional
3
+ from curl_cffi import requests as cffi_requests
4
+ from scrapling.fetchers import StealthyFetcher as ScraplingFetcher
5
+
6
+ try:
7
+ from camoufox.async_api import AsyncCamoufox
8
+ HAS_CAMOUFOX = True
9
+ except ImportError:
10
+ HAS_CAMOUFOX = False
11
+
12
+ class StealthFetcher:
13
+ """
14
+ Scraprime's core fetching engine.
15
+ Implements a 3-tiered fallback system to bypass WAFs (Cloudflare, DataDome, Akamai)
16
+ while maintaining maximum speed and minimizing resource usage.
17
+ """
18
+
19
+ def __init__(self, proxy: Optional[dict] = None):
20
+ """
21
+ Initializes the fetcher with optional proxy configuration.
22
+
23
+ :param proxy: Dictionary containing proxy details.
24
+ Example: {"server": "http://ip:port", "username": "user", "password": "pass"}
25
+ """
26
+ self.proxy = proxy
27
+
28
+ async def fetch(self, url: str, headers: Optional[dict] = None) -> str:
29
+ """
30
+ Main entry point. Attempts to fetch HTML using 3 tiers.
31
+ Tier 1: Fast HTTP (curl_cffi)
32
+ Tier 2: Stealth HTTP (scrapling)
33
+ Tier 3: Full Browser (Camoufox)
34
+
35
+ :return: Raw HTML string if successful.
36
+ :raises: Exception if all 3 tiers fail.
37
+ """
38
+ print(f"[\033[94mScraprime\033[0m] Initiating fetch for: {url}")
39
+
40
+ # TIER 1
41
+ html_content = await self._fetch_tier1_curl(url, headers)
42
+ if html_content:
43
+ return html_content
44
+
45
+ # TIER 2
46
+ html_content = await self._fetch_tier2_scrapling(url, headers)
47
+ if html_content:
48
+ return html_content
49
+
50
+ # TIER 3
51
+ html_content = await self._fetch_tier3_camoufox(url)
52
+ if html_content:
53
+ return html_content
54
+
55
+ raise Exception(f"[\033[91mScraprime\033[0m] All 3 tiers failed to fetch URL: {url}")
56
+
57
+ def _is_waf_blocked(self, status_code: int, text: str) -> bool:
58
+ """Helper to detect WAF blocks based on status codes and HTML signatures."""
59
+ if status_code in [403, 429, 503]:
60
+ return True
61
+ if "challenge-platform" in text or "datadome" in text.lower() or "Access Denied" in text:
62
+ return True
63
+ return False
64
+
65
+ async def _fetch_tier1_curl(self, url: str, headers: Optional[dict] = None) -> Optional[str]:
66
+ """
67
+ Tier 1: Fastest. Uses curl_cffi to impersonate Chrome's TLS/JA3 fingerprint.
68
+ """
69
+ print("[\033[94mScraprime\033[0m] Tier 1 (curl_cffi) attempting fast request...")
70
+ try:
71
+ response = await asyncio.to_thread(
72
+ cffi_requests.get,
73
+ url,
74
+ impersonate="chrome120",
75
+ proxies=self.proxy,
76
+ headers=headers,
77
+ timeout=15
78
+ )
79
+
80
+ if self._is_waf_blocked(response.status_code, response.text):
81
+ print("[\033[93mScraprime\033[0m] Tier 1 detected WAF block. Falling back...")
82
+ return None
83
+
84
+ print("[\033[92mScraprime\033[0m] Tier 1 successful.")
85
+ return response.text
86
+
87
+ except Exception as e:
88
+ print(f"[\033[93mScraprime\033[0m] Tier 1 Exception: {e}. Falling back...")
89
+ return None
90
+
91
+ async def _fetch_tier2_scrapling(self, url: str, headers: Optional[dict] = None) -> Optional[str]:
92
+ """
93
+ Tier 2: Medium speed. Uses Scrapling's built-in stealth fetcher to solve basic JS challenges.
94
+ """
95
+ print("[\033[94mScraprime\033[0m] Tier 2 (scrapling) attempting stealth request...")
96
+ try:
97
+ # Removed timeout=20 because scrapling interprets it as milliseconds
98
+ response = await asyncio.to_thread(
99
+ ScraplingFetcher.fetch,
100
+ url,
101
+ proxies=self.proxy,
102
+ headers=headers
103
+ )
104
+
105
+ if self._is_waf_blocked(response.status, response.body):
106
+ print("[\033[93mScraprime\033[0m] Tier 2 detected WAF block. Falling back...")
107
+ return None
108
+
109
+ print("[\033[92mScraprime\033[0m] Tier 2 successful.")
110
+ return response.body
111
+
112
+ except Exception as e:
113
+ print(f"[\033[93mScraprime\033[0m] Tier 2 Exception: {e}. Falling back...")
114
+ return None
115
+ async def _fetch_tier3_camoufox(self, url: str) -> Optional[str]:
116
+ """
117
+ Tier 3: Slowest but most powerful. Full C++ spoofed Firefox browser for heavy WAFs.
118
+ """
119
+ if not HAS_CAMOUFOX:
120
+ print("[\033[91mScraprime\033[0m] Camoufox is not installed. Cannot use Tier 3.")
121
+ return None
122
+
123
+ print("[\033[94mScraprime\033[0m] Tier 3 (camoufox) launching full browser...")
124
+
125
+ async with AsyncCamoufox(
126
+ headless=True,
127
+ humanize=True, # Simulate human mouse movements
128
+ geoip=True, # Match browser timezone/location to proxy IP
129
+ block_images=False, # WAFs flag browsers that don't load images
130
+ os=['windows'], # Spoof Windows hardware
131
+ proxy=self.proxy
132
+ ) as browser:
133
+ page = await browser.new_page()
134
+ try:
135
+ await page.goto(url, wait_until="networkidle", timeout=45000)
136
+
137
+ # Wait extra time for invisible JS challenges (like DataDome) to solve
138
+ await page.wait_for_timeout(5000)
139
+
140
+ html = await page.content()
141
+
142
+ if self._is_waf_blocked(200, html):
143
+ print("[\033[91mScraprime\033[0m] Tier 3 still blocked. Giving up.")
144
+ return None
145
+
146
+ print("[\033[92mScraprime\033[0m] Tier 3 successful.")
147
+ return html
148
+
149
+ except Exception as e:
150
+ print(f"[\033[91mScraprime\033[0m] Tier 3 Browser Error: {e}")
151
+ return None
152
+ finally:
153
+ await browser.close()
154
+
155
+ # ==========================================
156
+ # USAGE EXAMPLE (Sync wrapper for easy testing)
157
+ # ==========================================
158
+ def fetch_sync(url: str, proxy: Optional[dict] = None) -> str:
159
+ """Synchronous convenience function for users who don't want to use asyncio.run()."""
160
+ fetcher = StealthFetcher(proxy=proxy)
161
+ loop = asyncio.get_event_loop()
162
+ return loop.run_until_complete(fetcher.fetch(url))
@@ -0,0 +1,51 @@
1
+ from typing import Optional, Dict, Any
2
+ from pydantic import BaseModel, HttpUrl, Field
3
+
4
+ class ProxyConfig(BaseModel):
5
+ """
6
+ Pydantic model for configuring proxies.
7
+ Ensures the user provides the correct structure for curl_cffi and Camoufox.
8
+ """
9
+ server: str = Field(..., description="Proxy server address (e.g., http://gateway.ip:port)")
10
+ username: Optional[str] = None
11
+ password: Optional[str] = None
12
+
13
+ def to_dict(self) -> Dict[str, str]:
14
+ """Converts the model to the dictionary format expected by curl_cffi/camoufox."""
15
+ proxy_dict = {"server": self.server}
16
+ if self.username:
17
+ proxy_dict["username"] = self.username
18
+ if self.password:
19
+ proxy_dict["password"] = self.password
20
+ return proxy_dict
21
+
22
+ class FetchRequest(BaseModel):
23
+ """
24
+ Pydantic model for orchestrating a complete Scraprime fetch + parse + webhook flow.
25
+ """
26
+ url: HttpUrl = Field(..., description="The target URL to scrape.")
27
+ proxy: Optional[ProxyConfig] = None
28
+ headers: Optional[Dict[str, str]] = None
29
+ webhook_url: Optional[HttpUrl] = Field(None, description="Optional n8n/Make.com webhook URL to send data to.")
30
+
31
+ class ScrapedItem(BaseModel):
32
+ """
33
+ Pydantic model for standardizing the output data before sending to webhooks.
34
+ """
35
+ url: str
36
+ data: Dict[str, Any] = Field(default_factory=dict, description="The extracted data payload.")
37
+
38
+ # ==========================================
39
+ # USAGE EXAMPLE
40
+ # ==========================================
41
+ if __name__ == "__main__":
42
+ # Example of how a user would validate their config before running
43
+ try:
44
+ config = FetchRequest(
45
+ url="https://www.very.co.uk/product",
46
+ proxy=ProxyConfig(server="http://1.2.3.4:8080", username="user", password="pass")
47
+ )
48
+ print("Configuration valid!")
49
+ print(f"Proxy dict for fetcher: {config.proxy.to_dict()}")
50
+ except Exception as e:
51
+ print(f"Configuration invalid: {e}")
@@ -0,0 +1,93 @@
1
+ import re
2
+ from typing import Optional
3
+ from selectolax.parser import HTMLParser
4
+
5
+ class AdaptiveParser:
6
+ """
7
+ Scraprime's parsing engine.
8
+ Uses Selectolax for fast DOM traversal and regex for auto-healing data extraction.
9
+ """
10
+
11
+ def __init__(self, html: str):
12
+ """
13
+ Initializes the parser with raw HTML.
14
+
15
+ :param html: Raw HTML string fetched by StealthFetcher.
16
+ """
17
+ self.tree = HTMLParser(html)
18
+
19
+ def extract_meta(self, property_name: str) -> Optional[str]:
20
+ """
21
+ Extracts content from <meta> tags. Highly reliable for prices and titles.
22
+ Example: <meta property="product:price:amount" content="479.00">
23
+
24
+ :param property_name: The property attribute value to search for.
25
+ :return: The content attribute value or None.
26
+ """
27
+ element = self.tree.css_first(f'meta[property="{property_name}"]')
28
+ if element:
29
+ return element.attrib.get('content')
30
+ return None
31
+
32
+ def extract_css(self, selector: str) -> Optional[str]:
33
+ """
34
+ Standard CSS selector extraction.
35
+
36
+ :param selector: CSS selector string (e.g., 'h1.product-title').
37
+ :return: The text of the first matching element or None.
38
+ """
39
+ element = self.tree.css_first(selector)
40
+ if element:
41
+ return element.text(strip=True)
42
+ return None
43
+
44
+ def extract_nearby_regex(self, anchor_text: str, pattern: str) -> Optional[str]:
45
+ """
46
+ Auto-Healing Tool. Searches the entire page text for a regex pattern.
47
+ If an anchor is provided, it tries to find the pattern near that text.
48
+
49
+ :param anchor_text: Text to find the starting point (e.g., "Add to Cart").
50
+ :param pattern: Regex pattern to search for (e.g., r'[\£\$\€]\d+[.,]\d{2}').
51
+ :return: The matched string or None.
52
+ """
53
+ # Get all visible text on the page
54
+ page_text = self.tree.text()
55
+
56
+ # Find all matches in the entire page
57
+ matches = list(re.finditer(pattern, page_text))
58
+ if not matches:
59
+ return None
60
+
61
+ # Try to find a match close to the anchor text
62
+ anchor_idx = page_text.find(anchor_text)
63
+ if anchor_idx != -1:
64
+ # Find the match with the smallest distance to the anchor
65
+ closest_match = min(matches, key=lambda m: abs(m.start() - anchor_idx))
66
+ return closest_match.group(0).strip()
67
+
68
+ # If anchor not found, just return the first match found on the page
69
+ return matches[0].group(0).strip()
70
+
71
+ # ==========================================
72
+ # USAGE EXAMPLE
73
+ # ==========================================
74
+ if __name__ == "__main__":
75
+ mock_html = """
76
+ <html>
77
+ <head>
78
+ <meta property="og:title" content="Narva Sofa" />
79
+ <meta property="product:price:amount" content="479.00" />
80
+ </head>
81
+ <body>
82
+ <h1 class="title">Narva Fabric 2 Seater Sofa</h1>
83
+ <div class="price">£479.00</div>
84
+ <button>Add to Basket</button>
85
+ </body>
86
+ </html>
87
+ """
88
+
89
+ parser = AdaptiveParser(mock_html)
90
+
91
+ print(f"Title (Meta): {parser.extract_meta('og:title')}")
92
+ print(f"Price (Meta): {parser.extract_meta('product:price:amount')}")
93
+ print(f"Price (Nearby): {parser.extract_nearby_regex('Add to Basket', r'[\£\$\€]\d+[.,]\d{2}')}")
@@ -0,0 +1,71 @@
1
+ import asyncio
2
+ import aiohttp
3
+ import json
4
+ from typing import Optional, Dict, Any
5
+
6
+ async def send_to_n8n(webhook_url: str, payload: Dict[str, Any]) -> bool:
7
+ """
8
+ Asynchronously sends a JSON payload to an n8n, Make.com, or Zapier webhook.
9
+
10
+ :param webhook_url: The webhook URL provided by the automation tool.
11
+ :param payload: A dictionary containing the scraped data.
12
+ :return: True if successful, False otherwise.
13
+ """
14
+ if not webhook_url:
15
+ return False
16
+
17
+ print(f"[\033[94mScraprime\033[0m] Dispatching data to webhook: {webhook_url}")
18
+
19
+ try:
20
+ # Use aiohttp for non-blocking HTTP POST request
21
+ async with aiohttp.ClientSession() as session:
22
+ headers = {"Content-Type": "application/json"}
23
+ async with session.post(webhook_url, json=payload, headers=headers, timeout=10) as response:
24
+
25
+ if response.status in [200, 201, 202]:
26
+ print("[\033[92mScraprime\033[0m] Webhook successfully received.")
27
+ return True
28
+ else:
29
+ text = await response.text()
30
+ print(f"[\033[91mScraprime\033[0m] Webhook failed. Status: {response.status}, Response: {text}")
31
+ return False
32
+
33
+ except asyncio.TimeoutError:
34
+ print("[\033[91mScraprime\033[0m] Webhook timed out.")
35
+ return False
36
+ except Exception as e:
37
+ print(f"[\033[91mScraprime\033[0m] Webhook Exception: {e}")
38
+ return False
39
+
40
+ def send_to_n8n_sync(webhook_url: str, payload: Dict[str, Any]) -> bool:
41
+ """
42
+ Synchronous wrapper for send_to_n8n.
43
+ Useful for users who are not running an async event loop.
44
+ """
45
+ loop = asyncio.get_event_loop()
46
+ return loop.run_until_complete(send_to_n8n(webhook_url, payload))
47
+
48
+
49
+ # ==========================================
50
+ # USAGE EXAMPLE
51
+ # ==========================================
52
+ if __name__ == "__main__":
53
+ # Simulated scraped data
54
+ scraped_data = {
55
+ "url": "https://www.very.co.uk/narva-sofa",
56
+ "product_name": "Narva Fabric 2 Seater Sofa",
57
+ "price": "479.00",
58
+ "stock_status": "In Stock"
59
+ }
60
+
61
+ # Put your actual n8n webhook URL here to test it!
62
+ # n8n webhooks usually look like: https://your-n8n.app/webhook/xyz-123
63
+ test_webhook_url = "https://webhook.site/your-unique-id"
64
+
65
+ print("Testing sync webhook sender...")
66
+ success = send_to_n8n_sync(test_webhook_url, scraped_data)
67
+
68
+ if success:
69
+ print("Check your webhook tool! The data should be there.")
70
+ else:
71
+ print("Webhook failed. Check the URL or network.")
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: scraprime
3
+ Version: 0.1.0
4
+ Summary: Prime infrastructure for undetectable scraping. 3-tier WAF bypass & auto-healing parsers.
5
+ Home-page: https://github.com/yourusername/scraprime
6
+ Author: Anees Ur Rahman
7
+ Author-email: anisshewa@gmail.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: curl_cffi>=0.7.0
13
+ Requires-Dist: scrapling>=0.2.0
14
+ Requires-Dist: camoufox[geoip]>=0.4.0
15
+ Requires-Dist: aiohttp>=3.9.0
16
+ Requires-Dist: pydantic>=2.0.0
17
+ Requires-Dist: selectolax>=0.3.21
18
+ Dynamic: author
19
+ Dynamic: author-email
20
+ Dynamic: classifier
21
+ Dynamic: home-page
22
+ Dynamic: requires-dist
23
+ Dynamic: requires-python
24
+ Dynamic: summary
@@ -0,0 +1,11 @@
1
+ setup.py
2
+ scraprime/__init__.py
3
+ scraprime/fetcher.py
4
+ scraprime/models.py
5
+ scraprime/parser.py
6
+ scraprime/webhook.py
7
+ scraprime.egg-info/PKG-INFO
8
+ scraprime.egg-info/SOURCES.txt
9
+ scraprime.egg-info/dependency_links.txt
10
+ scraprime.egg-info/requires.txt
11
+ scraprime.egg-info/top_level.txt
@@ -0,0 +1,6 @@
1
+ curl_cffi>=0.7.0
2
+ scrapling>=0.2.0
3
+ camoufox[geoip]>=0.4.0
4
+ aiohttp>=3.9.0
5
+ pydantic>=2.0.0
6
+ selectolax>=0.3.21
@@ -0,0 +1 @@
1
+ scraprime
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,25 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="scraprime",
5
+ version="0.1.0",
6
+ description="Prime infrastructure for undetectable scraping. 3-tier WAF bypass & auto-healing parsers.",
7
+ author="Anees Ur Rahman",
8
+ author_email="anisshewa@gmail.com",
9
+ url="https://github.com/yourusername/scraprime",
10
+ packages=find_packages(),
11
+ install_requires=[
12
+ "curl_cffi>=0.7.0",
13
+ "scrapling>=0.2.0",
14
+ "camoufox[geoip]>=0.4.0", # <-- This ensures geoip installs automatically!
15
+ "aiohttp>=3.9.0",
16
+ "pydantic>=2.0.0",
17
+ "selectolax>=0.3.21",
18
+ ],
19
+ python_requires=">=3.10",
20
+ classifiers=[
21
+ "Programming Language :: Python :: 3",
22
+ "License :: OSI Approved :: MIT License",
23
+ "Operating System :: OS Independent",
24
+ ],
25
+ )