Dernière mise à jour : 30 octobre 2026.
Surveillance des prix e-commerce avec Playwright et les proxies résidentiels
Le prix affiché d’un produit n’est pas toujours une valeur unique et universelle. Une boutique en ligne peut montrer des informations différentes selon la page produit, la devise, la destination de livraison, la locale du navigateur, les cookies ou le compte connecté.
La surveillance des prix ne se réduit donc pas à lire un nombre dans le HTML. Un moniteur utile doit aussi consigner les conditions dans lesquelles la page a été contrôlée — et distinguer une observation de prix réelle d’une requête ou d’une extraction échouée.
Dans ce tutoriel, nous construirons un petit moniteur Python avec Playwright. Il ouvre un catalogue de produits, capture les noms, les prix affichés et la disponibilité, puis enregistre des fichiers JSON et CSV horodatés. Un proxy résidentiel est facultatif : il modifie la route réseau utilisée par le navigateur, mais il ne définit pas automatiquement le pays de livraison, la devise ou l’état du compte d’une boutique.
Portée de cette démo : l’exemple utilise Books to Scrape , un site d’entraînement dont les prix sont attribués au hasard et n’ont aucune signification réelle. Il convient pour apprendre le flux de surveillance, pas pour démontrer des différences de prix régionales. Pour étudier de véritables écarts de marché, utilisez une boutique à laquelle vous êtes autorisé à accéder et qui fait réellement varier ses données affichées selon le marché.
Quand utiliser Playwright ?
Privilégiez d’abord une API produit officielle ou un flux de données lorsqu’il fournit les informations et la couverture dont vous avez besoin. C’est en général plus stable et plus facile à maintenir que l’automatisation de navigateur.
Playwright est utile lorsque :
- les informations produit pertinentes sont rendues par JavaScript ;
- vous devez inspecter le contenu qu’un navigateur reçoit réellement ;
- la disponibilité ou le prix visibles d’une boutique ne sont pas exposés via un flux adapté.
Pour une page statique, un client HTTP classique peut être plus simple et plus rapide. Ce guide se concentre sur une exécution de navigateur et un contexte de test configuré. Ce n’est pas un planificateur multi-pays prêt à l’emploi.
Pour une planification plus large, consultez notre guide de surveillance des prix e-commerce . Si vous avez besoin d’aide pour configurer un proxy dans Playwright, voir Comment utiliser des proxies résidentiels avec Playwright .
Que doit contenir un enregistrement de surveillance des prix ?
Une observation de prix n’est utile que si vous pouvez comprendre comment elle a été collectée. Au minimum, enregistrez :
| Champ | Pourquoi c’est important |
|---|---|
| Nom du produit et URL | Identifie l’article surveillé |
| Horodatage UTC | Établit le moment de l’observation |
| Libellé de marché | Décrit la configuration de test ; ce n’est pas une preuve de localisation IP |
| IP de sortie du proxy | Aide à diagnostiquer la route réseau, si un proxy est utilisé |
| Locale et fuseau horaire du navigateur | Consigne les préférences du navigateur susceptibles d’affecter la présentation |
| Pays de livraison | Consigne le paramètre de livraison réellement utilisé, le cas échéant |
| État du compte | Distingue les observations déconnectées et connectées |
| Texte brut du prix | Préserve exactement ce que la page a affiché |
| Montant analysé et code devise | Facilite la comparaison des observations valides |
| Texte et statut de disponibilité | Préserve le libellé de la page et une interprétation normalisée |
| Statut d’exécution et d’extraction | Sépare un contrôle échoué d’une observation valide |
Ne traitez pas ces variables comme interchangeables. Un résultat de géolocalisation IP ne prouve pas quel marché de livraison la boutique a utilisé. Une locale de navigateur ne définit pas une adresse de livraison. Un symbole monétaire seul peut être ambigu — par exemple, $ peut désigner plusieurs devises.
Le script d’exemple enregistre le code devise configuré. Il ne détecte pas automatiquement la devise à partir de la page.
1. Créer le projet
Créez un dossier avec ces fichiers :
price-monitor/
├── .env
├── requirements.txt
└── monitor.py
Ajoutez les dépendances dans requirements.txt :
playwright>=1.40
python-dotenv>=1.0
Installez-les ainsi que le navigateur Chromium :
python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
python -m playwright install chromium
Sous Linux, installez les dépendances système de Chromium si Playwright signale des bibliothèques partagées manquantes :
sudo .venv/bin/playwright install-deps chromium
2. Configurer le contexte de test
Créez un fichier .env :
# Optional proxy settings. Leave all three blank for a direct practice run.
PROXY_SERVER=http://gw.kindproxy.com:12000
PROXY_USERNAME=your_dashboard_username
PROXY_PASSWORD=your_dashboard_password
PRODUCT_URL=https://books.toscrape.com/
REGION_LABEL=UK
BROWSER_LOCALE=en-GB
BROWSER_TIMEZONE=Europe/London
CURRENCY_CODE=GBP
DELIVERY_COUNTRY=
ACCOUNT_STATE=signed_out
Utilisez le point de terminaison et les identifiants fournis dans votre compte. Copiez exactement le nom d’utilisateur et le mot de passe générés ; n’ajoutez pas manuellement de paramètres de session ou de localisation d’un autre fournisseur. Conservez .env hors du contrôle de version et ne publiez jamais d’identifiants proxy dans des captures d’écran ou des journaux.
Pour un exercice en connexion directe, laissez PROXY_SERVER, PROXY_USERNAME et PROXY_PASSWORD vides.
Les valeurs REGION_LABEL, DELIVERY_COUNTRY et ACCOUNT_STATE sont des métadonnées enregistrées avec le résultat. Cette démo ne vérifie pas que le proxy sort dans le pays nommé, n’entre pas d’adresse de livraison et ne se connecte pas à une boutique. N’ajoutez ces étapes que si vous disposez d’un compte de test autorisé et d’un flux de travail adapté, et consignez l’état réellement utilisé.
Playwright prend en charge la configuration du proxy au lancement du navigateur, y compris des champs séparés pour le nom d’utilisateur et le mot de passe des proxies authentifiés. Voir la documentation réseau Playwright Python .
3. Exécuter le moniteur
Enregistrez le code suivant sous monitor.py. Les sélecteurs sont écrits pour les cartes du catalogue Books to Scrape. Ils devront être adaptés pour un autre site.
import asyncio
import csv
import json
import os
import re
from datetime import datetime, timezone
from decimal import Decimal, InvalidOperation
from pathlib import Path
from urllib.parse import urljoin
from dotenv import load_dotenv
from playwright.async_api import async_playwright
BASE_DIR = Path(__file__).resolve().parent
load_dotenv(BASE_DIR / ".env")
PRODUCT_URL = os.getenv("PRODUCT_URL", "https://books.toscrape.com/").strip()
REGION = os.getenv("REGION_LABEL", "not-set").strip()
LOCALE = os.getenv("BROWSER_LOCALE", "en-GB").strip()
BROWSER_TIMEZONE = os.getenv("BROWSER_TIMEZONE", "Europe/London").strip()
CURRENCY = os.getenv("CURRENCY_CODE", "GBP").strip().upper() or None
DELIVERY_COUNTRY = os.getenv("DELIVERY_COUNTRY", "").strip() or None
ACCOUNT_STATE = os.getenv("ACCOUNT_STATE", "signed_out").strip()
OUTPUT_DIR = BASE_DIR / "output"
JSON_PATH = OUTPUT_DIR / "snapshots.json"
CSV_PATH = OUTPUT_DIR / "snapshots.csv"
# These selectors are specific to Books to Scrape's catalogue cards.
CARD_SELECTOR = "article.product_pod"
TITLE_SELECTOR = "h3 a"
PRICE_SELECTOR = ".price_color"
AVAILABILITY_SELECTOR = ".availability"
CSV_FIELDS = [
"record_type", "checked_at", "status", "error",
"product_name", "product_url", "region", "locale",
"browser_timezone", "delivery_country", "account_state",
"proxy_exit_ip", "price_text", "price_amount",
"currency", "availability_text", "availability_status",
"comparison",
]
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat(timespec="seconds")
def parse_price(raw_text: str) -> str | None:
"""Parse a decimal amount from the demo site's GBP-style price text."""
match = re.search(r"\d[\d,]*(?:\.\d{1,2})?", raw_text)
if not match:
return None
try:
return format(Decimal(match.group(0).replace(",", "")), ".2f")
except InvalidOperation:
return None
def normalize_availability(raw_text: str) -> str:
value = raw_text.casefold()
if "in stock" in value:
return "in_stock"
if "out of stock" in value:
return "out_of_stock"
return "unknown"
def load_records() -> list[dict]:
if not JSON_PATH.exists():
return []
try:
data = json.loads(JSON_PATH.read_text(encoding="utf-8"))
return data if isinstance(data, list) else []
except (OSError, json.JSONDecodeError):
return []
def save_records(records: list[dict]) -> None:
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
JSON_PATH.write_text(
json.dumps(records, ensure_ascii=False, indent=2),
encoding="utf-8",
)
with CSV_PATH.open("w", newline="", encoding="utf-8-sig") as file:
writer = csv.DictWriter(file, fieldnames=CSV_FIELDS, extrasaction="ignore")
writer.writeheader()
writer.writerows(records)
def compare_with_previous(records: list[dict], current: dict) -> str:
if current.get("status") != "success" or not current.get("price_amount"):
return "not_compared"
dimensions = (
"product_url", "region", "locale", "browser_timezone",
"delivery_country", "account_state", "currency",
)
previous = next(
(
item for item in reversed(records)
if item.get("record_type") == "product"
and item.get("status") == "success"
and item.get("price_amount") is not None
and all(item.get(key) == current.get(key) for key in dimensions)
),
None,
)
if previous is None:
return "baseline"
old_price = Decimal(str(previous["price_amount"]))
new_price = Decimal(str(current["price_amount"]))
if new_price < old_price:
return "price_decreased"
if new_price > old_price:
return "price_increased"
return "no_change"
def base_record(record_type: str, status: str, **extra) -> dict:
record = {
"record_type": record_type,
"checked_at": utc_now(),
"status": status,
"error": None,
"product_name": None,
"product_url": None,
"region": REGION,
"locale": LOCALE,
"browser_timezone": BROWSER_TIMEZONE,
"delivery_country": DELIVERY_COUNTRY,
"account_state": ACCOUNT_STATE,
"proxy_exit_ip": None,
"price_text": None,
"price_amount": None,
"currency": CURRENCY,
"availability_text": None,
"availability_status": "unknown",
"comparison": "not_compared",
}
record.update(extra)
return record
async def main() -> None:
if not PRODUCT_URL:
raise ValueError("PRODUCT_URL must be set.")
proxy_server = os.getenv("PROXY_SERVER", "").strip()
proxy_username = os.getenv("PROXY_USERNAME", "").strip()
proxy_password = os.getenv("PROXY_PASSWORD", "").strip()
if bool(proxy_username) != bool(proxy_password):
raise ValueError("Set both PROXY_USERNAME and PROXY_PASSWORD, or neither.")
if (proxy_username or proxy_password) and not proxy_server:
raise ValueError("Set PROXY_SERVER when proxy credentials are provided.")
proxy = None
if proxy_server:
proxy = {"server": proxy_server}
if proxy_username:
proxy["username"] = proxy_username
proxy["password"] = proxy_password
records = load_records()
run_record = base_record("run", "started")
new_records = [run_record]
async with async_playwright() as playwright:
browser = await playwright.chromium.launch(proxy=proxy)
try:
context = await browser.new_context(
locale=LOCALE,
timezone_id=BROWSER_TIMEZONE,
)
page = await context.new_page()
exit_ip = None
if proxy:
try:
ip_response = await page.goto(
"https://api.ipify.org?format=json",
wait_until="domcontentloaded",
timeout=30_000,
)
if ip_response and ip_response.ok:
ip_data = json.loads(await page.locator("body").inner_text())
exit_ip = ip_data.get("ip")
except Exception as exc:
new_records.append(
base_record(
"diagnostic", "failed",
error=f"Proxy exit-IP check failed: {type(exc).__name__}: {exc}",
)
)
try:
response = await page.goto(
PRODUCT_URL,
wait_until="domcontentloaded",
timeout=45_000,
)
if response and response.status >= 400:
raise RuntimeError(f"Target returned HTTP {response.status}")
body_text = (await page.locator("body").inner_text()).casefold()
challenge_markers = (
"verify you are human",
"unusual traffic",
"captcha",
"access denied",
"automated requests",
)
if any(marker in body_text for marker in challenge_markers):
raise RuntimeError(
"Challenge or access-restriction page detected; "
"the monitor stopped without retrying or rotating IPs."
)
cards = page.locator(CARD_SELECTOR)
await cards.first.wait_for(state="visible", timeout=15_000)
count = await cards.count()
for index in range(count):
card = cards.nth(index)
try:
title_link = card.locator(TITLE_SELECTOR)
title = (await title_link.get_attribute("title")) or (
await title_link.inner_text()
).strip()
href = await title_link.get_attribute("href")
product_url = urljoin(PRODUCT_URL, href or "")
price_text = (await card.locator(PRICE_SELECTOR).inner_text()).strip()
price_amount = parse_price(price_text)
availability_text = (
await card.locator(AVAILABILITY_SELECTOR).inner_text()
).strip()
if price_amount is None:
item = base_record(
"product", "extraction_failed",
error="Price text was present but could not be parsed.",
product_name=title,
product_url=product_url,
proxy_exit_ip=exit_ip,
price_text=price_text,
availability_text=availability_text,
availability_status=normalize_availability(availability_text),
)
else:
item = base_record(
"product", "success",
product_name=title,
product_url=product_url,
proxy_exit_ip=exit_ip,
price_text=price_text,
price_amount=price_amount,
availability_text=availability_text,
availability_status=normalize_availability(availability_text),
)
item["comparison"] = compare_with_previous(records + new_records, item)
new_records.append(item)
except Exception as exc:
new_records.append(
base_record(
"product", "extraction_failed",
error=f"{type(exc).__name__}: {exc}",
proxy_exit_ip=exit_ip,
)
)
run_record["status"] = "success"
run_record["error"] = None
run_record["products_seen"] = count
except Exception as exc:
run_record["status"] = "check_failed"
run_record["error"] = f"{type(exc).__name__}: {exc}"
run_record["products_seen"] = 0
finally:
await context.close()
finally:
await browser.close()
records.extend(new_records)
save_records(records)
print(f"Run status: {run_record['status']}")
print(f"Records written: {len(new_records) - 1} product/diagnostic records")
print(f"JSON: {JSON_PATH}")
print(f"CSV: {CSV_PATH}")
if run_record["error"]:
print(f"Error: {run_record['error']}")
if __name__ == "__main__":
asyncio.run(main())
Exécutez-le :
python monitor.py
L’exécution réussie de la démo s’est terminée avec 20 enregistrements produits et a enregistré les deux fichiers de sortie :

