mirror of
https://github.com/R0m1k3/Priceflow.git
synced 2026-10-18 12:48:53 +02:00
Merge pull request #182 from R0m1k3/claude/rebuild-search-amazon-01LBnzN1wfNMe1EG9HcpMCdK
Claude/rebuild search amazon 01 l bnz n1wf n me1 eg9 hcp m cd k
This commit is contained in:
3 files changed
+120
-11
No files matched your search
@@ -625,7 +625,7 @@ async def search_products(
|
||||
for site in active_sites:
|
||||
matched = False
|
||||
|
||||
# Normalize domain for comparison (remove www., e-, etc.)
|
||||
# Normalize domain for comparison (remove www., lowercase, etc.)
|
||||
normalized_domain = site.domain.lower().replace("www.", "").strip()
|
||||
|
||||
for key in SITE_CONFIGS.keys():
|
||||
@@ -639,16 +639,48 @@ async def search_products(
|
||||
logger.debug(f"✓ Matched {site.domain} → {key} (exact)")
|
||||
break
|
||||
|
||||
# 2. Key contains domain or domain contains key
|
||||
# 2. Normalize punctuation (. vs - vs nothing) and compare
|
||||
# e.leclerc → eleclerc, e-leclerc.com → eleclecrcom
|
||||
domain_no_punct = normalized_domain.replace("-", "").replace(".", "")
|
||||
key_no_punct = normalized_key.replace("-", "").replace(".", "")
|
||||
|
||||
# Exact match without punctuation
|
||||
if domain_no_punct == key_no_punct:
|
||||
site_keys.append(key)
|
||||
matched = True
|
||||
logger.debug(f"✓ Matched {site.domain} → {key} (normalized punctuation - exact)")
|
||||
break
|
||||
|
||||
# Contains match without punctuation (handles .com, .fr suffixes)
|
||||
# eleclerc in eleclecrcom → True
|
||||
if domain_no_punct in key_no_punct or key_no_punct in domain_no_punct:
|
||||
site_keys.append(key)
|
||||
matched = True
|
||||
logger.debug(f"✓ Matched {site.domain} → {key} (normalized punctuation - contains)")
|
||||
break
|
||||
|
||||
# 3. Key contains domain or domain contains key
|
||||
if normalized_key in normalized_domain or normalized_domain in normalized_key:
|
||||
site_keys.append(key)
|
||||
matched = True
|
||||
logger.debug(f"✓ Matched {site.domain} → {key} (contains)")
|
||||
break
|
||||
|
||||
# 3. Remove prefixes like "e-" and try again
|
||||
domain_without_prefix = normalized_domain.replace("e-", "").replace("la-", "")
|
||||
key_without_prefix = normalized_key.replace("e-", "").replace("la-", "")
|
||||
# 4. Remove prefixes like "e-", "e.", "la-", "la." and try again
|
||||
domain_without_prefix = (
|
||||
normalized_domain
|
||||
.replace("e-", "")
|
||||
.replace("e.", "")
|
||||
.replace("la-", "")
|
||||
.replace("la.", "")
|
||||
)
|
||||
key_without_prefix = (
|
||||
normalized_key
|
||||
.replace("e-", "")
|
||||
.replace("e.", "")
|
||||
.replace("la-", "")
|
||||
.replace("la.", "")
|
||||
)
|
||||
|
||||
if domain_without_prefix == key_without_prefix:
|
||||
site_keys.append(key)
|
||||
@@ -656,6 +688,24 @@ async def search_products(
|
||||
logger.debug(f"✓ Matched {site.domain} → {key} (without prefix)")
|
||||
break
|
||||
|
||||
# 5. Last resort: fuzzy match on core name (remove all punct + common prefixes)
|
||||
domain_core = (
|
||||
domain_no_punct
|
||||
.replace("e", "", 1) # Remove first 'e' if present
|
||||
.replace("la", "", 1) # Remove first 'la' if present
|
||||
)
|
||||
key_core = (
|
||||
key_no_punct
|
||||
.replace("e", "", 1)
|
||||
.replace("la", "", 1)
|
||||
)
|
||||
|
||||
if len(domain_core) > 5 and domain_core in key_core:
|
||||
site_keys.append(key)
|
||||
matched = True
|
||||
logger.debug(f"✓ Matched {site.domain} → {key} (fuzzy core)")
|
||||
break
|
||||
|
||||
if not matched:
|
||||
logger.warning(f"⚠️ No config found for site: {site.domain} (id={site.id})")
|
||||
|
||||
|
||||
@@ -80,11 +80,30 @@ class FnacParser(BaseParser):
|
||||
if not self.filter_by_query(title, query):
|
||||
continue
|
||||
|
||||
# Extract image
|
||||
# Extract image - try in link first, then parent
|
||||
image_url = None
|
||||
parent = link.find_parent(["article", "li", "div"])
|
||||
if parent:
|
||||
# Try picture > source first
|
||||
|
||||
# PRIORITY 1: Try in the link itself
|
||||
picture = link.find("picture")
|
||||
if picture:
|
||||
source = picture.find("source")
|
||||
if source and source.get("srcset"):
|
||||
image_url = source.get("srcset").split(",")[0].split()[0]
|
||||
if not image_url:
|
||||
img = picture.find("img")
|
||||
if img:
|
||||
image_url = self._get_image_src(img)
|
||||
|
||||
# Try img directly in link
|
||||
if not image_url:
|
||||
img = link.find("img")
|
||||
if img:
|
||||
image_url = self._get_image_src(img)
|
||||
|
||||
# PRIORITY 2: Try in parent container
|
||||
if not image_url and parent:
|
||||
# Try picture > source
|
||||
picture = parent.find("picture")
|
||||
if picture:
|
||||
source = picture.find("source")
|
||||
@@ -101,8 +120,10 @@ class FnacParser(BaseParser):
|
||||
if img:
|
||||
image_url = self._get_image_src(img)
|
||||
|
||||
if image_url:
|
||||
image_url = self.make_absolute_url(image_url)
|
||||
if image_url:
|
||||
image_url = self.make_absolute_url(image_url)
|
||||
else:
|
||||
self.logger.warning(f"No image found for Fnac product: {title[:40]}...")
|
||||
|
||||
# Extract price
|
||||
price = None
|
||||
|
||||
@@ -50,6 +50,8 @@ class GenericParser(BaseParser):
|
||||
Uses:
|
||||
- product_selector: CSS selector for product links
|
||||
- product_image_selector: CSS selector for images (optional)
|
||||
|
||||
If configured selector fails, tries common fallback selectors.
|
||||
"""
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
products = []
|
||||
@@ -62,8 +64,44 @@ class GenericParser(BaseParser):
|
||||
|
||||
links = soup.select(product_selector)
|
||||
|
||||
# If configured selector fails, try common fallback selectors
|
||||
if not links:
|
||||
self.logger.warning(f"No products found for {self.site_key} with selector: {product_selector}")
|
||||
self.logger.warning(f"No products with configured selector: {product_selector}")
|
||||
self.logger.info("Trying fallback selectors...")
|
||||
|
||||
# Common e-commerce selectors (ordered by specificity)
|
||||
fallback_selectors = [
|
||||
# Product-specific URLs
|
||||
"a[href*='/produit']",
|
||||
"a[href*='/products/']", # Shopify
|
||||
"a[href*='/product']",
|
||||
"a[href*='/p/']",
|
||||
"a[href*='/item']",
|
||||
# Common class patterns
|
||||
"article a[href]",
|
||||
".product a[href]",
|
||||
".product-card a[href]",
|
||||
".product-item a[href]",
|
||||
"[class*='product'] a[href]",
|
||||
# Shopify specific
|
||||
"a.product-card__link",
|
||||
"a[href*='/collections/']",
|
||||
# Generic structure
|
||||
"article[class*='product'] a",
|
||||
"div[class*='product'] a",
|
||||
"li[class*='product'] a",
|
||||
# Very generic (last resort)
|
||||
"a[class*='product']",
|
||||
]
|
||||
|
||||
for fallback in fallback_selectors:
|
||||
links = soup.select(fallback)
|
||||
if links and len(links) >= 3: # At least 3 links to be credible
|
||||
self.logger.info(f"✓ Found {len(links)} links with fallback: {fallback}")
|
||||
break
|
||||
|
||||
if not links:
|
||||
self.logger.warning(f"No products found for {self.site_key} even with fallbacks")
|
||||
return []
|
||||
|
||||
self.logger.info(f"Found {len(links)} product links for {self.site_name}")
|
||||
|
||||
Reference in new issue
Block a user