mirror of
https://github.com/R0m1k3/Priceflow.git
synced 2026-10-17 12:18:17 +02:00
- Change BROWSERLESS_URL to use internal Docker port 3000 (not external 3012) - Search each domain separately for better results - Add detailed logging for debugging search issues - Improve domain matching logic
179 lines
5.3 KiB
Python
179 lines
5.3 KiB
Python
"""
|
|
DuckDuckGo Search Service - Recherche de produits via DuckDuckGo
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
from urllib.parse import urlparse
|
|
|
|
from duckduckgo_search import DDGS
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SearchResult:
|
|
"""Résultat de recherche DuckDuckGo"""
|
|
|
|
def __init__(self, url: str, title: str, snippet: str, source: str):
|
|
self.url = url
|
|
self.title = title
|
|
self.snippet = snippet
|
|
self.source = source
|
|
|
|
def __repr__(self):
|
|
return f"SearchResult(url={self.url}, title={self.title[:30]}...)"
|
|
|
|
|
|
async def search(
|
|
query: str,
|
|
domains: list[str],
|
|
max_results: int = 20,
|
|
timeout: float = 15.0,
|
|
) -> list[SearchResult]:
|
|
"""
|
|
Effectue une recherche DuckDuckGo ciblée sur les domaines spécifiés.
|
|
Recherche chaque domaine séparément pour de meilleurs résultats.
|
|
|
|
Args:
|
|
query: Terme de recherche (ex: "lutin de noel")
|
|
domains: Liste des domaines à cibler (ex: ["amazon.fr", "fnac.com"])
|
|
max_results: Nombre maximum de résultats à retourner
|
|
timeout: Timeout en secondes
|
|
|
|
Returns:
|
|
Liste de SearchResult avec url, title, snippet, source
|
|
"""
|
|
if not domains:
|
|
logger.warning("Aucun domaine spécifié pour la recherche")
|
|
return []
|
|
|
|
# Nettoyer les domaines (enlever protocole, www, etc.)
|
|
clean_domains = [_clean_domain(d) for d in domains]
|
|
clean_domains = [d for d in clean_domains if d]
|
|
|
|
logger.info(f"Domaines ciblés: {clean_domains}")
|
|
|
|
all_results = []
|
|
seen_urls = set()
|
|
results_per_domain = max(5, max_results // len(clean_domains))
|
|
|
|
# Rechercher sur chaque domaine séparément
|
|
for domain in clean_domains:
|
|
# Format de requête simple: "query site:domain"
|
|
full_query = f"{query} site:{domain}"
|
|
logger.info(f"Recherche DuckDuckGo: {full_query}")
|
|
|
|
try:
|
|
loop = asyncio.get_event_loop()
|
|
results = await loop.run_in_executor(
|
|
None,
|
|
lambda d=domain, q=full_query: _do_search(q, results_per_domain, timeout),
|
|
)
|
|
|
|
logger.info(f"DuckDuckGo [{domain}]: {len(results)} résultats bruts")
|
|
|
|
for item in results:
|
|
url = item.get("href", "") or item.get("link", "")
|
|
if not url or url in seen_urls:
|
|
continue
|
|
|
|
seen_urls.add(url)
|
|
url_domain = _extract_domain(url)
|
|
|
|
# Vérifier que l'URL appartient au domaine ciblé
|
|
if domain not in url_domain and not url_domain.endswith(domain):
|
|
logger.debug(f"URL ignorée (domaine {url_domain} != {domain}): {url}")
|
|
continue
|
|
|
|
all_results.append(
|
|
SearchResult(
|
|
url=url,
|
|
title=item.get("title", ""),
|
|
snippet=item.get("body", "") or item.get("snippet", ""),
|
|
source=url_domain,
|
|
)
|
|
)
|
|
|
|
if len(all_results) >= max_results:
|
|
break
|
|
|
|
except Exception as e:
|
|
logger.error(f"Erreur recherche sur {domain}: {e}")
|
|
continue
|
|
|
|
if len(all_results) >= max_results:
|
|
break
|
|
|
|
logger.info(f"DuckDuckGo: {len(all_results)} résultats totaux")
|
|
return all_results
|
|
|
|
|
|
def _do_search(query: str, max_results: int, timeout: float) -> list[dict]:
|
|
"""Effectue la recherche synchrone DuckDuckGo"""
|
|
try:
|
|
with DDGS() as ddgs:
|
|
results = list(ddgs.text(
|
|
query,
|
|
region="fr-fr",
|
|
safesearch="off",
|
|
max_results=max_results,
|
|
))
|
|
logger.info(f"DDGS.text('{query[:50]}...') retourne {len(results)} résultats")
|
|
|
|
# Log les premiers résultats pour debug
|
|
for i, r in enumerate(results[:3]):
|
|
logger.info(f" Résultat {i+1}: {r.get('href', 'N/A')[:80]}")
|
|
|
|
return results
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de la recherche DuckDuckGo: {e}")
|
|
return []
|
|
|
|
|
|
def _extract_domain(url: str) -> str:
|
|
"""Extrait le domaine d'une URL"""
|
|
try:
|
|
parsed = urlparse(url)
|
|
domain = parsed.netloc.lower()
|
|
# Retirer www. si présent
|
|
if domain.startswith("www."):
|
|
domain = domain[4:]
|
|
return domain
|
|
except Exception:
|
|
return ""
|
|
|
|
|
|
def _clean_domain(domain: str) -> str:
|
|
"""Nettoie un domaine (enlève protocole, www, slash final, etc.)"""
|
|
if not domain:
|
|
return ""
|
|
|
|
domain = domain.strip()
|
|
|
|
# Si c'est une URL complète, extraire le domaine
|
|
if domain.startswith(("http://", "https://")):
|
|
domain = _extract_domain(domain)
|
|
else:
|
|
# Nettoyer le domaine directement
|
|
domain = domain.lower()
|
|
if domain.startswith("www."):
|
|
domain = domain[4:]
|
|
# Retirer le slash final
|
|
domain = domain.rstrip("/")
|
|
|
|
return domain
|
|
|
|
|
|
async def health_check() -> bool:
|
|
"""Vérifie que DuckDuckGo est accessible"""
|
|
try:
|
|
loop = asyncio.get_event_loop()
|
|
results = await loop.run_in_executor(
|
|
None,
|
|
lambda: _do_search("test", 1, 5.0),
|
|
)
|
|
return len(results) > 0
|
|
except Exception as e:
|
|
logger.warning(f"DuckDuckGo health check failed: {e}")
|
|
return False
|