---
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.
