--- name: scrape_gumroad_discover kind: function lang: py domain: datascience version: "1.0.0" purity: impure signature: "def scrape_gumroad_discover(taxonomy: str, sort: str = 'best_selling', max_products: int = 300, page_size: int = 100) -> list[dict]" description: "Scrapea el marketplace publico de Gumroad Discover usando el endpoint JSON verificado gumroad.com/products/search (taxonomy+sort+from+size). Recolecta los productos de una taxonomy (nicho) ordenados por el criterio elegido y estampa en cada producto el total de la taxonomy (saturacion del nicho). Normaliza cada producto a un dict plano con id, seller_name, ratings, precio (cents/usd), pay-what-you-want/free, native_type, url y metadatos de scrape (taxonomy, total_in_taxonomy, sort_used, rank 0-based). Solo stdlib (urllib+json+time)." tags: [gumroad, scraping, market-intel, trends, datascience] uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "error_go_core" imports: [] tested: true tests: ["test_normaliza_producto_a_dict_plano", "test_paginacion_para_al_agotar_ventana", "test_sort_invalido_lanza_valueerror", "test_body_no_json_lanza_runtimeerror"] test_file_path: "python/functions/datascience/scrape_gumroad_discover_test.py" file_path: "python/functions/datascience/scrape_gumroad_discover.py" params: - name: taxonomy desc: "Slug de taxonomy / nicho de Gumroad (ej. 'design', 'business-and-money', '3d'). Determina el segmento de mercado scrapeado y el valor total_in_taxonomy (numero total de productos = saturacion del nicho) que se estampa en cada producto." - name: sort desc: "Criterio de orden. Uno de: best_selling, most_reviewed, hot_and_new, highest_rated, newest, price_asc, price_desc. Cualquier otro valor lanza ValueError. Default 'best_selling'." - name: max_products desc: "Cota superior de productos a recolectar entre paginas. Default 300. La ventana de paginacion de Gumroad es finita (from~960 aun devuelve datos), asi que valores muy altos pueden recibir menos productos de los pedidos." - name: page_size desc: "Numero de productos pedidos por pagina via 'size'. Gumroad admite al menos 300. Una pagina que devuelve menos de page_size items señala el fin de la ventana y detiene la paginacion. Default 100." output: "Lista de dicts planos, uno por producto, con exactamente estas claves: id, permalink, name, seller_name, ratings_count, ratings_avg, price_cents, currency_code, price_usd (float = price_cents/100), is_pay_what_you_want (bool), is_free (bool = price_cents==0), native_type, url, taxonomy (el arg), total_in_taxonomy (el 'total' del JSON = saturacion del nicho), sort_used (el arg sort), rank (posicion 0-based en el orden devuelto)." --- ## Ejemplo ```python import sys, os sys.path.insert(0, os.path.join("python", "functions")) from datascience.scrape_gumroad_discover import scrape_gumroad_discover # Top best-sellers del nicho "design" en Gumroad Discover rows = scrape_gumroad_discover(taxonomy="design", sort="best_selling", max_products=300, page_size=100) print(len(rows), "productos") print("saturacion del nicho:", rows[0]["total_in_taxonomy"]) print(rows[0]) # {'id': '...', 'permalink': '...', 'name': '...', 'seller_name': '...', # 'ratings_count': 128, 'ratings_avg': 4.9, 'price_cents': 2900, # 'currency_code': 'usd', 'price_usd': 29.0, 'is_pay_what_you_want': False, # 'is_free': False, 'native_type': 'digital', 'url': 'https://...', # 'taxonomy': 'design', 'total_in_taxonomy': 4213, 'sort_used': 'best_selling', 'rank': 0} # Productos mas nuevos de un nicho concreto nuevos = scrape_gumroad_discover(taxonomy="3d", sort="newest", max_products=50) ``` ## Cuando usarla Usala cuando quieras hacer market intelligence sobre productos digitales: descubrir que se vende mas en un nicho de Gumroad, medir la saturacion del nicho (`total_in_taxonomy`) y capturar precios, valoraciones y vendedores para decidir si un nicho merece la pena o esta saturado. Es la fuente de un pipeline de deteccion de oportunidades de producto digital (grupo `market-intel`): scrapea varias taxonomies/sorts, cruza los snapshots y prioriza nichos con demanda alta y competencia manejable. Llamala antes de cualquier analisis de catalogo digital; el dict devuelto es plano y esta listo para insertar en una tabla tras añadir `snapshot_date`/`scraped_at`. ## Gotchas - **ratings.count son REVIEWS, no ventas**: `ratings_count` cuenta valoraciones dejadas, NO unidades vendidas. Como proxy de ventas hay que multiplicar por un factor incierto (solo una fraccion de compradores valora, y esa fraccion varia por nicho/precio). Trata `ratings_count` como un limite inferior ruidoso de la demanda, no como ventas reales. - **price=0 no siempre significa gratis util**: `price_cents==0` marca `is_free=True`, pero puede tratarse de un producto pay-what-you-want (`is_pay_what_you_want=True`) con minimo 0, no de un regalo. Cruza siempre `is_free` con `is_pay_what_you_want` antes de sacar conclusiones de precio. - **Ventana de paginacion finita**: `page`/`per_page` se IGNORAN (siempre devuelven desde 0); solo `from`+`size` paginan. La ventana es amplia pero finita (from~960 aun devuelve, mas alla se agota). Pedir `max_products` muy alto puede recibir menos productos de los pedidos: la funcion para cuando una pagina devuelve menos de `page_size` items. - **Cloudflare bloquea sin UA de navegador**: el endpoint exige `Accept: application/json` y un `User-Agent` de navegador. Sin ello Gumroad/Cloudflare puede devolver una pagina de challenge en HTML (no JSON) o redirigir. La funcion ya envia un UA de Chrome; si aun asi recibe un body no-JSON lanza `RuntimeError` claro — en ese caso cae al navegador del ecosistema (browser MCP / CDP). - **Moneda no siempre USD**: `price_usd` es solo `price_cents/100` por conveniencia; si `currency_code != 'usd'` el valor NO esta convertido a dolares. Conserva y usa `currency_code` para convertir tu mismo.