Cours Python · Module 4: Données, API et bases de données

Web scraping - collecter des données sur le web

12 min de lecture
Dans cette leçon12

Content de te revoir ! Ici Darwin, avec une leçon passionnante.

Tu as appris à récupérer des données depuis des API REST - des interfaces élégantes et structurées. Mais que faire si un site web n'a pas d'API ? Que faire si les données sur les espèces sont cachées dans un tableau HTML sur une page web ?

Alors nous utilisons le web scraping - le téléchargement et l'extraction automatiques de données depuis des sites web !

Analogie safari : Une API REST, c'est comme un rapport officiel d'un parc national au format JSON. Le web scraping, c'est comme lire une brochure touristique et en extraire manuellement les informations : « Ah, ici il est écrit qu'il y a 120 lions, et là que l'habitat est la savane » !

Qu'est-ce que le web scraping ?

Le web scraping est l'extraction automatique de données depuis des sites web en :

  1. Téléchargeant le HTML de la page (comme un navigateur)
  2. Analysant la structure HTML (le DOM)
  3. Trouvant les éléments qui t'intéressent (tableaux, listes, titres)
  4. Extrayant les données (texte, liens, images)

Quand utiliser le web scraping ?

À utiliser quand :

  • Le site n'a pas d'API
  • L'API est payante, mais les données sont publiques
  • Tu as besoin de données historiques (archives)
  • Tu agrèges des données de plusieurs sources

