Jitendra12421 commited on
Commit
933a006
·
verified ·
1 Parent(s): 404322a

Apply advanced bot bypass to ScreenerScraper

Browse files
Files changed (1) hide show
  1. feature_pipeline.py +127 -44
feature_pipeline.py CHANGED
@@ -1,7 +1,11 @@
 
1
 
2
  import re
3
  import threading
4
  from datetime import datetime
 
 
 
5
 
6
  import numpy as np
7
  import pandas as pd
@@ -10,42 +14,92 @@ from bs4 import BeautifulSoup
10
  from requests.adapters import HTTPAdapter
11
  from urllib3.util.retry import Retry
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  class _ThreadLocalSessionFactory:
15
- def __init__(self, headers=None, timeout=(3.5, 10.0), pool_maxsize=32):
 
 
 
 
 
 
16
  self._local = threading.local()
17
- self.headers = headers or {"User-Agent": "Mozilla/5.0"}
18
  self.timeout = timeout
19
  self.pool_maxsize = pool_maxsize
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  def get(self) -> requests.Session:
22
  if not hasattr(self._local, "session"):
23
- session = requests.Session()
24
- session.headers.update(self.headers)
25
- retry = Retry(
26
- total=2,
27
- connect=2,
28
- read=2,
29
- backoff_factor=0.15,
30
- status_forcelist=(429, 500, 502, 503, 504),
31
- allowed_methods=frozenset(["GET"]),
32
- raise_on_status=False,
33
- )
34
- adapter = HTTPAdapter(
35
- max_retries=retry,
36
- pool_connections=self.pool_maxsize,
37
- pool_maxsize=self.pool_maxsize,
38
- )
39
- session.mount("https://", adapter)
40
- session.mount("http://", adapter)
41
- self._local.session = session
42
- return self._local.session
43
 
 
44
 
45
  class ScreenerScraper:
46
- def __init__(self):
47
- self._session_factory = _ThreadLocalSessionFactory()
48
- self.timeout = self._session_factory.timeout
 
 
 
 
 
 
 
 
49
 
50
  def _session(self) -> requests.Session:
51
  return self._session_factory.get()
@@ -57,31 +111,60 @@ class ScreenerScraper:
57
  except Exception:
58
  return BeautifulSoup(html, "html.parser")
59
 
60
- def _fetch_html(self, ticker: str, consolidated: bool = True) -> str:
61
- t = ticker.upper().strip()
62
- urls = [
63
- f"https://www.screener.in/company/{t}/consolidated/",
64
- f"https://www.screener.in/company/{t}/",
65
- ] if consolidated else [
66
- f"https://www.screener.in/company/{t}/",
67
- f"https://www.screener.in/company/{t}/consolidated/",
68
- ]
69
-
70
- last_status = None
71
- last_text = ""
72
  session = self._session()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
  for url in urls:
75
  try:
76
- response = session.get(url, timeout=self.timeout)
77
- last_status = response.status_code
78
- last_text = response.text
79
- if response.status_code == 200:
80
- return response.text
81
- except requests.RequestException:
82
  continue
83
 
84
- raise RuntimeError(f"Failed to fetch data for {t}. Last status: {last_status}")
 
 
 
85
 
86
  @staticmethod
87
  def _clean_text(value: str) -> str:
 
1
+ from __future__ import annotations
2
 
3
  import re
4
  import threading
5
  from datetime import datetime
6
+ from dataclasses import dataclass
7
+ from typing import Optional, Tuple
8
+ from urllib.parse import quote
9
 
10
  import numpy as np
11
  import pandas as pd
 
14
  from requests.adapters import HTTPAdapter
15
  from urllib3.util.retry import Retry
16
 
17
+ def _browser_headers() -> dict:
18
+ return {
19
+ "User-Agent": (
20
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
21
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
22
+ "Chrome/125.0.0.0 Safari/537.36"
23
+ ),
24
+ "Accept": (
25
+ "text/html,application/xhtml+xml,application/xml;"
26
+ "q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8"
27
+ ),
28
+ "Accept-Language": "en-US,en;q=0.9",
29
+ "Accept-Encoding": "gzip, deflate, br",
30
+ "Connection": "keep-alive",
31
+ "Upgrade-Insecure-Requests": "1",
32
+ "Cache-Control": "no-cache",
33
+ "Pragma": "no-cache",
34
+ "Referer": "https://www.screener.in/",
35
+ }
36
 
37
  class _ThreadLocalSessionFactory:
