BeautifulSoup in 2026: de vriendelijkste HTML-parser is ook de langzaamste (met 12–17x)

Laatst bijgewerkt op July 17, 2026
BeautifulSoup in 2026: de vriendelijkste HTML-parser is ook de langzaamste (met 12–17x)
AI Samenvatting
Deze review zet BeautifulSoup neer als de meest toegankelijke Python HTML-parser en maakt de prijs van die gebruiksvriendelijkheid heel concreet. De vergelijking met C-gestuurde parsers kijkt naar snelheid, tolerantie voor kapotte HTML, dekking van CSS-selectors, objectretentie en encoding-herstel. Het artikel laat zien dat BeautifulSoup duidelijk trager is, vaak 12 tot 17 keer, maar legt ook uit waarom ontwikkelaars het toch blijven gebruiken: leesbare API’s, vergevingsgezinde parsing, sterke soupsieve-selectorondersteuning en uitstekende ergonomie voor rommelige eenmalige scraping-taken. Het is een praktische gids voor wanneer die snelheidsbelasting acceptabel is en wanneer een snellere parser de betere technische keuze is.

BeautifulSoup is de bibliotheek waar bijna iedereen als eerste naar grijpt wanneer ze in Python een webpagina scrapen, en inderdaad: het is de langzaamste van de serieuze HTML-parsers. Beide dingen kloppen, en geen van beide is een verwijt. Het interessante is dat “langzaamste” hier neerkomt op een exact, meetbaar cijfer in plaats van op een vaag onderbuikgevoel.

Ik heb bs4 (dat is beautifulsoup4, versie 4.15.0, uitgebracht in juni 2026, onder MIT-licentie) aan een combinatie van nieuwe functietests en hergebruikte timingdata uit dezelfde benchmarkopstelling onderworpen, en het beeld is consistent: je betaalt grofweg een snelheidsverlies van één orde van grootte in ruil voor de vriendelijkste API en de sterkste fouttolerantie in dit veld. Of dat een slimme ruil is, hangt volledig af van je workload, dus in deze review zetten we beide kanten naast elkaar.

Wat BeautifulSoup eigenlijk is (en wat niet)

De meeste tutorials slaan juist het belangrijkste stuk over: BeautifulSoup parseert geen HTML. Het is een wrapper. Onder de motorkap geef je het document door aan een van drie echte parsers — Python’s ingebouwde html.parser, lxml, of html5lib — en daarna wikkelt bs4 de boom die daaruit komt in één enkele, heel gebruiksvriendelijke navigatie- en zoek-API. De taak van bs4 is niet om te parsen. De taak is om het resultaat prettig door te bladeren.

De auteur noemt het zelf een “screen-scraping library”, en de belofte is altijd hetzelfde geweest: richt het op HTML die zo rommelig is dat een browser er moeilijk over doet, en toch haalt het de data eruit die je zoekt. Die reputatie is verdiend, met één asterisk waar we zo op terugkomen.

Een paar feiten die we eerst even moeten vastzetten:

VeldWaarde
Pakketbeautifulsoup4 (importeren als bs4)
Geteste versie4.15.0 (geüpload op 2026-06-07)
Python-vereiste>=3.7.0
LicentieMIT
Canonieke homepagecrummy.com/software/BeautifulSoup
Broncode + bugtrackerLaunchpadniet GitHub
OnderhoudActief (4.15.0 in juni 2026, zes releases in het afgelopen jaar)

Die regel “niet GitHub” is belangrijker dan hij lijkt. bs4 is een 20 jaar oude bibliotheek die draait op crummy.com en Launchpad, dus de gebruikelijke GitHub-starcheck zegt hier weinig. Beoordeel de gezondheid liever op releasefrequentie, en op dat vlak is het pakket springlevend.

Een subtiele nuance over licenties, voor wie met compliance te maken heeft: de wrapper zelf is MIT, maar wat “bs4 gebruiken” precies toevoegt aan je dependency tree hangt af van welke backend je installeert. html.parser zit in de Python-standaardbibliotheek (PSF-licentie, geen extra dependencies). lxml is BSD, maar leunt op libxml2/libxslt — een externe C-dependency die je óf zelf compileert óf als vooraf gebouwde wheel binnenhaalt. html5lib is volledig in Python en MIT. Wil je de schoonste dependency footprint, dan geeft de ingebouwde html.parser je die — wat toevallig ook de backend is met de grootste adder onder het gras. Daar zo meer over.

