BeautifulSoup en Python est la bibliothèque vers laquelle la plupart des gens se tournent quand ils ont besoin d'extraire des données structurées d'un document HTML complexe. Elle transforme une page brute en un arbre d'objets Python navigable, puis vous offre une petite API lisible pour trouver les éléments qui vous intéressent et lire leur texte ou leurs attributs. Vous n'avez pas besoin d'apprendre un langage de requête ni d'écrire un analyseur ; vous décrivez ce que vous voulez avec un nom de balise, un attribut ou un sélecteur CSS, et BeautifulSoup vous le remet.

Ce guide est une visite pratique de cette API. Nous installons BeautifulSoup avec un analyseur rapide, construisons un soup à partir d'un balisage d'exemple, puis parcourons find et find_all, les méthodes de sélecteur CSS select et select_one, la navigation dans l'arbre par parent et frère, et la lecture du texte par rapport aux attributs. Nous terminons avec un exemple pratique réaliste qui extrait une liste d'enregistrements et suit la pagination. Une chose à garder à l'esprit tout au long : BeautifulSoup ne fait qu'analyser. Il ne récupère jamais une URL ni n'exécute JavaScript, donc le HTML que vous lui donnez doit déjà contenir les données que vous voulez.

Ce que BeautifulSoup fait, et ce qu'il ne fait pas

BeautifulSoup est une bibliothèque d'analyse. Vous lui passez une chaîne de HTML ou XML et elle construit un arbre que vous pouvez parcourir et rechercher. C'est l'intégralité du travail. Elle n'ouvre pas de connexions réseau, elle n'exécute pas de scripts, et elle n'a aucune idée de ce qu'un navigateur rendrait. Tout ce que vous extrayez doit être présent dans le balisage que vous passez.

Cette frontière est importante car les deux moitiés d'un scrape sont des préoccupations distinctes. Récupérer la page est un problème ; l'analyser en est un autre. Pour les pages statiques, vous pouvez associer BeautifulSoup à la bibliothèque requests pour obtenir le HTML. Pour les pages qui construisent leur contenu côté client avec JavaScript, une simple récupération renvoie une coquille presque vide et il n'y a rien pour BeautifulSoup à trouver. Nous revenons sur ce cas plus tard. Pour l'instant, traitez BeautifulSoup comme la moitié analyse du pipeline et rien de plus.

Installer BeautifulSoup et un analyseur

BeautifulSoup lui-même est fourni dans le paquet beautifulsoup4. Il a aussi besoin d'un analyseur pour faire le vrai travail de lecture du HTML. La bibliothèque standard inclut html.parser, qui n'a aucune dépendance supplémentaire et convient à la plupart des tâches. Pour la vitesse et la tolérance aux balisages cassés, installez aussi lxml et utilisez-le comme analyseur.

bash
python -m venv bs_env
source bs_env/bin/activate

pip install beautifulsoup4 lxml requests

Sous Windows, activez l'environnement avec bs_env\Scripts\activate au lieu de la ligne source. L'installation de requests est optionnelle ; nous ne l'utilisons que pour récupérer des pages statiques dans l'exemple pratique. Une fois tout en place, vous importez la classe depuis bs4, pas depuis un paquet nommé d'après la bibliothèque.

Créer un soup

Construire un soup prend deux arguments : le balisage et le nom de l'analyseur. Pour suivre sans toucher un site en direct, commencez par une chaîne HTML inline pour que l'entrée soit prévisible.

python
from bs4 import BeautifulSoup

html = """
<html>
  <body>
    <h1 id="title">Books</h1>
    <ul class="catalog">
      <li class="book"><a href="/b/1">Dune</a><span class="price">12.99</span></li>
      <li class="book"><a href="/b/2">Neuromancer</a><span class="price">9.50</span></li>
    </ul>
  </body>
</html>
"""

soup = BeautifulSoup(html, "lxml")
print(soup.title)  # None here; no <title> in the markup
print(soup.h1.get_text())  # Books

