# Analyse Store Backmarket

Date: 2026-01-13
Auteur: PriceWatch Development Team

## Résumé Exécutif

Backmarket.fr est un marketplace de produits reconditionnés (smartphones, laptops, tablets, etc.). Le parsing est **très fiable** grâce à l'utilisation extensive de **JSON-LD schema.org**, mais nécessite **Playwright obligatoire** en raison de la protection anti-bot.

**Score de parsing**: ⭐⭐⭐⭐⭐ (5/5) - Excellent
**Difficulté d'accès**: 🔒🔒🔒 (3/5) - Anti-bot fort

---

## 🎯 Spécificités Backmarket

### 1. Marketplace de Reconditionné

Contrairement à Amazon ou Cdiscount qui vendent du neuf, Backmarket vend **exclusivement du reconditionné**.

**Implications**:
- Chaque produit a plusieurs **offres** avec différents états/conditions
- Prix variable selon la **condition** choisie (Correct, Bon, Excellent, etc.)
- Le grade/condition doit être extrait et stocké dans `specs["Condition"]`

**Grades Backmarket**:
- **Correct** - État correct avec traces d'usage visibles
- **Bon** - Quelques rayures légères
- **Très bon** - Très peu de rayures
- **Excellent** - Quasiment aucune trace d'usage
- **Comme neuf** - État neuf

### 2. Protection Anti-bot Forte

❌ **HTTP simple ne fonctionne PAS** - retourne **403 Forbidden**
✅ **Playwright OBLIGATOIRE** pour récupérer le contenu

```python
# ❌ NE FONCTIONNE PAS
result = fetch_http("https://www.backmarket.fr/fr-fr/p/iphone-15-pro")
# → 403 Forbidden

# ✅ FONCTIONNE
result = fetch_playwright("https://www.backmarket.fr/fr-fr/p/iphone-15-pro", headless=True)
# → 200 OK avec contenu complet
```

**Temps de chargement**: ~2-3 secondes avec Playwright

---

## 📊 Structure HTML & Extraction

### JSON-LD Schema.org (Source Prioritaire)

Backmarket utilise **schema.org Product** de manière **complète et fiable**.

**Exemple de JSON-LD**:
```json
{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "iPhone 15 Pro",
  "image": "https://d2e6ccujb3mkqf.cloudfront.net/...",
  "offers": {
    "@type": "Offer",
    "price": "571.00",
    "priceCurrency": "EUR",
    "availability": "https://schema.org/InStock"
  }
}
```

**Avantages**:
- ✅ Données structurées et stables
- ✅ Prix toujours au format numérique propre
- ✅ Devise explicite
- ✅ URL d'image de haute qualité

**Extraction prioritaire** dans `BackmarketStore._extract_json_ld()`:
```python
def _extract_json_ld(self, soup: BeautifulSoup) -> dict:
    """Extrait les données depuis JSON-LD schema.org."""
    json_ld_scripts = soup.find_all("script", {"type": "application/ld+json"})

    for script in json_ld_scripts:
        data = json.loads(script.string)
        if data.get("@type") == "Product":
            return {
                "name": data.get("name"),
                "price": float(data["offers"]["price"]),
                "priceCurrency": data["offers"]["priceCurrency"],
                "images": [data.get("image")]
            }
```

### Sélecteurs CSS (Fallback)

Si JSON-LD n'est pas disponible, on utilise des sélecteurs CSS relativement stables.

**Sélecteurs identifiés**:

| Champ | Sélecteur | Stabilité |
|-------|-----------|-----------|
| **Titre** | `h1.heading-1` | ⭐⭐⭐⭐ Stable |
| **Prix** | `div[data-test='price']` | ⭐⭐⭐⭐ Stable |
| **Condition** | `button[data-test='condition-button']` | ⭐⭐⭐⭐ Stable |
| **Images** | `img[alt]` | ⭐⭐⭐ Moyen (filtrer par nom produit) |
| **Stock** | `button[data-test='add-to-cart']` | ⭐⭐⭐⭐ Stable |
| **Specs** | `dl > dt, dd` | ⭐⭐⭐ Moyen |
| **Category** | `nav[aria-label='breadcrumb'] a` | ⭐⭐⭐ Moyen |

**Stabilité des sélecteurs**: **Bonne** - Backmarket utilise des classes sémantiques (`heading-1`) et des attributs `data-test` qui sont plus stables que les classes générées aléatoirement.

---

## 🔧 Implémentation Technique

### URL Pattern & SKU Extraction

**Format URL**: `https://www.backmarket.fr/{locale}/p/{slug}`

**Exemples**:
- `https://www.backmarket.fr/fr-fr/p/iphone-15-pro` → SKU = `"iphone-15-pro"`
- `https://www.backmarket.com/en-us/p/macbook-air-m2` → SKU = `"macbook-air-m2"`