À éviter quand :

  • Le site a une API officielle (utilise l'API !)
  • Les conditions d'utilisation interdisent le scraping
  • Les données sont protégées juridiquement
  • Tu risques de violer le RGPD/la vie privée

Légalité et éthique

IMPORTANT : Le web scraping est légal quand :

  • Tu scrapes des données publiques (accessibles sans connexion)
  • Tu respectes robots.txt (https://example.com/robots.txt)
  • Tu ne surcharges pas le serveur (rate limiting !)
  • Tu ne violes pas de droits d'auteur

Mauvaises pratiques :

  • Ignorer robots.txt
  • Des milliers de requêtes par seconde (DDoS)
  • Scraper des données personnelles
  • Contourner une connexion / un paywall

Règle d'or : comporte-toi comme un humain - visite le site de façon raisonnable, ne surcharge pas le serveur !

BeautifulSoup4 - un parseur HTML en Python

BeautifulSoup4 (bs4) est la bibliothèque la plus populaire pour analyser du HTML/XML en Python.

Installation

1pip install beautifulsoup4
2pip install requests  # Pour télécharger le HTML
3pip install lxml      # Parseur rapide (optionnel)

Utilisation de base

1import requests
2from bs4 import BeautifulSoup
3
4# 1. Télécharger le HTML
5response = requests.get("https://example.com/species")
6html_content = response.text
7
8# 2. Analyser le HTML
9soup = BeautifulSoup(html_content, "html.parser")  # ou "lxml"
10
11# 3. Trouver des éléments
12title = soup.find("h1")
13print(title.text)  # Texte du titre
14
15# 4. Trouver tous les éléments
16species_items = soup.find_all("div", class_="species-card")
17for item in species_items:
18    name = item.find("span", class_="name").text
19    print(name)

Anatomie du HTML - qu'est-ce qu'on analyse ?

Le HTML est un arbre d'éléments (balises) :

1<div class="species-card" id="lion" data-population="120">
2    <h2>Lion</h2>
3    <p class="description">Panthera leo</p>
4    <img src="/images/lion.jpg" alt="Lion">
5    <a href="/species/lion">Plus d'informations</a>
6</div>

Éléments à extraire :

  • Texte : <h2>Lion</h2> -> "Lion"
  • Attributs : class="species-card", id="lion"
  • Liens : <a href="/species/lion">
  • Images : <img src="/images/lion.jpg">
  • Données : data-population="120"

Trouver des éléments - les sélecteurs

find() - le premier élément correspondant

1from bs4 import BeautifulSoup
2
3html = """
4<div class="container">
5    <h1>Safari Data</h1>
6    <div class="species">Lion</div>
7    <div class="species">Elephant</div>
8</div>
9"""
10
11soup = BeautifulSoup(html, "html.parser")
12
13# Trouver le premier élément
14title = soup.find("h1")
15print(title.text)  # "Safari Data"
16
17# Trouver le premier élément avec une classe
18species = soup.find("div", class_="species")
19print(species.text)  # "Lion" (le premier !)

find_all() - tous les éléments correspondants

1# Trouver tous les éléments de classe "species"
2all_species = soup.find_all("div", class_="species")
3print(len(all_species))  # 2
4
5for species in all_species:
6    print(species.text)
7# Lion
8# Elephant
9
10# Limiter les résultats
11first_two = soup.find_all("div", limit=2)

Sélecteur CSS - select()

1# Sélecteurs CSS (comme en CSS !)
2all_species = soup.select(".species")  # Classe
3title = soup.select_one("#main-title")  # ID
4links = soup.select("div.container a")  # Imbriqué

Sélecteurs CSS :

  • .species - classe
  • #lion - ID
  • div.species - balise + classe
  • div > p - enfant direct
  • div p - tout niveau d'imbrication
  • [data-population] - attribut

Extraction de données

Texte d'un élément

1html = '<div class="name">Lion <span>(Panthera leo)</span></div>'
2soup = BeautifulSoup(html, "html.parser")
3
4# .text - tout le texte (y compris les balises imbriquées)
5print(soup.find("div").text)  # "Lion (Panthera leo)"
6
7# .get_text() - avec un séparateur
8print(soup.find("div").get_text(strip=True))  # "Lion(Panthera leo)" (sans espaces)
9
10# .string - seulement le texte direct (None si imbriqué)
11print(soup.find("div").string)  # None (à cause de <span>)

Attributs d'un élément

1html = '<img src="/lion.jpg" alt="Lion" data-population="120">'
2soup = BeautifulSoup(html, "html.parser")
3img = soup.find("img")
4
5# Accès comme à un dictionnaire
6print(img["src"])              # "/lion.jpg"
7print(img["alt"])              # "Lion"
8print(img["data-population"])  # "120"
9
10# .get() - sûr (renvoie None si absent)
11print(img.get("title"))        # None
12print(img.get("src", "default.jpg"))  # "/lion.jpg"
13
14# Tous les attributs
15print(img.attrs)  # {'src': '/lion.jpg', 'alt': 'Lion', 'data-population': '120'}

Liens et URL

1html = '<a href="/species/lion" class="link">Voir le lion</a>'
2soup = BeautifulSoup(html, "html.parser")
3link = soup.find("a")
4
5# URL
6print(link["href"])  # "/species/lion"
7
8# Texte du lien
9print(link.text)     # "Voir le lion"
10
11# Tous les liens de la page
12for link in soup.find_all("a"):
13    print(f"{link.text}: {link.get('href')}")
1html = """
2<div class="parent">
3    <h2>Title</h2>
4    <p class="child1">Paragraphe 1</p>
5    <p class="child2">Paragraphe 2</p>
6</div>
7"""
8soup = BeautifulSoup(html, "html.parser")
9
10# Enfants
11parent = soup.find("div", class_="parent")
12for child in parent.children:
13    if child.name:  # Ignorer les nœuds texte
14        print(child.name)  # h2, p, p
15
16# Descendants - tous les éléments imbriqués
17for descendant in parent.descendants:
18    if hasattr(descendant, 'name') and descendant.name:
19        print(descendant.name)
20
21# Parent
22p = soup.find("p")
23print(p.parent.name)  # "div"
24
25# Frères et sœurs (siblings)
26h2 = soup.find("h2")
27for sibling in h2.next_siblings:
28    if sibling.name:
29        print(sibling.text)  # Paragraphe 1, Paragraphe 2

Exemple safari - scraping des données d'espèces

1import requests
2from bs4 import BeautifulSoup
3from typing import List, Dict
4import time
5
6class SafariWebScraper:
7    """Scraper de données d'espèces depuis un site web Safari"""
8
9    def __init__(self, base_url: str):
10        self.base_url = base_url
11        self.session = requests.Session()
12        self.session.headers.update({
13            "User-Agent": "Safari-Research-Bot/1.0 (Educational purposes)"
14        })
15
16    def get_soup(self, url: str) -> BeautifulSoup:
17        """Télécharge et analyse le HTML"""
18        response = self.session.get(url, timeout=10)
19        response.raise_for_status()
20        return BeautifulSoup(response.text, "html.parser")
21
22    def scrape_species_list(self, url: str) -> List[Dict[str, str]]:
23        """
24        Scrape la liste d'espèces depuis un tableau HTML
25
26        Structure d'exemple :
27        <table class="species-table">
28            <tr>
29                <td class="name">Lion</td>
30                <td class="scientific">Panthera leo</td>
31                <td class="population">120</td>
32                <td class="habitat">Savanna</td>
33            </tr>
34        </table>
35        """
36        soup = self.get_soup(url)
37        species_list = []
38
39        # Trouver le tableau
40        table = soup.find("table", class_="species-table")
41        if not table:
42            print("Attention : Tableau introuvable")
43            return []
44
45        # Parcourir les lignes (en sautant l'en-tête)
46        rows = table.find_all("tr")[1:]  # [1:] saute l'en-tête
47
48        for row in rows:
49            cells = row.find_all("td")
50            if len(cells) >= 4:
51                species = {
52                    "common_name": cells[0].text.strip(),
53                    "scientific_name": cells[1].text.strip(),
54                    "population": cells[2].text.strip(),
55                    "habitat": cells[3].text.strip()
56                }
57                species_list.append(species)
58
59        return species_list
60
61    def scrape_species_cards(self, url: str) -> List[Dict[str, str]]:
62        """
63        Scrape les espèces depuis des cartes HTML
64
65        <div class="species-card">
66            <h3>Lion</h3>
67            <p class="scientific">Panthera leo</p>
68            <span class="population" data-count="120">120 individus</span>
69            <img src="/images/lion.jpg" alt="Lion">
70            <a href="/species/lion">Plus</a>
71        </div>
72        """
73        soup = self.get_soup(url)
74        species_list = []
75
76        # Trouver toutes les cartes
77        cards = soup.find_all("div", class_="species-card")
78
79        for card in cards:
80            # Extraction sûre (des éléments peuvent manquer)
81            name_tag = card.find("h3")
82            scientific_tag = card.find("p", class_="scientific")
83            pop_tag = card.find("span", class_="population")
84            img_tag = card.find("img")
85            link_tag = card.find("a")
86
87            species = {
88                "common_name": name_tag.text.strip() if name_tag else "Inconnu",
89                "scientific_name": scientific_tag.text.strip() if scientific_tag else "Inconnu",
90                "population": pop_tag.get("data-count", "0") if pop_tag else "0",
91                "image_url": img_tag["src"] if img_tag else None,
92                "detail_url": link_tag["href"] if link_tag else None
93            }
94            species_list.append(species)
95
96        return species_list
97
98    def scrape_species_detail(self, url: str) -> Dict[str, any]:
99        """
100        Scrape les détails d'une espèce
101
102        <div class="species-detail">
103            <h1>Lion</h1>
104            <div class="info">
105                <dl>
106                    <dt>Scientific name:</dt>
107                    <dd>Panthera leo</dd>
108                    <dt>Status:</dt>
109                    <dd class="endangered">Endangered</dd>
110                    <dt>Habitat:</dt>
111                    <dd>Savanna, steppe</dd>
112                </dl>
113            </div>
114            <div class="description">
115                <p>Le lion est le plus grand félin d'Afrique...</p>
116            </div>
117        </div>
118        """
119        soup = self.get_soup(url)
120
121        # Nom
122        title = soup.find("h1")
123        common_name = title.text.strip() if title else "Inconnu"
124
125        # Liste de définition - paires dt/dd
126        info = {}
127        dl = soup.find("dl")
128        if dl:
129            terms = dl.find_all("dt")
130            definitions = dl.find_all("dd")
131
132            for term, definition in zip(terms, definitions):
133                key = term.text.strip().replace(":", "").lower()
134                value = definition.text.strip()
135                info[key] = value
136
137        # Description
138        description_div = soup.find("div", class_="description")
139        description = description_div.get_text(strip=True) if description_div else ""
140
141        return {
142            "common_name": common_name,
143            "scientific_name": info.get("scientific name", "Inconnu"),
144            "status": info.get("status", "Inconnu"),
145            "habitat": info.get("habitat", "Inconnu"),
146            "description": description
147        }
148
149    def scrape_with_pagination(self, base_url: str, max_pages: int = 5) -> List[Dict]:
150        """
151        Scrape plusieurs pages avec pagination
152
153        Schéma d'URL : /species?page=1, /species?page=2, ...
154        """
155        all_species = []
156
157        for page_num in range(1, max_pages + 1):
158            url = f"{base_url}?page={page_num}"
159            print(f"Scraping de la page {page_num} : {url}")
160
161            soup = self.get_soup(url)
162
163            # Vérifier s'il y a une page suivante
164            next_button = soup.find("a", class_="next-page")
165            if not next_button:
166                print(f"Dernière page : {page_num}")
167                break
168
169            # Scraper les données de la page
170            species = self.scrape_species_cards(url)
171            all_species.extend(species)
172
173            # Rate limiting - attends entre les requêtes !
174            time.sleep(1)  # 1 seconde entre les pages
175
176        return all_species
177
178
179# === DÉMONSTRATION ===
180
181print("=== WEB SCRAPER SAFARI ===\n")
182
183scraper = SafariWebScraper(base_url="https://example-safari.com")
184
185# 1. Scraper la liste depuis un tableau
186print("1. Scraping du tableau des espèces...")
187species_table = scraper.scrape_species_list("https://example-safari.com/species-table")
188print(f"   {len(species_table)} espèces trouvées")
189for species in species_table[:3]:
190    print(f"   - {species['common_name']} ({species['scientific_name']})")
191
192# 2. Scraper des cartes
193print("\n2. Scraping des cartes d'espèces...")
194species_cards = scraper.scrape_species_cards("https://example-safari.com/species")
195print(f"   {len(species_cards)} cartes trouvées")
196
197# 3. Scraper des détails
198print("\n3. Scraping des détails du lion...")
199lion_detail = scraper.scrape_species_detail("https://example-safari.com/species/lion")
200print(f"   Nom : {lion_detail['common_name']}")
201print(f"   Statut : {lion_detail['status']}")
202print(f"   Habitat : {lion_detail['habitat']}")
203
204# 4. Scraper avec pagination
205print("\n4. Scraping avec pagination...")
206all_species = scraper.scrape_with_pagination("https://example-safari.com/species", max_pages=3)
207print(f"   Total : {len(all_species)} espèces sur 3 pages")
208
209print("\nScraping terminé")

Selenium - scraper des pages dynamiques

Problème : certaines pages chargent les données via JavaScript - BeautifulSoup voit un HTML vide !

Solution : Selenium - il automatise un vrai navigateur !

Installation

1pip install selenium
2# Télécharge ChromeDriver : https://chromedriver.chromium.org/

Les bases de Selenium

1from selenium import webdriver
2from selenium.webdriver.common.by import By
3from selenium.webdriver.support.ui import WebDriverWait
4from selenium.webdriver.support import expected_conditions as EC
5import time
6
7# Lancer le navigateur
8driver = webdriver.Chrome()  # Nécessite ChromeDriver
9
10try:
11    # Ouvrir la page
12    driver.get("https://example.com/species")
13
14    # Attendre que l'élément se charge (JavaScript !)
15    wait = WebDriverWait(driver, 10)
16    species_cards = wait.until(
17        EC.presence_of_all_elements_located((By.CLASS_NAME, "species-card"))
18    )
19
20    # Extraire les données
21    for card in species_cards:
22        name = card.find_element(By.TAG_NAME, "h3").text
23        population = card.find_element(By.CLASS_NAME, "population").text
24        print(f"{name}: {population}")
25
26    # Cliquer sur le bouton « Charger plus »
27    load_more = driver.find_element(By.ID, "load-more-btn")
28    load_more.click()
29    time.sleep(2)  # Attendre le chargement
30
31    # Défiler vers le bas (défilement infini)
32    driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
33    time.sleep(2)
34
35finally:
36    # Ferme toujours le navigateur !
37    driver.quit()

Mode headless - sans interface graphique

1from selenium.webdriver.chrome.options import Options
2
3options = Options()
4options.add_argument("--headless")  # Pas de fenêtre du navigateur
5options.add_argument("--no-sandbox")
6options.add_argument("--disable-dev-shm-usage")
7
8driver = webdriver.Chrome(options=options)
9# Le reste comme ci-dessus

Bonnes pratiques du web scraping

1. Vérifie robots.txt

1import requests
2
3response = requests.get("https://example.com/robots.txt")
4print(response.text)
5
6# Vérifie si ton scraper est autorisé :
7# User-agent: *
8# Disallow: /admin/
9# Crawl-delay: 10  # Attends 10s entre les requêtes

2. Rate Limiting - ne surcharge pas le serveur !

1import time
2import random
3
4for url in urls:
5    scrape_page(url)
6
7    # Attendre entre les requêtes
8    time.sleep(random.uniform(1, 3))  # 1-3 secondes au hasard

3. User-Agent - identifie-toi

1headers = {
2    "User-Agent": "MyResearchBot/1.0 (+https://mysite.com/bot-info; contact@mysite.com)"
3}
4response = requests.get(url, headers=headers)

4. Gestion des erreurs

1from requests.exceptions import RequestException
2
3def safe_scrape(url):
4    try:
5        response = requests.get(url, timeout=10)
6        response.raise_for_status()
7        soup = BeautifulSoup(response.text, "html.parser")
8        return soup
9
10    except RequestException as e:
11        print(f"Erreur : {e}")
12        return None

5. Cache - ne télécharge pas deux fois la même chose

1import pickle
2from pathlib import Path
3
4def get_cached_html(url, cache_dir="cache"):
5    Path(cache_dir).mkdir(exist_ok=True)
6    cache_file = Path(cache_dir) / f"{hash(url)}.pkl"
7
8    # Vérifier le cache
9    if cache_file.exists():
10        with open(cache_file, "rb") as f:
11            return pickle.load(f)
12
13    # Télécharger et enregistrer
14    response = requests.get(url)
15    html = response.text
16
17    with open(cache_file, "wb") as f:
18        pickle.dump(html, f)
19
20    return html

Pièges courants

1. La structure HTML change

Les sites web changent leur mise en page - ton scraper cesse de fonctionner !

Solution : utilise plusieurs sélecteurs, une gestion des erreurs

1# Plusieurs tentatives pour trouver un élément
2name = (
3    soup.find("h3", class_="species-name") or
4    soup.find("span", class_="name") or
5    soup.find("div", attrs={"data-type": "name"})
6)

2. JavaScript dynamique

BeautifulSoup ne voit que le HTML initial, pas le résultat du JS !

Solution : Selenium, ou analyse l'API (regarde l'onglet Network dans les DevTools)

3. Les mécanismes anti-scraping

  • CAPTCHA
  • Rate limiting (429 Too Many Requests)
  • Bannissement d'IP
  • Cookies/sessions requis

Solution : respecte les règles, n'attaque pas, envisage d'utiliser l'API

Résumé

Dans cette leçon, tu as appris :

  • Ce qu'est le web scraping et quand l'utiliser
  • La légalité et l'éthique du scraping (robots.txt, rate limiting)
  • BeautifulSoup4 : find(), find_all(), select()
  • L'extraction : texte, attributs, liens
  • La navigation dans le DOM : children, parent, siblings
  • Selenium pour les pages dynamiques (JavaScript)
  • Les bonnes pratiques : User-Agent, rate limiting, cache, gestion des erreurs
  • Les pièges courants et comment les éviter

Point de contrôle

Avant de continuer :

  • Tu sais analyser du HTML avec BeautifulSoup
  • Tu sais trouver des éléments : find(), select()
  • Tu sais extraire du texte et des attributs
  • Tu comprends la différence entre BeautifulSoup et Selenium
  • Tu connais l'éthique du web scraping

Analogie safari : Le web scraping, c'est comme lire des brochures touristiques sur les parcs nationaux et collecter manuellement les données dans un tableur - chronophage, mais efficace quand il n'y a pas de rapports officiels (d'API) !

Dans la prochaine leçon, Darwin t'enseignera les bases de données SQL - comment stocker et gérer de grands ensembles de données de façon structurée !

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. BeautifulSoup sert à :

  2. 2. La méthode soup.find_all('a') renvoie :

Tâches pratiques dans le jeu

  • Tri horizontal

    Remets les éléments dans le bon ordre :

  • Éditeur de code

    Récupère la page et trouve tous les éléments <h1>

  • Éditeur de code

    Trouve tous les liens (attribut href) de la page

  • Tri vertical

    Remets dans l'ordre les étapes du web scraping :

  • Clique dans l'ordre

    Clique dans l'ordre : soup = BeautifulSoup(html, 'html.parser')

Articles utiles