Échangez "lxml" pour "html.parser" si vous n'avez pas installé lxml ; le reste de l'API est identique. Accéder à une balise par nom, comme soup.h1, renvoie le premier élément correspondant comme raccourci. C'est pratique pour des vérifications rapides mais limité, donc la vraie recherche se fait via les méthodes ci-dessous.

Choisissez un analyseur délibérément

L'analyseur que vous choisissez change la façon dont le HTML cassé est réparé. html.parser est intégré et sans dépendance. lxml est plus rapide et plus indulgent envers les pages malformées, qui représentent la plupart des pages réelles. html5lib analyse exactement comme un navigateur mais est plus lent. Quand deux analyseurs ne sont pas d'accord sur une page délicate, c'est généralement la cause, donc nommez l'analyseur explicitement plutôt que de laisser BeautifulSoup deviner.

find et find_all

Les deux méthodes de base sont find et find_all. find renvoie le premier élément qui correspond, ou None si rien ne correspond. find_all renvoie une liste de chaque correspondance, vide quand rien ne correspond. Les deux prennent un nom de balise et des filtres optionnels.

python
first_book = soup.find("li")
print(first_book.a.get_text())  # Dune

all_books = soup.find_all("li")
print(len(all_books))  # 2

for book in all_books:
    print(book.a.get_text())

Les filtres affinent la recherche. Vous pouvez filtrer sur une classe CSS, un id, un attribut arbitraire ou un dictionnaire d'attributs. Parce que class est un mot réservé en Python, BeautifulSoup utilise l'argument nommé class_ avec un trait de soulignement final.

python
# By class
prices = soup.find_all("span", class_="price")

# By id
heading = soup.find(id="title")

# By any attribute, via the attrs dict
links = soup.find_all("a", attrs={"href": True})

# Limit how many you get back
one_link = soup.find_all("a", limit=1)

Vous pouvez aussi passer une liste de noms de balises pour correspondre à l'un d'eux, ou une expression régulière compilée pour correspondre à des noms de balises ou des valeurs d'attributs par schéma. Pour la plupart des scrapings, les filtres de classe et d'attribut couvrent le terrain, et les méthodes de sélecteur CSS ci-dessous sont souvent plus propres pour les conditions imbriquées.

select et select_one avec des sélecteurs CSS

Si vous pensez déjà en sélecteurs CSS, select et select_one vous permettent de réutiliser directement cette connaissance. select renvoie une liste de chaque correspondance ; select_one renvoie la première correspondance ou None. Ils acceptent la même syntaxe de sélecteur que vous écririez dans une feuille de style ou passeriez à document.querySelectorAll.

python
# Descendant: every <a> inside a .book li
titles = soup.select("li.book a")

# First price under the catalog list
first_price = soup.select_one("ul.catalog .price")

# Attribute selector
internal = soup.select("a[href^='/b/']")

# By id
heading = soup.select_one("#title")

Les sélecteurs brillent quand la cible est définie par sa position dans l'arbre, comme « le lien à l'intérieur du deuxième élément de liste ». Une longue chaîne d'appels find se lit moins bien que le sélecteur équivalent en une ligne. Que vous préfériez find_all ou select est surtout une question de goût ; les deux sont interchangeables pour la plupart des tâches, et un script unique mélange souvent les deux. Pour une comparaison plus approfondie des styles de sélecteurs, voir le web scraping avec XPath et les sélecteurs CSS.

Une fois que vous avez un élément, vous pouvez vous déplacer dans l'arbre par rapport à lui au lieu de chercher depuis le sommet à nouveau. Chaque balise expose son parent, ses enfants et ses frères et sœurs, ce qui est exactement ce dont vous avez besoin quand la donnée que vous voulez est près d'un élément que vous avez déjà trouvé.

python
price = soup.select_one(".price")

# Up: the <li> that contains this price
row = price.parent

# Down: direct children, ignoring whitespace text nodes
children = [c for c in row.children if c.name]

# Sideways: the <a> just before the price in the same <li>
title_link = price.find_previous_sibling("a")
print(title_link.get_text())  # Dune

