Cours Python · Module 4: Données, API et bases de données
Web scraping - collecter des données sur le web
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 :
- Téléchargeant le HTML de la page (comme un navigateur)
- Analysant la structure HTML (le DOM)
- Trouvant les éléments qui t'intéressent (tableaux, listes, titres)
- 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- IDdiv.species- balise + classediv > p- enfant directdiv 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')}")Naviguer dans l'arbre DOM
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 2Exemple 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-dessusBonnes 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êtes2. 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 hasard3. 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 None5. 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 htmlPiè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. BeautifulSoup sert à :
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')