feat: Add dedicated Amazon extraction without generic filters

- Create _find_amazon_product_links() function for Amazon-specific extraction
- Use ASIN-based deduplication for Amazon products
- Bypass all generic filters (_is_non_product_url, _is_navigation_link) for Amazon
- Multiple fallback methods for Amazon product detection:
  - data-component-type="s-product-image"
  - .s-result-item links
  - All /dp/ links in main content
- Clean Amazon URLs by removing tracking parameters
- Extract title from img alt as fallback for Amazon

This separates Amazon's extraction logic from the generic e-commerce filters
that were designed for French sites like Cdiscount, Fnac, etc.
This commit is contained in:
Claude committed 2025-11-24 08:39:11 +00:00
1 parent 10ec21f4f5
commit 698dbaa029
1 file changed
+164 -56
+164 -56
View File
@@ -1054,74 +1054,119 @@ async def _search_site_browserless( # noqa: PLR0912, PLR0915
results = [] results = []
seen_urls = set() seen_urls = set()
# Trouver les liens produits # ===== AMAZON: Utiliser extraction dédiée sans les filtres génériques =====
links = [] if is_amazon:
if product_selector: logger.info(f"{domain}: Utilisation extraction Amazon dédiée")
# Essayer plusieurs sélecteurs séparés par virgule links = _find_amazon_product_links(soup)
selectors = [s.strip() for s in product_selector.split(",")]
for sel in selectors: for link in links:
try: href = link.get("href", "")
found = soup.select(sel) if not href:
links.extend(found)
except Exception:
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 pour Amazon
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://www.amazon.fr{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 Amazon (enlever les paramètres de tracking)
href = link.get("href", "") if "?" in href:
if not href: href = href.split("?")[0]
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 # Extraire le titre
href = _clean_product_url(href, domain) title = _extract_title(link)
if not title or len(title) < MIN_TITLE_LENGTH:
# Pour Amazon, essayer aussi alt de l'image
img = link.find("img")
if img and img.get("alt"):
title = img.get("alt").strip()
# Éviter les doublons if not title or len(title) < MIN_TITLE_LENGTH:
if href in seen_urls: continue
continue
seen_urls.add(href)
# Vérifier que c'est bien un lien vers ce domaine results.append(SearchResult(
url_domain = _extract_domain(href) url=href,
if domain not in url_domain and url_domain not in domain: title=title,
# logger.debug(f"Ignored external link: {href}") snippet="",
continue source=domain,
))
# Filtrer les liens non-produits if len(results) >= max_results:
if _is_non_product_url(href): break
# logger.debug(f"Ignored non-product link: {href}")
continue
# Extraire le titre logger.info(f"{domain}: {len(results)} résultats Amazon extraits")
title = _extract_title(link)
if not title or len(title) < MIN_TITLE_LENGTH:
# logger.debug(f"Ignored link with no title: {href}")
continue
results.append(SearchResult( # ===== AUTRES SITES: Utiliser la logique générique avec filtres =====
url=href, else:
title=title, # Trouver les liens produits
snippet="", links = []
source=domain, if product_selector:
# On ne peut pas extraire le prix facilement ici sans scraper chaque page # Essayer plusieurs sélecteurs séparés par virgule
# Le scraping détaillé se fera dans la phase 2 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é")
if len(results) >= max_results: # Fallback: détection générique si le sélecteur n'a rien trouvé
break 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")
logger.info(f"{domain}: {len(results)} résultats extraits") for link in links:
href = link.get("href", "")
if not href:
continue
# Construire l'URL complète
if href.startswith("/"):
href = f"https://{domain}{href}"
elif not href.startswith("http"):
href = urljoin(final_url, href)
# Nettoyer l'URL
href = _clean_product_url(href, domain)
# Éviter les doublons
if href in seen_urls:
continue
seen_urls.add(href)
# Vérifier que c'est bien un lien vers ce domaine
url_domain = _extract_domain(href)
if domain not in url_domain and url_domain not in domain:
continue
# Filtrer les liens non-produits (pas pour Amazon qui a sa propre logique)
if _is_non_product_url(href):
continue
# Extraire le titre
title = _extract_title(link)
if not title or len(title) < MIN_TITLE_LENGTH:
continue
results.append(SearchResult(
url=href,
title=title,
snippet="",
source=domain,
))
if len(results) >= max_results:
break
logger.info(f"{domain}: {len(results)} résultats extraits")
# If we got results, success! Return them # If we got results, success! Return them
if len(results) > 0: if len(results) > 0:
@@ -1215,6 +1260,69 @@ def _get_default_wait_selector(domain: str) -> str | None:
return None return None
def _find_amazon_product_links(soup: BeautifulSoup) -> list:
"""
Extraction dédiée pour Amazon - simple et directe, sans tous les filtres génériques.
Amazon utilise des structures spécifiques qui ne nécessitent pas le filtrage complexe
utilisé pour les autres sites e-commerce.
"""
links = []
# Amazon utilise des data-attributes spécifiques pour les résultats de recherche
# Méthode 1: Liens avec data-component-type="s-product-image"
for item in soup.select('[data-component-type="s-product-image"] a'):
href = item.get("href", "")
if href and "/dp/" in href:
links.append(item)
# Méthode 2: Liens dans les cartes de produits Amazon
if not links:
for item in soup.select('.s-result-item a.a-link-normal[href*="/dp/"]'):
links.append(item)
# Méthode 3: Tous les liens avec /dp/ dans le main content
if not links:
main_content = soup.select_one('#search, .s-main-slot, #s-results-list-atf')
if main_content:
for a in main_content.find_all('a', href=True):
href = a.get('href', '')
if '/dp/' in href and href not in [l.get('href', '') for l in links]:
links.append(a)
# Méthode 4: Fallback - tous les liens /dp/ sur la page
if not links:
for a in soup.find_all('a', href=True):
href = a.get('href', '')
if '/dp/' in href or '/gp/product/' in href:
# Ignorer les liens de navigation Amazon
if '/ref=nb_sb' not in href and '/ref=cs_' not in href:
links.append(a)
# Dédupliquer par ASIN (l'identifiant produit Amazon)
seen_asins = set()
unique_links = []
for link in links:
href = link.get('href', '')
# Extraire l'ASIN du lien
if '/dp/' in href:
parts = href.split('/dp/')
if len(parts) > 1:
asin = parts[1].split('/')[0].split('?')[0]
if asin and asin not in seen_asins and len(asin) == 10:
seen_asins.add(asin)
unique_links.append(link)
elif '/gp/product/' in href:
parts = href.split('/gp/product/')
if len(parts) > 1:
asin = parts[1].split('/')[0].split('?')[0]
if asin and asin not in seen_asins and len(asin) == 10:
seen_asins.add(asin)
unique_links.append(link)
logger.info(f"Amazon: {len(unique_links)} produits uniques trouvés (ASINs: {list(seen_asins)[:5]}...)")
return unique_links
def _find_product_links(soup: BeautifulSoup, domain: str) -> list: def _find_product_links(soup: BeautifulSoup, domain: str) -> list:
"""Trouve les liens produits de manière générique""" """Trouve les liens produits de manière générique"""
links = [] links = []