BeautifulSoup er det bibliotek, næsten alle griber efter første gang, de skal scrape en webside i Python, og det er faktisk også den langsomste af de seriøse HTML-parsere. Begge dele passer, og ingen af dem er en kritik. Det interessante er, at “langsommest” viser sig at være et konkret tal, man kan regne på, ikke bare en mavefornemmelse.
Jeg satte bs4, altså beautifulsoup4, version 4.15.0, udgivet i juni 2026 under MIT-licens, igennem en kombination af nye kapabilitetstests og genbrugte timingdata fra samme benchmark-setup, og billedet er klart: Du bytter en hastighedsstraff på omkring en størrelsesorden for den mest brugervenlige API og den stærkeste fejl-tolerance i feltet. Om det er en god handel, afhænger helt af din workload, så denne gennemgang holder begge sider af regnestykket fremme hele vejen.
Hvad BeautifulSoup egentlig er — og ikke er
De fleste tutorials springer det over, der betyder mest: BeautifulSoup parser ikke HTML. Det er en wrapper. Under motorhjelmen sender den dit dokument videre til en af tre rigtige parsere — Python’s indbyggede html.parser, lxml eller html5lib — og pakker derefter træet, de laver, ind i én samlet, meget venlig navigations- og søge-API. bs4’s opgave er ikke at parse. Den skal gøre resultatet behageligt at arbejde med.
Skaberens egen beskrivelse er en “screen-scraping library”, og salgsbudskabet har altid været det samme: Peg den mod HTML, der er så fejlbehæftet, at en browser ville gyse, og den finder stadig de data, du bad om. Det ry er fortjent, med én vigtig fodnote, som vi kommer til.
Her er et par fakta, der er gode at få på plads først:
| Felt | Værdi |
|---|---|
| Pakke | beautifulsoup4 (importér som bs4) |
| Testet version | 4.15.0 (uploadet 2026-06-07) |
| Python-krav | >=3.7.0 |
| Licens | MIT |
| Officiel hjemmeside | crummy.com/software/BeautifulSoup |
| Kilde + bug tracker | Launchpad — ikke GitHub |
| Vedligeholdelse | Aktiv (4.15.0 i juni 2026, seks releases det seneste år) |
Linjen med “ikke GitHub” betyder mere, end man lige skulle tro. bs4 er et 20 år gammelt bibliotek, der bor på crummy.com og Launchpad, så den sædvanlige GitHub-stjerne-måling er ikke særlig relevant. Vurder i stedet sundheden ud fra release-frekvens — og dér står det klart, at projektet lever fint.
En lille nuance om licens, for alle der skal forklare det til et compliance-team: Wrapperen er MIT, men det, man faktisk får med ind i dependency-træet, afhænger af den backend, man installerer. html.parser er en del af Python standardbiblioteket (PSF-licens, ingen ekstra dependencies). lxml er BSD, men bygger oven på libxml2/libxslt — en ekstern C-dependency, som du enten kompilerer selv eller henter som en færdig wheel. html5lib er ren Python og MIT. Vil du have det mest rene dependency-footprint, giver den indbyggede html.parser dig det — og den backend er samtidig den, der har den største hage. Mere om det om lidt.
Hastighedsstraffen, gjort op i tal
Lad os lægge tallet frem med det samme, for det er hovedpointen, og det ville være uærligt at gemme det væk. På en realistisk parse-og-udtræk-opgave — parse strengen, træk alle <h3 class="title"> og alle <a href> ud — er BeautifulSoup den langsomste parser i sammenligningen, og det er ikke et tæt løb.

