mirror of
https://github.com/R0m1k3/Priceflow.git
synced 2026-10-24 07:38:13 +02:00
4 files changed
+60
-5
No files matched your search
@@ -16,6 +16,9 @@ FROM python:3.12-slim
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Enable unbuffered logging
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
# Install uv
|
# Install uv
|
||||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ SITE_CONFIGS = {
|
|||||||
"search_url": "https://www.carrefour.fr/s?q={query}",
|
"search_url": "https://www.carrefour.fr/s?q={query}",
|
||||||
"product_selector": "a.product-card-click-wrapper",
|
"product_selector": "a.product-card-click-wrapper",
|
||||||
"wait_selector": "a.product-card-click-wrapper",
|
"wait_selector": "a.product-card-click-wrapper",
|
||||||
|
"product_image_selector": "article > div > div > img:not([class])",
|
||||||
"category": "Grande Surface",
|
"category": "Grande Surface",
|
||||||
"requires_proxy": True,
|
"requires_proxy": True,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -84,10 +84,21 @@ class NewSearchService:
|
|||||||
async def scrape_item(result: SearchResult) -> SearchResult:
|
async def scrape_item(result: SearchResult) -> SearchResult:
|
||||||
"""Scrape details for a single item"""
|
"""Scrape details for a single item"""
|
||||||
try:
|
try:
|
||||||
|
# Determine if proxy is needed based on source config
|
||||||
|
config = SITE_CONFIGS.get(result.source) if result.source in SITE_CONFIGS else None
|
||||||
|
# Fallback: check if source name matches a config key
|
||||||
|
if not config:
|
||||||
|
for key, cfg in SITE_CONFIGS.items():
|
||||||
|
if cfg["name"] == result.source:
|
||||||
|
config = cfg
|
||||||
|
break
|
||||||
|
|
||||||
|
use_proxy = config.get("requires_proxy", False) if config else False
|
||||||
|
|
||||||
# Use browserless to get content and screenshot
|
# Use browserless to get content and screenshot
|
||||||
html, screenshot_path = await browserless_service.get_page_content(
|
html, screenshot_path = await browserless_service.get_page_content(
|
||||||
result.url,
|
result.url,
|
||||||
use_proxy="amazon" in result.source.lower(),
|
use_proxy=use_proxy,
|
||||||
wait_selector=None
|
wait_selector=None
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -140,7 +151,7 @@ class NewSearchService:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
# Phase 1: Parse results
|
# Phase 1: Parse results
|
||||||
initial_results = NewSearchService._parse_results(html_content, site_key, search_url)
|
initial_results = NewSearchService._parse_results(html_content, site_key, search_url, query)
|
||||||
|
|
||||||
# Phase 2: Scrape details for each result (Parallel)
|
# Phase 2: Scrape details for each result (Parallel)
|
||||||
# Limit concurrency to avoid overloading
|
# Limit concurrency to avoid overloading
|
||||||
@@ -156,12 +167,16 @@ class NewSearchService:
|
|||||||
return enriched_results
|
return enriched_results
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_results(html: str, site_key: str, base_url: str) -> list[SearchResult]:
|
def _parse_results(html: str, site_key: str, base_url: str, query: str) -> list[SearchResult]:
|
||||||
"""Parse HTML content to extract search results"""
|
"""Parse HTML content to extract search results"""
|
||||||
config = SITE_CONFIGS[site_key]
|
config = SITE_CONFIGS[site_key]
|
||||||
soup = BeautifulSoup(html, "html.parser")
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
|
# Prepare query words for filtering
|
||||||
|
# Split by whitespace, lowercase, remove special chars if needed, filter out short words
|
||||||
|
query_words = [w.lower() for w in query.split() if len(w) > 2]
|
||||||
|
|
||||||
# Select product links
|
# Select product links
|
||||||
links = soup.select(config["product_selector"])
|
links = soup.select(config["product_selector"])
|
||||||
|
|
||||||
@@ -209,12 +224,47 @@ class NewSearchService:
|
|||||||
if not title or len(title) < 3:
|
if not title or len(title) < 3:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# STRICT FILTERING: Check if all query words are in the title
|
||||||
|
title_lower = title.lower()
|
||||||
|
if query_words:
|
||||||
|
all_words_found = True
|
||||||
|
for word in query_words:
|
||||||
|
if word not in title_lower:
|
||||||
|
all_words_found = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if not all_words_found:
|
||||||
|
# logger.debug(f"Skipping result '{title}' - does not contain all query words: {query_words}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Extract Image URL (New)
|
||||||
|
image_url = None
|
||||||
|
if "product_image_selector" in config:
|
||||||
|
# Try to find image relative to the link or its container
|
||||||
|
# This is tricky because 'link' is just the <a> tag.
|
||||||
|
# We might need to look up to a container.
|
||||||
|
container = link.find_parent("article") or link.find_parent("div", class_=lambda x: x and "product" in x)
|
||||||
|
if container:
|
||||||
|
img_el = container.select_one(config["product_image_selector"])
|
||||||
|
if img_el:
|
||||||
|
image_url = img_el.get("src") or img_el.get("data-src")
|
||||||
|
|
||||||
|
# Fallback image extraction
|
||||||
|
if not image_url:
|
||||||
|
img = link.find("img")
|
||||||
|
if img:
|
||||||
|
image_url = img.get("src") or img.get("data-src")
|
||||||
|
|
||||||
|
if image_url and not image_url.startswith("http"):
|
||||||
|
image_url = urljoin(base_url, image_url)
|
||||||
|
|
||||||
# Create result
|
# Create result
|
||||||
results.append(SearchResult(
|
results.append(SearchResult(
|
||||||
url=full_url,
|
url=full_url,
|
||||||
title=title,
|
title=title,
|
||||||
snippet=f"Product from {config['name']}",
|
snippet=f"Product from {config['name']}",
|
||||||
source=config["name"]
|
source=config["name"],
|
||||||
|
image_url=image_url
|
||||||
))
|
))
|
||||||
|
|
||||||
logger.info(f"Found {len(results)} results for {site_key}")
|
logger.info(f"Found {len(results)} results for {site_key}")
|
||||||
@@ -245,7 +295,7 @@ class NewSearchService:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Phase 1: Parse results
|
# Phase 1: Parse results
|
||||||
initial_results = NewSearchService._parse_results(html_content, site_key, search_url)
|
initial_results = NewSearchService._parse_results(html_content, site_key, search_url, query)
|
||||||
|
|
||||||
# Phase 2: Scrape details for each result (Parallel)
|
# Phase 2: Scrape details for each result (Parallel)
|
||||||
# We want to yield results as they complete, not wait for all
|
# We want to yield results as they complete, not wait for all
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ services:
|
|||||||
browserless:
|
browserless:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
volumes:
|
volumes:
|
||||||
|
- ./app:/app/app
|
||||||
- screenshots_data:/app/screenshots
|
- screenshots_data:/app/screenshots
|
||||||
networks:
|
networks:
|
||||||
- nginx_default
|
- nginx_default
|
||||||
|
|||||||
Reference in new issue
Block a user