amogaddy commited on
Commit
6234198
·
verified ·
1 Parent(s): b1a33c2

Fix: DuckDuckGo bloccava le richieste automatizzate, sostituito con Bing (testato e funzionante)

Browse files
Files changed (1) hide show
  1. scraper.py +94 -35
scraper.py CHANGED
@@ -1,11 +1,13 @@
1
- from urllib.parse import quote
 
 
2
  import trafilatura
3
  from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError
4
  from errors import get_logger, GenerAIError, ErrorCode, fmt_exc
5
 
6
  log = get_logger("scraper")
7
 
8
- SEARCH_URL = "https://html.duckduckgo.com/html/"
9
  USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
10
  NAV_TIMEOUT_MS = 15000
11
 
@@ -17,23 +19,42 @@ def _launch_browser(p):
17
  )
18
 
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  def _search_links(page, query: str, max_results: int) -> list[dict]:
21
- """Cerca su DuckDuckGo (HTML, no JS) e restituisce [{title, url, snippet}]."""
22
- url = f"{SEARCH_URL}?q={quote(query)}&kl=it-it"
23
  page.goto(url, timeout=NAV_TIMEOUT_MS, wait_until="domcontentloaded")
24
 
25
  hits = []
26
- rows = page.locator(".result").all()[: max_results * 2]
27
  for row in rows:
28
  try:
29
- link = row.locator("a.result__a").first
 
 
30
  href = link.get_attribute("href")
31
- title = link.inner_text().strip()
32
- if not href or not href.startswith("http"):
33
  continue
34
- snippet_el = row.locator(".result__snippet").first
35
- snippet = snippet_el.inner_text().strip() if snippet_el.count() else ""
36
- hits.append({"title": title, "url": href, "snippet": snippet})
 
 
 
37
  except Exception:
38
  continue
39
  return hits
@@ -53,49 +74,36 @@ def _extract_text(page, url: str, snippet: str) -> str | None:
53
  no_fallback=False,
54
  )
55
  except PlaywrightTimeoutError:
56
- log.warning("[%s] Timeout caricamento pagina: %s", ErrorCode.WEB_FETCH_FAILED.value, url)
57
  text = None
58
  except Exception as e:
59
- log.warning("[%s] Fetch fallito per %s — %s", ErrorCode.WEB_FETCH_FAILED.value, url, fmt_exc(e))
60
  text = None
61
 
62
- # Fallback allo snippet del motore di ricerca se l'estrazione non basta
63
- if not text or len(text) < 100:
64
  text = snippet
65
-
66
- if not text or len(text) < 50:
67
  return None
68
  return text[:2000]
69
 
70
 
71
- def search_and_extract(query: str, max_results: int = 3) -> list[dict]:
72
- """Cerca sul web tramite Chromium (Playwright) ed estrae testo pulito.
73
-
74
- Ritorna [] se la ricerca fallisce completamente. Interfaccia invariata
75
- rispetto alla precedente implementazione basata su SearXNG.
76
- """
77
- log.info("Ricerca web (Chromium) per: %r", query)
78
-
79
  try:
80
  with sync_playwright() as p:
81
  browser = _launch_browser(p)
82
- context = browser.new_context(user_agent=USER_AGENT)
83
  page = context.new_page()
84
  try:
85
  hits = _search_links(page, query, max_results)
86
  except PlaywrightTimeoutError:
87
- log.warning("[%s] Timeout ricerca DuckDuckGo per: %r", ErrorCode.WEB_SEARCH_FAILED.value, query)
88
  hits = []
89
  except Exception as e:
90
- log.warning("[%s] Ricerca DuckDuckGo fallita: %s", ErrorCode.WEB_SEARCH_FAILED.value, fmt_exc(e))
91
  hits = []
92
 
93
- if not hits:
94
- browser.close()
95
- log.warning("[%s] Nessun risultato per: %r", ErrorCode.WEB_NO_RESULTS.value, query)
96
- return []
97
-
98
- results = []
99
  for hit in hits:
100
  if len(results) >= max_results:
101
  break
@@ -112,8 +120,59 @@ def search_and_extract(query: str, max_results: int = 3) -> list[dict]:
112
  err = GenerAIError(ErrorCode.WEB_SEARCH_FAILED, f"Browser Chromium non avviato: {fmt_exc(e)}", cause=e)
113
  err.log(log)
114
  return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
  if not results:
117
- log.warning("[%s] Nessun testo utile estratto per: %r", ErrorCode.WEB_NO_RESULTS.value, query)
118
 
119
  return results
 
1
+ import base64
2
+ from urllib.parse import quote, urlparse, parse_qs
3
+ import requests
4
  import trafilatura
5
  from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError
6
  from errors import get_logger, GenerAIError, ErrorCode, fmt_exc
7
 
8
  log = get_logger("scraper")
9
 
10
+ SEARCH_URL = "https://www.bing.com/search"
11
  USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
12
  NAV_TIMEOUT_MS = 15000
13
 
 
19
  )
20
 
21
 
22
+ def _decode_bing_redirect(href: str) -> str:
23
+ """Bing avvolge i link organici in redirect di tracking (bing.com/ck/a?...&u=<base64>)."""
24
+ if "bing.com/ck/a" not in href:
25
+ return href
26
+ try:
27
+ u = parse_qs(urlparse(href).query).get("u", [""])[0]
28
+ if u.startswith("a1"):
29
+ u = u[2:]
30
+ pad = "=" * (-len(u) % 4)
31
+ return base64.urlsafe_b64decode(u + pad).decode("utf-8", errors="ignore")
32
+ except Exception:
33
+ return href
34
+
35
+
36
  def _search_links(page, query: str, max_results: int) -> list[dict]:
