Section 11.2.13 : Web Scraping — BeautifulSoup
🎯 Objectif pédagogique
Extraire des données de pages web avec BeautifulSoup. Vous serez capable de parser du HTML, extraire des informations structurées, et construire vos propres datasets à partir du web — une compétence inestimable quand les données n'existent pas sous forme d'API.
Quand le web est votre base de données
Certaines données n'existent pas en API ou en CSV. Les offres d'emploi sur un site, les prix d'un e-commerce, les avis clients — ces données sont dans le HTML de pages web. Le web scraping consiste à les extraire automatiquement.
Marc voulait comparer les salaires entre différents sites d'emploi. Pas d'API disponible. Solution : scraper les pages et construire son propre dataset.
Éthique et légalité du scraping
Avant de scraper un site, vérifiez :
- →robots.txt — le fichier qui indique ce qui est autorisé (
site.com/robots.txt) - →Conditions d'utilisation — certains sites interdisent le scraping
- →Fréquence — ne bombardez jamais un serveur (ajoutez des pauses)
- →Données personnelles — ne scrapez pas de données RGPD Le scraping pour usage personnel et éducatif est généralement toléré. Le scraping commercial peut poser problème.
BeautifulSoup — Parser le HTML
pip install beautifulsoup4 requests
from bs4 import BeautifulSoup
import requests
# Récupérer une page
url = "https://quotes.toscrape.com/" # Site de test pour le scraping
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
# Extraire le titre
print(soup.title.text) # "Quotes to Scrape"
# Trouver des éléments
quotes = soup.find_all('span', class_='text')
authors = soup.find_all('small', class_='author')
for quote, author in zip(quotes, authors):
print(f'"\{quote.text\}" — \{author.text\}')
Sélecteurs CSS
# Par tag
soup.find('h1') # Premier h1
soup.find_all('p') # Tous les p
# Par classe
soup.find_all('div', class_='quote')
soup.select('.quote') # CSS selector
# Par ID
soup.find(id='main-content')
soup.select('#main-content')
# Imbriqués
soup.select('div.quote span.text') # span.text dans div.quote
# Attributs
soup.find_all('a', href=True) # Tous les liens
soup.find_all('img', src=True) # Toutes les images
# Texte
soup.find_all(string='Python') # Éléments contenant "Python"
Exemple complet : scraper des citations
from bs4 import BeautifulSoup
import requests
import pandas as pd
import time
all_quotes = []
base_url = "https://quotes.toscrape.com/page/\{\}/"
for page in range(1, 6): # Pages 1 à 5
url = base_url.format(page)
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
for quote_div in soup.find_all('div', class_='quote'):
text = quote_div.find('span', class_='text').text
author = quote_div.find('small', class_='author').text
tags = [tag.text for tag in quote_div.find_all('a', class_='tag')]
all_quotes.append(\{
'text': text,
'author': author,
'tags': ', '.join(tags),
'page': page
\})
time.sleep(1) # Politesse : 1 seconde entre chaque requête
# Créer un DataFrame
df = pd.DataFrame(all_quotes)
print(f"\n\{len(df)\} citations récupérées")
print(df.head())
# Analyse
print(f"\nAuteurs les plus cités :")
print(df['author'].value_counts().head(5))
# Sauvegarder
df.to_csv('quotes.csv', index=False)
Gérer les cas complexes
# Headers (se faire passer pour un navigateur)
headers = \{
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
\}
response = requests.get(url, headers=headers)
# Gestion d'erreurs
try:
response = requests.get(url, timeout=10)
response.raise_for_status() # Lève une exception si status != 200
except requests.RequestException as e:
print(f"Erreur : \{e\}")
# Encoding
response.encoding = 'utf-8' # Forcer l'encodage si nécessaire
🏋️ Exercice pratique (25 minutes)
# Scraper https://quotes.toscrape.com
# 1. Récupérez les 10 premières pages
# 2. Créez un DataFrame avec : texte, auteur, tags, numéro de page
# 3. Quel auteur a le plus de citations ?
# 4. Quel tag est le plus fréquent ?
# 5. Sauvegardez en CSV
Pourquoi est-il important d'ajouter time.sleep() entre les requêtes de scraping ?
Que fait soup.find_all('div', class_='quote') ?