# scraper_nutripaw.py — Generado por CULTIVA IA · Skill: scraper-web-extraccion-datos
# NutriPaw España · Monitorización de precios competidores · v1.0.0
import requests, csv, json, hashlib, time
from datetime import datetime
from bs4 import BeautifulSoup
from playwright.sync_api import sync_playwright
from supabase import create_client
TARGETS = [
{"name": "ZooPetFood", "url": "https://zoopetfood.es/perros/pienso", "mode": "static"},
{"name": "NaturalMascota", "url": "https://naturalmascota.com/alimentos/perros", "mode": "js"},
{"name": "PatasMilanesas", "url": "https://patasmilanesas.es/catalogo?cat=perros", "mode": "proxy"},
]
SELECTORS = {
"container": "div.product-card",
"fields": {
"nombre": "h3.product-title", "marca": "span.brand",
"precio_eur": "span.price", "precio_kg": "span.price-per-kg",
"puntuacion": "span.rating-value", "num_reviews": "span.review-count",
"url_producto": "a.product-link", "disponibilidad": "span.stock-status",
}
}
def content_hash(item):
key = f"{item.get('nombre','')}|{item.get('url_producto','')}"
return hashlib.md5(key.encode()).hexdigest()
def transform(raw_items, source):
clean = []
seen = set()
for item in raw_items:
if not item.get("nombre") or not item.get("precio_eur"): continue
price_str = item["precio_eur"].replace("€","").replace(",",".").strip()
try: price_cents = int(float(price_str) * 100)
except ValueError: continue
h = content_hash(item)
if h in seen: continue
seen.add(h)
clean.append({**item, "price_cents": price_cents, "source": source,
"content_hash": h, "scraped_at": datetime.utcnow().isoformat()})
return clean
def load_supabase(products, url, key):
client = create_client(url, key)
for i in range(0, len(products), 100):
client.table("competitor_products").upsert(
products[i:i+100], on_conflict="content_hash"
).execute()
print(f" ✓ {len(products)} registros cargados en Supabase")
06:02:00[INFO] Pipeline iniciado · NutriPaw Inteligencia Competitiva v1.0.0
06:02:01[OK] Target 1/3: ZooPetFood · mode=static · robots.txt OK
06:02:12[OK] ZooPetFood scraped: 48 productos · 3 páginas · 11.2s
06:02:13[INFO] Target 2/3: NaturalMascota · mode=js · Playwright headless
06:02:24[OK] NaturalMascota scraped: 52 productos · 4 páginas · 10.8s
06:02:25[WARN] PatasMilanesas: 403 en intento directo · fallback a Bright Data Proxy
06:02:31[OK] PatasMilanesas scraped via proxy: 42 productos · 1 página · 9.2s · 0.12 MB
06:02:31[INFO] Transform: 142 raw items · normalizando precios · deduplicando...
06:02:31[OK] Transform completado: 138 items limpios · 4 duplicados descartados
06:02:32[INFO] Load: Supabase project=nutripaw-analytics · table=competitor_products
06:02:40[OK] Batch 1/2 cargado: 100 registros · upserted
06:02:48[OK] Batch 2/2 cargado: 38 registros · upserted
06:02:48[WARN] 7 cambios de precio detectados vs. ejecución anterior
06:02:48[OK] Notificación Slack enviada → #nutripaw-pricing · 7 price changes
06:02:48[OK] Pipeline completado en 48.3s · 138/142 registros cargados (97.2%)