Quelques notes qui évitent la confusion. .children et .contents incluent les nœuds texte comme les espaces entre les balises, donc filtrer sur c.name ne garde que les vrais éléments. .find_next_sibling et .find_previous_sibling ignorent ces nœuds texte pour vous et acceptent un nom de balise pour correspondre. Utilisez .find_parent pour remonter jusqu'à un ancêtre spécifique plutôt que simplement le parent immédiat. La navigation relative est la façon la plus fiable de gérer les pages où la valeur utile est à côté d'une étiquette stable.

Obtenir le texte et les attributs

L'extraction se résume à deux choses : le texte à l'intérieur d'un élément et les valeurs de ses attributs. Pour le texte, get_text renvoie tout le contenu de chaîne d'un élément et de ses descendants joints ensemble. Passez strip=True pour supprimer les espaces environnants, ce que vous voudrez presque toujours.

python
link = soup.select_one("li.book a")

# Text content
print(link.get_text(strip=True))  # Dune

# Attribute by key; raises KeyError if absent
print(link["href"])  # /b/1

# Safe attribute read with a default
print(link.get("title", ""))

Lire un attribut avec des crochets, comme link["href"], lève une KeyError quand l'attribut est absent, donc préférez link.get("href") quand un attribut peut ne pas exister. La différence entre texte et attributs déroute les débutants : le label visible d'un lien vient de get_text, mais son URL de destination vient de l'attribut href, et les deux n'ont rien à voir l'un avec l'autre.

Se prémunir contre les éléments manquants

Quand un sélecteur ne trouve rien, find et select_one renvoient None, et appeler .get_text() sur None lève une AttributeError. Les pages réelles sont incohérentes : toutes les lignes n'ont pas de prix, toutes les cartes n'ont pas une note. Vérifiez qu'un élément existe avant d'y lire, ou encapsulez un petit assistant qui renvoie None quand la recherche échoue, pour qu'un champ absent ne fasse pas planter une exécution entière.

Un exemple pratique : extraire une liste d'enregistrements

Assemblez maintenant les pièces sur une page statique construite pour la pratique du scraping. Le site quotes.toscrape.com sert du HTML simple rendu côté serveur, donc requests peut le récupérer et BeautifulSoup peut l'analyser directement. Chaque citation se trouve dans un bloc div.quote avec le texte, l'auteur et une liste de tags, ce qui est un bon substitut au type d'enregistrement répété que vous scrapez dans de vrais travaux.

python
import requests
from bs4 import BeautifulSoup

def parse_quotes(html):
    soup = BeautifulSoup(html, "lxml")
    records = []
    for block in soup.select("div.quote"):
        text_el = block.select_one("span.text")
        author_el = block.select_one("small.author")
        tags = [t.get_text(strip=True) for t in block.select("a.tag")]
        records.append({
            "quote": text_el.get_text(strip=True) if text_el else None,
            "author": author_el.get_text(strip=True) if author_el else None,
            "tags": tags,
        })
    return records

url = "https://quotes.toscrape.com/"
resp = requests.get(url, timeout=15)
if resp.status_code == 200:
    for row in parse_quotes(resp.text):
        print(row)

Le schéma ici est celui que vous réutilisez partout : sélectionnez le conteneur répétitif avec select, puis exécutez une deuxième requête ciblée à l'intérieur de chaque conteneur pour extraire les champs individuels. Limiter les recherches par champ à block plutôt qu'au document entier est ce qui empêche l'auteur de la ligne deux de s'infiltrer dans la ligne un. Vérifier chaque élément avant d'appeler get_text signifie qu'une citation sans auteur donne None au lieu de faire planter la boucle.

Suivre la pagination

Une page est une démonstration ; un jeu de données complet en couvre généralement plusieurs. Le site de pratique lie la page suivante via un élément li.next > a, donc la boucle est simple : analysez la page actuelle, cherchez le lien de page suivante, résolvez-le par rapport à l'URL de base et arrêtez quand le lien est absent.

python
import time
from urllib.parse import urljoin

base = "https://quotes.toscrape.com/"
next_url = base
all_rows = []

