--- name: scrape_aliexpress_trending kind: function lang: py domain: datascience version: "1.0.0" purity: impure signature: "def scrape_aliexpress_trending(query: str | None = None, category: str | None = None, limit: int = 40, ship_to: str = 'ES') -> list[dict]" description: "Capta productos populares de AliExpress como señal de e-commerce/dropshipping (orders, rating, precio). Hace una request HTTP a la página de listado ordenada por número de pedidos y extrae el JSON embebido en el HTML (window.runParams / _dida_config). Best-effort: ante anti-bot lanza RuntimeError, ante HTML sin JSON devuelve []. NUNCA inventa datos." tags: [aliexpress, ecommerce, dropshipping, trends, market-intel, datascience] params: - name: query desc: "Texto de búsqueda (ej. 'kitchen gadgets'). Si se da, manda en la URL sobre category." - name: category desc: "ID numérico de categoría AliExpress o slug. Ignorado si hay query. None usa un listado 'hot products' genérico." - name: limit desc: "Número máximo de productos a devolver. Default 40." - name: ship_to desc: "Código de país ISO-2 (ES, US, GB, DE, ...) que fija región y moneda via cookies de AliExpress. Default 'ES'." output: "Lista de dicts con claves exactas (casan 1:1 con la tabla Postgres aliexpress_trends, sin id/snapshot_date/scraped_at): category (str|None), product_id (str), title (str|None), price (float|None), currency (str|None), orders (int|None), rating (float|None), url (str). Lista vacía si el HTML no traía JSON parseable." uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "error_go_core" imports: [requests] tested: false tests: [] test_file_path: "" file_path: "python/functions/datascience/scrape_aliexpress_trending.py" --- ## Ejemplo ```python import sys, os sys.path.insert(0, os.path.join("python", "functions")) from datascience.scrape_aliexpress_trending import scrape_aliexpress_trending # Top productos por número de pedidos para una búsqueda concreta, enviando a España. rows = scrape_aliexpress_trending(query="phone holder", limit=20, ship_to="ES") for r in rows[:3]: print(r["title"], "->", r["orders"], "pedidos |", r["price"], r["currency"]) # Cada dict (las 8 claves casan con la tabla aliexpress_trends): # {"category": "phone holder", "product_id": "100500...", "title": "...", # "price": 3.21, "currency": "EUR", "orders": 12000, "rating": 4.8, # "url": "https://www.aliexpress.com/item/100500....html"} ``` ## Cuando usarla Cuando necesites una señal de qué productos están vendiendo bien en AliExpress para research de dropshipping o market-intel: detectar tendencias, sourcing de productos ganadores, o alimentar un histórico (tabla `aliexpress_trends`) que cruce orders / rating / precio por categoría. Úsala antes de decidir un nicho o para vigilar periódicamente una keyword. El output va directo a un `INSERT` Postgres (las 8 claves coinciden con las columnas no autogeneradas). ## Gotchas - **Anti-bot fuerte (CRÍTICO):** AliExpress bloquea agresivamente headless/datacenter con captcha (`/_____tmd_____/punish`), 403/429 y fingerprinting. Desde una IP de datacenter o un patrón de scraping evidente, esta función **lanzará `RuntimeError`** con frecuencia. Para extracción fiable y sostenida, la alternativa robusta es el **browser MCP/CDP con sesión real** (Chrome del usuario, cookies legítimas), no `requests`. Esta función es la vía barata; si falla repetidamente, sube de nivel. - **JSON embebido volátil:** el nombre/estructura del blob (`window.runParams`, `_dida_config_`, `_init_data_`) cambia con frecuencia. Se prueban varios patrones y un walk genérico, pero si AliExpress cambia el layout la función devuelve `[]` (HTML válido sin JSON parseable) — **NO inventa datos**. Diferencia clave: `RuntimeError` = bloqueado; `[]` = layout cambiado o shell vacío. - **Región/moneda dependen de `ship_to`:** se setean por cookies (`aep_usuc_f`, `intl_locale`). Un `ship_to` no mapeado cae a `ES`/`EUR`. El `currency` devuelto depende de lo que AliExpress decida servir, no se fuerza tras el fetch. - **`orders`/`price`/`rating` pueden venir `None`** si el item no expone ese campo en el JSON (productos nuevos sin ventas, listados sin rating). No asumir no-null. - **Una sola página:** devuelve hasta `limit` items de la primera página de resultados; no pagina. Para más volumen, llamar con queries/categorías distintas. - **Sin reintentos ni rotación de proxy/UA:** es una request única con headers fijos. Para uso periódico, orquestar reintentos y backoff fuera de la función.