BeautifulSoup est la bibliothèque vers laquelle presque tout le monde se tourne pour analyser une page web en Python pour la première fois, et c’est effectivement la plus lente des vrais analyseurs HTML. Ces deux affirmations sont vraies, et aucune n’est une critique. Ce qui est intéressant, c’est que ce « plus lent » se traduit par un chiffre précis, mesurable, et donc achetable — pas seulement une impression.
J’ai passé bs4 (c’est-à-dire beautifulsoup4, version 4.15.0, publiée en juin 2026, sous licence MIT) à la fois à travers de nouveaux tests de fonctionnalité et à travers des données de timing réutilisées du même banc de test, et le constat est cohérent : vous échangez une pénalité de vitesse d’environ un ordre de grandeur contre l’API la plus agréable à utiliser et la meilleure tolérance aux erreurs du secteur. Est-ce un bon échange ? Tout dépend de votre charge de travail, et cette analyse garde donc les deux côtés sur la table.
Ce qu’est vraiment BeautifulSoup — et ce qu’il n’est pas
La plupart des tutoriels sautent la partie la plus importante : BeautifulSoup ne parse pas le HTML. C’est une couche d’enrobage. En interne, il remet votre document à l’un des trois vrais parseurs — le html.parser intégré à Python, lxml ou html5lib — puis encapsule l’arbre obtenu dans une API unique, très simple pour naviguer et rechercher. Le rôle de bs4 n’est pas d’analyser. Son rôle est de rendre le résultat agréable à parcourir.
Son propre auteur le décrit comme une « bibliothèque de screen scraping », et la promesse a toujours été la même : lui donner un HTML si mal formé qu’un navigateur grimacerait, et il ira quand même récupérer les données demandées. Cette réputation est méritée, avec une réserve dont nous parlerons plus loin.
Quelques faits utiles à fixer avant d’aller plus loin :
| Champ | Valeur |
|---|---|
| Package | beautifulsoup4 (importé comme bs4) |
| Version testée | 4.15.0 (mise en ligne le 2026-06-07) |
| Compatibilité Python | >=3.7.0 |
| Licence | MIT |
| Site officiel | crummy.com/software/BeautifulSoup |
| Code source + suivi des bugs | Launchpad — pas GitHub |
| Maintenance | Active (4.15.0 en juin 2026, six versions sur les douze derniers mois) |
Cette mention « pas GitHub » compte plus qu’il n’y paraît. bs4 est une bibliothèque vieille de 20 ans, hébergée sur crummy.com et Launchpad, donc le réflexe habituel du nombre d’étoiles GitHub n’a pas vraiment de sens ici. Il vaut mieux juger sa santé à la fréquence des versions — et, de ce point de vue, elle se porte très bien.
Petit point licence, pour celles et ceux qui doivent répondre à une équipe conformité : le wrapper est sous MIT, mais ce que « utiliser bs4 » ajoute réellement à votre arborescence de dépendances dépend du backend choisi. html.parser fait partie de la bibliothèque standard Python (licence PSF, zéro dépendance supplémentaire). lxml est sous BSD, mais s’appuie sur libxml2/libxslt — une dépendance C externe à compiler ou à installer via un wheel précompilé. html5lib est en pur Python et sous MIT. Si vous voulez l’empreinte de dépendances la plus légère, le html.parser intégré est le meilleur choix — qui, justement, est aussi le backend avec le plus gros piège. J’y reviens tout de suite.
Le coût de vitesse, chiffré
Posons le chiffre tout de suite, parce que c’est l’élément principal, et le cacher serait malhonnête. Sur une tâche réaliste de type « parser puis extraire » — analyser la chaîne, récupérer tous les <h3 class="title"> et tous les <a href> — BeautifulSoup est le parseur le plus lent de cette comparaison, et ce n’est pas serré.

