---
name: parse_amazon_ranking_html
kind: function
lang: py
domain: datascience
version: "1.0.0"
purity: pure
signature: "def parse_amazon_ranking_html(html: str, marketplace: str = 'amazon.es', list_type: str = 'bestsellers', max_items: int = 50) -> list[dict]"
description: "Parser PURO de HTML de rankings Amazon (Best Sellers y Movers & Shakers): recibe el HTML de la pagina (de requests o de outerHTML renderizado por CDP) y devuelve una lista de productos (rank, ASIN, titulo, precio, rating, reseñas, pct_change). Nucleo compartido por el scraper HTTP y el scraper CDP."
tags: [amazon, scraping, parser, market-intel, datascience, dropship]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [bs4]
tested: true
tests: ["test_parsea_dos_cards_con_todos_los_campos", "test_contrato_de_claves_exacto", "test_pct_change_solo_en_movers_shakers", "test_html_vacio_devuelve_lista_vacia", "test_max_items_limita_resultados"]
test_file_path: "python/functions/datascience/parse_amazon_ranking_html_test.py"
file_path: "python/functions/datascience/parse_amazon_ranking_html.py"
params:
  - name: html
    desc: "HTML crudo de una pagina de ranking Amazon, o el outerHTML del contenedor del grid (.p13n-desktop-grid) renderizado via CDP. Puede ser el documento entero o solo el grid."
  - name: marketplace
    desc: "Dominio Amazon (amazon.es, amazon.com, ...). Se usa para construir URLs absolutas de producto y para inferir la moneda fallback cuando el precio no trae simbolo."
  - name: list_type
    desc: "'bestsellers' o 'movers_shakers'. Solo afecta a si se parsea pct_change (movers) o se fuerza a None (bestsellers)."
  - name: max_items
    desc: "Numero maximo de productos devueltos. Default 50."
output: "Lista de dicts, uno por producto, con exactamente estas claves: marketplace, list_type, category (siempre None aqui — lo rellena el caller que conoce la URL), rank, asin, title, price, currency, rating, reviews, pct_change, url. None donde no haya dato. price/rating/pct_change son float; rank/reviews son int. pct_change solo se rellena en movers_shakers. HTML vacio o sin cards -> lista vacia (nunca lanza)."
---

## Ejemplo

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

# `html` puede venir de requests.get(...).text o de un outerHTML renderizado por CDP.
html = open("/tmp/amazon_grid.html").read()
rows = parse_amazon_ranking_html(html, marketplace="amazon.es", list_type="movers_shakers", max_items=30)
print(len(rows), "productos")
print(rows[0])
# {'marketplace': 'amazon.es', 'list_type': 'movers_shakers', 'category': None,
#  'rank': 1, 'asin': 'B0...', 'title': '...', 'price': 13.95, 'currency': 'EUR',
#  'rating': 4.0, 'reviews': 666, 'pct_change': 150.0, 'url': 'https://www.amazon.es/dp/B0...'}
```

## Cuando usarla

Usala cuando ya tengas el HTML de una pagina de ranking de Amazon (Best Sellers o Movers & Shakers) y quieras extraer los productos sin volver a escribir selectores DOM. Es el bloque de parsing reutilizable: la usan tanto `scrape_amazon_bestsellers` (fetch HTTP con requests) como `scrape_amazon_movers_cdp` (fetch renderizado via Chrome DevTools Protocol). Si construyes otro fetcher (proxy, browser MCP, HAR replay), pasale el HTML aqui en vez de duplicar el parser.

## Notas

- **Funcion pura**: sin red ni I/O; para un HTML fijo devuelve siempre lo mismo. Por eso es testeable con fixtures y compartible entre estrategias de fetch.
- **Plantillas multiples**: Amazon sirve varias plantillas DOM a la vez (A/B test) y las rota. Cada campo usa varios selectores fallback; un campo que ninguna plantilla conocida expone se devuelve `None` en vez de petar.
- **Seleccion de cards**: prioriza el wrapper del grid (`div[id="gridItemRoot"]`) sobre el faceout interno, porque el badge de rank (`span.zg-bdg-text`) es hermano del faceout DENTRO del wrapper — seleccionar el faceout solo perderia el rank.
- **pct_change defensivo**: apunta solo al badge de subida de ranking de movers (`.zg-percent-change` y variantes), NO al `%` generico de descuento/ahorro (`apex-savings-percent`) de cards de oferta, que daria un pct_change falso.
- **category = None**: el parser no conoce la URL, asi que deja `category` en `None`; el caller (que si sabe que categoria pidio) lo rellena.
- **rank fallback posicional**: si Amazon no renderiza el badge de rank, se usa la posicion (1-indexada) del item en el grid.
