Comment Scraper les Données AliExpress à Grande Échelle

Apprenez à scraper les prix, stocks et variantes AliExpress en Python, détectez les blocages masqués en HTTP 200, et collectez à grande échelle avec Bright Data.
23 min de lecture
How to Scrape AliExpress blog image

Le scraping d’AliExpress collecte le prix, le stock, les variantes, la note et les avis dont la surveillance des prix et le suivi des concurrents ont besoin. La majeure partie n’est pas dans le HTML, car la page la charge ensuite depuis une API interne signée.

Dans mes tests, une requête simple a renvoyé un HTTP 200 sans prix, donc un pipeline enregistrerait un succès avec une ligne vide. Depuis une seule adresse IP, l’API signée a commencé à bloquer les appels en une dizaine de requêtes.

Ce guide vous montre comment obtenir ces données, détecter un échec qui ressemble à un succès, et utiliser l’AliExpress Scraper API de Bright Data à grande échelle.

TL;DR

  • L’objet window.runParams que les anciens tutoriels analysent est vide. Les données de prix, de stock et de SKU se chargent depuis mtop.aliexpress.pdp.pc.query, qui nécessite une signature MD5.
  • Détectez les blocages par des marqueurs, pas par des codes de statut. Les requêtes bloquées ont renvoyé un HTTP 200, un en-tête bxpunish, et un marqueur de corps tel que rgv587_flag.
  • Une signature correcte n’a pas empêché les blocages. Deux séries de 60 appels depuis une seule adresse IP, sans délai, ont obtenu des données pour 7 et 15 d’entre eux.
  • À grande échelle, scrapez les pages produits publiques /item/. L’AliExpress Scraper API de Bright Data extrait des données structurées, et le Browser API permet à votre script de lire la réponse API complète de chaque page.

Ce qui se passe quand vous scrapez AliExpress avec un client HTTP

Commencez par une seule requête HTTP, envoyée sans navigateur ni proxy. Si elle expire, votre réseau ne peut pas atteindre AliExpress directement, et un proxy ou l’une des méthodes gérées ci-dessous envoie la requête depuis un réseau qui le peut.

Tout ce qui suit utilise 3 packages. Le script Scraper API d’une section ultérieure lit votre clé API Bright Data depuis une variable d’environnement :

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

Récupérez une page produit avec curl_cffi et lisez le <title>, l’objet window.runParams que les anciens tutoriels analysent, et l’objet bootstrap que la page transmet à son propre JavaScript. Ces 3 valeurs montrent si un client HTTP simple suffit :

import re
from curl_cffi import requests as cffi

# any product URL that still exists works
ITEM = "https://www.aliexpress.com/item/1005001398867186.html"

# no User-Agent of your own: curl_cffi sends one that matches the browser it impersonates
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 récupération réussit :

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

La page est une coquille HTML, sans prix, stock, SKU, note, ni avis. Avant qu’aucun JavaScript ne s’exécute, la coquille possède déjà les balises Open Graph. og:title contient le vrai nom du produit alors que <title> est vide, et og:image, og:url, et og:type (défini sur product) sont également présents. og:description ne contient qu’un slogan générique AliExpress, et la page n’a pas de JSON-LD, donc aucune des deux sources ne fournit de données produit.

Un navigateur qui charge la même URL dans la même heure affiche le prix, la note et les variantes :

Page produit AliExpress pour un disjoncteur différentiel résiduel TOMZN rendu dans un navigateur, montrant un prix de vente de 7,73 $ et un prix catalogue de 14,40 $, une note de 4,7 sur 80 avis et 489 ventes, sept boutons de variante de courant nominal de 16A à 63A, le panneau vendeur TOMZN Official Store, et des boutons Acheter maintenant et Ajouter au panier

Rendu via le Browser API de Bright Data avec le ciblage géographique réglé sur les États-Unis. L’API interne d’AliExpress renvoie le même prix catalogue de 14,40 $ et la note de 4,7 pour ce produit. Le prix de vente provient d’une promotion temporaire et change au fil du temps.

Chercher ce prix catalogue dans le code source de la page ne trouve rien :

Onglet source de la page du navigateur pour www.aliexpress.us/item/3256801212552434.html avec la barre de recherche cherchant 14.40 et signalant 0/0 correspondance, au-dessus du début de la section head de la page, où une balise meta og:title nomme le TOMZN RCBO Circuit Breaker 16-63A 220V Surge Protector et og:description contient un slogan générique AliExpress

Capturé sur la vitrine US, qui sert la même coquille de 77 Ko.

Les champs du catalogue sont peu coûteux à collecter. Une seule requête GET sans déblocage renvoie les champs du catalogue, vous pouvez donc associer les identifiants de produits aux titres et images principales sans rendu. Les pipelines de prix et d’assortiment ont besoin des données de prix, de stock et de variantes, qui proviennent de la passerelle API interne d’AliExpress. Planifiez ces 2 tâches séparément.

Laissez curl_cffi choisir le User-Agent. impersonate="chrome" est un alias plutôt qu’une version. Dans le code source de la bibliothèque, curl_cffi 0.16.1 fait correspondre l’alias à chrome150 via une constante DEFAULT_CHROME, et cette constante change à chaque sortie d’un nouveau profil. Si vous ne définissez pas l’en-tête, la bibliothèque envoie aussi le User-Agent correspondant Chrome/150.

Si vous passez votre propre User-Agent, il remplace celui de la bibliothèque, donc une chaîne codée en dur Chrome/141 est envoyée avec une poignée de main TLS Chrome 150. L’incohérence ne génère aucune erreur, donc vous pouvez la déployer sans vous en apercevoir. Soit omettez l’en-tête et laissez la bibliothèque associer le User-Agent à la poignée de main, soit figez le profil à une version et faites correspondre la chaîne. Les ingénieurs qui construisent des clients TLS rapportent qu’une identité incohérente est plus facile à détecter qu’une identité cohérente, et que le User-Agent devient obsolète bien avant la poignée de main TLS.