De snelheidsbelasting, in cijfers

Laten we eerst het getal noemen, want dat is de headline en het zou oneerlijk zijn om die te verstoppen. Bij een realistische parse-then-extract-taak — de string parsen, alle <h3 class="title"> en alle <a href> eruit halen — is BeautifulSoup de langzaamste parser in deze vergelijking, en dat verschil is niet klein.

BeautifulSoup speed tax: 232 ms versus 15 ms C parsers

Deze timings zijn hergebruikt uit de selectolax-benchmarkopstelling (dezelfde machine, dezelfde 3-run-methode, peildatum 2026-07-13); deze review draait geen eigen timingbenchmark opnieuw, om CPU-contentie en dubbel werk te vermijden. Mediaan p50-latency, in milliseconden:

Paginaformaatbs4 (html.parser)bs4 (lxml)selectolax-Lexborlxmlbs4-hp tragerbs4-lxml trager
1 KB0.3230.2810.0270.03612.0x10.5x
10 KB2.0491.7050.1600.16612.8x10.7x
100 KB20.59916.5401.4641.42314.1x11.3x
1 MB232.558181.85514.90114.17715.6x12.2x
10 MB2788.7462261.557159.935172.93317.4x14.1x

Dus bs4(html.parser) draait ongeveer 12–17x langzamer dan een C-parser zoals selectolax-Lexbor, en overstappen naar de lxml-backend brengt het slechts terug naar 10.5–14x — nog steeds ruim één orde van grootte trager. De reden is structureel, niet een bug: ongeacht welke backend het parsen doet, bs4 bouwt voor elk afzonderlijk knooppunt een compleet Python-object (Tag of NavigableString). Die objectlaag is een belasting die C-parsers simpelweg niet betalen.

Let erop dat de vermenigvuldigingsfactor oploopt naarmate pagina’s groter worden — 12.0x bij 1 KB, 17.4x bij 10 MB. Dat vertelt je dat dit geen vaste opstartoverhead is die je kunt wegmiddelen. Het is een belasting per knooppunt die lineair schaalt met het aantal knooppunten dat je opbouwt.

Nu de herformulering, want “10x trager” klinkt vaak erger dan het in de praktijk is. Op een pagina van 1 MB is dat 232 ms tegenover 15 ms. Als je taak is “een paar honderd tot een paar duizend pagina’s van een paar honderd KB per stuk scrapen”, dan is dat absolute verschil praktisch onmerkbaar — je voelt het niet, en optimaliseren levert je weinig op. Als je taak een pipeline van een miljoen pagina’s is, dan is dezelfde verhouding het verschil tussen een job die afkomt en een job die strandt. Zelfde getal, totaal andere conclusie. Weeg het af tegen je echte volume, niet tegen de benchmark.

Nee, van backend wisselen lost het niet op

Er bestaat een hardnekkige mythe dat je bs4 gewoon de lxml-backend kunt geven en dan lxml-snelheid krijgt. Dat werkt niet, en het is goed om te begrijpen waarom. Bij een CSS-query over een batch van 100.000 knooppunten (selecteer elke <a> en lees de href, met de boom al opgebouwd) ziet de throughputverdeling er zo uit:

ParserQuery p50Knooppunten/sec
lxml33.30 ms3,002,646
selectolax-Modest34.19 ms2,924,550
selectolax-Lexbor39.46 ms2,534,027
bs4 (lxml)250.56 ms399,111

bs4(lxml) haalt ongeveer 399.000 knooppunten per seconde — grofweg 6.3–7.5x langzamer dan de drie C-engines, ook al is de backend zelf wel degelijk lxml. De backend versnelt het opbouwen van de boom. Query’s en traversals lopen nog steeds via soupsieve naar bs4 Tag-objecten, en elk gematcht knooppunt wordt opnieuw in Python verpakt. Dus het mentale model “geef bs4 lxml en het is lxml-snel” klopt niet: de backend versnelt één fase, en de traagste fase is niet die fase.

