Files
fn_registry/python/functions/datascience/scrape_competitor_prices.md
T
egutierrez e1e9bb7499 feat(shell): auto-commit con 31 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-14 23:55:16 +02:00

5.8 KiB

name, kind, lang, domain, version, purity, signature, description, tags, params, output, uses_functions, uses_types, returns, returns_optional, error_type, imports, tested, tests, test_file_path, file_path
name kind lang domain version purity signature description tags params output uses_functions uses_types returns returns_optional error_type imports tested tests test_file_path file_path
scrape_competitor_prices function py datascience 1.0.0 impure def scrape_competitor_prices(targets: list[dict]) -> list[dict] Vigila precios de la competencia: dada una lista de objetivos (URL de producto + competidor), hace GET con headers realistas (timeout + 1 reintento) y extrae el precio actual de cada pagina con una cascada de estrategias (CSS selector, JSON-LD offers, meta tags, heuristica de clases). Normaliza a float (tolera coma/punto, simbolos, miles) y detecta in_stock. Devuelve una fila por target con claves 1:1 de la tabla Postgres competitor_prices; si falla un target devuelve price=None sin abortar los demas.
competitor
pricing
scraping
market-intel
datascience
recon
name desc
targets Lista de dicts, uno por producto a vigilar. Cada dict: competitor (str, nombre/id del competidor), product_key (str, clave interna estable), product_name (str, nombre legible), url (str, URL de la pagina del producto), price_selector (str, opcional, selector CSS que apunta al nodo del precio — lo mas robusto), currency (str, opcional, codigo de moneda a estampar, default 'EUR').
Lista de dicts, una fila por target, con EXACTAMENTE estas claves (casan 1:1 con la tabla Postgres competitor_prices, sin id/snapshot_date/scraped_at): competitor (str), product_key (str), product_name (str), url (str), price (float | None), currency (str), in_stock (bool | None). price=None si no se pudo extraer; in_stock=None si la pagina fallo.
false error_go_core
requests
beautifulsoup4
lxml
false
python/functions/datascience/scrape_competitor_prices.py

Ejemplo

import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from datascience.scrape_competitor_prices import scrape_competitor_prices

targets = [
    {
        "competitor": "books-to-scrape",
        "product_key": "light-in-the-attic",
        "product_name": "A Light in the Attic",
        "url": "http://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html",
        "price_selector": "p.price_color",   # el selector por target es lo mas fiable
        "currency": "GBP",
    },
    {
        "competitor": "competidor_b",
        "product_key": "SKU-4242",
        "product_name": "Filtro de aceite XYZ",
        "url": "https://www.ejemplo-tienda.com/producto/4242",
        # sin price_selector -> autodeteccion JSON-LD / meta / heuristica de clases
        "currency": "EUR",
    },
]

rows = scrape_competitor_prices(targets)
# rows[0] -> {"competitor": "books-to-scrape", "product_key": "light-in-the-attic",
#             "product_name": "A Light in the Attic", "url": "...",
#             "price": 51.77, "currency": "GBP", "in_stock": True}
# Listo para INSERT en la tabla competitor_prices (anade tu snapshot_date/scraped_at).

Cuando usarla

Cuando necesites un snapshot puntual del precio de uno o varios productos de la competencia para alimentar una tabla de market intelligence (competitor_prices). Util en un cron/pipeline que lee una lista de objetivos, scrapea, y persiste una fila por producto. Pasa price_selector por target siempre que conozcas el sitio: es la via mas robusta. Si no lo pasas, la funcion intenta autodetectar (JSON-LD offers.price, meta tags de precio, clases comunes de e-commerce). Las filas salen con las claves exactas de la tabla destino, asi que el caller solo anade snapshot_date/scraped_at antes del INSERT.

Gotchas

  • Funcion impura: hace I/O de red (HTTP GET). Depende del HTML real de cada sitio en el momento de la llamada.
  • El scraping de precios es muy especifico por sitio. Sin price_selector, la autodeteccion acierta en muchos e-commerce estandar (los que exponen JSON-LD Product/Offer, meta og:price:amount/itemprop=price, o clases tipicas .price), pero falla en SPAs / paginas JS-rendered (React/Vue/Angular que pintan el precio tras cargar) y en sitios con anti-bot (Cloudflare, captchas, fingerprinting). Para esos casos el GET devuelve un HTML sin el precio o un challenge, y la fila sale con price=None.
  • Para sitios JS-rendered o con anti-bot usa el navegador del ecosistema (browser MCP / CDP: page_perceive, cdp_get_text, cdp_perceive_outline) para renderizar la pagina y extraer el precio del DOM ya pintado, en lugar de esta funcion de HTTP puro. Esta funcion es para HTML servidor-renderizado.
  • price_selector por target es lo mas fiable: evita depender de la heuristica y sobrevive mejor a cambios de plantilla. Define uno por competidor en tu lista de objetivos.
  • Normalizacion de precio: tolera 1.299,99 € (europeo: punto miles, coma decimal), $1,299.99 (US), 29,90, 1299.99. Heuristica: el separador mas a la derecha es el decimal cuando hay ambos; con solo coma, se trata como decimal si quedan 2 digitos detras, si no como miles. Casos exoticos (3 decimales, formatos regionales raros) pueden malinterpretarse — verifica con price_selector apuntando al nodo limpio.
  • in_stock es heuristico: True salvo que el texto de la pagina contenga marcadores de agotado (agotado, sin stock, out of stock, sold out, etc.). Falsos positivos/negativos posibles si el sitio usa otra redaccion o muestra esos terminos en contexto no relacionado. None si la pagina fallo al cargar.
  • Tolerancia a fallos por target: si un target peta (red, timeout, HTML invalido), su fila sale con price=None/in_stock=None y el resto del batch continua. Nunca aborta toda la lista por un fallo individual.
  • Reintento unico: cada GET reintenta una vez ante error de transporte. No hay backoff exponencial ni rotacion de proxies/User-Agent; para scraping a escala o contra anti-bot fuerte, eso queda fuera del alcance de esta funcion.