Le drapeau isCSR montre comment la page est rendue. Sa valeur est true car AliExpress a déplacé la page de détail produit vers un rendu côté client (CSR). window.runParams est un objet vide inutilisé que les extracteurs basés sur des regex continuent de faire correspondre. Un parseur qui le lit ne génère aucune erreur et ne renvoie rien.

Chaque client que j’ai essayé a obtenu un HTTP 200 et la même coquille de 77 Ko. J’ai envoyé 20 requêtes consécutives vers cette URL depuis une adresse IP résidentielle sans proxy, et toutes ont renvoyé un HTTP 200. Les requêtes se sont déroulées en une seule session à une requête toutes les 0,4 secondes, sans rotation de User-Agent. J’ai aussi récupéré la même URL en tant que python-requests/2.32.5, en tant que curl/8.7.1, et avec 3 profils d’imitation TLS.

Envoyer le User-Agent de Googlebot fait passer "crawler" de false à true dans l’objet bootstrap. La réponse diffère d’1 octet. Le drapeau n’a pas ajouté de contenu rendu côté serveur, donc usurper Googlebot ne change qu’une valeur booléenne et rien d’autre.

Sur une place de marché rendue côté client, vérifiez les données avant d’enregistrer une récupération comme un succès. Le code de statut montre seulement que la requête HTTP a réussi. Sur cette cible, la requête réussissait même quand les données étaient absentes.

Le blocage qui renvoie un HTTP 200

AliExpress bloque effectivement des requêtes, mais la ligne de statut HTTP ne montre pas le blocage.

Le robots.txt du site lui-même nomme un endpoint interne dans une règle spécifique à Googlebot, et cet endpoint peut renvoyer les données produit. Appelez-le directement et lisez les en-têtes de réponse avant le corps :

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 réponse provient du système anti-bot Baxia d’Alibaba, et elle a un code de statut de succès :

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

Le corps est un script qui redirige vers une URL _____tmd_____/punish via window.location et définit window._config_ = {"action":"captcha", ...}. Il se termine par <!--rgv587_flag:sm-->, le marqueur de blocage d’Alibaba. L’en-tête x-rule-name nomme la règle qui a correspondu, AE_GLOBAL_UNLOGIN, qui semble être la règle pour le trafic global non authentifié. Le même en-tête est aussi revenu sur des réponses normales non bloquées, donc il ne marque pas un blocage à lui seul.

Les vérifications basées sur le code de statut acceptent cette réponse bloquée. raise_for_status() ne lève pas d’erreur, et une vérification status_code == 200 renvoie vrai. Si un tableau de bord compte les réponses HTTP 2xx comme des succès, il signale un pipeline sain alors que les lignes sont vides.

Détectez donc les blocages par les en-têtes et le corps de la réponse, pas par le code de statut. Les chaînes d’erreur d’Alibaba peuvent changer sans préavis, mais un en-tête de réponse dédié et une chaîne marqueur dans le corps sont peu coûteux à vérifier et à étendre :

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

def is_blocked(resp, min_bytes=1024):
    """True when Baxia responded instead of the application."""
    if resp.headers.get("bxpunish"):
        return True
    body = resp.text
    if len(body) < min_bytes:          # treat a near-empty 200 as a block too
        return True
    return any(m in body[:4096] for m in BLOCK_MARKERS)

Dans mes tests, ces vérifications détectaient chaque blocage que le code de statut ne montrait pas. Exécutez les mêmes vérifications avant votre parseur, et ajoutez-les avant d’écrire des sélecteurs. Sans elles, vous n’apprenez l’existence d’un blocage sur cette cible que lorsque votre parseur ne trouve aucune donnée.

Enregistrez la raison de chaque requête échouée. Sur cette cible, une page de blocage, une coquille vide et un prix dans la mauvaise devise nécessitent chacun une correction différente, et un taux de succès global masque ces 3 cas. Séparez aussi les raisons par pays de sortie, car un prix dans la mauvaise devise ressemble quand même à un succès.

Signer soi-même les appels API produit

Les champs produit existent bien en JSON. Si vous chargez une page dans un navigateur et enregistrez son trafic XHR, vous pouvez voir que ces champs proviennent d’une passerelle API que la page appelle.

Un chargement de page effectue des appels vers la passerelle MTOP d’Alibaba à acs.aliexpress.com/h5/<api>/<version>/, chacun avec jsv, appKey, un timestamp en millisecondes t, et une signature sign de 32 caractères. mtop.aliexpress.pdp.pc.query renvoie le prix, le stock, le SKU, la boutique et la note. Le texte des avis provient d’un second appel signé, mtop.aliexpress.review.pc.list, sur la même passerelle, donc collecter les avis nécessite une seconde requête par produit. La lecture du script bootstrap inline montre que la page peut appeler plus d’une API produit :

Variante de page (viewName) API préchargée
newDetail mtop.aliexpress.pdp.pc.query
choiceDetail mtop.aliexpress.itemdetail.pc.asyncPCDetail
h5Detail mtop.aliexpress.itemdetail.msite

Les 6 pages produit que j’ai échantillonnées avaient viewName = 'newDetail'. Les 2 autres API, le mobile mtop.aliexpress.itemdetail.msite et mtop.aliexpress.itemdetail.pc.asyncPCDetail, n’étaient pas plus faciles d’accès. Les deux ont renvoyé FAIL_SYS_USER_VALIDATE avec bxpunish: 1 dès la première requête, envoyée avec la même charge utile que l’API desktop accepte.

Lisez viewName à chaque récupération pour voir quand une fiche commence à se rendre sous une autre variante de page. Un scraper codé en dur sur un seul nom d’API manquerait ce changement. Le script bootstrap de la page définit un délai de préchargement de 15 000 ms et annule son propre XHR à 10 000 ms, utilisez donc 10 000 ms comme limite supérieure pour vos propres délais d’attente.