Ces temps sont repris du banc de test de selectolax (même machine, même méthodologie en 3 passes, état au 2026-07-13) ; cette analyse ne relance aucun benchmark de timing elle-même, afin d’éviter la contention CPU et le travail en double. Latence médiane p50, en millisecondes :
| Taille de page | bs4 (html.parser) | bs4 (lxml) | selectolax-Lexbor | lxml | bs4-hp plus lent | bs4-lxml plus lent |
|---|---|---|---|---|---|---|
| 1 KB | 0.323 | 0.281 | 0.027 | 0.036 | 12.0x | 10.5x |
| 10 KB | 2.049 | 1.705 | 0.160 | 0.166 | 12.8x | 10.7x |
| 100 KB | 20.599 | 16.540 | 1.464 | 1.423 | 14.1x | 11.3x |
| 1 MB | 232.558 | 181.855 | 14.901 | 14.177 | 15.6x | 12.2x |
| 10 MB | 2788.746 | 2261.557 | 159.935 | 172.933 | 17.4x | 14.1x |
Donc bs4(html.parser) est environ 12 à 17x plus lent qu’un parseur C comme selectolax-Lexbor, et passer au backend lxml ne le ramène qu’à 10,5 à 14x — toujours un ordre de grandeur derrière. La raison est structurelle, pas un bug : quel que soit le backend utilisé pour parser, bs4 crée un objet Python complet (Tag ou NavigableString) pour chaque nœud. Cette couche de matérialisation des objets est une taxe que les parseurs C ne paient tout simplement pas.
Remarquez que le multiplicateur augmente avec la taille des pages — 12,0x à 1 KB, 17,4x à 10 MB. Cela montre qu’il ne s’agit pas d’un surcoût fixe de démarrage que l’on pourrait amortir. C’est une taxe par nœud, qui croît linéairement avec le nombre de nœuds créés.
Maintenant, changeons de perspective, parce que « 10x plus lent » fait plus peur que dans la pratique. Sur une page de 1 MB, on parle de 232 ms contre 15 ms. Si votre travail consiste à « extraire quelques centaines à quelques milliers de pages de quelques centaines de KB chacune », cette différence absolue est imperceptible — vous ne la sentirez pas, et l’optimiser ne vous apportera rien. Si votre travail est un pipeline d’un million de pages, le même ratio fait toute la différence entre un job qui termine et un job qui n’aboutit pas. Même nombre, verdict opposé. Il faut donc le juger à l’échelle réelle de votre volume, pas à l’échelle du benchmark.
Non, changer de backend ne règle pas le problème
On entend souvent qu’il suffit de donner à bs4 le backend lxml pour obtenir la vitesse de lxml. Ce n’est pas vrai, et il est utile de comprendre pourquoi. Sur une requête CSS en batch de 100 000 nœuds (sélectionner chaque <a> et lire son href, arbre déjà construit), l’écart de débit est très net :
| Parseur | Query p50 | Nœuds/sec |
|---|---|---|
| lxml | 33.30 ms | 3,002,646 |
| selectolax-Modest | 34.19 ms | 2,924,550 |
| selectolax-Lexbor | 39.46 ms | 2,534,027 |
| bs4 (lxml) | 250.56 ms | 399,111 |
bs4(lxml) atteint environ 399 000 nœuds/seconde — soit environ 6,3 à 7,5x plus lent que les trois moteurs C, alors même que son propre backend est lxml. Le backend accélère la construction de l’arbre. En revanche, les requêtes et la traversée passent toujours par soupsieve, puis par des objets Tag de bs4, et chaque nœud sélectionné est toujours emballé dans un objet Python. Donc l’idée « donnez-lui lxml et bs4 devient aussi rapide qu’lxml » est fausse : le backend accélère une phase, et la phase la plus lente n’est pas celle-là.
La mémoire et le démarrage à froid complètent le coût. Sur un document de 10 MB, bs4 utilise environ 1,5 à 1,75x la mémoire résidente de selectolax ou lxml (218–226 MB contre 129–145 MB) — même cause racine : un objet Python par nœud. Et importer bs4 prend environ 33,4 ms contre 14,1 ms pour lxml.html, donc c’est 2,36x plus lent à importer. Ce dernier point est négligeable dans un processus long, mais pour un outil CLI ou une fonction serverless qui redémarre souvent à froid, c’est un petit coût réel qu’il faut connaître.
Pourquoi ajouter des threads ne vous sauvera pas
Si votre réflexe face à une tâche lente et liée au CPU est « ajoutons des threads », bs4 va punir ce réflexe. Sur une page de 1 MB analysée 48 fois, en mono-thread puis avec quatre threads :
| Parseur | 1 thread | 4 threads | Accélération |
|---|---|---|---|
| selectolax-Lexbor | 0.563 s | 0.159 s | 3.54x |
| lxml | 0.459 s | 0.378 s | 1.21x |
| bs4 (lxml) | 6.945 s | 26.842 s | 0.26x |
Lisez bien la dernière ligne deux fois. Quatre threads rendent bs4 environ 3,9x plus lent, pas plus rapide. Le signal empirique est clair : la bibliothèque semble conserver le GIL. La construction d’arbre de bs4 est en pur Python, donc elle se sérialise sous le Global Interpreter Lock ; empiler des threads ne fait qu’ajouter du coût de planification à un travail qui ne peut pas réellement s’exécuter en parallèle. selectolax obtient son gain d’environ 3,5x parce que son cœur C libère le verrou ; bs4, lui, ne le peut pas.
Pour l’ère du free-threading, le conseil pratique est donc simple : si vous devez paralléliser BeautifulSoup, utilisez le multiprocessing (ProcessPoolExecutor), pas les threads. selectolax et lxml peuvent mieux tirer parti des threads ; bs4 non. Petite réserve méthodologique : il s’agit d’une seule observation avec un seul nombre de threads (4) sur une seule taille de page (1 MB), et le mécanisme « conserve le GIL » reste une hypothèse déduite du comportement en temps réel, pas quelque chose que j’ai confirmé en instrumentant précisément le chemin de code concerné. La direction est claire ; le mécanisme exact reste provisoire.
Le backend par défaut est le piège. Lisez ceci en premier.
Si vous ne retenez qu’une chose de cet article, retenez celle-ci. Un simple BeautifulSoup(html) sans second argument utilise html.parser, et html.parser n’implémente pas les règles HTML5 des balises de fin optionnelles. Cela semble théorique… jusqu’au moment où vos données sont silencieusement corrompues.