Disse målinger er genbrugt fra selectolax-benchmark-setupet (samme maskine, samme 3-run-metode, pr. 2026-07-13); denne artikel kører ikke egne timingbenchmarks igen for at undgå CPU-konkurrence og dobbeltarbejde. Median p50-latens i millisekunder:
| Sidestørrelse | bs4 (html.parser) | bs4 (lxml) | selectolax-Lexbor | lxml | bs4-hp langsommere | bs4-lxml langsommere |
|---|---|---|---|---|---|---|
| 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 |
Så bs4(html.parser) kører omkring 12–17x langsommere end en C-parser som selectolax-Lexbor, og skift til lxml-backend bringer det kun ned til 10.5–14x — stadig langt bagefter. Årsagen er strukturel, ikke en fejl: Uanset hvilken backend der parser, bygger bs4 et fuldt Python-objekt (Tag eller NavigableString) for hver eneste node. Det lag, hvor alle objekter materialiseres, er en straf, som C-parsere simpelthen ikke betaler.
Bemærk, at multiplikatoren stiger, når siderne bliver større — 12.0x ved 1 KB, 17.4x ved 10 MB. Det fortæller dig, at det her ikke bare er et fast startup-overhead, du kan udjævne væk. Det er en per-node-straf, der skalerer lineært med antallet af noder, du bygger.
Nu til perspektivet, for “10x langsommere” lyder værre, end det ofte er. På en side på 1 MB er det 232 ms mod 15 ms. Hvis dit job er “scrape et par hundrede til et par tusinde sider på hver nogle hundrede KB”, så er den absolutte forskel i praksis næsten usynlig — du mærker den ikke, og at optimere den væk giver dig intet. Hvis dit job er en pipeline på en million sider, er samme ratio forskellen på et job, der bliver færdigt, og et job, der ikke gør. Samme tal, helt forskellig konklusion. Vurder det ud fra dit faktiske volumen, ikke ud fra benchmarket.
Nej, at skifte backend løser ikke problemet
Der findes en sejlivet myte om, at man kan give bs4 lxml-backend og så få lxml-hastighed. Det kan man ikke, og det er værd at forstå hvorfor. På en batch CSS-forespørgsel med 100.000 noder (vælg hver <a> og læs dens href, med træet allerede bygget) er forskellen i throughput markant:
| Parser | Query p50 | Noder/sek |
|---|---|---|
| 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) klarer cirka 399.000 noder/sekund — omkring 6.3–7.5x langsommere end de tre C-engines, selv om dens egen backend er lxml. Backend’en gør træbygningen hurtigere. Men query og traversal går stadig gennem soupsieve ind i bs4 Tag-objekter, og hver matchet node bliver stadig pakket ind i Python. Så mentalmodellen “giv bs4 lxml, så bliver det lxml-hurtigt” er forkert: backend’en accelererer én fase, og den langsomste fase er ikke den.
Hukommelse og cold start fuldender billedet. På et dokument på 10 MB bruger bs4 omkring 1.5–1.75x så meget resident memory som selectolax eller lxml (218–226 MB mod 129–145 MB) — samme grundårsag, ét Python-objekt pr. node. Og at importere bs4 tager cirka 33.4 ms mod 14.1 ms for lxml.html, så den er 2.36x langsommere at importere. Det sidste er en afrundingsfejl i en langkørende proces, men for et CLI-værktøj eller en serverless funktion, der konstant cold-starter, er det en lille, reel omkostning, man bør kende til.
Hvorfor flere tråde ikke redder dig
Hvis din instinktive reaktion på en langsom CPU-bound opgave er “smid tråde på den”, så straffer bs4 den intuition. På en 1 MB side, parse udført 48 gange, enkelttrådet versus fire tråde:
| Parser | 1 tråd | 4 tråde | Speedup |
|---|---|---|---|
| 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 |
Læs nederste række to gange. Fire tråde gjorde bs4 cirka 3.9x langsommere, ikke hurtigere. Det empiriske signal er “holder sandsynligvis GIL”: bs4’s træbygning er ren Python, så den serialiseres under Global Interpreter Lock, og flere tråde giver bare ekstra planlægnings-overhead til et job, der reelt ikke kan køre parallelt. selectolax får sin ~3.5x speedup, fordi dens C-kerne frigiver låsen; bs4 har ikke samme spillerum.
For free-threading-æraen er den praktiske lære: Hvis du vil parallelisere BeautifulSoup, så brug multiprocessing (ProcessPoolExecutor), ikke tråde. selectolax og lxml kan skaleres med tråde; bs4 kan ikke. En lille forbeholdslinje om metodik: Det her er én observation ved ét trådtal (4) på én sidestørrelse (1 MB), og mekanismen “holder GIL” er en hypotese udledt af wall-clock-adfærd, ikke noget jeg har instrumenteret helt ned på præcis hvilken kodevej, der holder låsen. Retningen er klar; den nøjagtige mekanisme er foreløbig.
Standard-backenden er fælden. Læs dette først.
Hvis du kun tager én ting med dig fra denne gennemgang, så tag dette. En ren BeautifulSoup(html) uden andet argument bruger html.parser, og html.parser implementerer ikke HTML5’s regler for valgfrie slut-tags. Det lyder akademisk, indtil det stille og roligt korrumperer dine data.

