chore: sync project files
This commit is contained in:
399
BACKMARKET_ANALYSIS.md
Executable file
399
BACKMARKET_ANALYSIS.md
Executable file
@@ -0,0 +1,399 @@
|
||||
# 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é.
|
||||
Reference in New Issue
Block a user