38
+ def __init__(
39
+ self,
40
+ headers: Optional[dict] = None,
41
+ timeout: Tuple[float, float] = (5.0, 15.0),
42
+ pool_maxsize: int = 32,
43
+ prime_homepage: bool = True,
44
+ ):
45
  self._local = threading.local()
46
+ self.headers = headers or _browser_headers()
47
  self.timeout = timeout
48
  self.pool_maxsize = pool_maxsize
49
+ self.prime_homepage = prime_homepage
50
+
51
+ def _build_session(self) -> requests.Session:
52
+ session = requests.Session()
53
+ session.headers.update(self.headers)
54
+
55
+ retry = Retry(
56
+ total=3,
57
+ connect=3,
58
+ read=3,
59
+ backoff_factor=0.35,
60
+ status_forcelist=(429, 500, 502, 503, 504),
61
+ allowed_methods=frozenset(["GET"]),
62
+ raise_on_status=False,
63
+ )
64
+
65
+ adapter = HTTPAdapter(
66
+ max_retries=retry,
67
+ pool_connections=self.pool_maxsize,
68
+ pool_maxsize=self.pool_maxsize,
69
+ )
70
+
71
+ session.mount("https://", adapter)
72
+ session.mount("http://", adapter)
73
+ return session
74
 
75
  def get(self) -> requests.Session:
76
  if not hasattr(self._local, "session"):
77
+ self._local.session = self._build_session()
78
+
79
+ # Warm up cookies / anti-bot checks by visiting the homepage first.
80
+ if self.prime_homepage:
81
+ try:
82
+ self._local.session.get(
83
+ "https://www.screener.in/",
84
+ timeout=self.timeout,
85
+ )
86
+ except requests.RequestException:
87
+ pass
 
 
 
 
 
 
 
 
 
88
 
89
+ return self._local.session
90
 
91
  class ScreenerScraper:
92
+ def __init__(
93
+ self,
94
+ timeout: Tuple[float, float] = (5.0, 15.0),
95
+ pool_maxsize: int = 32,
96
+ ):
97
+ self._session_factory = _ThreadLocalSessionFactory(
98
+ timeout=timeout,
99
+ pool_maxsize=pool_maxsize,
100
+ prime_homepage=True,
101
+ )
102
+ self.timeout = timeout
103
 
104
  def _session(self) -> requests.Session:
105
  return self._session_factory.get()
 
111
  except Exception:
112
  return BeautifulSoup(html, "html.parser")
113
 
114
+ @staticmethod
115
+ def _normalize_ticker(ticker: str) -> str:
116
+ t = ticker.strip().upper()
117
+ return quote(t, safe="-._~")
118
+
119
+ def _fetch_url(self, url: str) -> str:
 
 
 
 
 
 
120
  session = self._session()
121
+ response = session.get(url, timeout=self.timeout)
122
+
123
+ if response.status_code == 200:
124
+ return response.text
125
+
126
+ preview = ""
127
+ try:
128
+ preview = response.text[:300].replace("\n", " ").replace("\r", " ")
129
+ except Exception:
130
+ preview = "<no response text>"
131
+
132
+ raise RuntimeError(
133
+ f"Request failed.\n"
134
+ f"URL: {url}\n"
135
+ f"Status: {response.status_code}\n"
136
+ f"Reason: {response.reason}\n"
137
+ f"Response preview: {preview}"
138
+ )
139
+
140
+ def _fetch_html(self, ticker: str, consolidated: bool = True) -> str:
141
+ t = self._normalize_ticker(ticker)
142
+
143
+ if consolidated:
144
+ urls = [
145
+ f"https://www.screener.in/company/{t}/consolidated/",
146
+ f"https://www.screener.in/company/{t}/",
147
+ ]
148
+ else:
149
+ urls = [
150
+ f"https://www.screener.in/company/{t}/",
151
+ f"https://www.screener.in/company/{t}/consolidated/",
152
+ ]
153
+
154
+ last_error: Optional[Exception] = None
155
 
156
  for url in urls:
157
  try:
158
+ html = self._fetch_url(url)
159
+ return html
160
+ except Exception as e:
161
+ last_error = e
 
 
162
  continue
163
 
164
+ raise RuntimeError(
165
+ f"Failed to fetch data for '{ticker}'. "
166
+ f"Last error: {last_error}"
167
+ ) from last_error
168
 
169
  @staticmethod
170
  def _clean_text(value: str) -> str: