fix: Improve French price extraction and cookie handling

AI Schema:
- Improve extraction prompt for French e-commerce pages
- Add specific instructions for French price format (comma decimal, space thousands)
- Add French stock status terms (Ajouter au panier, Disponible, etc.)
- Fix price normalization to handle French format:
  - "12,99" -> 12.99
  - "1 234,56" -> 1234.56
  - "1.234,56" -> 1234.56

Scraper Service:
- Add comprehensive cookie/popup selectors for French sites:
  - E.Leclerc, Auchan, Carrefour (OneTrust, Didomi)
  - Cdiscount (#footer_tc_privacy_button_2)
  - Boulanger (.bcom-consent-accept-all)
  - Gifi, Centrakor (Magento cookie buttons)
  - PrestaShop sites (Stokomani, B&M)
- Add common CMP platform selectors
- Add French text-based selectors for cookie acceptance
This commit is contained in:
Claude committed 2025-11-23 08:12:43 +00:00
1 parent ee40ae68fc
commit 3293b5d58a
2 files changed
+84 -20

No files matched your search

+32 -14
View File
@@ -73,14 +73,28 @@ class AIExtractionResponse(BaseModel):
@field_validator("price", mode="before")
@classmethod
def normalize_price(cls, v):
"""Normalize price to float or None."""
"""Normalize price to float or None, handling French format."""
if v is None or v in ("null", ""):
return None
if isinstance(v, str):
# Remove currency symbols and commas
cleaned = re.sub(r"[^\d.]", "", v)
# Handle French format: "1 234,56" -> "1234.56"
# First, remove spaces (thousand separators)
cleaned = v.replace(" ", "").replace("\u00a0", "")
# Replace comma with dot (French decimal separator)
# But only if there's no dot already (to handle "1.234,56" format)
if "," in cleaned and "." in cleaned:
# Format "1.234,56" - remove dots, replace comma with dot
cleaned = cleaned.replace(".", "").replace(",", ".")
elif "," in cleaned:
# Format "12,99" - just replace comma with dot
cleaned = cleaned.replace(",", ".")
# Remove currency symbols and other non-numeric chars (except dot)
cleaned = re.sub(r"[^\d.]", "", cleaned)
if cleaned:
return float(cleaned)
try:
return float(cleaned)
except ValueError:
return None
return None
return float(v)
@@ -116,23 +130,27 @@ class AIExtractionMetadata(BaseModel):
# Prompt template for schema-first extraction
EXTRACTION_PROMPT_TEMPLATE = """Extract product price and stock status from the image.
EXTRACTION_PROMPT_TEMPLATE = """Extract product price and stock status from this French e-commerce page.
**PRICE:**
- Find the main current price
- Ignore crossed-out prices
- Extract number only (no symbols)
**PRICE (IMPORTANT - French format):**
- French prices use COMMA for decimals: "12,99 €" means 12.99
- Thousands separator is SPACE or DOT: "1 234,56 €" or "1.234,56 €" means 1234.56
- Currency symbol is usually € at the end
- Look for: price tags, "Prix:", "€", numbers near "Ajouter au panier"
- Extract as DECIMAL NUMBER (convert comma to dot): 12,99 -> 12.99
- Ignore crossed-out/barré prices (old prices)
- If multiple prices, take the current/main price (not the original)
- If unclear: set null and confidence < 0.5
**STOCK:**
- TRUE if: "Add to Cart", "Buy Now", "In Stock", "Available"
- FALSE if: "Out of Stock", "Sold Out", "Unavailable", "Notify Me"
- TRUE if: "Ajouter au panier", "Acheter", "En stock", "Disponible", "Add to Cart"
- FALSE if: "Rupture", "Indisponible", "Épuisé", "Out of Stock", "Notify Me"
- NULL if unclear or not shown
**CONFIDENCE (0.0 to 1.0):**
- 0.9-1.0: Very certain
- 0.5-0.8: Moderately certain
- Below 0.5: Unsure
- 0.9-1.0: Price clearly visible
- 0.5-0.8: Price found but partially obscured or uncertain
- Below 0.5: Cannot find price reliably
Respond ONLY with valid JSON:
{{
+52 -6
View File
@@ -209,16 +209,64 @@ class ScraperService:
# Wait a bit for dynamic content if needed
await page.wait_for_timeout(2000)
# Try to close common popups (inclut Amazon)
logger.info("Attempting to close popups...")
# Try to close common popups and cookie banners
logger.info("Attempting to close popups and cookie banners...")
popup_selectors = [
# Amazon spécifique - boutons cookies
# === SITES SPÉCIFIQUES FRANÇAIS ===
# Amazon France
"#sp-cc-accept",
"#sp-cc-rejectall-link",
"input[data-action-type='DISMISS']",
"#a-popover-content-1 button",
"[data-action='a-popover-close']",
# Popups généraux
# E.Leclerc, Darty, Fnac
"#onetrust-accept-btn-handler",
".onetrust-accept-btn-handler",
# Auchan
"#popin_tc_privacy_button_2",
".popin_tc_privacy_button",
"#didomi-notice-agree-button",
# Carrefour
"[data-testid='accept-cookies-button']",
# Cdiscount
"#footer_tc_privacy_button_2",
".privacy_prompt_accept",
# Boulanger
".bcom-consent-accept-all",
"#cookieBanner-accept",
# Gifi, Centrakor, La Foir'Fouille (Magento)
".action-primary.action-accept",
"#btn-cookie-allow",
# PrestaShop (Stokomani, B&M, etc.)
".btn-primary[data-dismiss='modal']",
"#gdpr_consent_agree",
# === CMP (Consent Management Platforms) ===
".didomi-continue-without-agreeing",
"#tarteaucitronPersonalize2",
".tarteaucitronAllow",
"#tarteaucitronAllDenied2",
".cc-btn.cc-allow",
".cky-btn-accept",
"#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll",
"#CybotCookiebotDialogBodyButtonAccept",
".qc-cmp2-summary-buttons button:first-child",
"#axeptio_btn_acceptAll",
".axeptio_acceptAll",
# === TEXTE FRANÇAIS ===
"button:has-text('Tout accepter')",
"button:has-text('Accepter tout')",
"button:has-text('Accepter et fermer')",
"button:has-text('Accepter les cookies')",
"button:has-text('Accepter')",
"button:has-text('J\\'accepte')",
"button:has-text('OK')",
"button:has-text('Continuer')",
"a:has-text('Tout accepter')",
"a:has-text('Accepter')",
# === POPUPS GÉNÉRAUX ===
"button[aria-label='Close']",
"button[aria-label='close']",
"button[aria-label='Fermer']",
@@ -229,9 +277,7 @@ class ScraperService:
"[class*='modal'] button",
"button:has-text('No, thanks')",
"button:has-text('No thanks')",
"button:has-text('Accepter')",
"button:has-text('Accept')",
"button:has-text('Continuer')",
"a:has-text('No, thanks')",
"div[role='dialog'] button[aria-label='Close']",
]