Le script écrit :
output/
├── snapshots.json
└── snapshots.csv
Le fichier JSON conserve les enregistrements complets. Le fichier CSV est pratique pour une revue dans un tableur. Un enregistrement au niveau de l’exécution est sauvegardé même lorsque la navigation échoue ou qu’une page de défi est détectée ; une exécution infructueuse n’est pas convertie en observation à prix zéro.
Voici un exemple d’enregistrement produit extrait avec succès. Il inclut le prix affiché d’origine, le montant analysé, la devise configurée, la disponibilité et l’IP de sortie du proxy :

4. Comprendre la sortie et les règles de comparaison
Le moniteur conserve le texte de prix d’origine et un montant numérique analysé. Par exemple, une page peut afficher £51.77 ; l’analyseur stocke 51.77, tandis que le code devise provient de CURRENCY_CODE=GBP.
Cet analyseur est volontairement étroit. Il convient au format numérique de type GBP de la démo, pas à chaque locale. Un analyseur de production doit être adapté au format numérique et aux métadonnées de devise de la boutique cible.
La comparaison n’utilise que les enregistrements réussis antérieurs ayant la même URL produit, le même libellé de marché, la même locale, le même fuseau horaire, le même paramètre de pays de livraison, le même état de compte et la même devise configurée. Un prix manquant ou non analysable est marqué extraction_failed ; il n’est jamais traité comme zéro.
Le libellé de marché n’est encore qu’un libellé. Avant de comparer de vrais marchés, confirmez indépendamment le lieu de sortie réel du proxy et les paramètres de livraison et de compte de la boutique.
Dans la démo, la première exécution a créé la baseline pour 20 produits. Une deuxième exécution a ajouté 20 autres enregistrements, et les prix inchangés ont été correctement étiquetés no_change. Le champ delivery_country est resté vide faute de pays de livraison configuré ; c’est attendu pour le site d’entraînement Books to Scrape.