Geheugen en koude start maken het kostenplaatje compleet. Op een document van 10 MB gebruikt bs4 ongeveer 1.5–1.75x zoveel resident memory als selectolax of lxml (218–226 MB versus 129–145 MB) — dezelfde hoofdoorzaak, één Python-object per knooppunt. En het importeren van bs4 duurt ongeveer 33.4 ms tegenover 14.1 ms voor lxml.html, dus het is 2.36x trager om te importeren. Dat laatste is voor een langlopende service niet zo spannend, maar voor een CLI-tool of een serverless functie die telkens koud opstart, is het wel degelijk een kleine, reële kost om te kennen.

Waarom meer threads je niet gaan redden

Als je reflex bij een trage CPU-bound taak is “gooi er threads tegenaan”, dan straft bs4 die reflex af. Op een pagina van 1 MB, 48 keer geparsed, single-threaded versus vier threads:

Parser1 thread4 threadsSnelheidswinst
selectolax-Lexbor0.563 s0.159 s3.54x
lxml0.459 s0.378 s1.21x
bs4 (lxml)6.945 s26.842 s0.26x

Lees die onderste regel twee keer. Vier threads maakten bs4 ongeveer 3.9x trager, niet sneller. Het empirische signaal is “waarschijnlijk houdt het de GIL vast”: de tree construction van bs4 is pure Python, dus alles serialiseert onder de Global Interpreter Lock, en meer threads toevoegen betekent alleen maar scheduling-overhead op een job die niet echt parallel kan lopen. selectolax haalt zijn ~3.5x versnelling omdat de C-kern de lock vrijgeeft; bs4 heeft daarvoor geen ruimte.

Voor het tijdperk van free-threading is dit de praktische conclusie: als je BeautifulSoup wilt paralleliseren, gebruik multiprocessing (ProcessPoolExecutor), niet threads. selectolax en lxml kunnen op threads schalen; bs4 niet. Eén kanttekening bij de strengheid: dit is één observatie op één threadaantal (4) en één paginaformaat (1 MB), en het “houdt de GIL vast”-mechanisme is een hypothese die ik afleid uit wall-clock gedrag, niet iets wat ik heb bevestigd door instrumentatie van het exacte codepad dat de lock vasthoudt. De richting is duidelijk; het precieze mechanisme is voorlopig.

De standaardbackend is de valkuil. Lees dit eerst.

Als je maar één ding uit deze review haalt, laat het dan dit zijn. Een kale BeautifulSoup(html) zonder tweede argument gebruikt html.parser, en html.parser implementeert de HTML5-regels voor optionele eindtags niet. Dat klinkt academisch, totdat je data stilletjes wordt aangetast.

BeautifulSoup backend tolerance matrix: html.parser 12/15, lxml and html5lib 15/15

Ik heb 15 bewust fout opgemaakte HTML-samples door alle drie de backends gehaald, met voor elk voorbeeld vooraf een backend-agnostische structurele assertie vastgelegd (zodat niemand achteraf de winnaar kan kiezen). De scores:

BackendVoldoet aan verwachting / 15
lxml15
html5lib15
html.parser12

De drie mislukkingen hebben allemaal dezelfde hoofdoorzaak. Neem een niet-gesloten tabel: <table><tr><td>a<td>b<tr><td>c<td>d</table>. Onder html.parser komt de geëxtraheerde celtekst eruit als ['abcd','bcd','cd','d'] — elke <td> slokt alles erna op, omdat de parser de cellen nest in plaats van ze te sluiten. lxml en html5lib geven correct ['a','b','c','d'] terug. Ook losse lijstitems gedragen zich zo: <li>a<li>b<li>c levert onder html.parser de geneste ['abc','bc','c'] op, en onder de andere twee de nette ['a','b','c']. Dubbele attributen keren ook om — <div id="first" id="second"> houdt "second" vast onder html.parser, maar "first" onder lxml/html5lib, en de HTML5-specificatie zegt dat de eerste moet blijven.