**Regex d'extraction**:
```python
def extract_reference(self, url: str) -> Optional[str]:
    match = re.search(r"/p/([a-z0-9-]+)", url, re.IGNORECASE)
    if match:
        return match.group(1)
    return None
```

**Caractéristiques du slug**:
- Format kebab-case: `product-name-variant`
- Peut contenir chiffres: `iphone-15-pro`, `galaxy-s23`
- Stable dans le temps (identifiant produit)

### Canonicalization

On retire les paramètres de query et le fragment car ils ne changent pas le produit.

```python
def canonicalize(self, url: str) -> str:
    parsed = urlparse(url)
    return f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
```

**Exemples**:
- `https://www.backmarket.fr/fr-fr/p/iphone-15-pro?color=black` → `https://www.backmarket.fr/fr-fr/p/iphone-15-pro`
- `https://www.backmarket.fr/fr-fr/p/iphone-15-pro#specs` → `https://www.backmarket.fr/fr-fr/p/iphone-15-pro`

---

## 📈 Tests & Validation

### Test Coverage

**Tests unitaires**: 19 tests
**Tests fixtures**: 11 tests
**Total**: 30 tests - **100% PASS** ✅

**Coverage du store**: **85%**

**Fichiers de tests**:
- `tests/stores/test_backmarket.py` - Tests unitaires (match, canonicalize, extract_reference, parse)
- `tests/stores/test_backmarket_fixtures.py` - Tests avec HTML réel

### Fixture Réelle

**Produit**: iPhone 15 Pro (reconditionné)
**Fichier**: `pricewatch/app/stores/backmarket/fixtures/backmarket_iphone15pro.html`
**Taille**: 1.5 MB
**Date capture**: 2026-01-13
**Méthode**: Playwright (obligatoire)

**Données extraites de la fixture**:
```json
{
  "source": "backmarket",
  "title": "iPhone 15 Pro",
  "price": 571.0,
  "currency": "EUR",
  "reference": "iphone-15-pro",
  "images": ["https://d2e6ccujb3mkqf.cloudfront.net/..."],
  "is_complete": true
}
```

---

## ⚖️ Comparaison avec Autres Stores

| Aspect | Amazon | Cdiscount | **Backmarket** |
|--------|--------|-----------|----------------|
| **Anti-bot** | Faible | Fort (Baleen) | Fort (Cloudflare) |
| **Méthode** | HTTP OK | Playwright | **Playwright** |
| **JSON-LD** | Partiel | ✗ Non | **✓ Oui (complet)** |
| **Sélecteurs** | Stables (IDs) | Instables | **Stables (data-test)** |
| **SKU format** | `/dp/{ASIN}` | `/f-{cat}-{SKU}` | **/p/{slug}** |
| **Parsing fiabilité** | ⭐⭐⭐⭐ | ⭐⭐⭐ | **⭐⭐⭐⭐⭐** |
| **Vitesse fetch** | Rapide (200ms) | Lent (2-3s) | Lent (2-3s) |
| **Particularité** | - | Prix dynamiques | **Reconditionné** |

**Conclusion**: Backmarket est le store **le plus fiable pour le parsing** grâce au JSON-LD, mais nécessite Playwright comme Cdiscount.

---

## ✅ Avantages de Backmarket

1. **JSON-LD schema.org complet** → Parsing ultra-fiable
2. **Classes CSS stables** → Moins de casse que Cdiscount
3. **URL propre et prévisible** → SKU facile à extraire
4. **Format prix standardisé** → Toujours numérique dans JSON-LD
5. **Sélecteurs `data-test`** → Conçus pour être stables

---

## ❌ Inconvénients & Défis

### 1. Protection Anti-bot Obligatoire

**Impact**:
- ⏱️ Temps de fetch: ~2-3 secondes (vs 200ms pour HTTP)
- 💰 Coût: Plus de ressources CPU/mémoire
- 🐳 Docker: Nécessite installation de Playwright browsers

**Solution**: Utiliser le cache agressivement, éviter les fetch répétés.

### 2. Prix Variable selon Condition

Le même produit peut avoir 5-10 prix différents selon l'état.

**Problème**: Quel prix extraire ?
**Solution actuelle**: On extrait le prix de l'offre par défaut sélectionnée (souvent "Excellent")

**Amélioration future**: Extraire toutes les offres avec leurs conditions et prix.

### 3. Stock Complexe

Le stock dépend de l'offre ET de la condition choisie.

**Problème**: Un produit peut être "en stock" dans une condition mais "rupture" dans une autre.
**Solution actuelle**: On extrait le stock de l'offre par défaut.

### 4. 404 Fréquents

Les produits reconditionnés ont un **stock limité** et les URLs peuvent devenir invalides rapidement.

**Exemple testé**:
- `https://www.backmarket.fr/fr-fr/p/samsung-galaxy-s23` → 404
- `https://www.backmarket.fr/fr-fr/p/apple-macbook-air-133-pouces-m2-2022` → 404

