# Comment scraper cDiscount avec Python et requests en 2023?

> Sur cDiscount on peut acheter tout et n’importe quoi.  On va voir comment scraper les produits avec Python et requests.

- **Author**: Sasha Bouloudnine
- **Published**: 2023-06-29 (29 juin 2023)
- **Reading time**: 20 min
- **Canonical**: https://www.lobstr.io/fr/blog/comment-scraper-c-discount-avec-python-et-requests-en-2023

---

**[cDiscount](https://cdiscount.com/)**, c’est vraiment **un superbe site de e-commerce Français**. Selon la page de Wikipedia du groupe, on y trouve (tenez-vous bien) pas moins de **[100 millions de références](https://fr.wikipedia.org/wiki/Cdiscount#:~:text=Initialement%20sp%C3%A9cialis%C3%A9e%20dans%20la%20vente,mode%20et%20de%20l'alimentation.)**, et ce réparti dans plusieurs centaines de catégories, de l’univers de la maison, au high-tech, en passant par la mode et l’alimentation.

![comment-scraper-cdiscount-avec-python-et-requests-en-2023-image1.png](https://d37gzvgyugjozl.cloudfront.net/comment_scraper_cdiscount_avec_python_et_requests_en_2023_image1_07a832e705.png "width=1352;height=474")

Avec **[32 millions de visites mensuelles](https://www.similarweb.com/website/cdiscount.com/#overview)** en Mai 2023, selon le site de classement de trafic SimilarWeb, **le site se classe en 3e position dans la catégorie eCommerce & Shopping**, juste derrière l’indétronable [amazon.fr](https://www.amazon.fr/) qui totalise jusqu’à 5 fois plus de trafic, avec 160+ millions de visite mensuelle. Chapeau Jeff, joli coup de filet.

Toutefois, si l’on tape _scraping python cdiscount_ sur Google, on ne trouve aucune ressource sérieuse, si ce n’est ce thread du fameux [GrowthHackingForum](https://www.growthhacking.fr/t/scraping-cdiscount-avec-scrapy/21870/5), le site de référence du scraping en France. On nous parle d’une **mystérieuse URL POST**, sans que personne ne soit en mesure de donner une réponse explicite et fonctionnelle.

![comment-scraper-cdiscount-avec-python-et-requests-en-2023-image2.png](https://d37gzvgyugjozl.cloudfront.net/comment_scraper_cdiscount_avec_python_et_requests_en_2023_image2_cb7f2f08ab.png "width=1790;height=1202")

Est-ce qu’il est si compliqué de scraper de la donnée sur cDiscount?

Dans ce tutoriel, on va voir comment **scraper les données des produits sur cDiscount** avec Python3 et requests, et obtenir de façon programmatique, des données fraîches, structurées et massivement exploitables. 

Disclaimer: on va également percer le secret de cette mystérieuse requête POST…

Développeurs, data analysts, dirigeants soucieux de veiller sur les prix des barbecues de ses concurrents: cet article est fait pour vous!

## Prérequis

Avant de rentrer dans le dur, on va commencer par installer les outils nécessaires à notre collecte. 

D’abord assurez-vous bien entendu d’avoir installé **l’outil de programmation Python**, qu’il est possible de télécharger et d’installer depuis le site web, accessible [ici](https://www.python.org/). C’est avec ce langage que l’on va écrire le script de scraping cDiscount.

Puis **on va télécharger 3 librairies additionnelles**, avec __pip__, l’outil d’installation de Python: **[requests](https://pypi.org/project/requests/)**, la librairie la plus téléchargée pour Python, qui va nous permettre de naviguer programmatiquement sur Internet, **[retry](https://pypi.org/project/retry/)**, une librairie qui va nous permettre de recommencer certaine tâche si une erreur est rencontrée sur notre chemin, et **[lxml](https://lxml.de/)**, une librairie qui va nous permettre d’aller chercher les éléments dans le code source d’une page HTML.

Avec une installation comme suit:

```bash
$ pip3 install requests lxml retry
```

Nous sommes maintenant prêts pour la suite des opérations.

## Code Complet

Voilà le code complet, et directement accessible sur Github [ici](https://gist.github.com/lobstrio/f695937ea7eed124470c1c1475be24f0):

```python
import requests
import re
import json
from lxml import html
import time
from retry import retry
import csv

URL = 'https://www.cdiscount.com/search/10/barbecue.html'

HEADERS = {
    'authority': 'www.cdiscount.com',
    'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
    'accept-language': 'fr-FR,fr;q=0.9',
    'sec-ch-ua': '"Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"macOS"',
    'sec-fetch-dest': 'document',
    'sec-fetch-mode': 'navigate',
    'sec-fetch-site': 'none',
    'sec-fetch-user': '?1',
    'upgrade-insecure-requests': '1',
    'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
}

FIELDNAMES = [
    "sku", 
    "url", 
    "image_url", 
    "title", 
    "score", 
    "reviews_count", 
    "price", 
    "is_cdav"
]

DATA = []

class JavascriptChallengeError(Exception): 
    pass

class cDiscountScraper: 

    def __init__(self): 
        self.s = requests.Session()
        self.s.headers = HEADERS

    def solve_javascript_challenge(self, response): 
        print('solving javascript challenge')

        raw_data = re.findall(r'(?<=__blnChallengeStore=)\{[^\;]+', response.text)
        assert raw_data and len(raw_data) == 1
        raw_data = ''.join(raw_data)
        raw_data = json.loads(raw_data)

        challenge_cookie = raw_data['cookie']

        challenge_cookie.pop('maxAge')
        challenge_cookie_value = challenge_cookie['value']
        challenge_cookie_name = challenge_cookie['name']

        self.s.cookies.set_cookie(requests.cookies.create_cookie(**challenge_cookie))

        check_params = raw_data['checkChallengeParams']
        url = 'https://www.cdiscount.com/.well-known/baleen/challengejs/check?%s=%s' % (
            challenge_cookie_name, 
            challenge_cookie_value
        )
        data = '&'.join(['%s=%s' % (k,v) for k, v in check_params.items()])
        response = self.s.post(url, data)
        assert response.status_code == 200

    @retry(JavascriptChallengeError, tries=5, delay=5, backoff=1)
    def get_cdiscount_data(self): 
        print('start')

        response = self.s.get(URL, headers=HEADERS)
        with open('first_req.html', 'w') as f: 
            f.write(response.text)

        if 'Le JavaScript n\'est pas' in response.text: 
            self.solve_javascript_challenge(response)
            raise JavascriptChallengeError

        assert response.status_code == 200
        assert 'GEORGES' in response.text

        doc = html.fromstring(response.text)
        
        products = doc.xpath("//ul/[@id='lpBloc']/li[@data-sku]")
        assert products
        for product in products: 
            
            sku = product.get('data-sku')
            url = "".join(product.xpath('./a/@href'))
            assert url.startswith('https://www.cdiscount.com')

            image_url = "".join(product.xpath('.//li/img[@class="prdtBImg"]/@data-src')) or "".join(product.xpath(".//li/img[@class='prdtBImg']/@src"))
            title = "".join(product.xpath('.//h2[@class="prdtTit"]/text()'))
            score = "".join(product.xpath('.//span[@class="c-stars-result c-stars-result--small"]/@data-score'))
            if score: 
                score = float(score) / 20 

            reviews_count = "".join(product.xpath('.//span[@class="c-stars-result c-stars-result--small"]/following-sibling::span/text()'))
            if reviews_count: 
                reviews_count = int(reviews_count.strip('()'))

            price = "".join(product.xpath('.//span[contains(@class, "price priceColor hideFromPro") and not(contains(@class, "price--xs"))]/text()')).strip('€')
            is_cdav = len(product.xpath('.//div[@class="cdavZone"]')) > 0

            values = [sku, url, image_url, title, score, reviews_count, price, is_cdav]

            d = dict(zip(FIELDNAMES, values))
            print(*d.values())

            DATA.append(d)

        return DATA

    def write_csv(self, DATA): 
        print('starting writing csv')
        with open('cdiscount_results.csv', 'w') as f: 
            writer = csv.DictWriter(f, fieldnames=FIELDNAMES)
            writer.writeheader()
            for d in DATA: 
                writer.writerow(d)

if __name__ == '__main__':
    cdiscount_scraper = cDiscountScraper()
    DATA = cdiscount_scraper.get_cdiscount_data()
    cdiscount_scraper.write_csv(DATA)
    print('''~~ success
 _       _         _            
| |     | |       | |           
| | ___ | |__  ___| |_ __ __  
| |/ _ \| '_ \/ __| __/| '__|
| | (_) | |_) \__ \ |_ | |   
|_|\___/|_.__/|___/\__||_|   
''')
```

Pour utiliser ce script, c’est très simple: télécharger le fichier .py, et lancer le script comme suit:

```bash
$ python3 cdiscount_scraper.py 
start
solving javascript challenge
start
starting writing csv
~~ success
 _       _         _            
| |     | |       | |           
| | ___ | |__  ___| |_ __ __  
| |/ _ \| '_ \/ __| __/| '__|
| | (_) | |_) \__ \ |_ | |   
|_|\___/|_.__/|___/\__||_|   
```

Et voilà!

## Tutoriel étape par étape

A l’heure où cet article est écrit, l’été arrive. Et avec lui, son lot de délicieuses soirées barbecue entre amis, où [presque tout est permis](https://twitter.com/sandrousseau/status/1673210730672209922?s=20). 

Dans ce tutoriel, on va voir comment **scraper les données des 50 premiers barbecues en vente sur [cDiscount](https://www.cdiscount.com/)**, et directement accessibles via cet URL.

[https://www.cdiscount.com/search/10/barbecue.html#_his_](https://www.cdiscount.com/search/10/barbecue.html#_his_)

![comment-scraper-cdiscount-avec-python-et-requests-en-2023-image3.png](https://d37gzvgyugjozl.cloudfront.net/comment_scraper_cdiscount_avec_python_et_requests_en_2023_image3_7e87f79f3a.png "width=1999;height=1024")

Et on va récupérer **8 attributs distincts**, comme suit: 

* sku
* url
* image_url
* title
* score
* reviews_count
* price
* is_cdav

Et ce comme illustré ci-dessous: 

![comment-scraper-cdiscount-avec-python-et-requests-en-2023-image4.png](https://d37gzvgyugjozl.cloudfront.net/comment_scraper_cdiscount_avec_python_et_requests_en_2023_image4_954c55b826.png "width=1920;height=1304")

Ce tutoriel sera divisé en **4 étapes**: 

1. Résoudre l’erreur 403
2. Résoudre le challenge Javascript
3. Récupérer les données dans la page HTML
4. Enregistrer les données dans un fichier .csv

À nous les soirées protéinées au coin du feu. \
Allons-y!

### 1. Résoudre l’erreur 403

Intuitivement, on a envie d’écrire un script très simple: une requête simple vers l’URL de la page mentionnée plus haut, afin de récupérer les données présentes dans la page.

Un premier jet immédiat peut être construit comme suit: 


```python
import requests
s = requests.Session()
response = s.get("https://www.cdiscount.com/search/10/barbecue.html")
with open("premier_jet.html", "w") as f: 
    f.write(response.text)
```

Sauf que voilà, lorsque l’on ouvre la dite page _premier_jet.html_, voilà ce qu’on obtient: 

![comment-scraper-cdiscount-avec-python-et-requests-en-2023-image5.png](https://d37gzvgyugjozl.cloudfront.net/comment_scraper_cdiscount_avec_python_et_requests_en_2023_image5_5fab8a72b3.png "width=1999;height=872")

Une erreur 403. 
Coup dur.

Il est possible que cette première requête ait été un peu brute: pas de [headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) ni de [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies), directement identifiable comme étant une requête programmatique.

On va donc **ajouter des headers**, pour que cette requête se rapproche de celle d’un utilisateur traditionnel.

On se rend sur Chrome, et on va commencer par **ouvrir notre console de développeur**: _Clic droit > Inspecter > Network_. 

![comment-scraper-cdiscount-avec-python-et-requests-en-2023-image6.gif](https://d37gzvgyugjozl.cloudfront.net/comment_scraper_cdiscount_avec_python_et_requests_en_2023_image6_80431ce1be.gif)

On va ensuite accéder à notre URL riche de données à scraper, c'est-à-dire la page cDiscount avec les barbecues. 

Enfin, on va chercher, en utilisant la fonction recherche de la partie Network de la console, un mot présent sur la page. Ici le mot singulier: GEORGES. 

![comment-scraper-cdiscount-avec-python-et-requests-en-2023-image7.png](https://d37gzvgyugjozl.cloudfront.net/comment_scraper_cdiscount_avec_python_et_requests_en_2023_image7_fad7660cea.png "width=1999;height=1028")

> On ne considère pas la première requête, car il s’agit d’une requête qui ramène un fichier .jpg, c'est-à-dire l’image du produit en question. Pas très intéressant ici.

L’outil de recherche va nous permettre de **trouver la requête initiale**, ou requête originelle, à partir de laquelle la donnée a été amenée du serveur à notre navigateur.

Une fois identifiée la requête pertinente, on va la **copy as cURL**, ou copier au format cURL en bon français, ce qui va nous permettre de sauvegarder, sous un format donnée, l’ensemble des paramètres de la requête effectuée: 

![comment-scraper-cdiscount-avec-python-et-requests-en-2023-image8.png](https://d37gzvgyugjozl.cloudfront.net/comment_scraper_cdiscount_avec_python_et_requests_en_2023_image8_e3fcafef8b.png "width=1999;height=1199")

> cURL est un outil de ligne de commande qui permet de transférer de la donnée en utilisant divers protocoles. Dit plus simplement, c’est un outil qui permet de naviguer de façon programmatique sur Internet, et qu’il est possible d’utiliser directement depuis son terminal. Si vous souhaitez aller plus loin, [cet article d’IBM est bien construit](https://developer.ibm.com/articles/what-is-curl-command/).

Enfin, on va **convertir cette requête qui est au format cURL en requête Python/requests**, avec ce superbe site: [https://curlconverter.com/](https://curlconverter.com/). On va venir tout simplement copier cette requête dans notre code Python.

![comment-scraper-cdiscount-avec-python-et-requests-en-2023-image9.gif](https://d37gzvgyugjozl.cloudfront.net/comment_scraper_cdiscount_avec_python_et_requests_en_2023_image9_b5e3c00d54.gif)

Et notre notre code à cette étape: 

```python
import requests

headers = {
    'authority': 'www.cdiscount.com',
    'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
    'accept-language': 'fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7',
    'cache-control': 'max-age=0',
    'cookie': 'CookieId=CookieId=230502110614CEICTORB&IsA=0; s_ecid=MCMID%7C16797685366166076358869452603464651012; TCPID=123521161511551056407; UniqueVisitContext=UniqueVisitId__230502110615FTHBMDWJ__; tcId=5ee0070d-c515-4236-bb68-f3d43c97d3df; _cs_c=3; TC_AB=A; app_vi=34214400%7C; prio30j=prio4; _$culture=CultureName__fr-FR__; visid_incap_2465919=dUWI1haIR9GTqqcgIrNCOwqgfGQAAAAAQUIPAAAAAACHWaWcYx2zEp5qWrQtGrgx; _cs_id=80e36f8d-3dcf-ab92-c779-066825140290.1683018376.3.1685889050.1685889037.1590586488.1717182376625; TC_PRIVACY=1@078%7C2%7C2%7C199@@@1685889051491%2C1685889051491%2C1701441051491@; TC_PRIVACY_CENTER=; _$cst=0; prio7j=prio4; chcook7=direct; SitePersoCookie=PersoCountryKey__FR__PersoLatitudeKey__45.1833__PersoLongitudeKey__0.7167__PersoTownKey__Perigueux__GeolocPriorityKey__10__PersoPostalCodeKey__24000__PersoUrlGeoSCKey__/n-190904/marketplace/retrait-immediat-paris.html__ExpressSellerId____ExpressShopName____ExpressGlobalSellerId____ShowroomVendorId____RetailStoreName____VehicleId__0__AddressId____; _$dtype=t:d; cache_cdn=; mssctse=W2dNXeEyrPIlc-Vb0OXhfpSvUDZBb4zp_O66j14y6UwCtJ9SHjEV2xrq24P65vvL6F_Rm25RpjG0CEkiod405g; AMCV_6A63EE6A54FA13E60A4C98A7%40AdobeOrg=1585540135%7CMCIDTS%7C19538%7CMCMID%7C16797685366166076358869452603464651012%7CMCAID%7CNONE%7CMCOPTOUT-1683025575s%7CNONE%7CvVersion%7C4.4.0; svisit=1; _$3custinf=AUT=0&XV1=0; s_cc=true; msswt=; s_pv=Recherche; msstvt=sN_Ypy2CO9_uH8xyMeB6Fv2fX_kGMtDY2oND-8_-lWTWuedd3ScG-4nLN4_lQVFy5wViq0BE3ivAYsnKanzoMQHgwnnSckuvFPg8yUubjzcr3HqX6UVH-sRcT8sSs10am8fzDvaNdYlrSCq2yhry8g; visit_baleen_ACM-655d43=cgDlAhbnqqYt9gTtRmBvPwVdT4hUToPAJr4Fu40-2VXUXZaHLgC04uiwcEhMl6TT7nIJYN1wYJoNIJxtPFz6FuPjeNF5ICa8GtFxrrdBTJjTA8HYk_k4wF7SqhK3ZWKXRW6DAYSNNFFW8UYUvJqV1p9DNjR01DBAPxauyeUtuKA-cTKEGkg0xCCbLqQiAuSAXLA8_GC0aEwlDs6qeWfDh126UIg6JBVPz8xWct_1oWJGgDk6qh3T1uj1RyFVQiWY4GFpSzTKA3EFrzx7rPpi_A; s_nr=1688044980321-Repeat; el2=0; tp=13575; VisitContextCookie=HGCJjPLdLJynSn3G8iVsFKxxgj6bTclHIMhEATBOtwStuI82FaEUuw; s_ppv=Recherche%2C11%2C4%2C1528',
    'if-none-match': 'W/"fh_340c19a1c0ba672b7cdc06d54faa410dc8f8e7ff"',
    'sec-ch-ua': '"Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"macOS"',
    'sec-fetch-dest': 'document',
    'sec-fetch-mode': 'navigate',
    'sec-fetch-site': 'same-origin',
    'sec-fetch-user': '?1',
    'upgrade-insecure-requests': '1',
    'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
}

response = requests.get('https://www.cdiscount.com/search/10/barbecue.html', headers=headers)
with open('second_essai.html', 'w') as g: 
	g.write(response.text)
```

> Pour des raisons de lisibilité, on a décommenter les cookies présents au niveau du headers, et supprimer les cookies qui étaient au format dictionnaire. Il s’agit seulement d’un ajustement esthétique, ça n’a aucune incidence sur la nature de la requête.

On lance notre code Python. Un fichier est enregistré, le bien nommé _second_essai.html_. Et en l’ouvrant avec Chrome… malheur, un écran complètement blanc: 

![comment-scraper-cdiscount-avec-python-et-requests-en-2023-image10.png](https://d37gzvgyugjozl.cloudfront.net/comment_scraper_cdiscount_avec_python_et_requests_en_2023_image10_c51e891a35.png "width=1999;height=1151")

⚪

Pas une trace de barbecue qui soit. 
Inexploitable.

Est-ce qu’il est l’heure de faire appel à _la mystérieuse requête POST_?

On va voir ça dans la seconde partie.

### 2. Résoudre le challenge Javascript

On a installé les librairies de développement, on est allé chercher la requête pertinente… et c’est la douche froide. Une page blanche, avec rien de lisible, et rien d’exploitable.

On va commencer par **aller voir ce qui se cache au niveau du code source de la page**. On fait donc _Clic droit > Afficher le code source_: 

![comment-scraper-cdiscount-avec-python-et-requests-en-2023-image11.png](https://d37gzvgyugjozl.cloudfront.net/comment_scraper_cdiscount_avec_python_et_requests_en_2023_image11_29855d860b.png "width=1999;height=1204")

Et au niveau du code source, on va observer **2 éléments intéressants**.

D’abord, on nous indique qu’**il faut activer le Javascript afin d’accéder à la page**. Il s’agit donc bien d’un challenge Javascript, c’est-à-dire que que le site exécute normalement un script Javascript présent sur la page. 

L’exécution de ce script permet d’habitude à un navigateur usuel où le Javascript est activé par défault - Chrome, Firefox, tous les navigateurs en fait - d’être perçu comme _licite_ par le site cible. Le plus souvent, par l’intermédiaire d’un cookie, qui agit comme un permis d’entrée.

👮

Comme nous utilisons un navigateur Python, le code Javascript n’est pas exécuté du tout, et la page est inaccessible. 

![comment-scraper-cdiscount-avec-python-et-requests-en-2023-image12.png](https://d37gzvgyugjozl.cloudfront.net/comment_scraper_cdiscount_avec_python_et_requests_en_2023_image12_89c7274cce.png "width=1999;height=761")

Si on regarde plus haut, bingo! Il y a bien du code Javascript, et notamment un cookie stocké dans la variable _blnChallengeStore_.

**Ce code Javascript**, qui est exécuté passivement par un navigateur usuel, **il va donc falloir qu’on l’exécute nous-même, activement**, avec du code en Python. Mais qu’est-ce qu’il fait au juste ce bout de script?

On va commencer par **le [déminifier](https://en.wiktionary.org/wiki/unminified#:~:text=unminified%20(not%20comparable),Not%20minified.)**, c'est-à-dire y ajouter des espaces et des couleurs, afin que le code soit facilement lisible. On utilise pour ça l’outil en ligne [Unminify.com](https://unminify.com/), et voilà ce qui en sort: 

![comment-scraper-cdiscount-avec-python-et-requests-en-2023-image13.png](https://d37gzvgyugjozl.cloudfront.net/comment_scraper_cdiscount_avec_python_et_requests_en_2023_image13_8882340da1.png "width=1874;height=1302")

Soit vous êtes un dingue de Javascript, soit, on a pas bien avancé.

Afin de gagner du temps, on va demander ça directement à [ChatGPT](https://chat.openai.com/). L’intelligence artificielle, développée par la société [OpenAI](https://chat.openai.com/), a été entraînée sur la base de données de [StackOverFlow](https://stackoverflow.com/): elle maîtrise **[l’intégralité des langages de programmation usuels](https://www.quora.com/How-many-programming-languages-does-ChatGPT-know)**.

Voilà un imprimé écran du prompt, et la réponse complète accessible [ici](https://chat.openai.com/share/693cd616-30b5-46d7-b199-1996a7cafa8d): 

![comment-scraper-cdiscount-avec-python-et-requests-en-2023-image14.png](https://d37gzvgyugjozl.cloudfront.net/comment_scraper_cdiscount_avec_python_et_requests_en_2023_image14_3f656d09c3.png "width=1999;height=832")

Pour faire simple, le script Javascript exécute les actions suivantes: 

* il récupère le cookie présent dans la page
* il assigne le cookie au navigateur
* il réalise une requête POST vers un URL construit à partir de la valeur du cookie de la page

Oui, vous avez bien lu, **c’est ici qu’intervient la mystérieuse requête POST**.

C’est elle qui va nous permettre de contourner ce premier challenge Javascript.
On va donc exactement reproduire ces étapes là, à l’aide du code Python! 

On va donc d’abord commencer par **récupérer la valeur du cookie présent dans le texte**: 

```python
# on récupère la valeur du cookie dans le code source avec une regex
raw_data = re.findall(r'(?<=__blnChallengeStore=)\{[^\;]+', response.text)
assert raw_data and len(raw_data) == 1
raw_data = ''.join(raw_data)
raw_data = json.loads(raw_data)
```

> On utilise ici une regex, qui permet d’aller chercher rapidement dans le code source une chaîne de caractères avec un certain format. On ne peut que vous recommander l’excellent outil de création et de test [regex101](https://regex101.com/).

On va ensuite **assigner le cookie à notre session de navigation**: 

```python
# on assigne le cookie à notre navigateur
challenge_cookie = raw_data['cookie']

challenge_cookie.pop('maxAge')
challenge_cookie_value = challenge_cookie['value']
challenge_cookie_name = challenge_cookie['name']

self.s.cookies.set_cookie(requests.cookies.create_cookie(**challenge_cookie))
```

Enfin, **on réalise cette fameuse requête POST**: 

```python
# on réalise la mystérieuse requête POST
check_params = raw_data['checkChallengeParams']
url = 'https://www.cdiscount.com/.well-known/baleen/challengejs/check?%s=%s' % (
	challenge_cookie_name, 
	challenge_cookie_value
)
data = '&'.join(['%s=%s' % (k,v) for k, v in check_params.items()])
response = self.s.post(url, data)
assert response.status_code == 200
```

Et à ce stade, voilà notre code Python entier: 

```python
import requests
import re
import json
from retry import retry

URL = 'https://www.cdiscount.com/search/10/barbecue.html'

HEADERS = {
    'authority': 'www.cdiscount.com',
    'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
    'accept-language': 'fr-FR,fr;q=0.9',
    'sec-ch-ua': '"Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"macOS"',
    'sec-fetch-dest': 'document',
    'sec-fetch-mode': 'navigate',
    'sec-fetch-site': 'none',
    'sec-fetch-user': '?1',
    'upgrade-insecure-requests': '1',
    'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
}

class JavascriptChallengeError(Exception): 
	pass

class cDiscountScraper: 

	def __init__(self):
		self.s = requests.Session()
		self.s.headers = HEADERS

	def solve_javascript_challenge(self, response): 
		print('solving javascript challenge')

		# on récupère la valeur du cookie dans le code source avec une regex
		raw_data = re.findall(r'(?<=__blnChallengeStore=)\{[^\;]+', response.text)
		assert raw_data and len(raw_data) == 1
		raw_data = ''.join(raw_data)
		raw_data = json.loads(raw_data)

		# on assigne le cookie à notre navigateur
		challenge_cookie = raw_data['cookie']

		challenge_cookie.pop('maxAge')
		challenge_cookie_value = challenge_cookie['value']
		challenge_cookie_name = challenge_cookie['name']

		self.s.cookies.set_cookie(requests.cookies.create_cookie(**challenge_cookie))

		# on réalise la mystérieuse requête POST
		check_params = raw_data['checkChallengeParams']
		url = 'https://www.cdiscount.com/.well-known/baleen/challengejs/check?%s=%s' % (
			challenge_cookie_name, 
			challenge_cookie_value
		)
		data = '&'.join(['%s=%s' % (k,v) for k, v in check_params.items()])
		response = self.s.post(url, data)
		assert response.status_code == 200

	@retry(JavascriptChallengeError, tries=5, delay=5, backoff=1)
	def get_cdiscount_data(self): 
		print('start')

		response = self.s.get(URL, headers=HEADERS)
		with open('main_response.html', 'w') as f: 
			f.write(response.text)

		if 'Le JavaScript n\'est pas' in response.text: 
			self.solve_javascript_challenge(response)
			raise JavascriptChallengeError


if __name__ == '__main__':
	cdiscount_scraper = cDiscountScraper()
	cdiscount_scraper.get_cdiscount_data()
```

> Comme vous le voyez, la librairie [retry](https://pypi.org/project/retry/) nous permet, si un élément suspect est détecté sur la page, de lever une certaine erreur, de la résoudre, puis de **recommencer** la première étape. Astucieux, et robuste.

Et alors… est-ce que ça a marché?

On va ouvrir le fichier _main_response.html_, qui est enregistré chaque fois que la requête vers l’URL principal est réalisée.

Et là, fantastique, de superbes outils de métal apparaissent, prêts à nous offrir des soirées d’été grasses et mémorables: 

![comment-scraper-cdiscount-avec-python-et-requests-en-2023-image15.png](https://d37gzvgyugjozl.cloudfront.net/comment_scraper_cdiscount_avec_python_et_requests_en_2023_image15_83bde7ca09.png "width=1999;height=1094")

Magnifique!
Le challenge Javascript a été résolu.

Et voilà, il ne nous reste plus qu’à récupérer les données présentes sur la page HTML, et à les enregistrer dans un fichier .csv.

Le plus dur a été fait.

### 3. Récupérer les données dans la page HTML

La route a été longue, et plutôt sinueuse, avec une première erreur 403, puis un écran blanc totalement déstabilisant.

Mais on s’en est sortis avec brio.

Dans cette partie, on va récupérer les données des **50 barbecues présents sur la page cDiscount**, et pour chaque barbecue, **8 attributs distincts**.

D’abord, on va stocker en haut de page la liste des attributs, et la liste qui contiendra à la fin de l’exécution du script, tous nos dictionnaires, avec 1 dictionnaire par produit.


```python
...

FIELDNAMES = [
	"sku", 
	"url", 
	"image_url", 
	"title", 
	"score", 
	"reviews_count", 
	"price", 
	"is_cdav"
]

DATA = []
```

Ensuite, avec la librairie [lxml](https://lxml.de/), on va **convertir le texte en document HTML**. Ça va nous permettre, ensuite, d’aller chercher les éléments en fonction de leur position et de leur nature au sein du document HTML.

```python
import lxml
...

class cDiscountScraper: 

	def __init__(self): 
		self.s = requests.Session()
		self.s.headers = HEADERS

...

	@retry(JavascriptChallengeError, tries=5, delay=5, backoff=1)
	def get_cdiscount_data(self): 
		print('start')

            ...

		doc = html.fromstring(response.text)
```

Et maintenant, comment récupérer ces données?

En observant le code HTML, dans la partie _Elements_ de l’outil d’inspection, on voit que les produits se trouvent chacun dans une [balise li](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/li), elle même présente sous une [balise ul](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ul) qui regroupe tous les produits présents sur la page.

![comment-scraper-cdiscount-avec-python-et-requests-en-2023-image16.png](https://d37gzvgyugjozl.cloudfront.net/comment_scraper_cdiscount_avec_python_et_requests_en_2023_image16_6100c36a52.png "width=1999;height=1014")

On va donc récupérer la liste des produits, en allant chercher les balises li présentes au sein de la balise ul qui a pour id_ iPbloc_. 

Voilà notre liste de produits!

```python
products = doc.xpath("//ul/[@id='lpBloc']/li[@data-sku]")
assert products
for product in products: 

	# récupérer les attributs de chaque produit
```

Et ensuite, on va aller **chercher les 8 attributs de chaque produit** en utilisant la même logique, en fonction de la position de chaque attribut au sein du code HTML. 

Avec un code comme suit: 

```python
sku = product.get('data-sku')
url = "".join(product.xpath('./a/@href'))
assert url.startswith('https://www.cdiscount.com')

image_url = "".join(product.xpath('.//li/img[@class="prdtBImg"]/@data-src')) or "".join(product.xpath(".//li/img[@class='prdtBImg']/@src"))

title = "".join(product.xpath('.//h2[@class="prdtTit"]/text()'))

score = "".join(product.xpath('.//span[@class="c-stars-result c-stars-result--small"]/@data-score'))
if score: 
	score = float(score) / 20 

reviews_count = "".join(product.xpath('.//span[@class="c-stars-result c-stars-result--small"]/following-sibling::span/text()'))
if reviews_count: 
	reviews_count = int(reviews_count.strip('()'))

price = "".join(product.xpath('.//span[contains(@class, "price priceColor hideFromPro") and not(contains(@class, "price--xs"))]/text()')).strip('€')
is_cdav = len(product.xpath('.//div[@class="cdavZone"]')) > 0
```

> Le parsing de code HTML, plus traditionnel et moins sujet à la spéculation, ne sera pas traité ici, sinon de façon superficielle. On ne peut que vous conseiller [l’excellent tutoriel de w3schools](https://www.w3schools.com/xml/xpath_intro.asp) ici, et des tests divers et variés en grandeur nature.

Enfin, on va stocker tout ça dans un dictionnaire, afin de s’appuyer sur une donnée lisible, facilement manipulable, et proprement structuré: 

```python
values = [sku, url, image_url, title, score, reviews_count, price, is_cdav]

d = dict(zip(FIELDNAMES, values))
print(d['sku'], d['title'], d['price'])

DATA.append(d)
```

Et voilà magnifique!

A ce stade, voilà notre code complet: 

```python
import requests
import re
import json
from lxml import html
from retry import retry
import csv

URL = 'https://www.cdiscount.com/search/10/barbecue.html'

HEADERS = {
    'authority': 'www.cdiscount.com',
    'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
    'accept-language': 'fr-FR,fr;q=0.9',
    'sec-ch-ua': '"Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"macOS"',
    'sec-fetch-dest': 'document',
    'sec-fetch-mode': 'navigate',
    'sec-fetch-site': 'none',
    'sec-fetch-user': '?1',
    'upgrade-insecure-requests': '1',
    'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
}

FIELDNAMES = [
	"sku", 
	"url", 
	"image_url", 
	"title", 
	"score", 
	"reviews_count", 
	"price", 
	"is_cdav"
]

DATA = []

class JavascriptChallengeError(Exception): 
	pass

class cDiscountScraper: 

	def __init__(self): 
		self.s = requests.Session()
		self.s.headers = HEADERS

	def solve_javascript_challenge(self, response): 
		print('solving javascript challenge')

		# on récupère la valeur du cookie dans le code source avec une regex
		raw_data = re.findall(r'(?<=__blnChallengeStore=)\{[^\;]+', response.text)
		assert raw_data and len(raw_data) == 1
		raw_data = ''.join(raw_data)
		raw_data = json.loads(raw_data)

		# on assigne le cookie à notre navigateur
		challenge_cookie = raw_data['cookie']

		challenge_cookie.pop('maxAge')
		challenge_cookie_value = challenge_cookie['value']
		challenge_cookie_name = challenge_cookie['name']

		self.s.cookies.set_cookie(requests.cookies.create_cookie(**challenge_cookie))

		# on réalise la mystérieuse requête POST
		check_params = raw_data['checkChallengeParams']
		url = 'https://www.cdiscount.com/.well-known/baleen/challengejs/check?%s=%s' % (
			challenge_cookie_name, 
			challenge_cookie_value
		)
		data = '&'.join(['%s=%s' % (k,v) for k, v in check_params.items()])
		response = self.s.post(url, data)
		assert response.status_code == 200

	@retry(JavascriptChallengeError, tries=5, delay=5, backoff=1)
	def get_cdiscount_data(self): 
		print('start')

		# on fait une première requête
		response = self.s.get(URL, headers=HEADERS)
		with open('first_req.html', 'w') as f: 
			f.write(response.text)

		# en cas de challenge javascript, on résoud ce challenge et on recommence
		if 'Le JavaScript n\'est pas' in response.text: 
			self.solve_javascript_challenge(response)
			raise JavascriptChallengeError

		# on s'assure que la réponse obtenue est exploitable
		# merci georgio
		assert response.status_code == 200
		assert 'GEORGES' in response.text

		# on parse le document avec lxml
		doc = html.fromstring(response.text)
		
		products = doc.xpath("//ul/[@id='lpBloc']/li[@data-sku]")
		assert products
		for product in products: 

			# on récupère les attributs de chaque produit
			
			sku = product.get('data-sku')
			url = "".join(product.xpath('./a/@href'))
			assert url.startswith('https://www.cdiscount.com')

			image_url = "".join(product.xpath('.//li/img[@class="prdtBImg"]/@data-src')) or "".join(product.xpath(".//li/img[@class='prdtBImg']/@src"))

			title = "".join(product.xpath('.//h2[@class="prdtTit"]/text()'))

			score = "".join(product.xpath('.//span[@class="c-stars-result c-stars-result--small"]/@data-score'))
			if score: 
				score = float(score) / 20 

			reviews_count = "".join(product.xpath('.//span[@class="c-stars-result c-stars-result--small"]/following-sibling::span/text()'))
			if reviews_count: 
				reviews_count = int(reviews_count.strip('()'))

			price = "".join(product.xpath('.//span[contains(@class, "price priceColor hideFromPro") and not(contains(@class, "price--xs"))]/text()')).strip('€')
			is_cdav = len(product.xpath('.//div[@class="cdavZone"]')) > 0

			values = [sku, url, image_url, title, score, reviews_count, price, is_cdav]

			d = dict(zip(FIELDNAMES, values))
			print(d['title'], d['score'], d['price'])

			DATA.append(d)

		return DATA

if __name__ == '__main__':
	cdiscount_scraper = cDiscountScraper()
	DATA = cdiscount_scraper.get_cdiscount_data()
```

Et à l’exécution du script, de superbes données, parfaitement récupérées depuis le code source: 

```bash
$ python3 cdiscount_scraper_202306.py
start
solving javascript challenge
start
TECTAKE Barbecue, Grill, Fumoir, Smoker Américain XXL avec thermomètre de température - Charbon de Bois 3.0 94,99
Barbecue charbon Premium Ø57cm - Charles - noir émaillé - aérateurs, acier émaillé, fumoir, récupérateur de cendres 4.0 90,99
Barbecue au gaz 3 brûleurs, tablettes rabattables - Bonacieux - noir, avec rangement 4.5 139,99
COSTWAY Barbecue à Charbon BBQ avec Tablette Latérale Pliable,2 Roues 2 Grilles Double Thermomètre 120,6x68,2x107,3 cm Noir 5.0 194,99
Barbecue à charbon GEORGES - Grille en acier chromé - Surface de cuisson : 54 cm - Noir 4.0 49,99
Barbecue à charbon WEBER Classic Kettle 47 cm - Noir 4.5 99,00
Barbecue à charbon WEBER Compact Kettle 47 cm  - Noir 4.5 81,99
...
```

Et après?

### 4. Enregistrer les données dans un fichier .csv

On a contourné les obstacles divers et variés présents sur notre route, et récupéré dans une page HTML plutôt chargée, tous les attributs de nos grosses machines à charbon.

Bravo!

Dans cette dernière partie, on va simplement **enregistrer ces données dans un fichier .csv**.

On dispose d’une liste de dictionnaires, pour cette dernière partie, on va donc utiliser l’outil _csvDictwriter_, un objet directement accessible depuis la librairie csv native de Python.

Le lien de la documentation est accessible juste [ici](https://docs.python.org/3/library/csv.html).

On va donc fonctionner comme suit: 

1. Créer un fichier .csv
2. Créer l’outil d’écriture càd le _writer_
3. Écrire les headers, càd la première ligne du fichier avec les labels
4. Écrire chaque dictionnaire dans une ligne à part entière

Et le code commenté ici: 

```python
import csv
...

class cDiscountScraper: 

...

	def write_csv(self, DATA): 
		print('starting writing csv')
		
		# on crée un fichier csv qui a pour nom 'cdiscount_results.csv'
		with open('cdiscount_results.csv', 'w') as f: 
			
			# on crée notre objet writer, en lui fournissant la liste des labels et le fichier créé f
			writer = csv.DictWriter(f, fieldnames=FIELDNAMES)
			
			# on crée la première ligne avec les labels
			writer.writeheader()

			# on enregistre chaque dictionnaire dans une ligne dédiée
			for d in DATA: 
				writer.writerow(d)

...

if __name__ == '__main__':
	cdiscount_scraper = cDiscountScraper()
	DATA = cdiscount_scraper.get_cdiscount_data()
	cdiscount_scraper.write_csv(DATA)
	print('success > done')
```


On lance le script et..
Ta-da!

Toutes les données ont été proprement collectées, et sont directement exploitables au format csv. C’est propre, structuré, et ultra-rapide.

![comment-scraper-cdiscount-avec-python-et-requests-en-2023-image17.png](https://d37gzvgyugjozl.cloudfront.net/comment_scraper_cdiscount_avec_python_et_requests_en_2023_image17_5b8e932dae.png "width=1999;height=1276")

## Bénéfices

On vient de le voir dans la partie précédente, la collecte de données produits sur cDiscount, à partir d’une URL fonctionne à merveille.

Quelles sont les bénéfices de cette solution?

La méthode est **rapide**. Ainsi, on a pu scraper les données d’une page produit cDiscount, soit 50 produits distincts, en un peu moins de 2 secondes. Étendue à plus large échelle, ça représente les données de 1800 produits récupérés chaque heure.

A titre de comparaison, on estime empiriquement qu’un être humain, pour réaliser la même tâche, va mettre 10 secondes par article, soit 1800 produits récupérés toutes les 5 heures. Il s’agit donc d’**un rapport de 1 à 5**, par rapport à une méthode traditionnelle.

Par ailleurs, cette méthode est **précise**. Sauf cas non anticipé, ce qui peut arriver, les données vont être collectées systématiquement de la même manière. Pas d’erreur, pas d’imprécision. Des données toujours précises et exploitables.

Enfin, le scraping avec python et requests sur cdiscount est une méthode **ultra compétitive**. Un [serveur sur scaleway](https://www.scaleway.com/fr/elastic-metal/) coûte approx. 50 EUR par mois, c’est à peine le prix d’une demi-journée d’effort manuel en France, payé au salaire minimum.


## Limitations

Si les avantages sont certains, quelles sont les désavantages?

D’abord, dans le cadre de ce tutoriel rapide, l’outil n’a **pas de pagination**. On récupère les données de la première page de produits, mais pas les données des pages suivantes. L’exhaustivité est donc limitée.

Par ailleurs, toujours dans le cadre de ce tutoriel de démonstration, on ne visite pas la page de chaque produit. Aussi, **on passe à côté d’une large gamme d’attributs**. Prix de la livraison, nom du vendeur, points fort, date de livraison, caractéristiques avancées du produit ou avis clients: tous ces éléments manquent.

Enfin, plus grave, il arrive (assez rapidement) que **cDiscount vous demande de résoudre un captcha**, ici un [hCaptcha](https://www.hcaptcha.com/). Ce tutoriel étant ce qu’il est, la résolution de ce nouveau challenge n’est pas prise en charge.

![comment-scraper-cdiscount-avec-python-et-requests-en-2023-image18.png](https://d37gzvgyugjozl.cloudfront.net/comment_scraper_cdiscount_avec_python_et_requests_en_2023_image18_299ffb6ef2.png "width=1999;height=1143")

🤖


## FAQ

### Est-ce que le scraping sur cDiscount est légal en France?

Oui, c’est **entièrement légal**!

Comme stipulé dans [l’article L342-3 du code de la propriété intellectuelle](https://www.legifrance.gouv.fr/codes/article_lc/LEGIARTI000044365654), lorsqu’une base de données est mise à disposition du public, celui-ci peut l’exploiter, tant qu’il s’agit d’une exploitation non exhaustive.

Vous trouverez un article détaillé [ici](https://www.lobstr.io/blog/data-scraping-illegal).

### Est-ce qu’il existe une solution no-code de scraping de produits cDiscount?

**Non**, pas pour le moment.
On y réfléchit!

🤓

### Pourquoi ne pas utiliser l’API officielle cDiscount?

C’est vrai, une API existe bien.
Et sa documentation est accessible ici: 
[https://marketplace.cdiscount.com/api/](https://marketplace.cdiscount.com/api/)

Toutefois, **l’API ne permet pas de collecter les données présentes sur le site**: catégories, produits, vendeurs, reviews.

Elle permet simplement d’effectuer programmatiquement des tâches du côté vendeur: modification de prix ou de stock, mise en ligne ou retrait de produit, logistique.

Pour tout ce qui est récupération de données publiquement accessibles, **<span style="text-decoration:underline;">le scraping est indispensable</span>**.

### Est-ce que le script de scraping cDiscount va toujours fonctionner?

A priori, **oui**!
Le scraper de produits à partir d’une URL de catégorie cDiscount a été construit pour être résilient dans le temps.

Toutefois, il peut arriver que la structure du site change. 
Dans ce cas là, n’hésitez pas à nous contacter via le chat, ou directement depuis **[notre page dédiée](https://www.lobstr.io/custom)**.

## Conclusion

Et voilà, c’est la fin de ce tutoriel!

Dans ce tutoriel, on a vu **comment scraper les données des produits à partir d’une catégorie cDiscount avec Python, et requests**. Et récupérer les attributs principaux: le titre, le prix, le nombre d’avis, ou le score.

Surtout, nous avons vu comment **résoudre deux challenges successifs**: la page 403, premier obstacle pour scraper brutal et pressé, et le challenge javascript, script présent sur la page qu’il a fallu nous même exécuter avec un code Python maison.

Attention, ce tutoriel, parce que c’est un tutoriel, est limité: pas de pagination, pas d’attributs supplémentaires, et surtout pas de gestion du hCaptcha, qui finit par limiter la navigation sur cDiscount.

Et dans ce cas là, on vous invite à **nous contacter directement [ici](https://www.lobstr.io/custom)**.

Happy scraping!

🦀

---

## Related articles

- [Comment télécharger des ebooks depuis un .onion avec Python3 ](https://www.lobstr.io/fr/blog/comment-telecharger-des-ebooks-depuis-un-onion-avec-python3-1)
- [Comment scraper des PDFs avec Python3 et Tika?](https://www.lobstr.io/fr/blog/comment-scraper-des-pdfs-avec-python3-et-tika)
- [Comment scraper des Tweets gratos avec Python et requests en 2023?](https://www.lobstr.io/fr/blog/comment-scraping-des-tweets-gratos-avec-python-et-requests-en-2023)

Tags: Tutoriels, Code, Python, Web Scraping, cDiscount, Price Monitoring

_Canonical HTML: https://www.lobstr.io/fr/blog/comment-scraper-c-discount-avec-python-et-requests-en-2023_

More for agents: https://www.lobstr.io/llms.txt, pricing in Markdown: https://www.lobstr.io/pricing.md