J’ai fait passer 15 échantillons HTML volontairement mal formés dans les trois backends, avec une assertion structurelle agnostique au backend, prédéfinie avant l’exécution pour chacun (afin que personne ne choisisse le gagnant après coup). Résultats :
| Backend | Conforme à l’attente / 15 |
|---|---|
| lxml | 15 |
| html5lib | 15 |
| html.parser | 12 |
Les trois échecs ont la même cause racine. Prenez un tableau non fermé : <table><tr><td>a<td>b<tr><td>c<td>d</table>. Avec html.parser, le texte extrait des cellules devient ['abcd','bcd','cd','d'] — chaque <td> avale tout ce qui suit, parce que le parseur imbrique les cellules au lieu de les fermer. lxml et html5lib renvoient correctement ['a','b','c','d']. Les listes non fermées se comportent pareil : <li>a<li>b<li>c donne ['abc','bc','c'] avec html.parser, et le résultat propre ['a','b','c'] avec les deux autres. Les attributs dupliqués changent aussi : <div id="first" id="second"> conserve "second" sous html.parser mais "first" sous lxml/html5lib, et la spécification HTML5 dit bien de garder le premier.
Pourquoi est-ce dangereux, et pas seulement agaçant ? Parce que cela se produit sans lever d’erreur. Un scraper qui fait tranquillement BeautifulSoup(html) et tombe sur un tableau ou une liste non fermée — ce qui arrive tristement souvent sur de vieux sites, du HTML écrit à la main ou des templates qui ont oublié une balise de fermeture — va mélanger du texte de cellules adjacentes dans un seul champ, vous renvoyer des données sales, et ne se plaindre à aucun moment. La correction tient en un argument : BeautifulSoup(html, "lxml") ou BeautifulSoup(html, "html5lib").
Pour être juste avec html.parser, les 12 autres cas mal formés sur 15 sortaient identiques dans les trois backends — imbrication incorrecte de balises comme <b><i></b></i>, absence de squelette html/body, attributs non quotés, balises de fermeture orphelines, commentaires non fermés, formulaires imbriqués, casse mélangée, et plus encore. La tolérance de bs4 est réellement forte dans l’ensemble ; les divergences se concentrent presque entièrement sur la famille des balises de fin optionnelles. Et rien de tout cela n’est une découverte : la documentation officielle de bs4, dans « Differences between parsers », dit déjà en termes simples que html.parser est « less lenient » (moins permissif). Ce que la matrice de HTML mal formé apporte, ce sont les cas précis et reproductibles où « moins permissif » se transforme en mauvais résultat.
Ce que vous ne perdez pas : l’API et le CSS sont son grand point fort
Donc bs4 est lent, monothread, et son backend par défaut peut piéger. Les gens continuent malgré tout à l’utiliser, parce que la moitié « conviviale » de l’échange est bien réelle — et les tests le confirment.

