Muttered3 commited on
Commit
5afe800
·
verified ·
1 Parent(s): cc64b3c

Update scraper.py

Browse files
Files changed (1) hide show
  1. scraper.py +105 -71
scraper.py CHANGED
@@ -4,104 +4,138 @@ import re
4
  import time
5
  import aiohttp
6
  from logger import get_logger
 
7
 
8
  log = get_logger()
9
 
10
  USER_AGENTS = [
11
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36",
12
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4_1) AppleWebKit/605.1.15 Version/17.4.1 Safari/605.1.15",
13
- "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/123.0.0.0 Safari/537.36",
14
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0"
15
  ]
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  async def check_fragment(word: str, proxy_url: str = None) -> str:
18
- url = f"https://fragment.com/username/{word}"
 
19
 
20
- # 4 Retries matching your advanced optimization matrix
21
  for attempt in range(1, 5):
 
 
 
 
 
 
 
 
22
  try:
23
  headers = {
24
  "User-Agent": random.choice(USER_AGENTS),
25
- "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
26
- "Accept-Language": "en-US,en;q=0.5",
27
- "Referer": "https://fragment.com/"
 
 
28
  }
29
-
30
- timeout = aiohttp.ClientTimeout(total=10)
 
 
 
 
 
 
31
  async with aiohttp.ClientSession(timeout=timeout) as session:
32
-
33
- # OPTIMIZATION: Start with allow_redirects=False to read metadata headers instantly
34
- async with session.get(url, headers=headers, proxy=proxy_url, allow_redirects=False) as resp:
35
- status = resp.status
36
- location = resp.headers.get("Location", "")
37
 
38
- # 🚀 TWO-WAY METADATA REDIRECT DETECTOR (Zero body-parsing overhead)
39
- if status in (301, 302, 303, 307, 308):
40
- location_lower = location.lower()
41
-
42
- # Case A: Explicit Search Page Redirect (Dropped / Unassigned Limbo)
43
- if "?query=" in location_lower or f"query={word.lower()}" in location_lower:
44
- return "UNAVAILABLE"
45
-
46
- # Case B: Firewall Soft-Ban Redirect (Kicked back to clean root index)
47
- elif location == "/" or location.rstrip('/') in ["https://fragment.com", "https://www.fragment.com"]:
48
- backoff = (2 ** attempt) + random.uniform(1.0, 3.0)
49
- log.warning(f"⚠️ Rate limit target hit on '{word}'. Backing off loop thread {backoff:.2f}s...")
50
- await asyncio.sleep(backoff)
51
- continue
52
-
53
- if status in [429, 403]:
54
- backoff = 4 + (2 ** attempt) + random.uniform(1.5, 3.5)
55
- await asyncio.sleep(backoff)
56
  continue
57
-
58
- if status == 404:
59
- return "UNAVAILABLE"
60
-
61
- # Standard Profile Resolution Block (No Redirects)
62
- html = await resp.text()
63
 
64
- if "Just a moment..." in html or "cloudflare" in html.lower() or 'cf-browser-verification' in html:
65
- await asyncio.sleep(random.uniform(3.0, 5.0))
 
 
66
  continue
67
-
68
- # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
69
- # ENGINE 1: PROFILE DOM TARGETING
70
- # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
71
- status_match = re.search(r'class="tm-section-header-status[^"]*"[^>]*>\s*([^<]+?)\s*</span>', html, re.IGNORECASE)
72
- if status_match:
73
- s = status_match.group(1).strip().lower()
74
- if "sold" in s: return "SOLD"
75
- if "taken" in s: return "TAKEN"
76
- if "auction" in s: return "ON_AUCTION"
77
- if "available" in s: return "AVAILABLE"
78
- if "sale" in s or "purchase" in s: return "FOR_SALE"
79
- return s.upper()
80
 
81
- if 'class="tm-status-taken"' in html: return "TAKEN"
82
- if 'class="tm-status-unavail"' in html or 'tm-section-header-status tm-status-unavail' in html:
83
- if "sold for" in html.lower() or "tm-username-usable" in html.lower() or "recently sold" in html.lower():
84
- return "SOLD"
85
- return "UNAVAILABLE"
86
-
87
- # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
88
- # ENGINE 2: FAILSAFE CONVERTED SEARCH LOOKUP LOOP
89
- # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
90
- clean_html = re.sub(r'\s+', ' ', html)
91
- search_regex = re.compile(rf'>@{word}<.*?class="[^"]*status[^"]*"[^>]*>\s*([^<]+?)\s*TL', re.IGNORECASE)
92
  match = search_regex.search(clean_html)
93
 
94
  if match:
95
  s = match.group(1).strip().lower()
96
- if "taken" in s: return "TAKEN"
97
- if "available" in s: return "AVAILABLE"
98
  if "sold" in s: return "SOLD"
99
- if "auction" in s: return "ON_AUCTION"
100
  if "unavailable" in s: return "UNAVAILABLE"
 
 
101
  return s.upper()
102
 
 
 
 
 
 
 
 
 
 
 
103
  except Exception as e:
104
- log.error(f"Execution fault for '{word}' over proxy: {str(e)}")
105
- await asyncio.sleep(1.5 * attempt)
106
-
107
  return "ERROR"
 
4
  import time
5
  import aiohttp
6
  from logger import get_logger
7
+ from state import state
8
 
9
  log = get_logger()
10
 
11
  USER_AGENTS = [
12
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
13
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4_1) AppleWebKit/605.1.15 Version/17.4.1 Safari/605.1.15",
14
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/123.0.0.0 Safari/537.36"
 
15
  ]
16
 