while next_url:
    resp = requests.get(next_url, timeout=15)
    if resp.status_code != 200:
        break

    soup = BeautifulSoup(resp.text, "lxml")
    all_rows.extend(parse_quotes(resp.text))

    next_link = soup.select_one("li.next a")
    next_url = urljoin(base, next_link["href"]) if next_link else None
    time.sleep(1)

print(f"Collected {len(all_rows)} quotes")

Deux détails rendent ceci robuste. urljoin transforme un href relatif comme /page/2/ en URL complète sans acrobaties de chaînes, donc il continue à fonctionner si la forme du chemin change. Le time.sleep(1) espace les requêtes pour ne pas surcharger le serveur, ce qui est à la fois poli et la façon la plus simple de rester sous une limite de débit. Pour un traitement plus complet de la récupération et de la structuration des données de bout en bout, voir comment scraper un site web avec Python.

Quand BeautifulSoup ne suffit pas : les pages JavaScript

Tout ce qui précède suppose que les données se trouvent dans le HTML que vous avez récupéré. Beaucoup de sites modernes ne fonctionnent pas ainsi. Ils envoient une coquille HTML minimale et construisent le vrai contenu dans le navigateur avec JavaScript, tirant des données depuis des appels API en arrière-plan après le chargement de la page. Récupérez-en une avec requests et le corps que vous passez à BeautifulSoup contient des conteneurs vides là où les enregistrements devraient être. BeautifulSoup fait correctement son travail ; les données n'étaient simplement jamais dans la chaîne.

Vous avez deux sorties. Vous pouvez exécuter un vrai navigateur vous-même avec un outil comme Selenium ou Playwright, attendre que le contenu soit rendu et passer le page_source rendu à BeautifulSoup. Ça fonctionne mais signifie gérer une flotte de navigateurs, et sur les sites protégés vous devez aussi gérer les proxies et les challenges. L'autre façon est de déléguer l'étape de récupération et de rendu à un service qui renvoie du HTML finalisé, puis analyser ce HTML avec le même code BeautifulSoup que vous avez déjà écrit. Dans les deux cas, la couche d'analyse ne change pas ; seule la façon d'obtenir le HTML change. Pour en savoir plus sur cette séparation, voir comment scraper des pages JavaScript avec Python.

Crawlbase Crawling API

BeautifulSoup ne fait qu'analyser ; il ne peut pas rendre une page JavaScript ni vous faire passer un blocage agressif. La Crawling API fait la moitié récupération et rendu pour vous : envoyez-lui une URL avec un token JS, elle exécute la page dans un vrai navigateur derrière des adresses IP résidentielles rotatives et renvoie le HTML finalisé. Vous analysez ensuite ce HTML avec exactement le même code BeautifulSoup de ce guide. Essayez-la sur le niveau gratuit d'abord.

Voici la forme de cet appariement. La récupération passe par la Crawling API avec un token JavaScript, et le corps renvoyé s'écoule directement dans votre analyseur existant.

python
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"})

response = api.get("https://example.com/spa-page", {"ajax_wait": "true", "page_wait": 4000})

if response["status_code"] == 200:
    html = response["body"].decode("utf-8")
    soup = BeautifulSoup(html, "lxml")
    # same find/select calls as before
    print(soup.select_one("h1").get_text(strip=True))

Si vous préférez router votre propre client via des adresses IP rotatives au lieu d'appeler un endpoint géré, le Smart AI Proxy vous offre la rotation résidentielle comme proxy interchangeable, et pour le JSON pré-analysé la Crawling API renvoie des champs structurés pour les sites pris en charge sans aucun BeautifulSoup du tout.

Récapitulatif

Points clés

  • BeautifulSoup ne fait qu'analyser. Il construit un arbre consultable à partir du HTML que vous avez déjà ; il ne récupère jamais une URL ni n'exécute JavaScript.
  • Installez beautifulsoup4 plus un analyseur. Utilisez html.parser pour zéro dépendance ou lxml pour la vitesse et la tolérance aux balisages cassés, et nommez l'analyseur explicitement.
  • Apprenez quatre méthodes. find et find_all cherchent par balise et filtres ; select et select_one cherchent par sélecteur CSS. Ils sont interchangeables pour la plupart des tâches.
  • Lisez séparément le texte et les attributs. get_text(strip=True) donne le contenu visible ; element["href"] ou element.get("href") donne une valeur d'attribut.
  • Ciblez, protégez et paginez. Sélectionnez le conteneur répétitif, interrogez chaque champ à l'intérieur, vérifiez None et suivez les liens de page suivante avec urljoin et un petit délai.
  • Pour les pages JavaScript, corrigez la récupération. Associez la Crawling API ou un navigateur sans interface graphique pour obtenir le HTML rendu, puis analysez-le avec le même code BeautifulSoup.