J’ai lancé 29 tests d’API couvrant la recherche, le CSS, la navigation dans l’arbre, l’extraction de texte et la modification du DOM. Les 29 ont réussi, avec un résultat validé en comparant la valeur réelle à une valeur attendue, et non à l’œil nu. Deux de ces capacités sont des avantages ergonomiques que les parseurs C n’offrent tout simplement pas :
- Prédicats fonctionnels dans
find/find_all. Vous pouvez écriresoup.find(lambda t: t.name == "a" and "btn" in t.get("class", []))et exprimer une condition complexe en une seule ligne Python — sans devoir faire un « tout sélectionner, puis filtrer » en deux étapes. - Navigation d’arbre nommée et bidirectionnelle.
.parent,.next_sibling,.find_parent,.stripped_strings,.descendants— les parcours se lisent presque comme de l’anglais et vont dans les deux sens. selectolax demande parfois plusieurs étapes pour certaines de ces opérations, ou ne les propose pas du tout.
Voilà la partie « ça vous fait gagner du temps de développement » rendue concrète. Ce n’est pas du marketing ; ce sont 29 validations vertes.
Deux pièges à noter, parce qu’un vrai audit mentionne les deux côtés. Premièrement, les attributs booléens : <input disabled> renvoie une chaîne vide "" pour disabled dans bs4 (selectolax renvoie None). Les deux sont falsy, donc if node.get("disabled") manque silencieusement un attribut booléen qui est pourtant bien présent dans les deux bibliothèques — le test sûr est "disabled" in tag.attrs. Deuxièmement, get_text(strip=True) concatène le texte des nœuds sans séparateur après suppression des espaces, donc "...with " + "link1" devient "withlink1". Il faut passer separator=" " si vous avez besoin de conserver les frontières entre les mots. Aucun de ces pièges n’est spécifique à bs4 ; ce sont des pièges qui existent dans plusieurs bibliothèques.
Et maintenant la surprise : choisir bs4 ne vous fait pas perdre la couverture CSS. Son moteur CSS, soupsieve, est le plus complet de toute cette comparaison. Sur la matrice de base en 41 cas (réutilisée depuis le banc de test selectolax), soupsieve obtient 41/41 — le seul score parfait du lot, devant les 39/41 de selectolax-Lexbor et les 37/41 de cssselect (lxml/parsel). J’ai ensuite exécuté 20 cas supplémentaires avancés, explicitement documentés par soupsieve, et il a fait 20/20, y compris des sélecteurs que Lexbor refuse purement et simplement : :lang(en), le :-soup-contains('featured') propre à soupsieve, :is(), :where() et :has(> a). Les seuls vrais manques sont XPath (soupsieve est limité au CSS) et les pseudo-éléments ::text / ::attr() de parsel, qui sont des extensions Scrapy. Si votre monde est XPath, la migration fera mal.
Le verdict de cette section est clair : ce que vous sacrifiez en choisissant BeautifulSoup, c’est la vitesse. Ce n’est ni l’ergonomie de l’API, ni la couverture CSS.
Deux pièges de production à prévoir dans le budget
Au-delà du backend par défaut, deux comportements vous poseront problème dans les charges longues ou les jeux de données non UTF-8.
Cycles de référence : appelez decompose() dans les boucles longues
Chaque Tag bs4 garde une référence vers son parent et vers ses enfants, ce qui forme un cycle de références. Le comptage de références de CPython ne peut pas libérer un cycle tout seul — c’est le travail du ramasse-miettes générationnel. Pour voir à quel point cela compte, j’ai construit puis supprimé un arbre 300 fois avec le GC désactivé, puis j’ai compté les objets Tag encore présents en mémoire :