Jeg kørte 15 bevidst fejlbehæftede HTML-eksempler gennem alle tre backends, med en backend-agnostisk strukturel assertion registreret på forhånd for hver enkelt, før testen kørte (så ingen kan vælge vinderen bagefter). Resultatet:
| Backend | Matcher forventning / 15 |
|---|---|
| lxml | 15 |
| html5lib | 15 |
| html.parser | 12 |
De tre fejl har samme rodårsag. Tag en uafsluttet tabel: <table><tr><td>a<td>b<tr><td>c<td>d</table>. I html.parser kommer den udtrukne celletekst ud som ['abcd','bcd','cd','d'] — hver <td> sluger alt efter sig, fordi parseren indlejrer cellerne i stedet for at lukke dem. lxml og html5lib returnerer korrekt ['a','b','c','d']. Almindelige listeelementer opfører sig på samme måde: <li>a<li>b<li>c giver den indlejrede ['abc','bc','c'] under html.parser og den rene ['a','b','c'] under de to andre. Dobbeltattributter vender også: <div id="first" id="second"> beholder "second" under html.parser, men "first" under lxml/html5lib, og HTML5-specifikationen siger, at den første skal bevares.
Her er hvorfor det er farligt og ikke bare irriterende: Det sker uden fejlmeddelelse. En scraper, der bare gør BeautifulSoup(html) og rammer en uafsluttet tabel eller liste — hvilket er deprimerende almindeligt på gamle sites, håndskrevet HTML og templates, der har glemt en closing tag — vil blande tilstødende celletekst sammen til ét felt, give dig beskidte data og aldrig klage. Løsningen er én ekstra parameter: BeautifulSoup(html, "lxml") eller BeautifulSoup(html, "html5lib").
For at være fair over for html.parser: De andre 12 af 15 fejlbehæftede samples kom ud identisk på tværs af alle tre backends, blandt andet forkert indlejrede tags som <b><i></b></i>, manglende html/body-skelet, attributter uden citationstegn, forældreløse slut-tags, uafsluttede kommentarer, indlejrede forms, blandet case og mere. bs4’s tolerance er altså virkelig stærk generelt; forskellene samler sig næsten udelukkende omkring familien af valgfrie slut-tags. Og intet af dette er en ny opdagelse — bs4’s egen dokumentation om “Differences between parsers” siger allerede med klare ord, at html.parser er “less lenient”. Det fejlbehæftede matrix viser blot de konkrete, reproducerbare tilfælde, hvor “less lenient” bliver til forkert output.
Hvad du ikke opgiver: API’et og CSS’en er den bedste del
Så bs4 er langsom, enkelttrådet og har en fælde i standard-backenden. Folk griber stadig efter den, fordi den “venlige” del af byttet er fuldstændig reel — og den holder også i test.