5. Dépannage
| Symptôme | À vérifier |
|---|---|
| Chromium ne démarre pas | Installer le navigateur et les dépendances système Linux |
| L’authentification du proxy échoue | Vérifier le point de terminaison et copier exactement le nom d’utilisateur/mot de passe générés |
| Le contrôle IP fonctionne mais la page cible échoue | La destination peut avoir des règles d’accès ou restrictions distinctes |
| Aucune carte de catalogue trouvée | Confirmer le chargement de la page et mettre à jour les sélecteurs CSS pour ce site |
| Le texte du prix est présent mais le montant est vide | Adapter parse_price() au format numérique du site |
La disponibilité est unknown | Examiner le libellé de disponibilité du site et mettre à jour le normaliseur |
| Une page CAPTCHA, unusual-traffic ou access-denied apparaît | Arrêter le contrôle, enregistrer l’échec et examiner les règles du site ainsi que votre cadence de requêtes |
Ne répondez pas à un défi en changeant répétitivement d’IP ou en cherchant à automatiser le défi. Un proxy résidentiel ne garantit pas l’accès ni n’empêche les pages CAPTCHA.
6. API, automatisation de navigateur ou proxy ?
Ces outils résolvent des problèmes différents :
- API ou flux officiel : meilleur point de départ lorsqu’il fournit des données produit structurées et autorisées.
- Playwright : utile lorsque les informations pertinentes sont rendues dans un navigateur ou que vous devez inspecter la page visible de la boutique.
- Proxy résidentiel : modifie la route réseau. Il peut aider à tester la réponse d’un site aux requêtes depuis différentes sorties réseau, mais il ne définit pas à lui seul le pays de livraison, la devise, les cookies ou l’état du compte.
Pour des catalogues plus volumineux, ajoutez planification, limites de débit, politiques de nouvelle tentative pour les erreurs réseau transitoires, alertes et rétention des données seulement une fois le flux à exécution unique fiable. Respectez les conditions du site cible, les indications robots le cas échéant, et le droit applicable. Préférez l’accès officiel aux données lorsqu’il est disponible.
Si vous évaluez des options de proxy pour un flux de surveillance autorisé, consultez les offres de proxies résidentiels KindProxy .
FAQ
Playwright peut-il surveiller les prix e-commerce ?
Oui. Playwright peut lire les champs visibles de prix et de disponibilité sur les pages auxquelles vous êtes autorisé à accéder. Les sélecteurs doivent correspondre au balisage du site cible, et chaque observation doit être validée avant d’être comparée aux données antérieures.
Un proxy résidentiel affiche-t-il le prix d’un autre pays ?
Pas automatiquement. Une boutique peut prendre en compte le lieu de sortie réseau, l’adresse de livraison, la devise, la locale, les cookies et l’état du compte. Un proxy ne modifie que la route réseau.
Un proxy empêche-t-il les pages CAPTCHA ?
Non. Un site peut toujours renvoyer une page CAPTCHA, un défi, un blocage ou une page incomplète. Enregistrez le contrôle échoué et examinez les règles d’accès plutôt que de faire tourner les IP de façon répétée.
Que faire si le prix ne peut pas être analysé ?
Conservez le texte brut, marquez l’enregistrement comme échec d’extraction et laissez le montant numérique vide. Ne transformez jamais une valeur manquante en zéro et ne la comptez pas comme une baisse de prix.
Réflexions finales
Une surveillance des prix fiable dépend de la qualité de l’observation, pas seulement du code de scraping. Consignez les conditions de test, préservez les valeurs brutes de la page, validez chaque extraction et ne comparez que des enregistrements réussis comparables.
L’exemple Books to Scrape démontre le flux de travail. Il n’établit pas un comportement tarifaire régional réel. Pour la production, adaptez les sélecteurs et les règles d’analyse à une boutique autorisée, vérifiez indépendamment les conditions de marché et traitez les restrictions d’accès comme des contrôles échoués — pas comme une raison de les contourner.