| Scénario | Tags conservés après del |
|---|---|
| GC désactivé | 120,900 (300 cycles, rien n’a été récupéré) |
| GC activé | 26,598 (le GC générationnel a déclenché pendant la boucle) |
Après gc.collect() forcé | 0 (tout a été récupéré) |
| Contrôle sans cycle (liste de chaînes, GC désactivé) | delta 0 |
Avec le GC désactivé, del soup ne récupère rien — les 120 900 objets restent en mémoire, parce que le cycle de références contourne le comptage de références. Un seul gc.collect() les a tous libérés. Le groupe de contrôle sans cycle (une simple liste de chaînes, connue pour ne pas former de cycle) affiche un delta nul, ce qui prouve que l’accumulation vient bien du cycle de bs4 et non d’un bruit de mesure. La documentation de bs4 dit d’ailleurs elle-même que les objets sont « densely interconnected ... exactly the sort a garbage collector would have trouble with » ; ce comportement est donc documenté, et le test ajoute ici le nombre d’objets retenus ainsi que la preuve que collect() remet le compteur à zéro.
La règle pratique : dans un pipeline qui analyse de nombreuses grosses pages dans une boucle serrée, si votre code (ou un réglage de très haut débit) désactive le GC ou ne le déclenche pas assez souvent, les arbres bs4 vont s’attarder en mémoire et la consommation va grimper. Appelez soup.decompose() après chaque page — bs4 le fournit précisément pour casser le cycle et libérer plus tôt. Les arbres C de selectolax et lxml n’ont pas ce problème.
Encodage : UnicodeDammit est l’avantage discret de bs4
bs4 intègre un composant que les parseurs rapides n’ont pas : UnicodeDammit, qui détecte l’encodage d’un document et le convertit automatiquement en Unicode. Je lui ai donné une matrice de 8 cas « encodage déclaré vs réel » :