Waarom dat gevaarlijk is in plaats van alleen irritant: het gebeurt zonder foutmelding. Een scraper die achteloos BeautifulSoup(html) gebruikt en een niet-gesloten tabel of lijst tegenkomt — wat bij oude sites, handgeschreven HTML en templates met vergeten sluit-tags pijnlijk vaak voorkomt — zal tekst uit aangrenzende cellen samenvoegen tot één veld, je vuile data geven, en geen enkele klacht laten horen. De oplossing is één argument: BeautifulSoup(html, "lxml") of BeautifulSoup(html, "html5lib").

Om html.parser recht te doen: de andere 12 van de 15 fout opgemaakte samples kwamen onder alle drie de backends identiek uit. Verkeerd geneste tags zoals <b><i></b></i>, ontbrekende html/body-skeletten, niet-geciteerde attributen, wees-sluit-tags, niet-gesloten comments, geneste forms, gemengde hoofdletters en meer — bs4 is in het algemeen echt heel tolerant. De afwijking zit vrijwel volledig in de familie van optionele eindtags. En dit is geen nieuwe ontdekking: de eigen documentatie van bs4 over “Differences between parsers” zegt al in gewone taal dat html.parser “less lenient” is. Wat de matrix met fout opgemaakte HTML toevoegt, zijn de concrete, reproduceerbare gevallen waarin “minder soepel” verandert in foutieve output.

Wat je níet opgeeft: de API en CSS zijn juist het sterkste punt

Dus bs4 is traag, single-threaded, en heeft een valkuil in de standaardbackend. Toch grijpen mensen er nog steeds naar, omdat de “vriendelijke” kant van de ruil volkomen echt is — en dat blijft overeind bij tests.

BeautifulSoup soupsieve CSS coverage wins with 41/41 plus 20/20

Ik heb 29 API-probes uitgevoerd die zoeken, CSS, boomnavigatie, textextractie en DOM-bewerking bestrijken. Alle 29 slaagden, en elk resultaat werd bepaald door de feitelijke returnwaarde te vergelijken met een verwachte waarde, niet op gevoel. Twee van die mogelijkheden zijn ergonomische voordelen die de C-parsers simpelweg niet bieden:

  • Functiepredicaten in find / find_all. Je kunt soup.find(lambda t: t.name == "a" and "btn" in t.get("class", [])) schrijven en een complexe voorwaarde in één regel Python uitdrukken — geen tweestapsproces van “selecteer alles en filter daarna” nodig.
  • Benoemde, bidirectionele boomnavigatie. .parent, .next_sibling, .find_parent, .stripped_strings, .descendants — de traversals lezen als gewone taal en gaan beide kanten op. selectolax heeft voor sommige hiervan meerdere stappen nodig, of biedt ze helemaal niet aan.

Dat is het deel van de ruil dat je ontwikkeltijd oplevert, concreet gemaakt. Geen marketing, maar 29 groene vinkjes.

Twee valkuilen om te onthouden, want een eerlijke review noemt beide kanten. Ten eerste, booleaanse attributen: <input disabled> geeft in bs4 een lege string "" terug voor disabled (selectolax geeft None). Beide zijn falsy, dus if node.get("disabled") mist stilletjes een booleaans attribuut dat in beide libraries echt aanwezig is — de veilige test is "disabled" in tag.attrs. Ten tweede: get_text(strip=True) plakt nodetekst aan elkaar zonder scheidingsteken na het strippen, dus "...with " + "link1" wordt "withlink1". Geef separator=" " mee wanneer je woordgrenzen nodig hebt. Geen van beide valkuilen is specifiek voor bs4; het zijn allebei cross-library gotchas.