37
+ """Cerca su Bing e restituisce [{title, url, snippet}]."""
38
+ url = f"{SEARCH_URL}?q={quote(query)}&setlang=it"
39
  page.goto(url, timeout=NAV_TIMEOUT_MS, wait_until="domcontentloaded")
40
 
41
  hits = []
42
+ rows = page.locator("li.b_algo").all()[: max_results * 2]
43
  for row in rows:
44
  try:
45
+ link = row.locator("h2 a").first
46
+ if not link.count():
47
+ continue
48
  href = link.get_attribute("href")
49
+ title = link.text_content() or ""
50
+ if not href:
51
  continue
52
+ snippet_el = row.locator("p, .b_lineclamp2, .b_lineclamp3, .b_lineclamp4").first
53
+ snippet = snippet_el.text_content() if snippet_el.count() else ""
54
+ real_url = _decode_bing_redirect(href)
55
+ if not real_url.startswith("http"):
56
+ continue
57
+ hits.append({"title": title.strip(), "url": real_url, "snippet": (snippet or "").strip()})
58
  except Exception:
59
  continue
60
  return hits
 
74
  no_fallback=False,
75
  )
76
  except PlaywrightTimeoutError:
77
+ log.debug("Timeout caricamento pagina: %s", url)
78
  text = None
79
  except Exception as e:
80
+ log.debug("Fetch fallito per %s — %s", url, fmt_exc(e))
81
  text = None
82
 
83
+ if not text or len(text) < 80:
 
84
  text = snippet
85
+ if not text or len(text) < 20:
 
86
  return None
87
  return text[:2000]
88
 
89
 
90
+ def _chromium_search(query: str, max_results: int) -> list[dict]:
91
+ """Cerca ed estrae testo usando Chromium headless (Playwright) via Bing."""
92
+ results: list[dict] = []
 
 
 
 
 
93
  try:
94
  with sync_playwright() as p:
95
  browser = _launch_browser(p)
96
+ context = browser.new_context(user_agent=USER_AGENT, locale="it-IT")
97
  page = context.new_page()
98
  try:
99
  hits = _search_links(page, query, max_results)
100
  except PlaywrightTimeoutError:
101
+ log.warning("[%s] Timeout ricerca Bing per: %r", ErrorCode.WEB_SEARCH_FAILED.value, query)
102
  hits = []
103
  except Exception as e:
104
+ log.warning("[%s] Ricerca Bing fallita: %s", ErrorCode.WEB_SEARCH_FAILED.value, fmt_exc(e))
105
  hits = []
106
 
 
 
 
 
 
 
107
  for hit in hits:
108
  if len(results) >= max_results:
109
  break
 
120
  err = GenerAIError(ErrorCode.WEB_SEARCH_FAILED, f"Browser Chromium non avviato: {fmt_exc(e)}", cause=e)
121
  err.log(log)
122
  return []
123
+ return results
124
+
125
+
126
+ def _wikipedia_search(query: str) -> list[dict]:
127
+ """Fallback diretto su Wikipedia italiana + inglese."""
128
+ results = []
129
+ for lang in ("it", "en"):
130
+ if len(results) >= 2:
131
+ break
132
+ try:
133
+ api = f"https://{lang}.wikipedia.org/w/api.php"
134
+ s = requests.get(api, params={
135
+ "action": "query", "list": "search",
136
+ "srsearch": query, "format": "json", "srlimit": 2,
137
+ }, timeout=8, headers={"User-Agent": "GenerAI/3.0"})
138
+ if s.status_code != 200:
139
+ continue
140
+ for hit in s.json().get("query", {}).get("search", []):
141
+ title = hit["title"]
142
+ p = requests.get(api, params={
143
+ "action": "query", "prop": "extracts",
144
+ "exintro": "1", "explaintext": "1",
145
+ "titles": title, "format": "json",
146
+ }, timeout=8, headers={"User-Agent": "GenerAI/3.0"})
147
+ if p.status_code != 200:
148
+ continue
149
+ for page in p.json().get("query", {}).get("pages", {}).values():
150
+ extract = page.get("extract", "").strip()
151
+ if len(extract) > 50:
152
+ results.append({
153
+ "title": page["title"],
154
+ "url": f"https://{lang}.wikipedia.org/wiki/{page['title'].replace(' ', '_')}",
155
+ "text": extract[:2000],
156
+ })
157
+ break
158
+ except Exception as e:
159
+ log.debug("Wikipedia %s fallita: %s", lang, fmt_exc(e))
160
+ if results:
161
+ log.info("Wikipedia fallback → %d risultati", len(results))
162
+ return results
163
+
164
+
165
+ def search_and_extract(query: str, max_results: int = 3) -> list[dict]:
166
+ """Cerca sul web con Chromium (Playwright, Bing) + fallback Wikipedia. Estrae testo pulito."""
167
+ log.info("Ricerca web (Chromium) per: %r", query)
168
+
169
+ results = _chromium_search(query, max_results)
170
+
171
+ if not results:
172
+ log.info("Chromium non ha prodotto risultati — provo Wikipedia...")
173
+ results = _wikipedia_search(query)
174
 
175
  if not results:
176
+ log.warning("[%s] Nessun risultato per: %r", ErrorCode.WEB_NO_RESULTS.value, query)
177
 
178
  return results