Dans le panneau Network du navigateur, filtré sur pdp.pc.query, la propre requête de la page contient toutes les valeurs dont votre code de signature a besoin :

Panneau Network de Chrome DevTools filtré sur pdp.pc.query, sur l'onglet Payload d'une requête vers mtop.aliexpress.pdp.pc.query, listant jsv 2.5.1, appKey 12574478, un t à 13 chiffres, un sign à 32 caractères, type et dataType originaljsonp, timeout 15000, callback mtopjsonp1, et un objet data avec productId 3256801212552434 et un bloc ext, avec une valeur de jeton masquée

L’appel propre de la page, capturé sur la vitrine US, où l’ID de cette fiche est 3256801212552434. La page envoie originaljsonp et ajoute un bloc timeout et ext. Le code de signature ci-dessous envoie jsonp et omet les deux, et la passerelle a accepté cette requête plus courte. Une valeur de jeton est masquée.

La valeur sign utilise le schéma de signature H5 d’Alibaba, suffisamment court pour être réimplémenté dans une seule fonction. Le schéma joint le jeton de session, le timestamp, la clé d’application et le corps exact de la requête avec des esperluettes, puis hache le résultat avec 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")                       # unsigned, returns the token cookie
token = s.cookies.get("_m_h5_tk", "").split("_")[0]
second = pdp(s, "1005001398867186", token)               # signed with that token
print("call 1:", len(first.text), "bytes |", first.text[:60])
print("call 2:", len(second.text), "bytes")

Le premier appel renvoie le jeton, et le second appel renvoie le produit :

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

L’appel 1 renvoie FAIL_SYS_TOKEN_EMPTY et un en-tête Set-Cookie avec _m_h5_tk. L’appel 2 est signé avec la première partie de la valeur de ce cookie et renvoie du JSONP, un appel de fonction enveloppant un objet JSON. Si vous retirez l’enveloppe mtopjsonp1(...), vous obtenez l’enregistrement complet du produit, y compris le titre, originalPrice 14,40 $, salePriceString 6,84 $, 59 924 unités en stock, une note de 4,7, l’ID vendeur 200422816, et categoryPath 13/5/141914.

Vous n’avez besoin que d’une petite partie de cette charge utile. La réponse comporte 27 modules de premier niveau, et 8 d’entre eux contiennent les champs qu’un pipeline de prix ou d’assortiment stockerait. Sérialisés en JSON compact, ces 8 modules font 16 494 octets, soit 15,9 % de l’objet résultat complet. Le plus gros module est SHIPPING avec 34 071 octets, et le second est GLOBAL_DATA avec 23 783 octets.

Signer ne nécessite qu’un hash MD5, un cookie, et une relance. Si vous avez déjà réimplémenté le schéma de signature H5 pour un autre site Alibaba, attendez-vous à quelques heures de travail. Alors, combien de temps votre code de signature continue-t-il de fonctionner ?

Combien d’appels API signés réussissent depuis une seule adresse IP

Exécuter l’appel signé en boucle répond rapidement à cette question. Le test utilise 10 identifiants de produits, un jeton, une adresse IP résidentielle, et aucun délai entre les appels. Le script compte chaque code ret que la passerelle renvoie :

# continues the session from the signing code: pdp(), s and token are defined there
from collections import Counter
import re

# ten product IDs, replace any that were removed
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)

La boucle a cessé d’obtenir des données après le 7e appel :

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

Le tableau ret de chaque réponse bloquée contenait aussi RGV587_ERROR::SM.

Le blocage s’est reproduit, mais le nombre de succès a changé. Une seconde série depuis la même adresse IP a envoyé 6,2 requêtes par seconde, enregistré le code de statut et la longueur de réponse de chaque appel, obtenu 15 succès sur 60, et a été bloquée pour la première fois à la requête 12. Elle a renvoyé un HTTP 200 pour les 60 appels, et ses 45 réponses bloquées faisaient toutes exactement 543 octets. Basez donc votre plan de capacité sur le blocage, pas sur l’un ou l’autre des nombres de succès.

Une nouvelle session n’a pas supprimé le blocage. Chacune de 3 nouvelles sessions consécutives a démarré avec un pot de cookies vide et a été bloquée avant que le client puisse obtenir un jeton. Une requête envoyée après une attente de 60 secondes, à nouveau avec un pot de cookies vide, a aussi été bloquée. Donc une relance automatique ajoute du trafic sans ajouter de données, et les ingénieurs proxy avertissent qu’une logique de relance comme celle-ci consomme du budget sans aucune erreur visible.

Ce test n’utilisait aucun délai entre les appels. Testez ensuite les variables qu’un scraper de production ajoute, telles qu’un long backoff, un nouveau jeton pour chaque lot, un débit de requête délibérément lent, et le bloc ext que la propre requête de la page signe. Ce bloc contient un drapeau crawler et une clé x-m-biz-bx-region.

La signature est restée correcte de la requête 7 à la requête 60, donc davantage de rétro-ingénierie est peu susceptible d’augmenter les nombres de succès de 7 et 15 sur 60 requêtes. Les tests excluent la session comme cause. Aucun test n’a changé l’adresse IP et l’empreinte séparément, donc le blocage pourrait dépendre de l’une ou de l’autre. Les systèmes anti-bot peuvent aussi lier une empreinte à travers de nombreuses adresses IP, donc les praticiens changent l’adresse IP, l’empreinte et les cookies ensemble.

Trois vérifications montrent que le client n’est pas en cause. Sur l’écho d’empreinte tls.peet.ws, le code de signature sur curl_cffi 0.16.1 correspondait à un vrai Chromium 152 sur cette machine, avec la même empreinte JA4, t13d1516h2_8daaf6152771_806a8c22fdea, et la même empreinte HTTP/2. Cinq empreintes de curl_cffi et des bibliothèques Rust rnet et wreq ont produit 100 réponses bloquées et aucun jeton, même si wreq expédie des profils Chrome plus récents que curl_cffi 0.16.1. Patchright, nodriver, et Camoufox ont chacun réussi 0 fois sur 8 tentatives depuis cette adresse IP, tandis que le Browser API de Bright Data, sur une IP de sortie différente, a renvoyé le produit dans la même minute.