En nu het verrassende deel: voor bs4 kiezen kost je geen CSS-dekking. De CSS-engine, soupsieve, is de meest complete implementatie in deze hele vergelijking. Op de basismatrix met 41 gevallen (hergebruikt uit de selectolax-opstelling) scoorde soupsieve 41/41 — de enige perfecte score in het veld, vóór selectolax-Lexbor met 39/41 en cssselect (lxml/parsel) met 37/41. Daarna heb ik nog 20 extra uitgebreide gevallen getest die de documentatie van soupsieve expliciet noemt, en ook daar ging het 20/20, inclusief selectors die Lexbor direct weigert: :lang(en), de alleen-voor-soupsieve geldige :-soup-contains('featured'), :is(), :where() en :has(> a). De echte hiaten zijn XPath (soupsieve is alleen CSS) en parsel’s ::text / ::attr() pseudo-elementen, die Scrapy-extensies zijn. Als je in XPath leeft, gaat die migratie pijn doen.

De conclusie voor dit stuk is duidelijk: wat je inlevert door voor BeautifulSoup te kiezen is snelheid. Het is niet API-ergonomie, en zeker niet CSS-dekking.

Twee productie-valkuilen waar je budget voor moet reserveren

Naast de standaardbackend zijn er twee gedragingen die je vooral zullen bijten in langlopende of niet-UTF-8 workloads.

Referentiecirkels: roep decompose() aan in lange lussen

Elke bs4 Tag heeft een verwijzing naar zijn ouder én naar zijn kinderen, en dat vormt een referentiecirkels. CPython’s reference counting kan een cyclus niet zelf opruimen — dat is het werk van de generational garbage collector. Om te zien hoeveel dat uitmaakt, heb ik een boom 300 keer opgebouwd en weer verwijderd met GC uitgeschakeld, en daarna geteld hoeveel Tag-objecten nog in geheugen zaten:

BeautifulSoup reference cycles retain 120,900 objects with GC off and 0 with GC on

ScenarioTags over na del
GC uit120,900 (300 cycli, niets opgeruimd)
GC aan26,598 (generational GC activeerde halverwege de lus)
Na geforceerde gc.collect()0 (alles opgeruimd)
Controle zonder cyclus (lijst strings, GC uit)delta 0

Met GC uit ruimde del soup niets op — alle 120.900 objecten bleven resident, omdat de referentiecirkels reference counting omzeilen. Eén gc.collect() maakte ze allemaal vrij. De controlegroep zonder cyclus (een gewone lijst strings, waarvan bekend is dat die geen cyclus bevat) had een delta van nul, wat bewijst dat de ophoping door bs4’s cyclus kwam en niet door meetruis. De eigen documentatie van bs4 zegt ook dat de objecten “densely interconnected ... exactly the sort a garbage collector would have trouble with” zijn, dus dit is gedocumenteerd gedrag; wat deze test toevoegt, is het aantal achtergebleven objecten en het bewijs dat collect() het tot nul reduceert.

De praktische regel: in een pipeline die veel grote pagina’s in een strakke lus verwerkt, als je code (of een instelling met hoge throughput) GC uitschakelt of niet vaak genoeg triggert, blijven bs4-bomen hangen en loopt geheugen op. Roep na elke pagina soup.decompose() aan — bs4 biedt die functie precies om de cyclus te breken en eerder vrij te geven. De C-bomen van selectolax en lxml hebben dit probleem helemaal niet.

Encoding: UnicodeDammit is bs4’s stille voordeel

bs4 levert een onderdeel mee dat de snelle parsers niet hebben: UnicodeDammit, dat de encoding van een document detecteert en het automatisch naar Unicode omzet. Ik heb het aan een matrix van 8 gevallen met “aangegeven versus werkelijke charset” onderworpen:

BeautifulSoup UnicodeDammit recovers 5 of 8 encoding cases

GevalWerkelijke encodingUnicodeDammit gokteHersteld?
utf8_no_declutf-8utf-8Ja
utf16_bomutf-16utf-16leJa
gbk_chinesegbkgb18030Ja (superset)
shiftjisshift_jiscp932Ja (superset)
latin1_declared_utf8latin-1 (aangegeven als utf-8)iso-8859-1Ja (de leugen genegeerd)
latin1_no_decllatin-1cp720Nee
cp1252_no_declcp1252cp862Nee
utf8_declared_latin1utf-8 (aangegeven als latin-1)iso-8859-1Nee (de leugen gevolgd)

