Web-scrapping en Python

A partir d'une URL, récupérer des données de manière récursive.

Posté le Sept. 21, 2020

Introduction

L'idée ici est de pouvoir aller chercher des données sur le net sans forcément passer par une API. Nous allons prendre pour exemple le site

https://www.lacentrale.fr/

A partir d'une url de recherche comme par exemple

https://www.lacentrale.fr/listing?energies=ess&gearbox=AUTO&makesModelsCommercialNames=PEUGEOT

nous souhaitons que le programme:

  1. Identifie la liste totale des URLs correspondant à des annonces
    • Récupère de manière itérative les urls des annonces sur la page qui les listes
    • Récupère les urls des pages suivantes pour pouvoir reprendre l'étape précédente
  2. Pour chaque url, le programme doit identifier les champs utiles et les stocker dans un tableau.

Pour récupérer le code html nous allons utiliser la bibliothèque requests Pour parser le code html, nous allons utiliser la bibliothèque beautifulsoup

1 - Lister les urls des annonces

Partant de l'url suivante

https://www.lacentrale.fr/listing?energies=ess&gearbox=AUTO&makesModelsCommercialNames=PEUGEOT

les critères de recherche figurent en argument et permettent de restreindre le nombre de résultats:

  • energies=ess
  • gearbox=AUTO
  • makesModelsCommercialNames=PEUGEOT

Dans un premier temps, nous allons identifier où se situent les urls des annonces dans la page listing. Pour ce faire, il est possible d'utiliser la console web, par exemple dans firefox, à l'aide du raccourcis Ctrl + Maj + k. L'onglet Inspecteur permet d'identifier les balises qui contiennent les annonces.

Dans notre cas, chaque annonce se trouve dans une balise div de la classe adContainer. Dans l'arborescence html, il y a une balise a qui permet d'accéder à la page de l'annonce juste après.

<div class="adContainer">
    <p class="favContainer"> ... </p>
    <p class="adContainer__hideButton"> ... </p>
    <a href="/auto-occasion-annonce-69106838313.html" ... > ... </a>
</div>

Le but de la manœuvre est donc de récupérer la valeur de l'attribut href qui autre que l'url de l'annonce. Voici la marche à suivre:

from bs4 import BeautifulSoup
import requests

# Url de la première page listing issue de notre recherche
url = 'https://www.lacentrale.fr/listing?energies=ess&gearbox=AUTO&makesModelsCommercialNames=PEUGEOT'

# Réccupération du code html
html = requests.get(url).text

# Création de l'objet soup qui permet de naviger dans l'arborescence html
soup = BeautifulSoup(html)

# Boucle sur les balises div de classe adContainer et extraction de l'url
for e in soup.find_all("div", {"class": "adContainer"}):
    sub_soup = BeautifulSoup(e.decode_contents())
    print("url : ", sub_soup.find("a")['href'])
  • soup.find_all("div", {"class": "adContainer"}) retourne une liste qui contient toutes les balises div de classe adContainer.
  • sub_soup.find("a")['href'] retourne la valeure de l'attribut href de la balise a

Pour la suite, nous définirons la fonction suivante qui prend en argument une url (listing) et qui retourne une liste d'urls (annonces).

def get_ad_urls(url):
    result = []
    html = requests.get(url).text
    soup = BeautifulSoup(html)
    for e in soup.find_all("div", {"class": "adContainer"}):
        sub_soup = BeautifulSoup(e.decode_contents())
        result.append(sub_soup.find("a")['href'])

    return result

2 - Accéder au listing suivant

Pour des soucis de performance, le listing d'annonce retourné par notre recherche de voiture est scindé en plusieurs pages.

Nous allons voir comment identifier la page suivante afin de pouvoir y rechercher les urls des annonces qui s'y trouvent.

En reprenant la démarche précédente, je vais vouloir identifier l'url qui permet d'accéder à la page suivante. A l'aide de la console, j'ai identifié la balise qui correspond:

<ul>
    ...
    <li class="arrow-btn ">
        <a href="/listing?....page=2" title="Page suivante">
            <i class="cbm-picto--arrowR"></i>
        </a>
    </li>
</ul>

Le code html ci-dessus est une structure de liste qui définie la pagination des différentes pages du listing des annonces recherchées. Notre but est d'extraire l'attribut href de la balise a de l’icône qui conduit à la page suivante du listing.

html = requests.get(url)
soup = BeautifulSoup(html.text)

# itération sur les balises i ayant la class arrow-btn
for e in soup.find_all("li", {"class": "arrow-btn"}):

    # filtre sur la flêche suivante
    if "suivante" in e.decode_contents():
        print(e.find("a")['href'])

Pour simplifier nous utiliserons la fonction suivante:

def get_lising_next(url):
    html = requests.get(url)
    soup = BeautifulSoup(html.text)
    for e in soup.find_all("li", {"class": "arrow-btn"}):
        if "suivante" in e.decode_contents():
            return e.find("a")['href']

3 - Réccupérer toutes les urls

L'objectif est donc d'obtenir toute les urls des annonces qui découlent d'une recherche.

