mirror of
https://github.com/R0m1k3/Priceflow.git
synced 2026-10-24 15:48:11 +02:00
feat: implement direct e-commerce search service with site-specific configurations using Playwright.
This commit is contained in:
1 parent
ee8a14668c
commit
ba28f0b711
1 file changed
+111
-101
@@ -586,124 +586,134 @@ async def _search_site_browserless( # noqa: PLR0912, PLR0915
|
|||||||
|
|
||||||
page = await context.new_page()
|
page = await context.new_page()
|
||||||
|
|
||||||
# Naviguer vers la page de recherche
|
# Naviguer vers la page de recherche
|
||||||
await page.goto(final_url, wait_until="domcontentloaded", timeout=int(timeout * 1000))
|
await page.goto(final_url, wait_until="domcontentloaded", timeout=int(timeout * 1000))
|
||||||
|
|
||||||
# Accepter les cookies si nécessaire
|
# Accepter les cookies si nécessaire
|
||||||
await _accept_cookies(page, domain)
|
await _accept_cookies(page, domain)
|
||||||
|
|
||||||
# Attendre que les résultats se chargent
|
# Attendre que les résultats se chargent
|
||||||
if wait_selector:
|
if wait_selector:
|
||||||
try:
|
|
||||||
await page.wait_for_selector(wait_selector, timeout=10000)
|
|
||||||
except Exception:
|
|
||||||
logger.debug(f"Selector {wait_selector} non trouvé sur {domain}, on continue...")
|
|
||||||
|
|
||||||
# Attendre un peu pour le JS (réduit pour parallélisation)
|
|
||||||
await asyncio.sleep(1.5)
|
|
||||||
|
|
||||||
# Extraire le HTML
|
|
||||||
html_content = await page.content()
|
|
||||||
|
|
||||||
# Check for CAPTCHA
|
|
||||||
if "Enter the characters you see below" in html_content or "Saisissez les caractères" in html_content:
|
|
||||||
logger.warning(f"CAPTCHA detected on {domain}!")
|
|
||||||
_dump_debug_html(html_content, domain, query)
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Parser le HTML avec BeautifulSoup
|
|
||||||
soup = BeautifulSoup(html_content, "lxml")
|
|
||||||
results = []
|
|
||||||
seen_urls = set()
|
|
||||||
|
|
||||||
# Trouver les liens produits
|
|
||||||
links = []
|
|
||||||
if product_selector:
|
|
||||||
# Essayer plusieurs sélecteurs séparés par virgule
|
|
||||||
selectors = [s.strip() for s in product_selector.split(",")]
|
|
||||||
for sel in selectors:
|
|
||||||
try:
|
try:
|
||||||
found = soup.select(sel)
|
await page.wait_for_selector(wait_selector, timeout=10000)
|
||||||
links.extend(found)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
logger.debug(f"Selector {wait_selector} non trouvé sur {domain}, on continue...")
|
||||||
|
|
||||||
|
# Attendre un peu pour le JS (réduit pour parallélisation)
|
||||||
|
await asyncio.sleep(1.5)
|
||||||
|
|
||||||
|
# Extraire le HTML
|
||||||
|
html_content = await page.content()
|
||||||
|
|
||||||
|
# Check for CAPTCHA
|
||||||
|
if "Enter the characters you see below" in html_content or "Saisissez les caractères" in html_content:
|
||||||
|
logger.warning(f"CAPTCHA detected on {domain}!")
|
||||||
|
_dump_debug_html(html_content, domain, query)
|
||||||
|
# Si c'est un captcha, on peut vouloir réessayer ou abandonner.
|
||||||
|
# Ici on retourne vide pour cette tentative, ce qui déclenchera peut-être un retry si on levait une exception,
|
||||||
|
# mais le code original retournait [].
|
||||||
|
# Pour le retry, on va lever une exception si c'est Amazon
|
||||||
|
if "amazon" in domain:
|
||||||
|
raise Exception("CAPTCHA detected")
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Parser le HTML avec BeautifulSoup
|
||||||
|
soup = BeautifulSoup(html_content, "lxml")
|
||||||
|
results = []
|
||||||
|
seen_urls = set()
|
||||||
|
|
||||||
|
# Trouver les liens produits
|
||||||
|
links = []
|
||||||
|
if product_selector:
|
||||||
|
# Essayer plusieurs sélecteurs séparés par virgule
|
||||||
|
selectors = [s.strip() for s in product_selector.split(",")]
|
||||||
|
for sel in selectors:
|
||||||
|
try:
|
||||||
|
found = soup.select(sel)
|
||||||
|
links.extend(found)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
logger.info(f"{domain}: {len(links)} liens trouvés avec sélecteur configuré")
|
||||||
|
|
||||||
|
# Fallback: détection générique si le sélecteur n'a rien trouvé
|
||||||
|
if not links:
|
||||||
|
logger.info(f"{domain}: Sélecteur configuré n'a rien trouvé, utilisation détection générique")
|
||||||
|
links = _find_product_links(soup, domain)
|
||||||
|
logger.info(f"{domain}: {len(links)} liens trouvés avec détection générique")
|
||||||
|
|
||||||
|
for link in links:
|
||||||
|
href = link.get("href", "")
|
||||||
|
if not href:
|
||||||
continue
|
continue
|
||||||
logger.info(f"{domain}: {len(links)} liens trouvés avec sélecteur configuré")
|
|
||||||
|
|
||||||
# Fallback: détection générique si le sélecteur n'a rien trouvé
|
# Construire l'URL complète
|
||||||
if not links:
|
if href.startswith("/"):
|
||||||
logger.info(f"{domain}: Sélecteur configuré n'a rien trouvé, utilisation détection générique")
|
href = f"https://{domain}{href}"
|
||||||
links = _find_product_links(soup, domain)
|
elif not href.startswith("http"):
|
||||||
logger.info(f"{domain}: {len(links)} liens trouvés avec détection générique")
|
href = urljoin(final_url, href)
|
||||||
|
|
||||||
for link in links:
|
# Nettoyer l'URL
|
||||||
href = link.get("href", "")
|
href = _clean_product_url(href, domain)
|
||||||
if not href:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Construire l'URL complète
|
# Éviter les doublons
|
||||||
if href.startswith("/"):
|
if href in seen_urls:
|
||||||
href = f"https://{domain}{href}"
|
continue
|
||||||
elif not href.startswith("http"):
|
seen_urls.add(href)
|
||||||
href = urljoin(final_url, href)
|
|
||||||
|
|
||||||
# Nettoyer l'URL
|
# Vérifier que c'est bien un lien vers ce domaine
|
||||||
href = _clean_product_url(href, domain)
|
url_domain = _extract_domain(href)
|
||||||
|
if domain not in url_domain and url_domain not in domain:
|
||||||
|
# logger.debug(f"Ignored external link: {href}")
|
||||||
|
continue
|
||||||
|
|
||||||
# Éviter les doublons
|
# Filtrer les liens non-produits
|
||||||
if href in seen_urls:
|
if _is_non_product_url(href):
|
||||||
continue
|
# logger.debug(f"Ignored non-product link: {href}")
|
||||||
seen_urls.add(href)
|
continue
|
||||||
|
|
||||||
# Vérifier que c'est bien un lien vers ce domaine
|
# Extraire le titre
|
||||||
url_domain = _extract_domain(href)
|
title = _extract_title(link)
|
||||||
if domain not in url_domain and url_domain not in domain:
|
if not title or len(title) < MIN_TITLE_LENGTH:
|
||||||
# logger.debug(f"Ignored external link: {href}")
|
# logger.debug(f"Ignored link with no title: {href}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Filtrer les liens non-produits
|
results.append(SearchResult(
|
||||||
if _is_non_product_url(href):
|
url=href,
|
||||||
# logger.debug(f"Ignored non-product link: {href}")
|
title=title,
|
||||||
continue
|
snippet="",
|
||||||
|
source=domain,
|
||||||
|
))
|
||||||
|
|
||||||
# Extraire le titre
|
if len(results) >= max_results:
|
||||||
title = _extract_title(link)
|
break
|
||||||
if not title or len(title) < MIN_TITLE_LENGTH:
|
|
||||||
# logger.debug(f"Ignored link with no title: {href}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
results.append(SearchResult(
|
logger.info(f"{domain}: {len(results)} résultats extraits")
|
||||||
url=href,
|
|
||||||
title=title,
|
|
||||||
snippet="",
|
|
||||||
source=domain,
|
|
||||||
# On ne peut pas extraire le prix facilement ici sans scraper chaque page
|
|
||||||
# Le scraping détaillé se fera dans la phase 2
|
|
||||||
))
|
|
||||||
|
|
||||||
if len(results) >= max_results:
|
|
||||||
break
|
|
||||||
|
|
||||||
logger.info(f"{domain}: {len(results)} résultats extraits")
|
|
||||||
|
|
||||||
if len(results) == 0:
|
|
||||||
logger.warning(f"{domain}: 0 results found. Dumping HTML for debugging.")
|
|
||||||
_dump_debug_html(html_content, domain, query)
|
|
||||||
|
|
||||||
return results
|
if len(results) == 0:
|
||||||
|
logger.warning(f"{domain}: 0 results found. Dumping HTML for debugging.")
|
||||||
|
_dump_debug_html(html_content, domain, query)
|
||||||
|
# Si 0 résultats sur Amazon, on peut vouloir réessayer
|
||||||
|
if "amazon" in domain:
|
||||||
|
raise Exception("No results found")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Erreur Browserless sur {domain}: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
finally:
|
|
||||||
# Fermer le contexte et la page, mais PAS le navigateur
|
|
||||||
try:
|
|
||||||
if page:
|
|
||||||
await page.close()
|
|
||||||
if context:
|
|
||||||
await context.close()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Erreur fermeture contexte {domain}: {e}")
|
logger.error(f"Erreur Browserless sur {domain} (tentative {attempt+1}): {e}")
|
||||||
|
if attempt == max_retries - 1:
|
||||||
|
return []
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Fermer le contexte et la page, mais PAS le navigateur
|
||||||
|
try:
|
||||||
|
if page:
|
||||||
|
await page.close()
|
||||||
|
if context:
|
||||||
|
await context.close()
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Erreur fermeture contexte {domain}: {e}")
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
def _get_default_search_url(domain: str) -> str | None:
|
def _get_default_search_url(domain: str) -> str | None:
|
||||||
|
|||||||
Reference in new issue
Block a user