| Cas | Encodage réel | Devine UnicodeDammit | Récupéré ? |
|---|---|---|---|
| utf8_no_decl | utf-8 | utf-8 | Oui |
| utf16_bom | utf-16 | utf-16le | Oui |
| gbk_chinese | gbk | gb18030 | Oui (superset) |
| shiftjis | shift_jis | cp932 | Oui (superset) |
| latin1_declared_utf8 | latin-1 (déclaré utf-8) | iso-8859-1 | Oui (a ignoré le mensonge) |
| latin1_no_decl | latin-1 | cp720 | Non |
| cp1252_no_decl | cp1252 | cp862 | Non |
| utf8_declared_latin1 | utf-8 (déclaré latin-1) | iso-8859-1 | Non (a suivi le mensonge) |
Cinq cas sur huit ont été récupérés. UTF-8, UTF-16 avec BOM, GBK, Shift-JIS, et même un latin-1 mal étiqueté sont revenus correctement, et les hypothèses sur-ensembles (GBK→gb18030, Shift-JIS→cp932) restent décodables. Deux modes d’échec sont importants à connaître : les courts échantillons latin-1/cp1252 sont parfois pris à tort pour des pages de code pages DOS, parce que le détecteur statistique n’est pas fiable sur de petits échantillons et que les caractères de dessin de boîtes DOS recoupent les points de code de Latin-1 ; et lorsqu’une déclaration <meta charset> est simplement fausse, UnicodeDammit fait confiance à cette déclaration. La documentation de bs4 signale les deux — un échantillon peut être « so short that Unicode, Dammit can't get a lock on it », et plus il y a de données, meilleure est l’estimation.
Face à selectolax, qui corrompt silencieusement les octets non UTF-8 et vous laisse le soin de les décoder vous-même, c’est un vrai avantage : bs4 essaie au moins de deviner et y parvient souvent. Mais ce n’est pas une garantie. Pour un encodage connu, ne jouez pas au devin et soyez explicite : BeautifulSoup(bytes, from_encoding="...").
Les backends divergent-ils vraiment sur des pages réelles ?
La matrice de HTML mal formé montre les backends diverger sur des entrées volontairement cassées. La question suivante est évidente : est-ce que cela se voit dans le monde réel ? J’ai donc fait passer les trois backends sur 11 pages réelles récupérées — BBC, Wikipedia, Craigslist, MDN, old.reddit, la documentation Python, Hacker News, Books to Scrape, webscraper.io, whitehouse.gov et une page de citations rendue en JavaScript — en comparant les comptes de liens, de titres et d’images.
Les trois ont été d’accord sur les 11 pages. Zéro divergence. Ce qui signifie que le désaccord entre backends de la section piège n’apparaît que sur du HTML volontairement mal formé ; quand un site de production moderne est suffisamment bien structuré — même s’il est un peu « sale » — le choix du backend ne change pas ce que vous extrayez. En pratique : pour les sites courants et bien formés, html.parser fait parfaitement l’affaire et évite une dépendance. Ce n’est que lorsque vous scrapez du HTML manifestement non standard, écrit à la main ou ancien que le choix du backend commence à influencer les résultats — et c’est là qu’il faut passer à lxml ou html5lib.
Petite parenthèse sur cette série, parce que c’est un vrai cas limite. La page MDN contient un élément <template>, et tous les backends bs4 ont renvoyé 508 liens — ce qui signifie que bs4 a aplati le contenu de <template> dans l’arbre principal. bs4 se place donc du même côté que lxml, et à l’opposé de selectolax-Lexbor, qui suit strictement la spécification HTML5 (un <template> est un DocumentFragment inerte) et n’en renvoie que 497, en ignorant silencieusement les 11 liens contenus dans le template. bs4 va donc bien capturer des données à l’intérieur d’un <template> — utile, mais aussi susceptible de récupérer du contenu « fantôme » qu’un navigateur n’affichera jamais. Aucun comportement n’est faux ; ce sont simplement des interprétations différentes de la spécification, et il faut savoir laquelle vous obtenez.
Où BeautifulSoup s’insère — et où il ne s’insère pas
Plutôt que de réduire tout cela à un score unique sur 100 (ce qui masquerait justement les arbitrages importants), voici un tableau par dimension, avec une réserve sur chaque ligne :
| Dimension | Ce que les tests ont montré | Réserve pour le lecteur |
|---|---|---|
| Installation / premier lancement | Simple wrapper, pas de navigateur ni de configuration ; html.parser sans dépendance ; wheels précompilés | Le backend lxml ajoute une dépendance C |
| Vitesse vs parseurs C | 12–17x plus lent (html.parser) / 10,5–14x (backend lxml), toutes tailles confondues | Un seul banc ; données selectolax réutilisées |
| Débit des requêtes CSS | ~6–7,5x plus lent sur 100k nœuds ; le backend lxml ne le sauve pas | Réutilisé ; paie la taxe Python par nœud |
| Mémoire | 1,5–1,75x selectolax/lxml ; le plus lourd | Réutilisé ; mesuré en RSS |
| Démarrage à froid / import | 2,36x plus lent (33,4 vs 14,1 ms) | Réutilisé ; impact modeste |
| Passage à l’échelle avec threads | bs4-lxml ~3,9x plus lent avec 4 threads (conserve le GIL) | Une seule observation ; utilisez multiprocessing |
| Ergonomie de l’API | 29/29 tests ; find à prédicat fonctionnel + navigation bidirectionnelle | Pièges : attribut booléen en chaîne vide et strip sans séparateur |
| Couverture CSS | soupsieve est le plus complet : 41/41 de base + 20/20 avancés ; prend en charge :lang | Pas de XPath, pas de ::text |
| Tolérance entre 3 backends | lxml/html5lib 15/15 ; html.parser 12/15 | Divergence uniquement sur HTML mal formé |
| Cohérence sur pages réelles | 3 backends d’accord sur 11/11 ; tous aplatisent <template> (508) | Sites bien formés : le backend n’a pas d’effet |
| GC des cycles de référence | L’arbre forme un cycle ; 300 boucles ont conservé 120 900 objets, collect() les a supprimés | Les boucles longues ont besoin de decompose() |
| Encodage | UnicodeDammit récupère 5/8 ; se trompe sur les petits échantillons, suit les mauvaises déclarations | Une seule observation |
| Maintenance | Active (4.15.0, juin 2026) ; MIT | Hébergé sur crummy/Launchpad, pas GitHub |
Alors, pour qui BeautifulSoup est-il fait ? Pour toute personne qui valorise une API lisible et un parsing indulgent plus que le débit brut, sur des volumes modérés — prototypes, scrapers ponctuels, outils internes, équipes où le temps développeur coûte plus cher que le temps d’exécution. Qui devrait regarder ailleurs ? Les pipelines d’un million de pages où la taxe de vitesse finit par représenter de vrais coûts, les charges qui nécessitent du parallélisme au niveau des threads, et toute personne mariée à XPath.
Un mot sur sa place dans une vraie chaîne de scraping, et sur l’endroit où notre propre outil intervient. BeautifulSoup suppose que vous avez déjà le HTML. Il ne récupère pas les pages, ne rend pas le JavaScript, et ne fait rien contre les défenses anti-bot ou les CAPTCHAs — c’est un autre problème, et un problème réellement difficile sur le web moderne. C’est ici qu’une API de scraping IA se place à une autre couche : la stack développeur de Thunderbit — une API REST, un serveur MCP et une CLI — gère la récupération, le rendu JavaScript et le problème anti-bot, puis renvoie soit du Markdown propre (POST /distill), soit du JSON structuré aligné sur un schéma (POST /extract), sans que vous ayez à écrire de sélecteurs. Les deux ne sont pas concurrents ; ils sont complémentaires. bs4 parse le HTML que vous avez déjà en main ; l’API, MCP et CLI de Thunderbit vous apportent le HTML auquel vous n’accédez pas facilement au départ. Si votre goulot d’étranglement est le parsing, bs4 est une bonne réponse. Si votre goulot d’étranglement est l’acquisition, c’est une autre couche.
Essayer Thunderbit pour l’extraction de données web
En résumé
BeautifulSoup vous offre l’API la plus conviviale, la meilleure tolérance au HTML mal formé et le moteur CSS le plus complet de cette comparaison — en échange d’une taxe de vitesse d’environ un ordre de grandeur et de l’empreinte mémoire la plus élevée. C’est l’arbitrage complet, dit sans détour. Le backend html.parser par défaut est le vrai piège : il déforme silencieusement les tableaux et listes non fermés, donc passez "lxml" ou "html5lib" dès que vos entrées risquent d’être sales. Les threads ne l’accéléreront pas — le multiprocessing, oui. Et dans les boucles longues, appelez decompose() sur chaque page pour éviter l’accumulation des cycles de référence.
Deux limites pour terminer. Tout ici a été mesuré sur une seule plateforme (macOS arm64, Python 3.14, wheels précompilés), et les multiplicateurs de timing sont repris du banc de test selectolax (même bench, état au 2026-07-13) plutôt que relancés — ils héritent donc de cette limite de plateforme unique, et un environnement Linux x86_64 ou une compilation depuis les sources pourrait faire bouger les chiffres exacts. Et rien dans ces résultats n’est une découverte nouvelle : bs4 est une bibliothèque vieille de 20 ans, donc tous les comportements testés sont soit documentés, soit déjà publiquement connus. La valeur n’est pas le scoop. C’est le fait de mettre un vrai chiffre sur des compromis que la documentation décrit seulement qualitativement.
Questions fréquentes
BeautifulSoup est-il lent ?
Oui, et de façon mesurable. Sur une tâche de type parser + extraction, il tourne environ 12 à 17x plus lentement qu’un parseur C comme selectolax-Lexbor avec le backend html.parser par défaut, et 10,5 à 14x plus lentement avec le backend lxml, parce qu’il crée un objet Python pour chaque nœud. L’importance de ce point dépend de l’échelle : sur une page de 1 MB, on parle de 232 ms contre 15 ms, ce qui est invisible pour quelques milliers de pages mais décisif pour un pipeline d’un million de pages.
Quel parseur BeautifulSoup dois-je utiliser — html.parser, lxml ou html5lib ?
Pour des sites classiques et bien formés, le html.parser par défaut suffit et n’ajoute aucune dépendance. Mais il n’implémente pas les balises de fin optionnelles HTML5, donc sur des tableaux ou listes non fermés, il mélange le texte adjacent sans signaler d’erreur. Si vos entrées peuvent être mal formées, écrites à la main ou anciennes, passez explicitement "lxml" ou "html5lib" — les deux ont obtenu un parfait 15/15 sur une matrice de HTML mal formé, là où html.parser s’est arrêté à 12/15.
BeautifulSoup peut-il analyser en parallèle avec des threads ?
Non. La construction d’arbre de bs4 est en pur Python et conserve le GIL, donc ajouter des threads le rend plus lent, pas plus rapide — lors des tests, quatre threads ont rendu une analyse d’une page de 1 MB environ 3,9x plus lente qu’un seul thread. Pour paralléliser bs4, utilisez le multiprocessing (ProcessPoolExecutor). Les bibliothèques avec un cœur C, comme selectolax et lxml, sont celles qui profitent réellement du parallélisme par threads.
BeautifulSoup gère-t-il bien le HTML cassé ?
Globalement oui — sur un ensemble de cas mal formés (balises mal imbriquées, squelette manquant, attributs non quotés, etc.), les trois backends ont retrouvé un résultat propre. Le seul point faible concerne html.parser et les balises de fin optionnelles : les <td> / <li> non fermés sont imbriqués au lieu d’être fermés, ce qui corrompt le texte extrait. Passez au backend lxml ou html5lib et ce type de problème disparaît.
BeautifulSoup vs lxml — lequel est meilleur ?
Ce sont deux outils différents. lxml est beaucoup plus rapide pour la construction de l’arbre et pour les requêtes, et il prend en charge XPath. BeautifulSoup enveloppe lxml (entre autres) dans une API bien plus conviviale et dispose en réalité d’une meilleure couverture CSS via soupsieve. N’espérez simplement pas que le backend lxml rende bs4 aussi rapide qu’lxml — le backend n’accélère que le parsing, tandis que les requêtes et la traversée paient toujours le coût d’un objet Python par nœud, ce qui le laisse environ 6 à 7,5x plus lent sur de grandes sélections en batch.
Essayer Thunderbit pour l’extraction de données web Get Started Free