Vijf van de acht hersteld. UTF-8, UTF-16 met BOM, GBK, Shift-JIS en zelfs verkeerd gelabelde latin-1 kwamen correct terug, en de superset-gissingen (GBK→gb18030, Shift-JIS→cp932) decoderen nog steeds prima. De twee faalmodi zijn het onthouden waard: korte latin-1/cp1252-byte samples worden soms aangezien voor DOS-codepagina’s, omdat de statistische detector op korte inputs niet betrouwbaar is en DOS-tekenboxen overlappen met latin-1-codepunten; en wanneer een <meta charset>-aanduiding gewoon fout is, vertrouwt UnicodeDammit die verklaring. De documentatie van bs4 wijst beide dingen al aan — een sample kan “so short that Unicode, Dammit can’t get a lock on it” zijn, en meer data geeft een betere gok.

Vergeleken met selectolax, dat non-UTF-8 bytes stilletjes kan verminken en verwacht dat jij ze zelf decodeert, is dit een echt voordeel: bs4 probeert tenminste te detecteren en slaagt vaak ook. Maar het is geen garantie. Voor een bekende encoding: sla het gokken over en wees expliciet: BeautifulSoup(bytes, from_encoding="...").

Vallen de backends in de praktijk ooit echt uiteen?

De matrix met fout opgemaakte HTML laat zien dat de backends uiteenlopen op expres kapot gemaakte input. De voor de hand liggende vervolgvraag is of dat in het wild echt telt, dus heb ik alle drie de backends losgelaten op 11 echte opgehaalde pagina’s — BBC, Wikipedia, Craigslist, MDN, old.reddit, Python docs, Hacker News, Books to Scrape, webscraper.io, whitehouse.gov, en een met JavaScript gerenderde quotes-pagina — en daarbij aantallen links, koppen en afbeeldingen vergeleken.

Alle drie waren het eens op alle 11 pagina’s. Nul divergentie. Dat betekent dat de backendverschillen uit het valkuil-gedeelte alleen zichtbaar worden bij bewust foutieve HTML; wanneer een moderne productiesite voldoende netjes is opgebouwd — zelfs een “rommelige” site — verandert de backendkeuze niets aan wat je eruit haalt. De praktische lezing: voor mainstream, redelijk goed gevormde sites is html.parser prima en bespaar je jezelf een dependency. Alleen wanneer je zichtbaar niet-standaard, handgeschreven of oude HTML scrape’t, gaat de backendkeuze je resultaten beïnvloeden, en dan stap je over op lxml of html5lib.

Nog één zijnoot uit die run, want het is een echt randgeval. De MDN-pagina bevat een <template>-element, en alle bs4-backends gaven 508 links terug — wat betekent dat bs4 de inhoud van <template> in de hoofdboom plat slaat. Daarmee zit bs4 aan dezelfde kant als lxml, en aan de andere kant van selectolax-Lexbor, dat strikt de HTML5-specificatie volgt (een <template> is een inert DocumentFragment) en 497 links teruggeeft, waarbij de 11 links in de template stilzwijgend worden genegeerd. Dus bs4 vangt ook data binnen een <template> op — handig, maar ook een manier om “spookcontent” op te pikken die een browser nooit zou renderen. Geen van beide gedragingen is fout; het zijn verschillende interpretaties van de specificatie, en je moet weten welke je krijgt.

Waar BeautifulSoup past — en waar niet

In plaats van dit allemaal in één 0–100-score te proppen, waardoor precies de afwegingen die ertoe doen verdwijnen, is hier een scorekaart per dimensie, met per regel een kanttekening:

DimensieWat de tests vondenKanttekening voor de lezer
Installatie / eerste runPure wrapper, geen browser/setup; html.parser geen extra deps; alle prebuilt wheelslxml-backend heeft een C-dependency nodig
Snelheid t.o.v. C-parsers12–17x trager (html.parser) / 10.5–14x (lxml-backend), alle formatenEén rig; selectolax-data hergebruikt
CSS-query throughput~6–7.5x trager op 100k knooppunten; lxml-backend redt het nietHergebruikt; betaalt de Python Tag-belasting
Geheugen1.5–1.75x selectolax/lxml; zwaarstHergebruikt; gemeten via RSS
Import cold start2.36x trager (33.4 vs 14.1 ms)Hergebruikt; kleine post
Thread-schaalbaarheidbs4-lxml ~3.9x trager bij 4 threads (houdt GIL vast)Eén observatie; gebruik multiprocessing
API-ergonomie29/29 probes; functiepredicaat-find + bidirectionele navigatieLege-string bool-attribuut en strip-woordgrens-valkuilen
CSS-dekkingsoupsieve sterkst: 41/41 basis + 20/20 uitgebreid; ondersteunt :langGeen XPath, geen ::text
Tolerantie over 3 backendslxml/html5lib 15/15; html.parser 12/15Divergentie alleen bij foutieve HTML
Consistentie op echte pagina’s3 backends eens in 11/11; alle drie flatten <template> (508)Goed gevormde sites: backend maakt niet uit
GC door referentiecirkelsBoom is een cyclus; 300 lussen hielden 120,900 objecten vast, collect bracht naar nulLange lussen hebben decompose() nodig
EncodingUnicodeDammit herstelt 5/8; gokt fout bij korte samples, volgt foute declaratiesEén observatie
OnderhoudActief (4.15.0, juni 2026); MITHome op crummy/Launchpad, niet GitHub

Dus voor wie is BeautifulSoup? Voor iedereen die een leesbare API en vergevingsgezinde parsing belangrijker vindt dan brute throughput, en die op middelgrote schaal werkt — prototypes, eenmalige scrapes, interne tools, teams waar ontwikkeltijd duurder is dan runtime. Wie moet elders kijken? Million-page pipelines waar de snelheidsbelasting in echt geld gaat lopen, workloads die thread-level parallelism nodig hebben, en iedereen die vastzit aan XPath.

Nog een noot over waar dit in een echte scraping stack past, en waar ons eigen gereedschap binnenkomt. BeautifulSoup gaat ervan uit dat je de HTML al hebt. Het haalt geen pagina’s op, rendert geen JavaScript en doet niets aan anti-botmaatregelen of CAPTCHA’s — dat is een aparte taak, en op het moderne web echt een lastige. Dáár zit een AI scraping API op een andere laag: de developer stack van Thunderbit — een REST API, een MCP-server en een CLI — handelt het ophalen, JavaScript-rendering en anti-botprobleem af, en geeft je vervolgens ofwel schone Markdown (POST /distill) of schema-matched gestructureerde JSON (POST /extract), zonder dat je zelf selectors hoeft te schrijven. De twee concurreren niet; ze vullen elkaar aan. bs4 parseert HTML die je al hebt; Thunderbit’s API, MCP en CLI geven je de HTML waar je anders moeilijk bij komt. Als je bottleneck parsing is, is bs4 prima. Als je bottleneck acquisitie is, dan zit je op een andere laag.

Probeer Thunderbit voor webdata-extractie

Conclusie

BeautifulSoup geeft je de vriendelijkste API, de sterkste tolerantie voor foutieve HTML en de meest complete CSS-engine in deze vergelijking — betaald met een snelheidsbelasting van ongeveer één orde van grootte en het zwaarste geheugengebruik. Dat is de hele deal, helder geformuleerd. De standaard html.parser-backend is de enige echte valkuil: die verminkt stilletjes niet-gesloten tabellen en lijsten, dus geef altijd "lxml" of "html5lib" mee wanneer je input rommelig kan zijn. Threads versnellen het niet — multiprocessing wel. En in lange loops moet je per pagina decompose() aanroepen om te voorkomen dat referentiecirkels zich opstapelen.

Twee beperkingen om mee af te sluiten. Alles hier is gemeten op één platform (macOS arm64, Python 3.14, vooraf gebouwde wheels), en de snelheidsvermenigvuldigingen zijn hergebruikt uit de selectolax-opstelling (dezelfde benchmark, peildatum 2026-07-13) in plaats van opnieuw uitgevoerd — dus ze dragen die platformbeperking mee, en een Linux x86_64- of brongecompileerde setup kan de exacte cijfers verschuiven. En niets in deze resultaten is een nieuwe ontdekking: bs4 is een 20 jaar oude bibliotheek, dus elk getest gedrag is óf gedocumenteerd óf publiek vastgelegd. De waarde zit niet in een scoop, maar in het omzetten van kwalitatieve afwegingen uit de documentatie naar harde cijfers.