**Impact**: Plus de gestion d'erreurs nécessaire dans le pipeline.

---

## 🎓 Recommandations

### Pour le Développement

1. **Toujours utiliser Playwright pour Backmarket** - Ne jamais tenter HTTP
2. **Prioriser JSON-LD** - C'est la source la plus fiable
3. **Extraire la condition** - Ajouter dans `specs["Condition"]`
4. **Gérer les 404 gracefully** - Produits limités en stock
5. **Cache long** - Minimiser les appels Playwright coûteux

### Pour les Tests

1. **Maintenir les fixtures à jour** - Les URLs expirent vite
2. **Tester avec différentes catégories** - Smartphones, laptops, tablets
3. **Tester les différentes conditions** - Prix varie selon état
4. **Tester les 404** - Cas fréquent pour le reconditionné

### Pour la Production

1. **Monitoring des 404** - Alertes sur produits devenus indisponibles
2. **Rotation des proxies** - Si scraping intensif
3. **Rate limiting** - Respecter le site (2-3s entre requêtes minimum)
4. **Cache agressif** - Playwright coûte cher en ressources

---

## 📝 Exemples d'Utilisation

### Scraping Simple

```python
from pricewatch.app.scraping.pw_fetch import fetch_playwright
from pricewatch.app.stores.backmarket.store import BackmarketStore

url = "https://www.backmarket.fr/fr-fr/p/iphone-15-pro"

# Fetch avec Playwright (obligatoire)
result = fetch_playwright(url, headless=True, timeout_ms=60000)

# Parse
store = BackmarketStore()
snapshot = store.parse(result.html, url)

print(f"Title: {snapshot.title}")
print(f"Price: {snapshot.price} {snapshot.currency}")
print(f"Condition: {snapshot.specs.get('Condition', 'N/A')}")
print(f"Complete: {snapshot.is_complete()}")
```

### Détection Automatique

```python
from pricewatch.app.core.registry import StoreRegistry

url = "https://www.backmarket.fr/fr-fr/p/iphone-15-pro"

# Détection automatique du store
store = StoreRegistry.detect(url)
print(f"Store détecté: {store.store_id}")  # → "backmarket"
print(f"Score: {store.match(url)}")  # → 0.9
```

### Pipeline Complet

```python
from pricewatch.app.cli.main import scrape_url

url = "https://www.backmarket.fr/fr-fr/p/iphone-15-pro"

# Pipeline complet: detect → fetch → parse → save
snapshot = scrape_url(
    url,
    use_playwright=True,  # Obligatoire pour Backmarket
    save_html=True,
    save_screenshot=True
)

print(f"✓ Scraped: {snapshot.title} - {snapshot.price} {snapshot.currency}")
```

---

## 🔍 Points d'Attention pour Debug

### Si le parsing échoue:

1. **Vérifier que Playwright est utilisé** - HTTP ne fonctionne jamais
2. **Vérifier le code status** - Peut être 404 si produit épuisé
3. **Inspecter le JSON-LD** - C'est la source prioritaire
4. **Vérifier la condition** - Prix peut varier selon offre sélectionnée
5. **Logs détaillés** - Activer `--debug` pour voir les erreurs

### Exemple de debug:

```bash
# Activer les logs détaillés
export PRICEWATCH_LOG_LEVEL=DEBUG

# Scraper avec sauvegarde HTML
pricewatch fetch https://www.backmarket.fr/fr-fr/p/iphone-15-pro \
  --playwright \
  --save-html \
  --debug
```

---

## 📚 Ressources

**Documentation officielle**: https://www.backmarket.fr
**Fichiers du projet**:
- `pricewatch/app/stores/backmarket/store.py` - Implémentation
- `pricewatch/app/stores/backmarket/selectors.yml` - Sélecteurs CSS
- `pricewatch/app/stores/backmarket/fixtures/README.md` - Documentation des fixtures
- `tests/stores/test_backmarket.py` - Tests unitaires
- `tests/stores/test_backmarket_fixtures.py` - Tests avec HTML réel

**Tests**:
```bash
# Tous les tests Backmarket
pytest tests/stores/test_backmarket*.py -v

# Avec coverage
pytest tests/stores/test_backmarket*.py --cov=pricewatch.app.stores.backmarket
```

---

## 🎉 Conclusion

Backmarket est un **excellent store à supporter** pour PriceWatch:
- ✅ Parsing **très fiable** grâce au JSON-LD
- ✅ Sélecteurs **stables** (data-test, classes sémantiques)
- ✅ Tests **complets** (30 tests, 100% pass)
- ⚠️ Nécessite **Playwright** (coût en performance)
- ⚠️ URLs peuvent **expirer** (stock limité)

**Score final**: ⭐⭐⭐⭐⭐ (5/5) pour la fiabilité du parsing.

**Recommandation**: Store prioritaire à maintenir pour le marché du reconditionné.