Foire aux questions

Comment installer BeautifulSoup en Python ?

Installez-le avec pip install beautifulsoup4. Le nom d'importation diffère du nom du paquet : vous écrivez from bs4 import BeautifulSoup dans votre code. BeautifulSoup a aussi besoin d'un analyseur pour faire le travail. Le html.parser intégré ne nécessite rien de plus, mais installer lxml avec pip install lxml vous donne un analyseur plus rapide et plus indulgent, ce qui vaut le coup pour les pages réelles.

Quelle est la différence entre find et find_all ?

find renvoie le premier élément unique correspondant à vos critères, ou None si rien ne correspond. find_all renvoie une liste de chaque élément correspondant, vide quand il n'y a pas de correspondances. Utilisez find quand vous attendez exactement un élément, comme le titre principal d'une page, et find_all quand vous collectez plusieurs éléments, comme chaque ligne d'une liste. Les équivalents en sélecteur CSS sont select_one et select.

Comment obtenir le texte à l'intérieur d'un élément par rapport à un attribut ?

Utilisez element.get_text(strip=True) pour le contenu texte visible, y compris le texte des balises imbriquées, avec les espaces environnants supprimés. Utilisez element["href"] pour lire une valeur d'attribut, ou element.get("href") pour la lire en toute sécurité avec une valeur par défaut quand l'attribut peut être absent. Le label et l'URL de destination d'un lien sont séparés : le label est du texte, l'URL est l'attribut href.

Pourquoi BeautifulSoup renvoie-t-il un résultat vide sur certaines pages ?

Presque toujours parce que les données ne se trouvent pas dans le HTML analysé. Beaucoup de sites rendent le contenu dans le navigateur avec JavaScript, donc une simple récupération renvoie une coquille vide et BeautifulSoup ne trouve correctement rien. BeautifulSoup n'exécute pas JavaScript. Pour gérer ces pages, obtenez d'abord le HTML rendu, soit avec un navigateur sans interface graphique comme Selenium ou Playwright, soit avec la Crawling API, puis analysez ce HTML rendu avec le même code.

BeautifulSoup peut-il gérer la pagination par lui-même ?

Pas par lui-même, car BeautifulSoup ne récupère pas de pages. Vous gérez la pagination avec une boucle : analysez la page actuelle, utilisez BeautifulSoup pour trouver le lien de page suivante, récupérez cette URL avec votre client HTTP et répétez jusqu'à ce qu'il n'y ait plus de lien suivant. Résolvez les liens relatifs avec urllib.parse.urljoin et ajoutez un court délai entre les requêtes pour ne pas surcharger le serveur.

Dois-je utiliser lxml ou html.parser comme analyseur ?

Utilisez lxml quand vous le pouvez : il est plus rapide et gère le HTML malformé plus gracieusement, ce qui couvre la plupart des pages réelles. Utilisez le html.parser intégré quand vous voulez zéro dépendance supplémentaire et que les pages sont bien formées. Pour un balisage qui doit être analysé exactement comme un navigateur le ferait, html5lib est le plus précis, au détriment de la vitesse. Passez toujours le nom de l'analyseur explicitement pour que le comportement reste cohérent entre les machines.

Commencer à construire

Crawlez n'importe quel site à grande échelle, sans combattre l'infrastructure.

Crawlbase gère les proxies, les empreintes et les CAPTCHA afin que votre équipe livre des pipelines de données au lieu de maintenir la plomberie de crawl. 1 000 requêtes gratuites, sans carte requise.

En libre-service · Sans appel commercial requis · Volumes de crawl entreprise disponibles