Cómo Scrapear Datos de AliExpress a Gran Escala

Aprende a scrapear precios, stock y variantes de AliExpress con Python, detecta bloqueos que devuelven HTTP 200, y recolecta a gran escala con Bright Data.
43 min de lectura
How to Scrape AliExpress blog image

Scrapear AliExpress recopila el precio, stock, variantes, calificación y reseñas que el monitoreo de precios y el seguimiento de competidores necesitan. La mayoría no está en el HTML, porque la página la carga después desde una API interna firmada.

En mis pruebas, una solicitud simple devolvió HTTP 200 y ningún precio, así que un pipeline registraría un éxito con una fila vacía. Desde una dirección IP, la API firmada comenzó a bloquear llamadas en apenas una docena de solicitudes.

Esta guía te muestra cómo obtener esos datos, detectar un fallo que parece un éxito, y usar el AliExpress Scraper API de Bright Data a gran escala.

Resumen

  • El objeto window.runParams que tutoriales antiguos parsean está vacío. Los datos de precio, stock y SKU se cargan desde mtop.aliexpress.pdp.pc.query, que necesita una firma MD5.
  • Detecta bloqueos por marcadores, no por códigos de estado. Las solicitudes bloqueadas devolvieron HTTP 200, un encabezado bxpunish, y un marcador en el cuerpo como rgv587_flag.
  • Una firma correcta no evitó los bloqueos. Dos tandas de 60 llamadas desde una dirección IP, sin demora, obtuvieron datos de 7 y 15.
  • A gran escala, scrapea las páginas públicas de productos /item/. El AliExpress Scraper API de Bright Data extrae datos estructurados, y el Browser API permite que tu script lea la respuesta completa de la API de cada página.

Qué sucede al scrapear AliExpress con un cliente HTTP

Empieza con una solicitud HTTP, enviada sin navegador ni proxy. Si expira, tu red no puede llegar directamente a AliExpress, y un proxy o uno de los métodos gestionados abajo envía la solicitud desde una red que sí puede.

Todo lo que sigue usa 3 paquetes. El script del Scraper API en una sección posterior lee tu clave de API de Bright Data desde una variable de entorno:

pip install curl_cffi playwright requests
export BRIGHTDATA_API_KEY="your-api-key"    # PowerShell: $env:BRIGHTDATA_API_KEY="your-api-key"

Obtén una página de producto con curl_cffi y lee el <title>, el objeto window.runParams que tutoriales antiguos parsean, y el objeto bootstrap que la página pasa a su propio JavaScript. Estos 3 valores muestran si un cliente HTTP simple es suficiente:

import re
from curl_cffi import requests as cffi

# cualquier URL de producto que aún exista funciona
ITEM = "https://www.aliexpress.com/item/1005001398867186.html"

# sin User-Agent propio: curl_cffi envía uno que coincide con el navegador que imita
r = cffi.get(ITEM, impersonate="chrome", timeout=30)
html = r.text

print("status      ", r.status_code)
print("bytes       ", len(html))
print("title       ", repr(re.search(r"<title>(.*?)</title>", html, re.S).group(1)))
print("runParams   ", repr(re.search(r"window\.runParams\s*=\s*(\{.*?\});", html, re.S).group(1)))
print("isCSR       ", re.search(r"isCSR\s*=\s*(\w+)", html).group(1))
print("crawler flag", re.search(r'"crawler":(\w+)', html).group(1))

La obtención tiene éxito:

status       200
bytes        77226
title        ''
runParams    '{\n                    }'
isCSR        true
crawler flag false

La página es un shell HTML, sin precio, stock, SKU, calificación ni reseñas. Antes de que se ejecute cualquier JavaScript, el shell ya tiene las etiquetas Open Graph. og:title contiene el nombre real del producto mientras que <title> está vacío, y og:image, og:url y og:type (establecido en product) también están ahí. og:description contiene solo un eslogan genérico de AliExpress, y la página no tiene JSON-LD, así que ninguna de las dos fuentes te da datos del producto.

Un navegador que carga la misma URL en la misma hora renderiza el precio, la calificación y las variantes:

Página de producto AliExpress de un disyuntor de corriente residual TOMZN renderizada en un navegador, mostrando un precio de oferta de $7.73 y un precio de lista de $14.40, una calificación de 4.7 con 80 reseñas y 489 vendidos, siete botones de variante de corriente nominal de 16A a 63A, el panel del vendedor TOMZN Official Store, y botones de Comprar ahora y Agregar al carrito

Renderizado a través del Browser API de Bright Data con segmentación geográfica configurada en EE. UU. La API interna de AliExpress devuelve el mismo precio de lista de $14.40 y calificación 4.7 para este producto. El precio de oferta proviene de una promoción temporal y cambia con el tiempo.

Buscar ese precio de lista en el código fuente de la página no encuentra nada:

Pestaña de ver código fuente del navegador para www.aliexpress.us/item/3256801212552434.html con la barra de búsqueda buscando 14.40 y reportando 0/0 coincidencias, encima del inicio de la sección head de la página, donde una etiqueta meta og:title nombra el TOMZN RCBO Circuit Breaker 16-63A 220V Surge Protector y og:description contiene un eslogan genérico de AliExpress

Capturado en la tienda de EE. UU., que sirve el mismo shell de 77 KB.

Los campos del catálogo son baratos de recopilar. Una sola solicitud GET sin desbloqueo devuelve los campos del catálogo, así que puedes mapear IDs de producto a títulos e imágenes principales sin renderizar. Los pipelines de precios y surtido necesitan datos de precio, stock y variantes, que provienen del gateway de API interno de AliExpress. Planifica esos 2 trabajos por separado.

Deja que curl_cffi elija el User-Agent. impersonate="chrome" es un alias en lugar de una versión. En el código fuente de la biblioteca, curl_cffi 0.16.1 mapea el alias a chrome150 mediante una constante DEFAULT_CHROME, y esa constante cambia cada vez que se lanza un nuevo perfil. Si no estableces el encabezado, la biblioteca también envía el User-Agent correspondiente Chrome/150.

Si pasas tu propio User-Agent, reemplaza el de la biblioteca, así que una cadena fija Chrome/141 se envía con un handshake TLS de Chrome 150. La discrepancia no genera ningún error, así que puedes desplegarla sin notarlo. Omite el encabezado y deja que la biblioteca empareje el User-Agent con el handshake, o fija el perfil a una versión y haz coincidir la cadena con él. Los ingenieros que construyen clientes TLS reportan que una identidad discordante es más fácil de detectar que una consistente, y que el User-Agent queda obsoleto mucho antes que el handshake TLS.

La bandera isCSR muestra cómo se renderiza la página. Su valor es true porque AliExpress movió la página de detalle de producto a renderizado del lado del cliente (CSR). window.runParams es un objeto vacío sin uso que los extractores basados en regex aún emparejan. Un parser que lo lee no genera ningún error y no devuelve nada.