Cette adresse IP était déjà bloquée quand ces 3 outils ont été exécutés, donc les 3 ont échoué de la même manière sur elle. Les zéros montrent la réputation de l’adresse IP, et ne classent pas les outils.

La première vérification, l’écho d’empreinte de TrackMe sur tls.peet.ws, renvoie chaque empreinte en une seule requête :

Page de l'API de fingerprinting TrackMe listant les empreintes JA3, JA4, p0f, Akamai HTTP/2 et PeetPrint pour le client demandeur, avec une valeur JA4 de t13d1516h2_8daaf6152771_806a8c22fdea et une empreinte Akamai HTTP/2 de 52d84b11737d980aef856699f885ca86, au-dessus d'un panneau de détails de requête montrant TLS 772, HTTP/2, et le support TLS 1.3 et 1.2

Vérifiez qu’un profil reproduit à la fois l’empreinte JA4 et l’empreinte HTTP/2 du navigateur dont il porte le nom.

Un profil plus récent n’était pas une meilleure correspondance ici. Le profil Chrome 152 de wreq a envoyé un ClientHello avec 17 extensions, et Chrome 152 lui-même en a envoyé 16. Le numéro de version dans le nom d’un profil ne garantit pas l’empreinte qu’il envoie, vérifiez donc chaque profil face à un écho d’empreinte avant de lui faire confiance. Ici, le profil par défaut de curl_cffi correspondait exactement au vrai navigateur.

La rétro-ingénierie d’un endpoint signé vous indique à quoi ressemblent les données, mais pas combien vous pouvez en collecter. Dans ces tests, ni une meilleure signature ni un meilleur navigateur n’ont augmenté cette quantité.

Laisser la page signer les appels API produit pour vous

Une deuxième façon d’obtenir la charge utile produit utilise l’automatisation de navigateur au lieu d’appels API depuis votre propre code. La page effectue déjà l’appel signé. Attachez un gestionnaire de réponses et lisez les réponses que la page reçoit. Via le Browser API, les requêtes de la page passent aussi par le réseau de proxy de Bright Data.

Lire la propre réponse de la page retire le jeton, le timestamp, la signature MD5, et le pot de cookies de votre code :

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):
    """Read the response to the call the page already makes."""
    bodies = []

    def keep(response):
        if "mtop.aliexpress.pdp.pc.query" not in response.url:
            return
        try:
            bodies.append(response.text())
        except Exception:
            pass                      # redirects and closed pages have no readable body

    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                      # keep whatever keep() collected before navigation ended
        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                  # skip a body that is not JSON, such as an HTML page
        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 connecte à une zone Browser API, et ses 3 espaces réservés proviennent des détails d’accès de cette zone :

Panneau de contrôle Bright Data sur l'onglet Overview d'une zone Browser API nommée scraping_browser2 sous Web Access, montrant les détails d'accès avec l'hôte brd.superproxy.io, un nom d'utilisateur, un mot de passe masqué, et un endpoint Puppeteer et Playwright montrant wss://brd-customer-, un ID de compte masqué, -zone-scraping_browser2, un mot de passe masqué et @brd.superproxy.io:9222

La ligne Puppeteer / Playwright est la valeur que WSS attend, et votre ID de compte, nom de zone et mot de passe remplissent ses 3 espaces réservés. L’ID de compte est masqué ici, et le Panneau de contrôle masque le mot de passe.

Conservez chaque réponse, pas seulement la première. La page appelle cet endpoint plusieurs fois par chargement. Enregistrez la taille de chaque corps de réponse que keep() collecte pour voir lequel contient le produit :

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 réponse de 151 octets définit le cookie de jeton. Le gros corps de réponse qui suit contient l’enregistrement du produit, y compris la carte de prix des SKU. Quand un chargement a plus d’une grosse réponse, capture_pdp renvoie la plus grande.

Ce que les fichiers robots.txt d’AliExpress autorisent

Lisez les fichiers robots.txt avant de choisir une méthode. Si une directive vous est inconnue, notre guide de robots.txt pour le web scraping l’explique. Les fichiers sur l’hôte API et l’hôte principal ont des règles différentes :

$ 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/*

La passerelle MTOP est entièrement interdite. Sur l’hôte principal, chaque règle mentionnant item s’applique à autre chose (/items/*, /item-img/*, et la règle Googlebot sur l’endpoint aeglodetailweb). Aucune règle ne s’applique à /item/1005001398867186.html, alors que tout /search/* est interdit. Utilisez les proxys résidentiels de Bright Data, ou l’une des méthodes ci-dessous, pour les chemins que robots.txt autorise.

Vérifiez aussi les sitemaps et fichiers manifestes d’agent de l’hôte principal. Le robots.txt du .com listait des sitemaps d’articles, et le premier fichier sitemap contenait 31 999 URLs /item/, donc les sitemaps sont une source directe d’identifiants de produits. Dans les tests, les 6 chemins de manifeste d’agent (/llms.txt et /ai.txt, plus agent.json, ai-plugin.json, mcp.json, et acp.json sous /.well-known/) redirigeaient avec un 302 vers /p/error/404.html.

www.aliexpress.us/robots.txt avait les mêmes règles que le fichier .com, sans les lignes de sitemap. Comme le site redirige les visiteurs entre les 2 hôtes selon la région, ces règles s’appliquent sur les deux hôtes. m.aliexpress.com publie un fichier plus long avec sa propre règle AdsBot, vérifiez donc l’hôte que vous appelez.

Un hôte AliExpress publie une exception, le seul chemin d’API signé qu’aucun de ces fichiers n’autorise. recom-acs.aliexpress.com sert Disallow: / suivi de Allow: /h5/mtop.relationrecommend.aliexpressseorecommend.recommend/*. Appelé avec une charge utile délibérément incomplète, cet endpoint a renvoyé FAIL_SYS_BIZPARAM_MISSED sans en-tête bxpunish, alors que les endpoints interdits renvoyaient FAIL_SYS_USER_VALIDATE avec cet en-tête. Donc robots.txt et le système anti-bot autorisaient tous deux le même endpoint.

La page produit est l’opposé de la passerelle. AliExpress la sert au public, robots.txt ne l’interdit pas, et chacun des 3 produits Bright Data y a accédé en test. Un rendu Web Unlocker, la réponse de la passerelle, et un enregistrement Scraper API avaient tous le même prix de vente de 6,84 $ dans les tests en locale US.

Scrapez les pages que le site rend publiques, et laissez votre infrastructure gérer les adresses IP et les empreintes. Cette approche est moins susceptible de casser après une refonte ou un changement de signature, alors qu’une routine de signature copiée du bundle JavaScript du site doit être refaite après l’un ou l’autre.

3 méthodes pour scraper AliExpress à grande échelle

Le Browser API, Web Unlocker, et Scraper API de Bright Data fournissent chacun les données produit sous une forme différente. Notre comparaison de Web Unlocker et du Browser API explique en quoi ils diffèrent en général. Chercher AliExpress dans la Scrapers Library du Panneau de contrôle montre les scrapers disponibles pour ce domaine :

Résultats de recherche de la Bright Data Scrapers Library pour "aliexpress", listant le domaine aliexpress.us sous Résultats pour Scrapers et un jeu de données prêt à l'emploi nommé "Aliexpress products", au-dessus d'un panneau Scraper Studio avec le titre "Créez un scraper pour n'importe quel site web avec Scraper Studio"

Une seule recherche renvoie les scrapers du domaine et un jeu de données AliExpress prêt à l’emploi.

Le Browser API exécute votre script dans un navigateur cloud géré avec déblocage intégré et permet à votre script de lire les réponses réseau que la page reçoit. Quand j’ai connecté Playwright via CDP et chargé la page produit, la page affichait le titre et un encart d’achat avec son prix, et mon script pouvait lire les 171 réponses de ce seul chargement. Le Browser API prend aussi en charge le ciblage géographique, donc l’encart d’achat affiche les prix dans la devise du pays de sortie que vous choisissez. Pour les prix US, ajoutez -country-us au nom d’utilisateur de la zone.

La tarification du Browser API est au Go de trafic, donc le poids de la page permet d’estimer le coût d’une exécution avant de démarrer. Sur 3 chargements, les octets transférés dans les événements Network.loadingFinished de CDP ont fait en moyenne 3,03 Mo par page produit, avec une plage de 2,68 Mo à 3,47 Mo. Le networkidle de Playwright attend 500 ms sans requêtes, mais cette page continuait d’envoyer des requêtes en arrière-plan. Donc capture_pdp attend domcontentloaded puis un temps fixe.

Web Unlocker gère le déblocage, rend la page, et renvoie un seul document. Sa sortie brute pour la page produit faisait 344 355 octets, contre 77 226 octets pour la coquille, donc le JavaScript s’est exécuté. Sa sortie markdown pour cette page faisait 21 Ko à 24 Ko de texte qu’un LLM ou une regex peut lire directement. La commande brightdata scrape du Bright Data CLI utilise la sortie markdown par défaut.

La tarification de Web Unlocker est par requête, et vous ne payez que pour celles qui réussissent. Définissez country sur chaque requête pour sélectionner la vitrine.

AliExpress Scraper API renvoie des données structurées. Vous envoyez des URLs de produits et recevez des enregistrements avec les champs déjà extraits, tels que le titre, le prix catalogue et de vente, la disponibilité, les spécifications, le vendeur, la note, les avis, et les URLs des images. L’AliExpress Image Scraper API se spécialise dans les données d’images. La tarification du Scraper API est par enregistrement livré, vous ne payez donc que pour les résultats.

Produit Unité de tarification Ce que vous recevez À utiliser quand
Browser API Go de trafic Une session navigateur et les réponses réseau de la page Vous voulez les propres réponses API de la page, telles que le prix de chaque variante
Web Unlocker Requête réussie HTML rendu ou markdown que vous analysez Vous maintenez déjà un parseur, ou vous envoyez des pages à un LLM
Scraper API Enregistrement livré Enregistrements produit structurés Vous voulez des enregistrements, pas des pages

Exécuter votre propre navigateur est la quatrième option, je l’ai donc aussi mesurée. Les 3 navigateurs furtifs (Patchright, nodriver, et Camoufox) sont open source et ont fait en moyenne 14,1 à 15,9 secondes par chargement. Ce temps est un minimum, car aucun des 24 chargements n’a atteint les données produit, et un chargement réussi passerait aussi du temps à les analyser.

Sur une adresse IP déjà bloquée, les 3 navigateurs furtifs ont renvoyé l’en-tête et le pied de page mais aucune donnée produit. Chaque produit Bright Data a des proxys intégrés, et un navigateur que vous exécutez vous-même peut utiliser les AliExpress Proxies de Bright Data.

Exécuter le Scraper API à grande échelle

Le Scraper API accepte des URLs sur un endpoint et renvoie des enregistrements sur un autre. Sa page du Panneau de contrôle montre les deux modes de scraper à côté d’une requête prête à l’emploi :

Écran de configuration de la Bright Data Scrapers Library pour l'endpoint "Aliexpress products - collect by URL", montrant deux URLs d'exemple préremplies dans la liste Inputs, un choix de mode de scraper entre Synchrone (temps réel) et Asynchrone, et un exemple de code Linux Bash de requête authentifiée à droite

Le panneau de gauche liste les 2 endpoints du domaine, Collect by URL et Discover by category URL. Le dataset_id dans l’exemple de code à droite est la même valeur que le script ci-dessous utilise.

Le mode synchrone renvoie un snapshot_id quand une collecte prend longtemps, et la collecte continue en arrière-plan. Pour un catalogue complet, déclenchez une collecte asynchrone par lots et enregistrez la progression après chacun, de sorte que si votre processus plante, l’exécution reprend et vous ne payez pas deux fois pour les mêmes URLs :

import json, os, time, requests

API = "https://api.brightdata.com/datasets/v3"
DATASET = "gd_mlj9v75u1w1jvaxvwp"          # AliExpress products
TOKEN = os.environ["BRIGHTDATA_API_KEY"]   # API key from your account settings
CHUNK = 40                                 # larger triggers ran faster per input
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:     # finished means downloaded, not triggered
            continue
        batch, t0 = urls[n:n + CHUNK], time.time()
        snapshot = done.get("snapshot")         # triggered before a crash: reuse that 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)            # record the trigger BEFORE the wait

        print(f"chunk {key}: collecting {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":   # listing removed on 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}: collected at {(time.time() - t0) / len(batch):.1f}s per input")

    return state

run(["https://www.aliexpress.com/item/1005001398867186.html"])   # pass your full URL list

include_errors=true ajoute le rapport d’erreurs aux résultats, donc chaque URL de votre liste a un statut.

Chaque ligne de rows.jsonl est un enregistrement produit. Ce script affiche quelques champs du premier enregistrement :

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 sortie montre le disjoncteur de la première section :

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']

Le prix catalogue, la note de 4,7 sur 80 avis, et les 7 variantes de courant nominal correspondent à la page rendue dans la première section. Le prix de vente provient d’une promotion temporaire, donc il diffère entre les captures.

Gardez chaque lot plein. Dans ces exécutions, un déclenchement de 40 URLs s’est terminé 2,8 fois plus vite par URL qu’un déclenchement de 20 URLs. Remplissez le dernier lot avec des URLs de votre prochaine exécution pour qu’il s’exécute à pleine taille. Pour des centaines de milliers d’URLs, laissez Bright Data livrer chaque instantané vers votre stockage cloud ou vers un webhook, afin que les résultats arrivent sans interrogation.

Relancer run() avec le même fichier d’état s’est terminé en 0,08 seconde, n’a envoyé aucun déclencheur, et n’a écrit aucune ligne en double. Le script gère aussi un lot qui a été déclenché avant que le processus ne plante pendant l’attente. Il ne saute un lot que si ses enregistrements ont déjà été téléchargés. Sinon, il réutilise l’instantané enregistré, donc un plantage pendant l’attente ne perd pas un instantané collecté.

Mesurez à quelle fréquence les prix changent, puis adaptez votre calendrier de collecte à ce rythme. Dans une vérification, j’ai capturé 12 produits deux fois, à 25 minutes d’intervalle, et aucun de leurs prix n’a changé. Cet échantillon ne peut pas montrer un taux de changement de prix, utilisez donc la méthode, pas le résultat. Deux instantanés de vos propres produits, pris à l’intervalle que vous envisagez, répondront à la question pour votre catalogue.

Discovery, l’endpoint de découverte par catégorie du scraper, liste les produits depuis une URL de départ. Avec la page de recherche en gros /w/wholesale-ergonomic-chair.html comme URL de départ, une requête type=discover_new&discover_by=category_url a renvoyé des URLs de produits dès la première exécution. Utilisez /w/wholesale-<mot-clé>.html comme modèle d’URL de départ, limitez chaque exécution de découverte avec limit_per_input pendant que vous calibrez, et transmettez les URLs renvoyées directement à l’exécution de collecte.

Si vous collectez des identifiants de produits depuis d’autres endroits, tels que les sitemaps d’articles ou les liens SEO d’une page produit, une seule requête GET sans rendu montre si chaque identifiant a encore une page produit. Les pages produit ont renvoyé 77,2 à 77,4 Ko avec un og:title rempli, alors que 3 identifiants collectés depuis ces liens SEO ont renvoyé 75,26 Ko avec tous les champs og: vides. Cette différence de 2 Ko est visible dans la réponse brute, donc éliminez les identifiants avec des champs og: vides avant de mettre en file une collecte.

2 vérifications qui gardent les données de prix comparables

Les données ont besoin de la même validation que les réponses HTTP. Chacune de ces 2 vérifications est peu coûteuse à ajouter et vous aide à repérer les prix qui ne sont pas comparables avant qu’ils n’atteignent un tableau de bord.

La remise variait selon le pays de sortie, pas seulement la devise. J’ai récupéré un identifiant de produit via Web Unlocker au format markdown depuis 6 pays de sortie, et chaque pays a renvoyé une promotion différente :

Pays de sortie Devise Prix de vente Prix catalogue Remise
États-Unis USD 6,84 $ 14,40 $ 52,5 %
Royaume-Uni GBP £9,83 £11,16 11,9 %
Allemagne EUR 11,69€ 13,29€ 12,0 %
Brésil BRL R$62,25 R$68,30 8,9 %
Australie AUD AU$18,59 AU$19,42 4,3 %
Canada CAD C$19,86 C$20,70 4,1 %

Même les pourcentages ne sont pas directement comparables, et chaque page montre pourquoi. L’encart d’achat US étiquette son prix Ends :, donc la ligne US est une offre à durée limitée capturée à un instant donné. La page UK dit Price includes VAT alors que la page US dit Tax excluded. La page allemande indique 30-day lowest price before the discount, la règle de prix de référence de l’UE qui tend à faire paraître les remises allemandes plus petites.

À cause de ces 3 différences, définissez un pays de sortie pour chaque série de prix, stockez-le dans la ligne, et ne comparez une série qu’avec elle-même. Web Unlocker le définit avec le paramètre country, et le Browser API le définit avec un suffixe -country-<code> sur le nom d’utilisateur de la zone. Si vous avez besoin des promotions elles-mêmes plutôt que d’un pipeline de prix, l’AliExpress Promotions Tracker surveille les promotions des concurrents pour vous.

Le symbole de la livre sterling UK est celui en pleine chasse. La vitrine UK utilise £, U+FFE1 FULLWIDTH POUND SIGN, donc une regex de devise écrite avec £ (U+00A3) ne correspond à rien. Les vitrines allemande et brésilienne utilisent une virgule comme séparateur décimal dans le champ de prix. Analysez la devise depuis le paramètre de vitrine que vous avez demandé et gardez l’analyse du nombre séparée du symbole.

Le prix de chaque variante et le chemin de catégorie complet

Le Browser API de Bright Data capture la carte de prix des SKU depuis une page produit AliExpress, et Web Unlocker renvoie le fil d’Ariane de catégorie depuis son pied de page. Les avis ont aussi un scraper dédié, l’AliExpress Reviews Scraper, dont les enregistrements incluent l’identifiant et l’URL du produit.

La propre réponse de la page contient la carte de prix des SKU. Les captures enregistrées avec capture_pdp incluaient des prix pour 141, 75, 20, 18, 15, et 6 SKU. Pour un produit, la carte de prix avait 6 prix distincts, de 2,56 à 10,96. La carte est dans le module PRICE, à côté du SKU présélectionné et du champ region :

Onglet Response de Chrome DevTools pour une requête pdp.pc.query montrant region US, selectedSkuId 12000059478006652, et une skuIdStrPriceInfoMap dont les trois premières entrées sont des packs de 1 listés à 7,61 $, 7,58 $ et 7,71 $ avec un prix de vente de 1,09 $ chacun, puis une entrée de pack de 10 listée à 57,34 $

Cette capture provient d’une fiche multipack. La page présélectionne un pack de 1 à 1,09 $, et la quatrième entrée est un pack de 10 listé à 57,34 $, donc la carte contient le prix propre à chaque variante.

Pour obtenir le prix de chaque variante, exécutez capture_pdp via le Browser API, lisez PRICE.skuIdStrPriceInfoMap, et stockez-le avec item_id comme clé. Lisez la carte depuis la région dont vous avez besoin des prix, car elle peut aussi varier selon la région. Pour le disjoncteur de la première section, la carte avait des prix de variantes différents dans une session US et un seul prix pour les 7 variantes dans une autre région.

Le markdown de Web Unlocker contient le chemin de catégorie. La page rendue liste les catégories du produit sous forme de fil d’Ariane dans le pied de page, et le GLOBAL_DATA.globalData de la passerelle a le même chemin dans son champ categoryPath, 13/5/141914. Le parseur ci-dessous lit la page produit comme markdown Web Unlocker, le format que brightdata scrape renvoie par défaut :

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):
    """Category IDs and URLs from the product page footer."""
    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 <product URL> -o page.md
    print(categories_from_markdown(fh.read()))

Le \s à la fin du premier motif est essentiel, car le pied de page place U+00A0, une espace insécable, immédiatement après to. Le même motif écrit avec une espace littérale ne trouve aucune correspondance. Avec \s, le parseur renvoie le chemin de catégorie :

[{'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'}]

Obtenir le chemin de catégorie nécessite une requête Web Unlocker par produit. Exécutez le parseur sur les produits dont vous avez besoin de la taxonomie, et stockez le résultat avec item_id comme clé. Le libellé du pied de page peut changer sans préavis, vérifiez donc d’abord le motif CRUMB si le parseur renvoie une liste vide.

Le Scraper Studio gère une liste de champs personnalisée. Quand vous décrivez les données dont vous avez besoin, son IA génère un scraper prêt à l’emploi avec proxys et déblocage intégrés, et vous pouvez ajouter ou modifier les champs de sortie plus tard. Pour la surveillance des prix en particulier, l’AliExpress Price Scraper API collecte les données de tarification AliExpress pour que vous puissiez suivre l’évolution des prix.

Détecter les changements de page et de schéma

Chaque échec constaté sur cette cible était silencieux, surveillez donc la page elle-même. Le script bootstrap de la page contient une version de build, cosmos_prefetch_mtop version 20240605.195626.856, et quand cette version change, la configuration de préchargement peut changer avec elle. Une empreinte détecte directement ce changement. Sans elle, votre propre scraper ne le montrerait que par des valeurs nulles.

Les 2 empreintes ci-dessous vérifient 3 étapes (la coquille HTML, la page rendue, et la ligne livrée), et aucune ne dépend de l’analyse des champs produit :

import hashlib, re

def shell_fingerprint(html):
    """From an unrendered GET. Detects a bootstrap or prefetch-config change."""
    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):
    """From a delivered row. Confirms the field set your pipeline expects."""
    keys = sorted(k for k in row if k not in ("input", "timestamp"))
    return {"fields_sha1": hashlib.sha1(",".join(keys).encode()).hexdigest()[:12]}

Sur des captures réelles, les deux empreintes ont renvoyé des valeurs stables :

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

Une troisième empreinte de la charge utile capturée semble utile, mais elle peut causer des faux positifs. Hacher l’ensemble des modules semble être la même idée, mais le nombre de modules variait selon la fiche et le moment de la capture, pas seulement avec un nouveau déploiement. Les charges utiles capturées avaient 28, 27, et 28 modules, et l’ensemble de clés PRICE différait entre 2 d’entre elles.

Le nombre a aussi changé au sein d’un même produit. Le même identifiant de produit a renvoyé 27 modules puis 26 sur 2 captures à quelques minutes d’intervalle, donc même un historique par produit nécessite un seuil plutôt qu’une vérification d’égalité. Basez vos alertes sur les empreintes de la coquille et de la ligne, car leurs valeurs sont restées identiques à chaque capture.

Quand acheter le jeu de données AliExpress à la place

Les 3 méthodes partent toutes d’URLs que vous avez déjà. Si votre question porte sur le marché entier, commencez par le jeu de données prêt à l’emploi :

Page Dataset Marketplace de Bright Data pour "Aliexpress products", montrant la description du jeu de données avec les boutons Talk to a data expert et Proceed to purchase, et un onglet Data sample avec des contrôles de filtre pour product_category, brand, store_country et star_rating à côté d'un tableau d'aperçu avec les colonnes url, item_id, variant_id, title et description

Le panneau de filtres vous permet de sélectionner les enregistrements avant de les payer.

Le jeu de données AliExpress réduit le travail de sélection et le temps de collecte. Vous filtrez le jeu de données sur les enregistrements dont vous avez besoin, dans la marketplace ou via l’API Filter Dataset, et ne payez que pour votre instantané filtré. Ces enregistrements existent déjà, il n’y a donc aucune exécution de collecte à attendre. Vous pouvez l’acheter une fois ou selon un calendrier de rafraîchissement que vous choisissez, et les prix actuels sont sur la page de tarification des jeux de données.

Estimez la taille de livraison avant de le commander. Les enregistrements de l’échantillon du jeu de données font en moyenne 12,75 Ko en JSON livré, prévoyez donc environ 1,3 Go pour 100 000 enregistrements. Le jeu de données se livre aussi en Parquet, et le même échantillon converti en Parquet était presque 8 fois plus petit. Cette différence de taille peut déterminer si vous interrogez le fichier en place ou le mettez d’abord en staging.

Utilisez le jeu de données pour les questions d’assortiment et de couverture à l’échelle du marché, et utilisez le Scraper API pour les fiches spécifiques que vous suivez. Le jeu de données se rafraîchit selon le calendrier que vous choisissez, et une collecte Scraper API renvoie chaque fiche au moment où vous l’exécutez.

Réflexions finales

Le schéma de signature d’AliExpress tient dans une seule fonction, mais en test, la passerelle bloquait les appels depuis une seule adresse IP en une dizaine de requêtes, même avec une signature correcte. Comme un blocage peut renvoyer un code de statut de succès, basez chaque vérification sur l’en-tête bxpunish et sur un champ que chaque produit possède, tel que le prix. Le robots.txt du site n’interdit pas le chemin public /item/, c’est donc le chemin à scraper à grande échelle.

L’AliExpress Scraper API de Bright Data renvoie des enregistrements structurés pour ces pages produit, le Browser API capture le prix de chaque variante, et le jeu de données AliExpress convient aux questions à l’échelle du marché entier. Commencez avec le niveau gratuit, qui inclut 5 000 requêtes par mois sans carte de crédit requise. Testez-le avec 40 URLs de votre catalogue, et vérifiez les enregistrements par rapport aux champs dont votre pipeline a besoin.

FAQ

AliExpress a-t-il une API pour les données produit ?

Oui, pour les vendeurs, affiliés, et dropshippers approuvés. Sur l’AliExpress Open Platform, un vendeur autorise une application pour sa propre boutique via OAuth 2.0, et l’API affiliée couvre le catalogue promotionnel. Aucune ne couvre la place de marché entière, donc les données de prix au niveau du marché nécessitent la collecte de données e-commerce.

Comment obtenir une URL ou un ID de produit AliExpress ?

Les URLs de produits suivent le modèle /item/<item_id>.html, et la découverte part de /w/wholesale-<mot-clé>.html. Les ID dépendent de l’hôte. Un produit a l’ID 1005001398867186 sur .com et 3256801212552434 sur .us. Indexez chaque produit par son URL finale après la redirection régionale, pour qu’il n’apparaisse pas sous 2 ID.

Le scraping des données produit AliExpress est-il gratuit ?

Oui, à petite échelle. Le niveau gratuit de Bright Data inclut 5 000 requêtes par mois sans carte de crédit requise, et couvre le Scraper API, Web Unlocker, et le Browser API, vous pouvez donc les tester sur vos propres URLs d’abord. Playwright et curl_cffi sont open source. À l’échelle de production, vous payez surtout pour les adresses IP, car 2 séries de 60 appels API signés depuis une seule adresse IP ont obtenu des données 7 et 15 fois.

Combien coûte une API scraper AliExpress ?

L’AliExpress Scraper API de Bright Data est tarifée par enregistrement livré, et Web Unlocker est tarifé par requête réussie, vous ne payez donc que pour les résultats. Le Browser API est tarifé au trafic, donc le poids de la page prédit son coût. Une page produit pesait environ 3 Mo en test. Les prix actuels sont sur la page de tarification de Bright Data.

Pourquoi mon scraper AliExpress renvoie-t-il des champs vides ?

Parce que le HTML initial n’a ni prix ni stock. window.runParams est un objet vide et <title> est vide, donc les extracteurs basés sur des sélecteurs ne correspondent à rien et ne lèvent aucune erreur. Le JavaScript de la page charge les champs plus tard depuis mtop.aliexpress.pdp.pc.query. Soit exécutez le JavaScript, soit appelez cette API directement.

Comment éviter d’être bloqué en scrapant AliExpress ?

Envoyez les requêtes via le Scraper API ou Web Unlocker de Bright Data, qui ont des proxys intégrés et la résolution de CAPTCHA AliExpress. En test, ni un nouveau pot de cookies ni une attente de 60 secondes n’ont supprimé un blocage d’une adresse IP. Les requêtes bloquées renvoyaient aussi un HTTP 200, vérifiez donc l’en-tête bxpunish, pas le code de statut.

Pourquoi les prix AliExpress changent-ils ?

Parce que les promotions sont temporaires et varient selon le pays. Dans un test sur 6 pays de sortie, la remise d’un produit allait de 52,5 % à 4,1 % du prix catalogue, et les pays différaient quant à l’inclusion de la taxe dans les prix. Définissez un pays de sortie pour chaque série de prix et stockez-le dans la ligne.

Existe-t-il un serveur MCP AliExpress ?

Oui. Le MCP Server de Bright Data permet à un agent IA de récupérer des pages produit AliExpress via ses outils scrape_as_markdown et scrape_batch, qui gèrent le déblocage et renvoient du markdown. Dans un appel de test, scrape_as_markdown a renvoyé le prix, la note, les variantes, et le chemin de catégorie du produit. La page AliExpress MCP explique la configuration.