Veelgestelde vragen

Is BeautifulSoup traag? Ja, meetbaar. Bij een parse-plus-extract-taak draait het ongeveer 12–17x langzamer dan een C-parser zoals selectolax-Lexbor met de standaard html.parser-backend, en 10.5–14x langzamer met de lxml-backend, omdat het voor elk knooppunt een Python-object aanmaakt. Of dat uitmaakt hangt af van de schaal: op een pagina van 1 MB is het 232 ms versus 15 ms, onzichtbaar voor een paar duizend pagina’s maar beslissend voor een pipeline van een miljoen pagina’s.

Welke BeautifulSoup-parser moet ik gebruiken — html.parser, lxml of html5lib? Voor nette, mainstream sites is de standaard html.parser prima en voegt die geen dependencies toe. Maar hij implementeert de HTML5-regels voor optionele eindtags niet, dus bij niet-gesloten tabellen of lijsten lekt aangrenzende tekst samen zonder foutmelding. Als je input foutief, handgeschreven of oud kan zijn, geef dan expliciet "lxml" of "html5lib" mee — beide haalden een nette 15/15 op een matrix met fout opgemaakte HTML, waar html.parser 12/15 scoorde.

Kan BeautifulSoup parallel parsen met threads? Nee. De tree construction van bs4 is pure Python en houdt de GIL vast, dus threads toevoegen maakt het trager, niet sneller — in tests was vier threads op een parse van 1 MB ongeveer 3.9x trager dan één thread. Gebruik multiprocessing (ProcessPoolExecutor) om bs4 te paralleliseren. Bibliotheken met C-cores, zoals selectolax en lxml, zijn de tools die wél thread-level parallelism benutten.

Kan BeautifulSoup goed overweg met kapotte HTML? In grote lijnen wel — over een reeks fout opgemaakte samples (verkeerd geneste tags, ontbrekende skeletjes, niet-geciteerde attributen en meer) herstelden alle drie de backends zich netjes. De zwakke plek is de standaard html.parser en optionele eindtags: niet-gesloten <td>/<li> worden genest in plaats van gesloten, waardoor geëxtraheerde tekst beschadigd raakt. Schakel over op de lxml- of html5lib-backend en dat probleem verdwijnt.

BeautifulSoup versus lxml — wat is beter? Het zijn verschillende gereedschappen. lxml is veel sneller bij zowel het bouwen van de boom als bij query’s, en ondersteunt XPath. BeautifulSoup wikkelt lxml (en andere parsers) in een veel vriendelijkere API en heeft via soupsieve zelfs bredere CSS-dekking. Verwacht alleen niet dat de lxml-backend bs4 ineens lxml-snel maakt — de backend versnelt alleen het parsen, terwijl query’s en traversals nog steeds de per-knooppunt Python-objectkost van bs4 betalen, waardoor het op grote batchselecties ongeveer 6–7.5x trager blijft.

Probeer Thunderbit voor webdata-extractie Get Started Free

Ke
Ke
CTO bij Thunderbit | Senior Data Scientist & ML-expert Met bijna tien jaar ervaring in machine learning en data science is Ke Shen alumnus van Columbia University en voormalig Senior Data Scientist bij Walmart Labs. Met diepgaande, door vakgenoten erkende expertise in Python, R, Java en statistiek deelt hij praktijkgerichte inzichten over hoe je complexe AI-algoritmen van theorie naar productieklare architectuur brengt.
Inhoudsopgave
Thunderbit · AI webdata-agent

Extraheer gegevens van elke pagina in 1 klik

Vertrouwd door meer dan 250.000 gebruikers
Gratis abonnement beschikbaar
Extraheer gegevens met AI
Zet gegevens eenvoudig over naar Google Sheets, Airtable of Notion
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week