url = '/listing?energies=ess&gearbox=AUTO&makesModelsCommercialNames=PEUGEOT&regions=FR-BRE'
url_template = 'https://www.lacentrale.fr{}'
ad_list = []

while url:
    ad_list += get_ad_urls(url_template.format(url))
    url = get_lising_next(url_template.format(url))

En partant de la recherche des peugeots essence en boite auto situées en Bretagne, nous aboutissons donc à un listing de 12 pages contenant 180 annonces.

4 - Parser une annonce

Voici la cœur du problème. Il s'agit de récupérer les champs qui nous intéressent à partir de l'url d'une annonce. Pour ce faire il y a plusieurs méthodes mais dans l'absolu, il faut partir de la façon dont les données sont contenues dans le code html. Dans notre cas, nous avons une balise qui contient une structure de donnée de type json et c'est ce nous allons rechercher.

<div id="trackingStateContainer" style="display: none;">
    {'seller': {'ref': 'C029085',
      'type': 'CONCESSIONNAIRE',
      'city': 'CESSON SEVIGNE'},
     'vehicle': {'vertical': 'auto',
      'price': {'price': 18875, 'isCrossed': False, 'isDropping': False},
      'options': [{'label': 'caméra de recul'},
       {'label': 'connexion SOS'},
       {'label': 'peinture métallisée'},
       {'label': 'roue de secours'}],
      'make': 'PEUGEOT',
      'model': '2008',
      'version': '(2) 1.2 PURETECH 130 S&S ALLURE EAT6',
      'commercialName': '2008',
      'category': 'SUV_4X4_CROSSOVER',
      'gearboxId': 'AUTO',
      'energy': 'ESSENCE',
      'year': '2019',
      'externalColor': 'gris',
      'firstHand': True,
      'doors': 5,
      'zipcode': '35510',
      'powerDIN': 131,
      'ratedHorsePower': 6,
      'mileage': 22341,
      'fourWheels': False,
     'classified': {'id': '69107169252',
      'ref': 'E107169252',
      'ownerCorrelationId': 'C029085',
      'url': 'https://www.lacentrale.fr/auto-occasion-annonce-69107169252.html'}}
 </div>

Je vais donc identifier la balise qui contient la structure de donnée pour ensuite la transformer en un objet JSON.

import json

url = '/auto-occasion-annonce-69107169252.html'
url_template = 'https://www.lacentrale.fr{}'
html = requests.get("https://www.lacentrale.fr/auto-occasion-annonce-69107169252.html").text

soup.find("div", {"id": "trackingStateContainer"}).getText()
json_data = json.loads(soup.find("div", {"id": "trackingStateContainer"}).getText())

L'objet json_data se comporte comme un dictionnaire, on peut accéder aux données grâce à la syntaxe json_data['key'], par exemple voici comment afficher le type de motorisation et le nom de la marque:

Nous allons donc créer un dictionnaire avec les champs suivants:

  • prix
  • marque
  • model
  • energie
  • année
  • codepostal
  • puissance
  • km
data = {'prix': json_data['vehicle']['price']['price'],
        'marque': json_data['vehicle']['make'],
        'model': json_data['vehicle']['model'],
        'energie': json_data['vehicle']['energy'],
        'année': json_data['vehicle']['year'],
        'codepostal': json_data['vehicle']['zipcode'],
        'puissance': json_data['vehicle']['powerDIN'],
        'km': json_data['vehicle']['mileage']}

Puis l'injecter dans un objet DataFrame:

df = pd.DataFrame()
df = df.append(data, ignore_index=True)

Pour simplifier, voici la fonction qui prend en entrée l'url d'une annonce et qui retourne le dictionnaire des données:

url = '/auto-occasion-annonce-69107169252.html'

def get_data(url):

    try:
        url_template = 'https://www.lacentrale.fr{}'
        html = requests.get(url_template.format(url)).text
        soup = BeautifulSoup(html)
        soup.find("div", {"id": "trackingStateContainer"}).getText()
        json_data = json.loads(soup.find("div", {"id": "trackingStateContainer"}).getText())

        return {'prix': json_data['vehicle']['price']['price'],
                'marque': json_data['vehicle']['make'],
                'model': json_data['vehicle']['model'],
                'energie': json_data['vehicle']['energy'],
                'année': json_data['vehicle']['year'],
                'codepostal': json_data['vehicle']['zipcode'],
                'puissance': json_data['vehicle']['powerDIN'],
                'km': json_data['vehicle']['mileage']}

    except Exception as e:
        print("Error : {}".format(e))
        return None

5 - Créer un tableau avec l'ensemble des annonces

En assemblant les briques précédentes, on peut aboutir à un tableau contenant l'ensemble des données souhaitées.

df = pd.DataFrame()

for ad_url in ad_list:
    data = get_data(ad_url)
    if data:
        df = df.append(data, ignore_index=True)

Les prochaines étapes pourraient être l'alimentation d'une base de donnée à travers un ORM comme par exemple SQLAlchemy:

#1 - SQLAlchemy

Le code source du développement est sur ma page Github