Cada cliente que probé obtuvo HTTP 200 y el mismo shell de 77 KB. Envié 20 solicitudes consecutivas a esa URL desde una dirección IP residencial sin proxy, y todas devolvieron HTTP 200. Las solicitudes se ejecutaron en una sola sesión a una solicitud cada 0.4 segundos, sin rotación de User-Agent. También obtuve la misma URL como python-requests/2.32.5, como curl/8.7.1, y como 3 perfiles de imitación TLS.

Enviar el User-Agent de Googlebot cambia "crawler" de false a true en el objeto bootstrap. La respuesta difiere en 1 byte. La bandera no añadió una carga renderizada por el servidor, así que suplantar a Googlebot solo cambia un valor booleano y nada más.

En un mercado renderizado por el cliente, verifica los datos antes de registrar una obtención como éxito. El código de estado solo muestra que la solicitud HTTP tuvo éxito. En este objetivo, la solicitud tuvo éxito incluso cuando faltaban los datos.

El bloqueo que devuelve HTTP 200

AliExpress sí bloquea solicitudes, pero la línea de estado HTTP no muestra el bloqueo.

El propio robots.txt del sitio nombra un endpoint interno en una regla específica para Googlebot, y ese endpoint puede devolver los datos del producto. Llámalo directamente y lee los encabezados de respuesta antes del cuerpo:

from curl_cffi import requests as cffi

PID = "1005001398867186"
url = f"https://www.aliexpress.com/aeglodetailweb/api/msite/item?productId={PID}"
r = cffi.get(url, impersonate="chrome", timeout=30,
             headers={"Accept": "application/json",
                      "Referer": f"https://www.aliexpress.com/item/{PID}.html"})

print("status", r.status_code, "bytes", len(r.text))
for k in ("server", "x-rule-name", "bxpunish", "set-cookie"):
    print(f"  {k:12s} {str(r.headers.get(k))[:70]}")
print("body marker:", "rgv587_flag" in r.text, "| x5secdata:", "x5secdata" in r.text)

La respuesta proviene del sistema antibot Baxia de Alibaba, y tiene un código de estado de éxito:

status 200 bytes 2399
  server       Tengine/AServer-Ingress
  x-rule-name  AE_GLOBAL_UNLOGIN
  bxpunish     1
  set-cookie   x5secdata=xffEdBbOh5ArPGTjyI_40B3_06C7jiK2nBFPGlpVzkk2x66w14DH6V
body marker: True | x5secdata: True

El cuerpo es un script que redirige a una URL _____tmd_____/punish mediante window.location y establece window._config_ = {"action":"captcha", ...}. Termina con <!--rgv587_flag:sm-->, el marcador de bloqueo de Alibaba. El encabezado x-rule-name nombra la regla que coincidió, AE_GLOBAL_UNLOGIN, que parece ser la regla para tráfico global no autenticado. El mismo encabezado también aparecía en respuestas normales sin bloqueo, así que no marca un bloqueo por sí solo.

Las verificaciones de código de estado aceptan esta respuesta bloqueada. raise_for_status() no lanza error, y una verificación status_code == 200 devuelve verdadero. Si un panel cuenta las respuestas HTTP 2xx como éxitos, reporta un pipeline saludable mientras las filas están vacías.

Así que detecta bloqueos por los encabezados y el cuerpo de la respuesta, no por el código de estado. Las cadenas de error de Alibaba pueden cambiar sin aviso, pero un encabezado de respuesta dedicado y una cadena marcadora en el cuerpo son baratos de verificar y baratos de extender:

BLOCK_MARKERS = ("rgv587_flag", "x5secdata", "_____tmd_____", "FAIL_SYS_USER_VALIDATE")

def is_blocked(resp, min_bytes=1024):
    """True cuando Baxia respondió en lugar de la aplicación."""
    if resp.headers.get("bxpunish"):
        return True
    body = resp.text
    if len(body) < min_bytes:          # trata un 200 casi vacío también como bloqueo
        return True
    return any(m in body[:4096] for m in BLOCK_MARKERS)

En mis ejecuciones, esas verificaciones detectaron cada bloqueo que el código de estado no mostraba. Ejecuta las mismas verificaciones antes de tu parser, y agrégalas antes de escribir cualquier selector. Sin ellas, te enteras de un bloqueo en este objetivo solo cuando tu parser no encuentra datos.

Registra la razón de cada solicitud fallida. En este objetivo, una página de bloqueo, un shell vacío y un precio en la moneda incorrecta necesitan cada uno una solución diferente, y una sola tasa de éxito general oculta las 3. Divide también las razones por país de salida, porque un precio en moneda incorrecta aún parece un éxito.

Firmar llamadas a la API de producto tú mismo

Los campos del producto sí existen como JSON. Si cargas una página en un navegador y registras su tráfico XHR, puedes ver que esos campos provienen de un gateway de API que la página llama.

Una carga de página hace llamadas al gateway MTOP de Alibaba en acs.aliexpress.com/h5/<api>/<version>/, cada una con jsv, appKey, una marca de tiempo en milisegundos t, y una sign de 32 caracteres. mtop.aliexpress.pdp.pc.query devuelve precio, stock, SKU, tienda y calificación. El texto de las reseñas proviene de una segunda llamada firmada, mtop.aliexpress.review.pc.list, en el mismo gateway, así que recopilar reseñas necesita una segunda solicitud por producto. Leer el script bootstrap en línea muestra que la página puede llamar a más de una API de producto:

Variante de página (viewName) API precargada
newDetail mtop.aliexpress.pdp.pc.query
choiceDetail mtop.aliexpress.itemdetail.pc.asyncPCDetail
h5Detail mtop.aliexpress.itemdetail.msite

Las 6 páginas de producto que muestreé tenían viewName = 'newDetail'. Las otras 2 APIs, la móvil mtop.aliexpress.itemdetail.msite y mtop.aliexpress.itemdetail.pc.asyncPCDetail, no fueron más fáciles de acceder. Ambas devolvieron FAIL_SYS_USER_VALIDATE con bxpunish: 1 en la primera solicitud, enviada con la misma carga útil que la API de escritorio acepta.

Lee viewName en cada obtención para ver cuándo un listado empieza a renderizarse como una variante de página diferente. Un scraper fijado a un solo nombre de API no notaría ese cambio. El script bootstrap de la página establece un tiempo límite de precarga de 15,000 ms y aborta su propio XHR a los 10,000 ms, así que usa 10,000 ms como límite superior para tus propios tiempos de espera.

En el panel de red del navegador, filtrado por pdp.pc.query, la propia solicitud de la página contiene cada valor que tu código de firma necesita:

Panel de red de Chrome DevTools filtrado a pdp.pc.query, en la pestaña Payload de una solicitud a mtop.aliexpress.pdp.pc.query, listando jsv 2.5.1, appKey 12574478, un t de 13 dígitos, un sign de 32 caracteres, type y dataType originaljsonp, timeout 15000, callback mtopjsonp1, y un objeto data con productId 3256801212552434 y un bloque ext, con un valor de token censurado