Jeg kørte 29 API-probes, der dækkede søgning, CSS, træ-navigation, tekstudtræk og DOM-modifikation. Alle 29 bestod, og hvert probes resultat blev beregnet ved at sammenligne det faktiske returresultat med en forventet værdi frem for at blive vurderet med øjemål. To af egenskaberne er især ergonomi, som C-parsere simpelthen ikke tilbyder:
- Funktionspredikater i
find/find_all. Du kan skrivesoup.find(lambda t: t.name == "a" and "btn" in t.get("class", []))og udtrykke en kompleks betingelse i én linje Python — uden den sædvanlige “vælg alt, filtrér bagefter”-dobbeltproces. - Navngiven, tovejs træ-navigation.
.parent,.next_sibling,.find_parent,.stripped_strings,.descendants— traversal læses næsten som almindeligt sprog og går begge veje. selectolax kræver flere trin til nogle af dem eller tilbyder dem slet ikke.
Det er den del, hvor bs4 køber udviklertid. Det er ikke marketing; det er 29 grønne flueben.
To fælder er værd at nævne, fordi en fair anmeldelse beskriver begge sider. Først boolean-attributter: <input disabled> returnerer en tom streng "" for disabled i bs4 (selectolax returnerer None). Begge dele er falsy, så if node.get("disabled") overser stille og roligt en boolean-attribut, der faktisk er til stede i begge biblioteker — den sikre test er "disabled" in tag.attrs. For det andet: get_text(strip=True) sætter node-tekst sammen uden separator efter stripping, så "...with " + "link1" bliver til "withlink1". Angiv separator=" ", når du har brug for ordgrænser. Ingen af fælderne er bs4-specifikke; begge er faldgruber på tværs af biblioteker.
Og her er den del, der overrasker mange: At vælge bs4 koster dig ikke CSS-dækning. Dens CSS-engine, soupsieve, er den mest komplette implementering i hele denne sammenligning. På basismatricen med 41 cases (genbrugt fra selectolax-setupet) scorede soupsieve 41/41 — den eneste perfekte score i feltet, foran selectolax-Lexbors 39/41 og cssselects (lxml/parsel) 37/41. Derefter kørte jeg 20 ekstra udvidede cases, som soupsieve’s docs selv fremhæver, og den gik 20/20, inklusive selectors som Lexbor direkte afviser: :lang(en), soupsieve-only :-soup-contains('featured'), :is(), :where() og :has(> a). De eneste reelle mangler er XPath (soupsieve er CSS-only) og parsels ::text / ::attr() pseudo-elementer, som er Scrapy-udvidelser. Lever du i XPath, vil den migration gøre ondt.
Konklusionen i dette afsnit er enkel: Det, du giver afkald på ved at vælge BeautifulSoup, er hastighed. Ikke API-ergonomi, og bestemt ikke CSS-dækning.
To produktionsfælder, det er værd at budgettere med
Ud over standard-backenden er der to adfærdsmønstre, der især bider dig i langkørende eller ikke-UTF-8 workloads.
Reference-cykler: Kald decompose() i lange loops
Hver bs4 Tag holder reference til både sin parent og sine children, og det skaber en reference-cyklus. CPython’s reference counting kan ikke selv rydde en cyklus op — det er den generationsbaserede garbage collector, der skal gøre det. For at se, hvor meget det betyder, byggede og slettede jeg et træ 300 gange med GC slået fra og talte derefter de overlevende Tag-objekter, der stadig lå i hukommelsen:

| Scenarie | Tags tilbage efter del |
|---|---|
| GC off | 120,900 (300 cyklusser, intet frigivet) |
| GC on | 26,598 (generations-GC kørte midt i loopet) |
Efter tvungen gc.collect() | 0 (alt frigivet) |
| Kontrol uden cyklus (liste af strenge, GC off) | delta 0 |
Med GC slået fra frigjorde del soup intet — alle 120.900 objekter blev liggende i resident memory, fordi reference-cyklussen gør reference counting nytteløs. Én enkelt gc.collect() fjernede dem alle. Kontrolgruppen uden cyklus (en almindelig liste af strenge, som vi ved ikke har en cyklus) havde et delta på nul, hvilket beviser, at ophobningen kom fra bs4’s cyklus og ikke fra måle-støj. bs4’s egen dokumentation siger, at objekterne er “densely interconnected ... exactly the sort a garbage collector would have trouble with”, så det er dokumenteret adfærd; det testen tilføjer, er antallet af tilbageholdte objekter og beviset for, at collect() nulstiller det.
Den praktiske regel: I en pipeline, der parser mange store sider i et stramt loop, vil bs4-træer blive hængende, og hukommelsen vokse, hvis din kode (eller en høj-throughput-indstilling) slår GC fra eller ikke udløser den ofte nok. Kald soup.decompose() efter hver side — bs4 giver netop den metode for at bryde cyklussen og frigøre tidligt. C-træerne fra selectolax og lxml har slet ikke dette problem.
Encoding: UnicodeDammit er bs4’s stille fordel
bs4 leverer en komponent, de hurtige parsere ikke har: UnicodeDammit, som aflæser et dokuments encoding og konverterer til Unicode automatisk. Jeg gav den en matrix med 8 cases, hvor deklareret og faktisk charset ikke nødvendigvis matcher:

| Case | Sand encoding | UnicodeDammit gættede | Genvundet? |
|---|---|---|---|
| utf8_no_decl | utf-8 | utf-8 | Ja |
| utf16_bom | utf-16 | utf-16le | Ja |
| gbk_chinese | gbk | gb18030 | Ja (superset) |
| shiftjis | shift_jis | cp932 | Ja (superset) |
| latin1_declared_utf8 | latin-1 (deklareret utf-8) | iso-8859-1 | Ja (ignorerede løgnen) |
| latin1_no_decl | latin-1 | cp720 | Nej |
| cp1252_no_decl | cp1252 | cp862 | Nej |
| utf8_declared_latin1 | utf-8 (deklareret latin-1) | iso-8859-1 | Nej (fulgte løgnen) |
Fem ud af otte blev genvundet korrekt. UTF-8, UTF-16 med BOM, GBK, Shift-JIS og endda forkert mærket latin-1 kom rigtigt ud, og supersæt-gættene (GBK→gb18030, Shift-JIS→cp932) dekoder stadig fint. De to fejltyper er værd at kende: korte latin-1/cp1252-byteprøver bliver fejlagtigt vurderet som DOS-code pages, fordi den statistiske detektor ikke er pålidelig på korte inputs, og fordi DOS-box-drawing-tegn overlapper Latin-1’s kodepunkter; og når en <meta charset>-deklaration simpelthen er forkert, stoler UnicodeDammit på deklarationen. bs4’s docs peger på begge dele — en prøve kan være “so short that Unicode, Dammit can't get a lock on it”, og mere data giver et bedre gæt.
Sammenlignet med selectolax, som stille og roligt korrumperer ikke-UTF-8-bytes og forventer, at du selv dekoder dem, er det en reel fordel: bs4 forsøger i det mindste at gætte encodingen og lykkes ofte. Men det er ingen garanti. Ved en kendt encoding er den sikre vej at være eksplicit: BeautifulSoup(bytes, from_encoding="...").
Er der overhovedet reel uenighed mellem backends på rigtige sider?
Den fejlbehæftede matrix viser, at backends kan divergere på bevidst ødelagt input. Det oplagte næste spørgsmål er, om det også betyder noget i praksis, så jeg kørte alle tre backends over 11 rigtige hentede sider — BBC, Wikipedia, Craigslist, MDN, old.reddit, Python docs, Hacker News, Books to Scrape, webscraper.io, whitehouse.gov og en JS-renderet quotes-side — og sammenlignede antal links, overskrifter og billeder.
Alle tre var enige på alle 11 sider. Nul divergence. Det betyder, at backend-uenigheden fra fældesektionen kun dukker op på bevidst fejlbehæftet HTML; når en moderne produktionsside er struktureret rimeligt godt — også selv om den er lidt rodet — ændrer backend-valget ikke det, du udtrækker. Den praktiske læsning er: På mainstream-sider, der er nogenlunde velstrukturerede, er html.parser helt fin og sparer dig dependency’en. Kun når du scraper synligt ikke-standardiseret, håndskrevet eller gammelt HTML, begynder backend-valget at flytte dine resultater, og dér skifter du til lxml eller html5lib.
En sidebemærkning fra den test, fordi det er et ægte edge case. MDN-siden indeholder et <template>-element, og alle bs4-backends returnerede 508 links — hvilket betyder, at bs4 flader <template>-indhold ud i det primære træ. Det placerer bs4 på samme side som lxml og modsat selectolax-Lexbor, som følger HTML5-specifikationen stramt (et <template> er et inert DocumentFragment) og returnerer 497, mens de 11 links inde i template’en stille og roligt droppes. Så bs4 vil fange data inde i en <template> — nyttigt, men også en måde at få fat i “spøgelsesindhold”, som en browser aldrig ville rendere. Ingen af adfærdstyperne er forkerte; de er bare forskellige fortolkninger af spec’en, og du bør vide, hvilken du får.
Hvor BeautifulSoup passer ind — og hvor det ikke gør
I stedet for at presse alt det her ned i én samlet 0–100-score (som netop ville skjule de trade-offs, der betyder noget), får du her et scorecard på dimensionsniveau med en lille fodnote til hver række:
| Dimension | Hvad testene fandt | Forbehold for læseren |
|---|---|---|
| Installation / første kørsel | Ren wrapper, ingen browser/setup; html.parser uden deps; alle wheels færdigbyggede | lxml-backenden kræver en C-dependency |
| Hastighed vs C-parsere | 12–17x langsommere (html.parser) / 10.5–14x (lxml-backend), alle størrelser | Ét setup; genbrugte selectolax-data |
| CSS-query throughput | ~6–7.5x langsommere på 100k noder; lxml-backend redder det ikke | Genbrugt; betaler Python Tag-skatten |
| Hukommelse | 1.5–1.75x selectolax/lxml; tungest | Genbrugt; målt via RSS |
| Import-cold-start | 2.36x langsommere (33.4 vs 14.1 ms) | Genbrugt; lille post |
| Trådskalering | bs4-lxml ~3.9x langsommere ved 4 tråde (holder GIL) | Én observation; brug multiprocessing |
| API-ergonomi | 29/29 probes; funktionspredikat-find + tovejsnavigation | Faldgruber med tom streng for bool-attribut og strip-ordgrænser |
| CSS-dækning | soupsieve stærkest: 41/41 base + 20/20 udvidet; understøtter :lang | Ingen XPath, ingen ::text |
| Tolerance på 3 backends | lxml/html5lib 15/15; html.parser 12/15 | Uenighed kun på fejlbehæftet HTML |
| Konsistens på rigtige sider | 3 backends enige 11/11; alle flader <template> ud (508) | Velstrukturerede sider: backend er ligegyldig |
| Reference-cyklus / GC | Træet er en cyklus; 300 loops beholdt 120,900 objekter, collect nulstillede det | Lange loops kræver decompose() |
| Encoding | UnicodeDammit genvinder 5/8; fejlbedømmer korte samples, følger dårlige deklarationer | Én observation |
| Vedligeholdelse | Aktiv (4.15.0, juni 2026); MIT | Hjemme på crummy/Launchpad, ikke GitHub |
Så hvem er BeautifulSoup til? Alle der værdsætter en læsbar API og tilgivende parsing over rå throughput, når volumen er moderat — prototyper, engangs-scrapes, interne værktøjer, teams hvor udviklertid er dyrere end runtime. Hvem bør kigge andre steder hen? Million-side-pipelines, hvor hastighedsstraffen bliver til rigtige penge, workloads der kræver parallelisering på trådniveau, og alle der er bundet til XPath.
En note om, hvor dette passer ind i en reel scraping-stack, og hvor vores eget værktøj kommer ind. BeautifulSoup forudsætter, at du allerede har HTML’en. Den henter ikke sider, den rendrer ikke JavaScript, og den gør intet ved anti-bot-beskyttelse eller CAPTCHA’er — det er en helt anden opgave, og en virkelig svær en på det moderne web. Det er her, en AI scraping API ligger i et andet lag: Thunderbit's developer stack — et REST API, en MCP-server og en CLI — håndterer hentning, JS-rendering og anti-bot-problemet og returnerer derefter enten ren Markdown (POST /distill) eller skemamatchende struktureret JSON (POST /extract), uden at du behøver at skrive selectors overhovedet. De to er ikke konkurrenter; de er komplementære. bs4 parser den HTML, du allerede har i hånden; Thunderbits API, MCP og CLI skaffer dig den HTML, du ikke let kan nå i første omgang. Hvis din flaskehals er parsing, er bs4 et fint svar. Hvis din flaskehals er anskaffelse, er det et andet lag.
Prøv Thunderbit til udtræk af webdata
Kort fortalt
BeautifulSoup giver dig den mest brugervenlige API, den stærkeste tolerance over for fejlbehæftet HTML og den mest komplette CSS-engine i denne sammenligning — købt med en hastighedsstraff på omkring en størrelsesorden og det tungeste hukommelsesforbrug. Det er hele byttet, sagt ligeud. Standard-backenden html.parser er den ene reelle fælde: Den forvrænger stille og roligt uafsluttede tabeller og lister, så send "lxml" eller "html5lib" med, hver gang inputtet kan være grimt. Tråde gør den ikke hurtigere — multiprocessing gør. Og i langkørende loops bør du kalde decompose() for hver side, så reference-cyklusserne ikke hobes op.
To begrænsninger til sidst. Alt her er målt på én platform (macOS arm64, Python 3.14, færdigbyggede wheels), og timingmultiplikatorerne er genbrugt fra selectolax-setupet (samme bench, pr. 2026-07-13) i stedet for at blive kørt igen — så de arver den ene platforms begrænsning, og en Linux x86_64- eller source-compiled opsætning kan flytte de præcise tal. Og intet i resultaterne er en ny opdagelse: bs4 er et 20 år gammelt bibliotek, så hver testet adfærd er enten dokumenteret eller offentligt registreret. Værdien ligger ikke i en scoop. Den ligger i at sætte et rigtigt tal på trade-offs, som dokumentationen kun beskriver kvalitativt.
Ofte stillede spørgsmål
Er BeautifulSoup langsom?
Ja, målbart. På en parse-plus-extract-opgave kører den omkring 12–17x langsommere end en C-parser som selectolax-Lexbor med standard-backenden html.parser, og 10.5–14x langsommere med lxml-backenden, fordi den bygger et Python-objekt for hver node. Om det betyder noget, afhænger af skalaen: På en side på 1 MB er det 232 ms mod 15 ms — usynligt for et par tusinde sider, men afgørende for en pipeline på en million sider.
Hvilken BeautifulSoup-parser skal jeg bruge — html.parser, lxml eller html5lib?
For velstrukturerede, mainstream sites er standarden html.parser fin og kræver ingen ekstra dependencies. Men den implementerer ikke HTML5’s valgfrie slut-tags, så på uafsluttede tabeller eller lister lækker tilstødende tekst sammen uden at fejle. Når inputtet kan være fejlbehæftet, håndskrevet eller gammelt, så angiv "lxml" eller "html5lib" eksplicit — begge scorede 15/15 på en matrix med fejlbehæftet HTML, hvor html.parser scorede 12/15.
Kan BeautifulSoup køre parallelt med tråde?
Nej. bs4’s træbygning er ren Python og holder GIL, så flere tråde gør den langsommere, ikke hurtigere — i test var fire tråde omkring 3.9x langsommere end én tråd på en 1 MB parse. Hvis du vil parallelisere bs4, så brug multiprocessing (ProcessPoolExecutor). Biblioteker med C-kerner, som selectolax og lxml, er dem, der kan få gavn af tråd-niveau parallelisme.
Håndterer BeautifulSoup ødelagt HTML godt?
Overordnet ja — på tværs af en række fejlbehæftede samples (forkert indlejrede tags, manglende skelet, attributter uden citationstegn og mere) kom alle tre backends pænt igennem. Den ene svaghed er standarden html.parser og valgfrie slut-tags: Uafsluttede <td>/<li> bliver indlejret i stedet for lukket og korrumperer den udtrukne tekst. Skift til lxml eller html5lib, og den type problem forsvinder.
BeautifulSoup vs lxml — hvad er bedst?
De er forskellige værktøjer. lxml er langt hurtigere til både træbygning og forespørgsler og understøtter XPath. BeautifulSoup wrapper lxml (blandt andre) i en meget mere brugervenlig API og har faktisk bredere CSS-dækning via soupsieve. Bare forvent ikke, at lxml-backenden gør bs4 lxml-hurtigt — backenden accelererer kun parsing, mens queries og traversal stadig betaler bs4’s per-node Python-objektkost, hvilket efterlader den cirka 6–7.5x langsommere på store batch-selekteringer.
Prøv Thunderbit til udtræk af webdata Get Started Free