17
+ # Thread-safe global cache for token balancing
18
+ _global_api_hash = None
19
+ _hash_fetched_at = 0.0
20
+ _hash_lock = asyncio.Lock()
21
+ _rate_limited_until = 0.0
22
+
23
+ async def _get_valid_api_hash(proxy_url: str = None) -> str:
24
+ """
25
+ Ensures a singular valid session context hash token is distributed across
26
+ all background threads simultaneously, mitigating Cloudflare handshake overhead.
27
+ """
28
+ global _global_api_hash, _hash_fetched_at
29
+
30
+ async with _hash_lock:
31
+ now = time.time()
32
+ # Recycle token cache window for 15 minutes before executing refreshing sequences
33
+ if _global_api_hash and (now - _hash_fetched_at < 900):
34
+ return _global_api_hash
35
+
36
+ headers = {
37
+ "User-Agent": random.choice(USER_AGENTS),
38
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
39
+ }
40
+
41
+ try:
42
+ timeout = aiohttp.ClientTimeout(total=10)
43
+ async with aiohttp.ClientSession(timeout=timeout) as session:
44
+ async with session.get("https://fragment.com/", headers=headers, proxy=proxy_url, allow_redirects=True) as resp:
45
+ html = await resp.text()
46
+
47
+ match = re.search(r'href="/api\?hash=([a-f0-9]+)"', html) or re.search(r'hash=([a-f0-9]{16,})', html)
48
+ if match:
49
+ _global_api_hash = match.group(1)
50
+ _hash_fetched_at = now
51
+ log.info(f"🌐 Regenerated Global API Session Token: {_global_api_hash}")
52
+ return _global_api_hash
53
+ except Exception as e:
54
+ log.error(f"Global Handshake Sync Failure: {str(e)}")
55
+
56
+ return _global_api_hash if _global_api_hash else "FAILED_TOKEN"
57
+
58
  async def check_fragment(word: str, proxy_url: str = None) -> str:
59
+ global _rate_limited_until
60
+ word = word.strip().replace("@", "").lower()
61
 
 
62
  for attempt in range(1, 5):
63
+ current_time = time.time()
64
+ if current_time < _rate_limited_until:
65
+ await asyncio.sleep(_rate_limited_until - current_time + random.uniform(0.1, 0.4))
66
+
67
+ api_hash = await _get_valid_api_hash(proxy_url)
68
+ if api_hash == "FAILED_TOKEN":
69
+ return "ERROR"
70
+
71
  try:
72
  headers = {
73
  "User-Agent": random.choice(USER_AGENTS),
74
+ "X-Requested-With": "XMLHttpRequest",
75
+ "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
76
+ "Origin": "https://fragment.com",
77
+ "Referer": "https://fragment.com/",
78
+ "Accept": "application/json, text/javascript, */*; q=0.01"
79
  }
80
+
81
+ payload = {
82
+ "query": word,
83
+ "type": "usernames",
84
+ "method": "searchAuctions"
85
+ }
86
+
87
+ timeout = aiohttp.ClientTimeout(total=12)
88
  async with aiohttp.ClientSession(timeout=timeout) as session:
89
+ async with session.post(f"https://fragment.com/api?hash={api_hash}", data=payload, headers=headers, proxy=proxy_url) as resp:
 
 
 
 
90
 
91
+ if resp.status in [429, 403]:
92
+ backoff = 4 + (2 ** attempt) + random.uniform(0.5, 1.5)
93
+ _rate_limited_until = time.time() + backoff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  continue
95
+
96
+ if resp.status != 200:
97
+ continue
98
+
99
+ response_json = await resp.json()
 
100
 
101
+ if not response_json.get("ok"):
102
+ # Force token reload sequence if token data gets invalidated by endpoint engine
103
+ global _global_api_hash
104
+ _global_api_hash = None
105
  continue
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
+ html_fragment = response_json.get("html", "")
108
+ if not html_fragment.strip():
109
+ return "AVAILABLE"
110
+
111
+ # ⚡ PERFORMANCE RE-ENGINEERING: Highly optimized structural lookup layout parsing
112
+ clean_html = re.sub(r'\s+', ' ', html_fragment)
113
+
114
+ # Target row validation isolating keyword structural bounds
115
+ search_regex = re.compile(rf'>@{word}<.*?class="[^"]*status[^"]*"[^>]*>\s*([^<]+?)\s*<', re.IGNORECASE)
 
 
116
  match = search_regex.search(clean_html)
117
 
118
  if match:
119
  s = match.group(1).strip().lower()
120
+ if "auction" in s or "bidding" in s: return "ON_AUCTION"
 
121
  if "sold" in s: return "SOLD"
 
122
  if "unavailable" in s: return "UNAVAILABLE"
123
+ if "taken" in s or "offer" in s: return "TAKEN"
124
+ if "sale" in s or "purchase" in s: return "FOR_SALE"
125
  return s.upper()
126
 
127
+ # Global failsafe text block resolution fallback
128
+ fallback_text = clean_html.lower()
129
+ if "on auction" in fallback_text: return "ON_AUCTION"
130
+ if "sold" in fallback_text: return "SOLD"
131
+ if "unavailable" in fallback_text: return "UNAVAILABLE"
132
+ if "taken" in fallback_text or "make an offer" in fallback_text: return "TAKEN"
133
+ if "for sale" in fallback_text: return "FOR_SALE"
134
+
135
+ return "AVAILABLE"
136
+
137
  except Exception as e:
138
+ log.error(f"Internal API pipeline fault for '{word}' via proxy: {str(e)}")
139
+ await asyncio.sleep(1.0 * attempt)
140
+
141
  return "ERROR"