feat: Add Amazon scraper service with persistent browser, anti-detection, and Pydantic schemas for product data.

This commit is contained in:
Michael committed 2025-12-24 23:10:20 +01:00
1 parent f8e4286bdf
commit fa4d454063
1 file changed
+55 -12
+55 -12
View File
@@ -318,6 +318,16 @@ class AmazonScraperService:
products = [] products = []
try: try:
# First, save a debug screenshot and HTML for analysis
try:
await page.screenshot(path="/tmp/amazon_dom_debug.png")
html = await page.content()
with open("/tmp/amazon_dom_debug.html", "w", encoding="utf-8") as f:
f.write(html)
logger.info("📸 Saved debug screenshot and HTML to /tmp/amazon_dom_debug.*")
except Exception as e:
logger.warning(f"Could not save debug files: {e}")
# Find all product cards using Playwright locators # Find all product cards using Playwright locators
product_cards = page.locator('div[data-component-type="s-search-result"]') product_cards = page.locator('div[data-component-type="s-search-result"]')
count = await product_cards.count() count = await product_cards.count()
@@ -336,27 +346,46 @@ class AmazonScraperService:
# Get ASIN # Get ASIN
asin = await card.get_attribute("data-asin") asin = await card.get_attribute("data-asin")
if not asin: if not asin:
logger.debug(f" Card {idx}: No ASIN, skipping")
continue continue
# Log card outer HTML for debugging (first 200 chars)
try:
outer_html = await card.evaluate("el => el.outerHTML")
logger.debug(f" Card {idx} ASIN={asin}: {outer_html[:200]}...")
except Exception:
pass
# Check if sponsored # Check if sponsored
sponsored = await card.locator('[data-component-type="sp-sponsored-result"]').count() > 0 sponsored = await card.locator('[data-component-type="sp-sponsored-result"]').count() > 0
# Get title - try multiple selectors # Get title - try multiple selectors
title = None title = None
for selector in ["h2 a span", "h2 span", "h2.s-line-clamp-2 span"]: for selector in ["h2 a span", "h2 span", "h2.s-line-clamp-2 span", "h2"]:
title_elem = card.locator(selector).first title_elem = card.locator(selector).first
if await title_elem.count() > 0: if await title_elem.count() > 0:
title = await title_elem.inner_text() try:
if title: title = await title_elem.inner_text()
title = title.strip() if title:
break title = title.strip()
break
except Exception:
pass
if not title: if not title:
# Try to get any text content from the card
try:
all_text = await card.inner_text()
logger.debug(f" Card {idx}: No h2 title. Card text preview: {all_text[:100]}...")
except Exception:
pass
continue continue
logger.debug(f" Card {idx}: Found title = {title[:50]}...")
# Get URL # Get URL
url = None url = None
link_elem = card.locator("h2 a").first link_elem = card.locator("h2 a, a[href*='/dp/']").first
if await link_elem.count() > 0: if await link_elem.count() > 0:
href = await link_elem.get_attribute("href") href = await link_elem.get_attribute("href")
if href and href != "#": if href and href != "#":
@@ -366,17 +395,26 @@ class AmazonScraperService:
url = href url = href
if not url: if not url:
logger.debug(f" Card {idx}: No valid URL found")
continue continue
logger.debug(f" Card {idx}: Found URL = {url[:60]}...")
# Get price - try multiple selectors # Get price - try multiple selectors
price = None price = None
for selector in [".a-price .a-offscreen", ".a-price-whole", "span.a-price span.a-offscreen"]: for selector in [
".a-price .a-offscreen",
".a-price-whole",
"span.a-price span.a-offscreen",
".a-price",
]:
price_elem = card.locator(selector).first price_elem = card.locator(selector).first
if await price_elem.count() > 0: if await price_elem.count() > 0:
try: try:
price_text = await price_elem.inner_text() price_text = await price_elem.inner_text()
price = parse_amazon_price(price_text) price = parse_amazon_price(price_text)
if price: if price:
logger.debug(f" Card {idx}: Found price = {price}€")
break break
except Exception: except Exception:
pass pass
@@ -393,12 +431,17 @@ class AmazonScraperService:
# Get rating # Get rating
rating = None rating = None
for selector in ['[aria-label*="étoile"]', '[aria-label*="star"]']: for selector in ['[aria-label*="étoile"]', '[aria-label*="star"]', "i.a-icon-star span"]:
rating_elem = card.locator(selector).first rating_elem = card.locator(selector).first
if await rating_elem.count() > 0: if await rating_elem.count() > 0:
try: try:
aria_label = await rating_elem.get_attribute("aria-label") aria_label = await rating_elem.get_attribute("aria-label")
rating = parse_rating(aria_label or "") if aria_label:
rating = parse_rating(aria_label)
else:
# Try inner text
rating_text = await rating_elem.inner_text()
rating = parse_rating(rating_text)
if rating: if rating:
break break
except Exception: except Exception:
@@ -406,7 +449,7 @@ class AmazonScraperService:
# Get reviews count # Get reviews count
reviews_count = None reviews_count = None
reviews_elem = card.locator("span.s-underline-text").first reviews_elem = card.locator("span.s-underline-text, span[aria-label*='évaluation']").first
if await reviews_elem.count() > 0: if await reviews_elem.count() > 0:
try: try:
reviews_text = await reviews_elem.inner_text() reviews_text = await reviews_elem.inner_text()
@@ -416,7 +459,7 @@ class AmazonScraperService:
# Get image URL # Get image URL
image_url = None image_url = None
img_elem = card.locator("img.s-image").first img_elem = card.locator("img.s-image, img").first
if await img_elem.count() > 0: if await img_elem.count() > 0:
image_url = await img_elem.get_attribute("src") image_url = await img_elem.get_attribute("src")
@@ -439,7 +482,7 @@ class AmazonScraperService:
sponsored=sponsored, sponsored=sponsored,
) )
products.append(product) products.append(product)
logger.debug(f" ✓ DOM [{len(products)}] {title[:40]}... - {price}€") logger.info(f" ✓ DOM [{len(products)}] {title[:40]}... - {price}€")
except Exception as e: except Exception as e:
logger.warning(f"Error extracting product {idx} from DOM: {e}") logger.warning(f"Error extracting product {idx} from DOM: {e}")