La propia llamada de la página, capturada en la tienda de EE. UU., donde el ID de este listado es 3256801212552434. La página envía originaljsonp y añade un bloque timeout y ext. El código de firma abajo envía jsonp y omite ambos, y el gateway aceptó esa solicitud más corta. Un valor de token está censurado.

El valor sign usa el esquema de firma H5 de Alibaba, que es lo suficientemente corto para reimplementarlo en una función. El esquema une el token de sesión, la marca de tiempo, la app key y el cuerpo exacto de la solicitud con ampersands, y luego aplica hash al resultado con MD5:

import hashlib, json, time
from curl_cffi import requests as cffi

APP_KEY, API, VER = "12574478", "mtop.aliexpress.pdp.pc.query", "1.0"
GATEWAY = f"https://acs.aliexpress.com/h5/{API}/{VER}/"

def sign(token, t, data):
    return hashlib.md5(f"{token}&{t}&{APP_KEY}&{data}".encode()).hexdigest()

def pdp(session, product_id, token=""):
    data = json.dumps({"productId": product_id, "_lang": "en_US", "_currency": "USD",
                       "country": "US", "province": "", "city": "", "channel": "",
                       "pdp_ext_f": "{}", "sourceType": "", "clientType": "pc"},
                      separators=(",", ":"))
    t = str(int(time.time() * 1000))
    params = {"jsv": "2.5.1", "appKey": APP_KEY, "t": t, "sign": sign(token, t, data),
              "api": API, "v": VER, "type": "jsonp", "dataType": "jsonp",
              "callback": "mtopjsonp1", "data": data}
    return session.get(GATEWAY, params=params, impersonate="chrome", timeout=30,
                       headers={"Referer": "https://www.aliexpress.com/"})

s = cffi.Session()
first = pdp(s, "1005001398867186")                       # sin firmar, devuelve la cookie del token
token = s.cookies.get("_m_h5_tk", "").split("_")[0]
second = pdp(s, "1005001398867186", token)               # firmada con ese token
print("call 1:", len(first.text), "bytes |", first.text[:60])
print("call 2:", len(second.text), "bytes")

La primera llamada devuelve el token, y la segunda llamada devuelve el producto:

call 1: 151 bytes | mtopjsonp1({"api":"mtop.aliexpress.pdp.pc.query","data":{},"ret
call 2: 103748 bytes

La llamada 1 devuelve FAIL_SYS_TOKEN_EMPTY y un encabezado Set-Cookie con _m_h5_tk. La llamada 2 se firma con la primera parte del valor de esa cookie y devuelve JSONP, una llamada de función que envuelve un objeto JSON. Si quitas el envoltorio mtopjsonp1(...), obtienes el registro completo del producto, incluyendo el título, originalPrice $14.40, salePriceString $6.84, 59,924 unidades de inventario, calificación 4.7, ID de vendedor 200422816, y categoryPath 13/5/141914.

Solo necesitas una pequeña parte de esa carga útil. La respuesta tiene 27 módulos de nivel superior, y 8 de ellos contienen los campos que un pipeline de precio o surtido almacenaría. Serializados como JSON compacto, esos 8 módulos son 16,494 bytes, que es el 15.9% del objeto de resultado completo. El módulo más grande es SHIPPING con 34,071 bytes, y el segundo más grande es GLOBAL_DATA con 23,783 bytes.

Firmar necesita solo un hash MD5, una cookie y un reintento. Si has reimplementado el esquema de firma H5 para otro sitio de Alibaba, espera unas horas de trabajo. Entonces, ¿cuánto tiempo sigue funcionando tu código de firma?

Cuántas llamadas firmadas a la API tienen éxito desde una dirección IP

Ejecutar la llamada firmada en un bucle responde esa pregunta rápidamente. La prueba usa 10 IDs de producto, un token, una dirección IP residencial, y sin demora entre llamadas. El script cuenta cada código ret que devuelve el gateway:

# continúa la sesión desde el código de firma: pdp(), s y token están definidos allí
from collections import Counter
import re

# diez IDs de producto, reemplaza los que hayan sido eliminados
IDS = ["3256812884305776", "1005007712136541", "1005002754717958", "1005002495501001",
       "2255799968025159", "1005008121783683", "3256809736486496", "1005008175462016",
       "1005012238762416", "3256812840307416"]

outcomes, first_fail = Counter(), None
for i in range(1, 61):
    r = pdp(s, IDS[i % len(IDS)], token)
    ret = re.search(r'"ret":\["([^"]+)"', r.text).group(1).split("::")[0]
    key = ret + (" +bxpunish" if r.headers.get("bxpunish") else "")
    outcomes[key] += 1
    if key != "SUCCESS" and first_fail is None:
        first_fail = i
print(dict(outcomes), "| first non-SUCCESS at", first_fail)

El bucle dejó de obtener datos después de la séptima llamada:

{'SUCCESS': 7, 'FAIL_SYS_USER_VALIDATE +bxpunish': 53} | first non-SUCCESS at 8

El array ret de cada respuesta bloqueada también contenía RGV587_ERROR::SM.

El bloqueo se repitió, pero el recuento de éxitos cambió. Una segunda ejecución desde la misma dirección IP envió 6.2 solicitudes por segundo, registró el código de estado y la longitud de respuesta de cada llamada, obtuvo 15 éxitos en 60, y fue bloqueada por primera vez en la solicitud 12. Devolvió HTTP 200 para las 60 llamadas, y las 45 respuestas bloqueadas tenían exactamente 543 bytes. Así que basa tu plan de capacidad en el bloqueo, no en ninguno de los dos recuentos de éxito.

Una nueva sesión no eliminó el bloqueo. Cada una de 3 nuevas sesiones seguidas comenzó con un contenedor de cookies vacío y fue bloqueada antes de que el cliente pudiera obtener un token. Una solicitud enviada después de esperar 60 segundos, de nuevo con un contenedor de cookies vacío, también fue bloqueada. Así que un reintento automático añade tráfico sin añadir datos, y los ingenieros de proxies advierten que una lógica de reintento como esta gasta presupuesto sin ningún error visible.

Esta prueba no usó demora entre llamadas. A continuación, prueba las variables que un scraper de producción añade, como un retroceso largo, un nuevo token para cada lote, una tasa de solicitud deliberadamente lenta, y el bloque ext que firma la propia solicitud de la página. Ese bloque contiene una bandera crawler y una clave x-m-biz-bx-region.

La firma se mantuvo correcta de la solicitud 7 a la 60, así que es improbable que más ingeniería inversa aumente los recuentos de éxito de 7 y 15 en 60 solicitudes. Las pruebas excluyen la sesión como causa. Ninguna prueba cambió la dirección IP y la huella digital por separado, así que el bloqueo podría depender de cualquiera de las dos. Los sistemas antibot también pueden vincular una huella digital a través de muchas direcciones IP, así que los profesionales cambian la dirección IP, la huella digital y las cookies juntas.

Tres verificaciones muestran que el cliente no es la causa. En el eco de huella digital de tls.peet.ws, el código de firma en curl_cffi 0.16.1 coincidió con un Chromium 152 real en esa máquina, con la misma huella digital JA4, t13d1516h2_8daaf6152771_806a8c22fdea, y la misma huella digital HTTP/2. Cinco huellas digitales de curl_cffi y las bibliotecas de Rust rnet y wreq produjeron 100 respuestas bloqueadas y ningún token, aunque wreq incluye perfiles de Chrome más nuevos que curl_cffi 0.16.1. Patchright, nodriver y Camoufox cada uno tuvo éxito 0 veces en 8 intentos desde esa dirección IP, mientras que el Browser API de Bright Data, en una IP de salida diferente, devolvió el producto en el mismo minuto.

Esa dirección IP ya estaba bloqueada cuando esas 3 herramientas se ejecutaron, así que las 3 fallaron de la misma manera en ella. Los ceros muestran la reputación de la dirección IP, y no clasifican a las herramientas.

La primera verificación, el eco de huella digital de TrackMe en tls.peet.ws, devuelve cada huella digital de una sola solicitud:

Página de API de fingerprinting de TrackMe listando huellas digitales JA3, JA4, p0f, HTTP/2 de Akamai y PeetPrint para el cliente solicitante, con un valor JA4 de t13d1516h2_8daaf6152771_806a8c22fdea y una huella digital HTTP/2 de Akamai de 52d84b11737d980aef856699f885ca86, encima de un panel de detalles de solicitud mostrando TLS 772, HTTP/2, y soporte para TLS 1.3 y 1.2

Verifica que un perfil reproduzca tanto la huella digital JA4 como la HTTP/2 del navegador cuyo nombre lleva.

Un perfil más nuevo no fue una mejor coincidencia aquí. El perfil de Chrome 152 de wreq envió un ClientHello con 17 extensiones, y el propio Chrome 152 envió 16. El número de versión en el nombre de un perfil no garantiza la huella digital que envía, así que verifica cada perfil contra un eco de huella digital antes de confiar en él. Aquí, el perfil predeterminado de curl_cffi coincidió exactamente con el navegador real.

La ingeniería inversa de un endpoint firmado te dice cómo son los datos, pero no cuántos puedes recopilar. En estas pruebas, ni una mejor firma ni un mejor navegador aumentaron esa cantidad.

Dejar que la página firme las llamadas a la API de producto por ti

Una segunda forma de obtener la carga útil del producto usa automatización de navegador en lugar de llamadas a la API desde tu propio código. La página ya hace la llamada firmada. Adjunta un manejador de respuesta y lee las respuestas que recibe la página. A través del Browser API, las solicitudes de la página también pasan por la red de proxies de Bright Data.

Leer la propia respuesta de la página elimina el token, la marca de tiempo, la firma MD5, y el contenedor de cookies de tu código:

import json, re
from playwright.sync_api import sync_playwright

WSS = "wss://brd-customer-<id>-zone-<zone>:<password>@brd.superproxy.io:9222"

def capture_pdp(product_id):
    """Lee la respuesta a la llamada que la página ya hace."""
    bodies = []

    def keep(response):
        if "mtop.aliexpress.pdp.pc.query" not in response.url:
            return
        try:
            bodies.append(response.text())
        except Exception:
            pass                      # redirecciones y páginas cerradas no tienen cuerpo legible

    with sync_playwright() as pw:
        browser = pw.chromium.connect_over_cdp(WSS, timeout=150_000)
        ctx = browser.contexts[0] if browser.contexts else browser.new_context()
        page = ctx.new_page()
        page.on("response", keep)
        try:
            page.goto(f"https://www.aliexpress.com/item/{product_id}.html",
                      wait_until="domcontentloaded", timeout=150_000)
            page.wait_for_timeout(13_000)
        except Exception:
            pass                      # conserva lo que keep() recopiló antes de que terminara la navegación
        browser.close()

    for body in sorted((b for b in bodies if isinstance(b, str)), key=len, reverse=True):
        m = re.match(r"\s*mtopjsonp\d*\((.*)\)\s*$", body, re.S)
        try:
            result = json.loads(m.group(1) if m else body).get("data", {}).get("result")
        except (ValueError, AttributeError):
            continue                  # omite un cuerpo que no es JSON, como una página HTML
        if isinstance(result, dict) and len(result) > 10:
            return result
    return None

result = capture_pdp("1005001398867186")
if result:
    print(len(result["PRICE"]["skuIdStrPriceInfoMap"]), "SKUs priced")

WSS se conecta a una zona de Browser API, y sus 3 marcadores de posición provienen de los detalles de acceso de esa zona:

Panel de control de Bright Data en la pestaña Overview de una zona de Browser API llamada scraping_browser2 bajo Web Access, mostrando Detalles de acceso con el host brd.superproxy.io, un nombre de usuario, una contraseña enmascarada, y un endpoint de Puppeteer y Playwright que muestra wss://brd-customer-, un ID de cuenta censurado, -zone-scraping_browser2, una contraseña enmascarada y @brd.superproxy.io:9222

La línea Puppeteer / Playwright es el valor que espera WSS, y tu ID de cuenta, nombre de zona y contraseña llenan sus 3 marcadores de posición. El ID de cuenta está censurado aquí, y el Panel de control enmascara la contraseña.

Conserva cada respuesta, no solo la primera. La página llama a ese endpoint más de una vez por carga. Registra el tamaño de cada cuerpo de respuesta que recopila keep() para ver cuál contiene el producto:

3256812884305776  sizes=[151, 170539]          141 SKUs priced
1005007712136541  sizes=[151, 244023]           75 SKUs priced
1005002754717958  sizes=[151, 105852, 106046]   20 SKUs priced
1005002495501001  sizes=[151,  96065]           18 SKUs priced
2255799968025159  sizes=[151,  93982]           15 SKUs priced
1005008121783683  sizes=[151,  63780]            6 SKUs priced

La respuesta de 151 bytes establece la cookie del token. El cuerpo de respuesta grande después de ella contiene el registro del producto, incluyendo el mapa de precios de SKU. Cuando una carga tiene más de una respuesta grande, capture_pdp devuelve la más grande.

Lo que permiten los archivos robots.txt de AliExpress

Lee los archivos robots.txt antes de elegir un método. Si una directiva te resulta nueva, nuestra guía de robots.txt para scraping web la explica. Los archivos en el host de la API y el host principal tienen reglas diferentes:

$ curl -s https://acs.aliexpress.com/robots.txt
User-agent: *
Disallow: /

$ curl -s https://www.aliexpress.com/robots.txt | grep -E '/item'
Disallow: */aeglodetailweb/api/msite/item?productId*
Disallow: /items/*
Disallow: /item-img/*
Disallow: /item-img/*

El gateway MTOP está completamente prohibido. En el host principal, cada regla que menciona item aplica a otra cosa (/items/*, /item-img/*, y la regla de Googlebot en el endpoint aeglodetailweb). Ninguna regla aplica a /item/1005001398867186.html, mientras que todo /search/* está prohibido. Usa los proxies residenciales de Bright Data, o uno de los métodos abajo, para las rutas que robots.txt permite.

Revisa también los sitemaps del host principal y los archivos de manifiesto de agentes. El robots.txt de .com listaba sitemaps de artículos, y el primer archivo de sitemap contenía 31,999 URLs de /item/, así que los sitemaps son una fuente directa de IDs de producto. En las pruebas, las 6 rutas de manifiesto de agentes (/llms.txt y /ai.txt, además de agent.json, ai-plugin.json, mcp.json, y acp.json bajo /.well-known/) redirigieron con un 302 a /p/error/404.html.

www.aliexpress.us/robots.txt tenía las mismas reglas que el archivo .com, sin las líneas de sitemap. Debido a que el sitio redirige a los visitantes entre los 2 hosts por región, esas reglas aplican en ambos hosts. m.aliexpress.com publica un archivo más largo con su propia regla de AdsBot, así que revisa el host que estás llamando.

Un host de AliExpress publica una excepción, la única ruta de API firmada que cualquiera de estos archivos permite. recom-acs.aliexpress.com sirve Disallow: / seguido de Allow: /h5/mtop.relationrecommend.aliexpressseorecommend.recommend/*. Llamado con una carga útil deliberadamente incompleta, ese endpoint devolvió FAIL_SYS_BIZPARAM_MISSED sin un encabezado bxpunish, mientras que los endpoints prohibidos devolvieron FAIL_SYS_USER_VALIDATE con ese encabezado. Así que robots.txt y el sistema antibot permitieron el mismo endpoint.

La página de producto es lo opuesto al gateway. AliExpress la sirve al público, robots.txt no la prohíbe, y cada uno de los 3 productos de Bright Data accedió a ella en las pruebas. Un renderizado de Web Unlocker, la respuesta del gateway, y un registro del Scraper API tenían todos el mismo precio de oferta de $6.84 en pruebas con configuración regional de EE. UU.

Scrapea las páginas que el sitio hace públicas, y deja que tu infraestructura maneje las direcciones IP y las huellas digitales. Ese enfoque es menos propenso a romperse después de un rediseño o un cambio de firma, mientras que una rutina de firma copiada del paquete de JavaScript del sitio tiene que rehacerse después de cualquiera de los dos.

3 métodos para scrapear AliExpress a gran escala

El Browser API, Web Unlocker, y Scraper API de Bright Data proporcionan cada uno los datos del producto en una forma diferente. Nuestra comparación de Web Unlocker y el Browser API explica en qué difieren en general. Buscar AliExpress en la biblioteca de scrapers del panel de control muestra los scrapers disponibles para este dominio:

Resultados de búsqueda de la Biblioteca de Scrapers de Bright Data para "aliexpress", listando el dominio aliexpress.us bajo Resultados para Scrapers y un conjunto de datos listo para usar llamado "Aliexpress products", encima de un panel de Scraper Studio con el encabezado "Construye un scraper para cualquier sitio web con Scraper Studio"

Una búsqueda devuelve los scrapers del dominio y un conjunto de datos de AliExpress listo para usar.

El Browser API ejecuta tu script en un navegador en la nube gestionado con desbloqueo integrado y permite que tu script lea las respuestas de red que recibe la página. Cuando conecté Playwright sobre CDP y cargué la página de producto, la página mostró el título y una caja de compra con su precio, y mi script pudo leer las 171 respuestas de esa única carga. El Browser API también admite segmentación geográfica, así que la caja de compra muestra precios en la moneda del país de salida que elijas. Para precios de EE. UU., añade -country-us al nombre de usuario de la zona.

El precio del Browser API es por GB de tráfico, así que el peso de la página te permite estimar el costo de una ejecución antes de empezar. A través de 3 cargas, los bytes transferidos en los eventos Network.loadingFinished de CDP promediaron 3.03 MB por página de producto, con un rango de 2.68 MB a 3.47 MB. El networkidle de Playwright espera 500 ms sin solicitudes, pero esta página seguía enviando solicitudes en segundo plano. Así que capture_pdp espera a domcontentloaded y luego un tiempo fijo.

Web Unlocker se encarga del desbloqueo, renderiza la página, y devuelve un solo documento. Su salida sin procesar para la página de producto fue de 344,355 bytes, comparado con el shell de 77,226 bytes, así que el JavaScript se ejecutó. Su salida en markdown para esta página fue de 21 KB a 24 KB de texto que un LLM o una regex pueden leer directamente. El comando brightdata scrape en la CLI de Bright Data usa la salida en markdown por defecto.

El precio de Web Unlocker es por solicitud, y pagas solo por las exitosas. Establece country en cada solicitud para seleccionar la tienda.

El AliExpress Scraper API devuelve datos estructurados. Envías URLs de producto y recibes registros con los campos ya extraídos, como el título, precio de lista y de oferta, disponibilidad, especificaciones, vendedor, calificación, reseñas, y URLs de imágenes. El AliExpress Image Scraper API se especializa en datos de imágenes. El precio del Scraper API es por registro entregado, así que pagas solo por los resultados.

Producto Unidad de precio Lo que recibes Úsalo cuando
Browser API GB de tráfico Una sesión de navegador y las respuestas de red de la página Quieres las propias respuestas de la API de la página, como el precio de cada variante
Web Unlocker Solicitud exitosa HTML renderizado o markdown que parseas Ya mantienes un parser, o envías páginas a un LLM
Scraper API Registro entregado Registros de producto estructurados Quieres registros, no páginas

Ejecutar tu propio navegador es la cuarta opción, así que también la medí. Los 3 navegadores sigilosos (Patchright, nodriver, y Camoufox) son de código abierto y promediaron de 14.1 a 15.9 segundos por carga. Ese tiempo es un mínimo, porque ninguna de las 24 cargas llegó a los datos del producto, y una carga exitosa también gastaría tiempo parseándolos.

En una dirección IP ya bloqueada, los 3 navegadores sigilosos devolvieron el encabezado y pie de página de la página pero ningún dato de producto. Cada producto de Bright Data tiene proxies integrados, y un navegador que ejecutes tú mismo puede usar los Proxies de AliExpress de Bright Data.

Ejecutar el Scraper API a gran escala

El Scraper API acepta URLs en un endpoint y devuelve registros en otro. Su página en el panel de control muestra ambos modos de scraper junto a una solicitud lista para ejecutar:

Pantalla de configuración de la Biblioteca de Scrapers de Bright Data para el endpoint "Aliexpress products - collect by URL", mostrando dos URLs de ejemplo precargadas en la lista de Inputs, una elección de modo de scraper entre Síncrono (tiempo real) y Asíncrono, y un ejemplo de código Linux Bash de solicitud autenticada a la derecha

El panel izquierdo lista los 2 endpoints del dominio, Collect by URL y Discover by category URL. El dataset_id en el ejemplo de código a la derecha es el mismo valor que usa el script abajo.

El modo síncrono devuelve un snapshot_id cuando una recolección toma mucho tiempo, y la recolección continúa en segundo plano. Para un catálogo completo, activa una recolección asíncrona en bloques y guarda el progreso después de cada uno, de modo que si tu proceso falla, la ejecución se reanuda y no pagas dos veces por las mismas URLs:

import json, os, time, requests

API = "https://api.brightdata.com/datasets/v3"
DATASET = "gd_mlj9v75u1w1jvaxvwp"          # Productos de AliExpress
TOKEN = os.environ["BRIGHTDATA_API_KEY"]   # Clave de API de la configuración de tu cuenta
CHUNK = 40                                 # activadores más grandes se ejecutaron más rápido por entrada
H = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}

def _await(snapshot, timeout=3600, poll=20):
    deadline = time.time() + timeout
    while time.time() < deadline:
        p = requests.get(f"{API}/progress/{snapshot}", headers=H, timeout=60).json()
        if p.get("status") in ("ready", "failed"):
            return p
        time.sleep(poll)
    raise TimeoutError(snapshot)

def _save(state, state_path):
    with open(state_path, "w") as fh:
        json.dump(state, fh)

def run(urls, state_path="run_state.json", out_path="rows.jsonl"):
    state = {"done": {}, "dead": []}
    if os.path.exists(state_path):
        with open(state_path) as fh:
            state = json.load(fh)

    for n in range(0, len(urls), CHUNK):
        key = str(n // CHUNK)
        done = state["done"].get(key, {})
        if done.get("records") is not None:     # terminado significa descargado, no activado
            continue
        batch, t0 = urls[n:n + CHUNK], time.time()
        snapshot = done.get("snapshot")         # activado antes de un fallo: reutiliza ese snapshot
        if not snapshot:
            r = requests.post(f"{API}/trigger?dataset_id={DATASET}&include_errors=true",
                              headers=H, json={"input": [{"url": u} for u in batch]},
                              timeout=120)
            r.raise_for_status()
            snapshot = r.json()["snapshot_id"]
            state["done"][key] = {"snapshot": snapshot}
            _save(state, state_path)            # registra el activador ANTES de la espera

        print(f"chunk {key}: recolectando {len(batch)} URLs")
        progress = _await(snapshot)
        rows = requests.get(f"{API}/snapshot/{snapshot}?format=json",
                            headers=H, timeout=600).json()
        with open(out_path, "a") as fh:
            for row in rows:
                if row.get("error_code") == "dead_page":   # listado eliminado en AliExpress
                    state["dead"].append(row["input"]["url"])
                elif not row.get("error"):
                    fh.write(json.dumps(row) + "\n")
        state["done"][key].update(records=progress.get("records"), errors=progress.get("errors"))
        _save(state, state_path)
        print(f"chunk {key}: recolectado a {(time.time() - t0) / len(batch):.1f}s por entrada")

    return state

run(["https://www.aliexpress.com/item/1005001398867186.html"])   # pasa tu lista completa de URLs

include_errors=true añade el informe de errores a los resultados, así que cada URL en tu lista tiene un estado.

Cada línea en rows.jsonl es un registro de producto. Este script imprime algunos campos del primer registro:

import json

with open("rows.jsonl") as fh:
    row = json.loads(fh.readline())
for key in ("item_id", "title", "brand", "price", "sale_price",
            "availability", "star_rating", "review_count"):
    print(f"{key:13s}", row[key])
print("variants     ", [o["option_name"] for o in row["variants"][0]["variant_options"]])

La salida muestra el disyuntor de la primera sección:

item_id       1005001398867186
title         TOMZN Main Switch Residual Current Circuit Breaker With Surge Protector RCBO MCB With Lightning Protection SPD
brand         TOMZN
price         $14.40
sale_price    $8.40
availability  in_stock
star_rating   4.7
review_count  80
variants      ['16A', '20A', '25A', '32A', '40A', '50A', '63A']

El precio de lista, la calificación de 4.7 con 80 reseñas, y las 7 variantes de corriente nominal coinciden con la página renderizada en la primera sección. El precio de oferta proviene de una promoción temporal, así que difiere entre capturas.

Mantén cada bloque lleno. En estas ejecuciones, un activador de 40 URLs terminó 2.8 veces más rápido por URL que uno de 20 URLs. Llena el último bloque con URLs de tu próxima ejecución para que se ejecute a tamaño completo. Para cientos de miles de URLs, deja que Bright Data entregue cada snapshot a tu almacenamiento en la nube o a un webhook, para que los resultados lleguen sin sondeo.

Ejecutar run() de nuevo con el mismo archivo de estado terminó en 0.08 segundos, no envió activadores, y no escribió filas duplicadas. El script también maneja un bloque que fue activado antes de que el proceso fallara durante la espera. Omite un bloque solo si sus registros ya fueron descargados. De lo contrario, reutiliza el snapshot guardado, así que un fallo durante la espera no pierde un snapshot recolectado.

Mide con qué frecuencia cambian los precios, luego ajusta tu calendario de recolección a esa tasa. En una verificación, capturé 12 productos dos veces, con 25 minutos de diferencia, y ninguno de sus precios cambió. Esa muestra no puede mostrar una tasa de cambio de precio, así que usa el método, no el resultado. Dos instantáneas de tus propios productos, tomadas en el intervalo que estés considerando, responderán la pregunta para tu catálogo.

Discovery, el endpoint de descubrimiento por categoría del scraper, lista productos desde una URL semilla. Con la página de búsqueda mayorista /w/wholesale-ergonomic-chair.html como semilla, una solicitud type=discover_new&discover_by=category_url devolvió URLs de producto en la primera ejecución. Usa /w/wholesale-<keyword>.html como tu patrón de URL semilla, limita cada ejecución de descubrimiento con limit_per_input mientras calibras, y pasa las URLs devueltas directamente a la ejecución de recolección.

Si recopilas IDs de producto de otros lugares, como los sitemaps de artículos o los enlaces SEO de una página de producto, una sola solicitud GET sin renderizar muestra si cada ID todavía tiene una página de producto. Las páginas de producto devolvieron 77.2 a 77.4 KB con un og:title poblado, mientras que 3 IDs recopilados de esos enlaces SEO devolvieron 75.26 KB con cada campo og: vacío. Esa diferencia de 2 KB es visible en la respuesta sin procesar, así que descarta los IDs con campos og: vacíos antes de poner en cola una recolección.

2 verificaciones que mantienen los datos de precio comparables

Los datos necesitan la misma validación que las respuestas HTTP. Cada una de estas 2 verificaciones es barata de añadir y te ayuda a detectar precios que no son comparables antes de que lleguen a un panel.

El descuento varió según el país de salida, no solo la moneda. Obtuve un ID de producto a través de Web Unlocker en formato markdown desde 6 países de salida, y cada país devolvió una promoción diferente:

País de salida Moneda Precio de oferta Precio de lista Descuento
Estados Unidos USD $6.84 $14.40 52.5%
Reino Unido GBP £9.83 £11.16 11.9%
Alemania EUR 11,69€ 13,29€ 12.0%
Brasil BRL R$62,25 R$68,30 8.9%
Australia AUD AU$18.59 AU$19.42 4.3%
Canadá CAD C$19.86 C$20.70 4.1%

Incluso los porcentajes no son directamente comparables, y cada página muestra por qué. La caja de compra de EE. UU. etiqueta su precio Ends :, así que la fila de EE. UU. es una oferta por tiempo limitado capturada en un momento específico. La página del Reino Unido dice Price includes VAT mientras que la página de EE. UU. dice Tax excluded. La página alemana indica 30-day lowest price before the discount, la regla de precio de referencia de la UE que tiende a hacer que los descuentos alemanes parezcan más pequeños.

Debido a esas 3 diferencias, establece un país de salida para cada serie de precios, guárdalo en la fila, y compara una serie solo consigo misma. Web Unlocker lo establece con el parámetro country, y el Browser API lo establece con un sufijo -country-<code> en el nombre de usuario de la zona. Si necesitas las promociones en sí en lugar de un pipeline de precios, el AliExpress Promotions Tracker monitorea las promociones de la competencia por ti.

El símbolo de libra del Reino Unido es el de ancho completo. La tienda del Reino Unido usa £, U+FFE1 FULLWIDTH POUND SIGN, así que una regex de moneda escrita con £ (U+00A3) no coincide con nada. Las tiendas alemana y brasileña usan una coma como separador decimal en el campo de precio. Parsea la moneda desde la configuración de tienda que solicitaste y mantén el parseo del número separado del símbolo.

El precio de cada variante y la ruta completa de categoría

El Browser API de Bright Data captura el mapa de precios de SKU de una página de producto de AliExpress, y Web Unlocker devuelve la ruta de categoría desde su pie de página. Las reseñas también tienen un scraper dedicado, el AliExpress Reviews Scraper, cuyos registros incluyen el ID y la URL del producto.

La propia respuesta de la página contiene el mapa de precios de SKU. Las capturas registradas con capture_pdp incluían precios para 141, 75, 20, 18, 15, y 6 SKUs. Para un producto, el mapa de precios tenía 6 precios distintos, de 2.56 a 10.96. El mapa está en el módulo PRICE, junto al SKU preseleccionado y el campo region:

Pestaña Response de Chrome DevTools para una solicitud pdp.pc.query mostrando region US, selectedSkuId 12000059478006652, y un skuIdStrPriceInfoMap cuyas primeras tres entradas son paquetes de 1 listados a $7.61, $7.58 y $7.71 con un precio de oferta de $1.09 cada uno, luego una entrada de paquete de 10 listada a $57.34

Esta captura proviene de un listado multipack. La página preselecciona un paquete de 1 a $1.09, y la cuarta entrada es un paquete de 10 listado a $57.34, así que el mapa contiene el precio propio de cada variante.

Para obtener el precio de cada variante, ejecuta capture_pdp a través del Browser API, lee PRICE.skuIdStrPriceInfoMap, y guárdalo con item_id como clave. Lee el mapa de la región cuyos precios necesitas, porque también puede variar por región. Para el disyuntor de la primera sección, el mapa tenía precios de variantes diferentes en una sesión de EE. UU. y un solo precio para las 7 variantes en otra región.

El markdown de Web Unlocker contiene la ruta de categoría. La página renderizada lista las categorías del producto como una ruta de navegación en el pie de página, y el GLOBAL_DATA.globalData del gateway tiene la misma ruta en su campo categoryPath, 13/5/141914. El parser abajo lee la página de producto como markdown de Web Unlocker, el formato que brightdata scrape devuelve por defecto:

import re

CRUMB   = re.compile(r"^This product belongs to\s.*$", re.M)
CAT_URL = re.compile(r"/category/(\d+)/([a-z0-9-]+)\.html")

def categories_from_markdown(md):
    """IDs y URLs de categoría del pie de página del producto."""
    line = CRUMB.search(md)
    if not line:
        return []
    return [{"category_id": cid,
             "url": f"https://www.aliexpress.com/category/{cid}/{slug}.html"}
            for cid, slug in CAT_URL.findall(line.group(0))]

with open("page.md", encoding="utf-8") as fh:    # brightdata scrape <URL de producto> -o page.md
    print(categories_from_markdown(fh.read()))

El \s al final del primer patrón es esencial, porque el pie de página coloca U+00A0, un espacio sin separación, justo después de to. El mismo patrón escrito con un espacio literal no encuentra coincidencias. Con \s, el parser devuelve la ruta de categoría:

[{'category_id': '13',     'url': '.../category/13/home-improvement.html'},
 {'category_id': '5',      'url': '.../category/5/electrical-equipments-supplies.html'},
 {'category_id': '141914', 'url': '.../category/141914/circuit-breakers.html'}]

Obtener la ruta de categoría necesita una solicitud de Web Unlocker por producto. Ejecuta el parser en los productos cuya taxonomía necesitas, y guarda el resultado con item_id como clave. El texto del pie de página puede cambiar sin aviso, así que verifica primero el patrón CRUMB si el parser devuelve una lista vacía.

Scraper Studio maneja una lista de campos personalizada. Cuando describes los datos que necesitas, su IA genera un scraper listo para ejecutar con proxies y desbloqueo integrados, y puedes agregar o cambiar campos de salida más tarde. Para el monitoreo de precios específicamente, el AliExpress Price Scraper API recopila datos de precios de AliExpress para que puedas rastrear cómo cambian los precios.

Detectar cambios en la página y el esquema

Cada fallo visto en este objetivo fue silencioso, así que monitorea la página misma. El script bootstrap de la página contiene una versión de compilación, cosmos_prefetch_mtop version 20240605.195626.856, y cuando esa versión cambia, la configuración de precarga puede cambiar con ella. Una huella digital detecta ese cambio directamente. Sin una, tu propio scraper solo lo mostraría como valores nulos.

Las 2 huellas digitales abajo verifican 3 etapas (el shell HTML, la página renderizada, y la fila entregada), y ninguna depende de parsear campos del producto:

import hashlib, re

def shell_fingerprint(html):
    """De un GET sin renderizar. Detecta un cambio de bootstrap o configuración de precarga."""
    view = re.search(r"viewName\s*=\s*'([^']+)'", html)
    api  = re.search(r"api:\s*'(mtop\.[^']+)'", html)
    ver  = re.search(r"cosmos_prefetch_mtop version ([\d.]+)", html)
    return {"viewName": view.group(1) if view else None,
            "prefetch_api": api.group(1) if api else None,
            "gop_version": ver.group(1) if ver else None}

def row_fingerprint(row):
    """De una fila entregada. Confirma el conjunto de campos que tu pipeline espera."""
    keys = sorted(k for k in row if k not in ("input", "timestamp"))
    return {"fields_sha1": hashlib.sha1(",".join(keys).encode()).hexdigest()[:12]}

En capturas reales, ambas huellas digitales devolvieron valores estables:

shell   unrendered GET : viewName=newDetail  api=mtop.aliexpress.pdp.pc.query  gop=20240605.195626.856
shell   rendered DOM   : viewName=newDetail  api=mtop.aliexpress.pdp.pc.query  gop=20240605.195626.856
row     1005012408061151 : fields_sha1=154f2f84da5f
row     1005001398867186 : fields_sha1=154f2f84da5f
row     1005011666876666 : fields_sha1=154f2f84da5f

Una tercera huella digital de la carga útil capturada parece útil, pero puede causar falsos positivos. Aplicar hash al conjunto de módulos parece la misma idea, pero el recuento de módulos variaba con el listado y el momento de captura, no solo con una nueva implementación. Las cargas útiles capturadas tenían 28, 27, y 28 módulos, y el conjunto de claves de PRICE difería entre 2 de ellas.

El recuento cambió también dentro de un mismo producto. El mismo ID de producto devolvió 27 módulos y luego 26 en 2 capturas con minutos de diferencia, así que incluso un historial por producto necesita un umbral en lugar de una verificación de igualdad. Basa tus alertas en las huellas digitales de shell y fila, porque sus valores se mantuvieron iguales en cada captura.

Cuándo comprar el conjunto de datos de AliExpress en su lugar

Los 3 métodos parten todos de URLs que ya tienes. Si tu pregunta es sobre todo el mercado, empieza con el conjunto de datos listo para usar:

Página del Mercado de Conjuntos de Datos de Bright Data para "Aliexpress products", mostrando la descripción del conjunto de datos con botones de Hablar con un experto en datos y Proceder a la compra, y una pestaña Data sample con controles de filtro para product_category, brand, store_country y star_rating junto a una tabla de vista previa de columnas url, item_id, variant_id, title y description

El panel de filtros te permite seleccionar registros antes de pagar por ellos.

El conjunto de datos de AliExpress reduce el trabajo de selección y el tiempo de recolección. Filtras el conjunto de datos a los registros que necesitas, en el mercado o mediante la Filter Dataset API, y pagas solo por tu instantánea filtrada. Esos registros ya existen, así que no hay ninguna ejecución de recolección que esperar. Puedes comprarlo una vez o en un calendario de actualización que elijas, y los precios actuales están en la página de precios de conjuntos de datos.

Estima el tamaño de entrega antes de pedirlo. Los registros en la muestra del conjunto de datos promedian 12.75 KB como JSON entregado, así que planifica alrededor de 1.3 GB por cada 100,000 registros. El conjunto de datos también se entrega en Parquet, y la misma muestra convertida a Parquet fue casi 8 veces más pequeña. Esa diferencia de tamaño puede determinar si consultas el archivo en su lugar o lo preparas primero.

Usa el conjunto de datos para preguntas de surtido y cobertura a nivel de mercado, y usa el Scraper API para los listados específicos que rastreas. El conjunto de datos se actualiza en el calendario que elijas, y una recolección del Scraper API devuelve cada listado en el momento en que lo ejecutas.

Reflexiones finales

El esquema de firma de AliExpress cabe en una función, pero en las pruebas, el gateway bloqueó llamadas desde una dirección IP dentro de la primera docena de solicitudes, incluso con una firma correcta. Debido a que un bloqueo puede devolver un código de estado de éxito, basa cada verificación en el encabezado bxpunish y en un campo que todo producto tiene, como el precio. El robots.txt del sitio no prohíbe la ruta pública /item/, así que esa es la ruta para scrapear a gran escala.

El AliExpress Scraper API de Bright Data devuelve registros estructurados para esas páginas de producto, el Browser API captura el precio de cada variante, y el conjunto de datos de AliExpress se adapta a preguntas de todo el mercado. Empieza con el nivel gratuito, que incluye 5,000 solicitudes al mes sin tarjeta de crédito requerida. Pruébalo con 40 URLs de tu catálogo, y verifica los registros contra los campos que tu pipeline necesita.

Preguntas frecuentes

¿Tiene AliExpress una API para datos de productos?

Sí, para vendedores, afiliados, y dropshippers aprobados. En el AliExpress Open Platform, un vendedor autoriza una app para su propia tienda mediante OAuth 2.0, y la API de afiliados cubre el catálogo promocional. Ninguna de ellas cubre todo el mercado, así que los datos de precios a nivel de mercado necesitan recolección de datos de comercio electrónico.

¿Cómo obtengo una URL o ID de producto de AliExpress?

Las URLs de producto siguen el patrón /item/<item_id>.html, y el descubrimiento empieza desde /w/wholesale-<keyword>.html. Los IDs dependen del host. Un producto tiene el ID 1005001398867186 en .com y 3256801212552434 en .us. Clasifica cada producto por su URL final después de la redirección regional, para que no aparezca bajo 2 IDs.

¿Es gratis hacer scraping de datos de productos de AliExpress?

Sí, a pequeña escala. El nivel gratuito de Bright Data incluye 5,000 solicitudes al mes sin tarjeta de crédito requerida, y cubre el Scraper API, Web Unlocker, y el Browser API, así que puedes probarlos primero en tus propias URLs. Playwright y curl_cffi son de código abierto. A escala de producción, pagas principalmente por direcciones IP, porque 2 tandas de 60 llamadas firmadas a la API desde una dirección IP obtuvieron datos 7 y 15 veces.

¿Cuánto cuesta una API de scraper de AliExpress?

El AliExpress Scraper API de Bright Data tiene precio por registro entregado, y Web Unlocker tiene precio por solicitud exitosa, así que pagas solo por resultados. El Browser API tiene precio por tráfico, así que el peso de la página predice su costo. Una página de producto pesó aproximadamente 3 MB en las pruebas. Los precios actuales están en la página de precios de Bright Data.

¿Por qué mi scraper de AliExpress devuelve campos vacíos?

Porque el HTML inicial no tiene precios ni stock. window.runParams es un objeto vacío y <title> está en blanco, así que los extractores basados en selectores no encuentran nada y no generan ningún error. El JavaScript de la página carga los campos después desde mtop.aliexpress.pdp.pc.query. Ejecuta el JavaScript o llama directamente a esa API.

¿Cómo evito ser bloqueado al hacer scraping de AliExpress?

Envía solicitudes a través del Scraper API o Web Unlocker de Bright Data, que tienen proxies integrados y resolución de CAPTCHA de AliExpress. En las pruebas, ni un nuevo contenedor de cookies ni una espera de 60 segundos eliminaron un bloqueo de una dirección IP. Las solicitudes bloqueadas también devolvieron HTTP 200, así que verifica el encabezado bxpunish, no el código de estado.

¿Por qué cambian los precios de AliExpress?

Porque las promociones son temporales y varían por país. En una prueba en 6 países de salida, el descuento de un producto varió de 52.5% a 4.1% sobre el precio de lista, y los países diferían en si los precios incluían impuestos. Establece un país de salida para cada serie de precios y guárdalo en la fila.

¿Existe un servidor MCP de AliExpress?

Sí. El MCP Server de Bright Data permite que un agente de IA obtenga páginas de producto de AliExpress mediante sus herramientas scrape_as_markdown y scrape_batch, que manejan el desbloqueo y devuelven markdown. En una llamada de prueba, scrape_as_markdown devolvió el precio, calificación, variantes, y ruta de categoría del producto. La página de AliExpress MCP explica la configuración.