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.
- scraprime-0.1.0/PKG-INFO +24 -0
- scraprime-0.1.0/scraprime/__init__.py +16 -0
- scraprime-0.1.0/scraprime/fetcher.py +162 -0
- scraprime-0.1.0/scraprime/models.py +51 -0
- scraprime-0.1.0/scraprime/parser.py +93 -0
- scraprime-0.1.0/scraprime/webhook.py +71 -0
- scraprime-0.1.0/scraprime.egg-info/PKG-INFO +24 -0
- scraprime-0.1.0/scraprime.egg-info/SOURCES.txt +11 -0
- scraprime-0.1.0/scraprime.egg-info/dependency_links.txt +1 -0
- scraprime-0.1.0/scraprime.egg-info/requires.txt +6 -0
- scraprime-0.1.0/scraprime.egg-info/top_level.txt +1 -0
- scraprime-0.1.0/setup.cfg +4 -0
- scraprime-0.1.0/setup.py +25 -0
scraprime-0.1.0/PKG-INFO
ADDED
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
scraprime
|
scraprime-0.1.0/setup.py
ADDED
|
@@ -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
|
+
)
|