BeautifulSoup on se kirjasto, johon lähes kaikki tarttuvat ensimmäisenä, kun he scrapaavat verkkosivua Pythonilla, ja se on ihan oikeasti vakavissaan tehtyjen HTML-parserien hitain. Molemmat väitteet pitävät paikkansa, eikä kumpikaan ole moite. Kiinnostava juttu on se, että “hitain” ei jää vain fiilikseksi, vaan siitä tulee tarkka ja mitattava luku.
Ajoin bs4:n (eli beautifulsoup4:n, versio 4.15.0, julkaistu kesäkuussa 2026, MIT-lisensoitu) läpi sekä uusien ominaisuustestien että saman benchmark-ympäristön aiempien ajoaikojen yhdistelmän, ja lopputulos on selvä: vaihdat noin kertaluokan verran nopeutta siinä, että saat kaikkein käyttäjäystävällisimmän API:n ja alan parhaan virhesiedon. Onko vaihto sen arvoinen, riippuu täysin omasta työkuormastasi, joten tämä arvio näyttää molemmat puolet reilusti.
Mitä BeautifulSoup oikeastaan on — ja mitä se ei ole
Useimmat oppaat ohittavat tärkeimmän pointin: BeautifulSoup ei itse parsii HTML:ää. Se on wrapperi. Kulissien takana se antaa dokumenttisi yhdelle kolmesta varsinaisesta parserista — Pythoniin sisäänrakennetulle html.parserille, lxml:lle tai html5lib:lle — ja pakkaa niiden rakentaman puun yhteen todella helppokäyttöiseen selaus- ja hakukerrokseen. bs4:n tehtävä ei ole parsia. Sen tehtävä on tehdä lopputuloksesta mukava käyttää.
Sen tekijä itse kutsuu sitä “screen-scraping libraryksi”, ja lupaus on ollut alusta asti sama: anna sille HTML, joka olisi selaimellekin hankala tapaus, ja se kaivaa silti esiin sen datan, jota pyydät. Tämä maine on ansaittu — yhdellä mutta tärkeällä varauksella, johon palataan kohta.
Muutama fakta kannattaa kirjata heti alkuun:
| Kenttä | Arvo |
|---|---|
| Paketti | beautifulsoup4 (importtina bs4) |
| Testattu versio | 4.15.0 (ladattu 2026-06-07) |
| Python-vaatimus | >=3.7.0 |
| Lisenssi | MIT |
| Virallinen kotisivu | crummy.com/software/BeautifulSoup |
| Lähdekoodi + bugiseuranta | Launchpad — ei GitHub |
| Ylläpito | Aktiivinen (4.15.0 kesäkuussa 2026, kuusi julkaisua viimeisen vuoden aikana) |
“Ei GitHub” on tärkeämpi huomio kuin miltä se ensiksi näyttää. bs4 on 20 vuotta vanha kirjasto, joka elää crummy.comissa ja Launchpadissa, joten tavallinen GitHub-tähtiin perustuva terveystsekkaus ei oikein toimi. Sen sijaan kuntoa kannattaa katsoa julkaisutahtiin — ja sillä mittarilla projekti on hyvin hengissä.
Yksi lisensointiin liittyvä yksityiskohta niille, joiden pitää raportoida compliance-tiimille: wrapperi on MIT-lisensoitu, mutta se, mitä “bs4:n käyttäminen” oikeasti tuo riippuvuuksiisi, riippuu siitä, minkä backendin asennat. html.parser on Pythonin standardikirjastoa (PSF-lisenssi, ei lisäriippuvuuksia). lxml on BSD-lisensoitu, mutta nojaa libxml2:een/libxslt:hen — ulkoiseen C-riippuvuuteen, joka pitää joko kääntää itse tai asentaa valmiina wheelinä. html5lib on puhdasta Pythonia ja MIT-lisensoitu. Jos haluat siisteimmän riippuvuuspinnan, sisäänrakennettu html.parser on siinä paras — ja sattumalta se on myös se backend, jossa on isoin miinus. Siitä lisää pian.
Nopeushinta, numeroiksi muutettuna
Laitetaan luvut ensin pöydälle, koska se on tämän jutun pääpointti, ja sen piilottaminen olisi epärehellistä. Realistinen parse-then-extract-tehtävä — parsitaan merkkijono, poimitaan kaikki <h3 class="title">-otsikot ja kaikki <a href> -linkit — tekee BeautifulSoupista tämän vertailun hitaimman parserin, eikä ero ole pieni.

Nämä ajoajat on lainattu selectolaxin benchmark-ympäristöstä (sama kone, sama 3 ajon metodologia, tilanne 2026-07-13); tätä arviota varten ei ajettu omia timing-benchmarikeja, jotta vältettiin CPU-ruuhka ja päällekkäinen työ. Mediaani p50-latenssi millisekunteina:
| Sivun koko | bs4 (html.parser) | bs4 (lxml) | selectolax-Lexbor | lxml | bs4-hp hitaampi | bs4-lxml hitaampi |
|---|---|---|---|---|---|---|
| 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 |
Eli bs4(html.parser) toimii noin 12–17x hitaammin kuin C-pohjainen parseri kuten selectolax-Lexbor, ja siirtyminen lxml-backendiin vie sen silti vain 10.5–14x hitaammaksi — eli edelleen selvästi yhden kertaluokan verran jäljessä. Syy on rakenteellinen, ei bugi: riippumatta siitä, mikä backend varsinaisesti parsii, bs4 rakentaa jokaisesta solmusta täyden Python-olion (Tag tai NavigableString). Tätä objektiksi muuttamisen kerrosta C-parserit eivät joudu maksamaan.
Huomaa myös, että kerroin kasvaa sivun kasvaessa — 12.0x 1 KB:lla, 17.4x 10 MB:lla. Se kertoo, ettei kyse ole kiinteästä käynnistysviiveestä, jonka voisi vain tasata isolla ajolla. Kyse on per-solmu-verosta, joka skaalautuu lineaarisesti rakennettavien solmujen määrän mukana.
Käännetään asia vielä toisin päin, koska “10x hitaampi” kuulostaa usein pahemmalta kuin se käytännössä on. 1 MB sivulla ero on 232 ms vs. 15 ms. Jos työsi on “scrapata muutamasta sadasta muutamaan tuhanteen sivua, muutamia satoja KB kerrallaan”, tämä absoluuttinen ero on käytännössä näkymätön — et tunne sitä, eikä optimoinnista ole sinulle hyötyä. Jos taas käsissäsi on miljoonan sivun putki, sama suhde tarkoittaa eroa työn valmistumisen ja epäonnistumisen välillä. Sama luku, täysin eri johtopäätös. Arvioi siis omaa volyymiasi, älä benchmarkia, vasten.
Ei, backendin vaihtaminen ei korjaa tätä
Pyörii sitkeä myytti, että jos annat bs4:lle lxml-backendin, saat lxml:n nopeuden. Et saa, ja on tärkeää ymmärtää miksi. 100 000 solmun CSS-kyselyssä (poimi jokainen <a> ja lue sen href, puu valmiiksi rakennettuna) läpäisykyky eroaa selvästi:
| Parseri | Query p50 | Solmua/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) pääsee noin 399 000 solmuun sekunnissa — eli noin 6.3–7.5x hitaammin kuin kolme C-moottoria, vaikka sen oma backend onkin lxml. Backend nopeuttaa puun rakentamista. Haku ja läpikäynti kulkevat silti soupsieven ja bs4:n Tag-objektien kautta, ja jokainen osuma paketoidaan edelleen Pythonissa. Siksi ajatus “anna bs4:lle lxml, niin siitä tulee lxml-nopea” on väärä: backend nopeuttaa vain yhtä vaihetta, eikä hitaus ole siinä vaiheessa.
Muisti ja cold start täydentävät kustannuksen. 10 MB dokumentilla bs4 käyttää noin 1.5–1.75x enemmän residenttiä muistia kuin selectolax tai lxml (218–226 MB vs. 129–145 MB) — sama juurisyy, yksi Python-olio per solmu. Ja bs4:n importointi kestää noin 33.4 ms verrattuna lxml.html:n 14.1 ms:iin, eli se on 2.36x hitaampi tuoda sisään. Jälkimmäinen on pitkään ajavassa prosessissa käytännössä merkityksetön, mutta CLI-työkalulle tai serverless-funktiolle, joka kylmäkäynnistyy usein, se on pieni mutta todellinen kustannus, joka kannattaa tietää.
Miksi useampi säie ei pelasta tätä
Jos ensimmäinen reaktiosi hitaaseen CPU-sidonnaiseen tehtävään on “laita säikeitä päälle”, bs4 rankaisee siitä. 1 MB sivulla, 48 ajokertaa, yksi säie vs. neljä säiettä:
| Parseri | 1 säie | 4 säiettä | Nopeutus |
|---|---|---|---|
| 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 |
Lue alin rivi kahdesti. Neljä säiettä teki bs4:stä noin 3.9x hitaamman, ei nopeamman. Empiirinen signaali viittaa siihen, että se pitää GIL:n: bs4:n puunrakennus on puhdasta Pythonia, joten se serialisoituu Global Interpreter Lockin alle, ja säikeiden lisääminen vain kasvattaa ajoituskustannusta tehtävässä, jota ei oikeasti voi ajaa rinnakkain. selectolax saa noin 3.5x nopeutuksen, koska sen C-ydin vapauttaa lukon; bs4:llä ei ole sitä mahdollisuutta.
Free-threading-aikakaudella käytännön opetus on tämä: jos sinun täytyy rinnakkaistaa BeautifulSoup, käytä multiprocessingia (ProcessPoolExecutor), älä säikeitä. selectolax ja lxml voivat skaalautua säikeillä; bs4 ei. Yksi tarkennus tarkkuuteen: tämä on yksi havainto yhdellä säitemäärällä (4) ja yhdellä sivukoolla (1 MB), ja “pitää GIL:n” -mekanismi on wall-clock-käyttäytymisestä johdettu hypoteesi, ei asia, jonka vahvistin instrumentoimalla, mikä koodipolku pitää lukon. Suunta on selvä; tarkka mekanismi on alustava.
Oletusbackend on ansa. Lue tämä ensin.
Jos otat tästä arviosta vain yhden asian, ota tämä. Pelkkä BeautifulSoup(html) ilman toista argumenttia käyttää html.parseria, eikä html.parser toteuta HTML5:n optional-end-tag-sääntöjä. Kuulostaa akateemiselta — kunnes se korruptoi datasi hiljaa.

Ajoin 15 tarkoituksella rikottua HTML-esimerkkiä kaikkien kolmen backendin läpi, ja jokaiselle oli ennen ajoa määritelty backend-agnostinen rakenteellinen tarkistus, jotta kukaan ei pääsisi valitsemaan voittajaa jälkikäteen. Tulokset:
| Backend | Vastaa odotusta / 15 |
|---|---|
| lxml | 15 |
| html5lib | 15 |
| html.parser | 12 |
Kolmella epäonnistumisella on sama juurisyy. Otetaan esimerkiksi sulkematon taulukko: <table><tr><td>a<td>b<tr><td>c<td>d</table>. html.parserilla poimittu soluteksti tulee ulos muodossa ['abcd','bcd','cd','d'] — jokainen <td> nielee kaiken jälkeisen, koska parseri sisäkkäistää solut sen sijaan että sulkisi ne. lxml ja html5lib palauttavat oikein ['a','b','c','d']. Sama tapahtuu paljaiden listaelementtien kanssa: <li>a<li>b<li>c antaa html.parserilla sisäkkäisen ['abc','bc','c'], kun taas kaksi muuta palauttavat siistin ['a','b','c']. Myös duplikaatti-attribuutit kääntyvät: <div id="first" id="second"> pitää html.parserilla arvon "second", mutta lxml/html5lib säilyttävät "first", ja HTML5-spesifikaatio sanoo, että ensimmäinen kuuluu säilyttää.
Miksi tämä on vaarallista eikä vain ärsyttävää: se tapahtuu ilman virheilmoitusta. Scraperi, joka huolettomasti käyttää BeautifulSoup(html)-kutsua ja törmää sulkemattomaan taulukkoon tai listaan — mikä on harmillisen yleistä vanhoilla sivuilla, käsin kirjoitetussa HTML:ssä ja templateissa, joista jäi sulkumerkki pois — vuotaa viereisen solun tekstin samaan kenttään, antaa likaisen datan eikä sano asiasta mitään. Korjaus on yksi argumentti: BeautifulSoup(html, "lxml") tai BeautifulSoup(html, "html5lib").
Annetaan reilusti tunnustusta html.parserille: muut 12/15 rikkinäisestä esimerkistä tulivat kaikilla kolmella backendillä identtisinä ulos — väärin sisäkkäiset tagit kuten <b><i></b></i>, puuttuva html/body-runko, lainausmerkeettömät attribuutit, orvot suljetut tagit, sulkeutumattomat kommentit, sisäkkäiset lomakkeet, sekamerkkikoko ja paljon muuta. bs4:n sietokyky on oikeasti vahva kautta linjan; erot keskittyvät lähes kokonaan optional-end-tag -tapauksiin. Eikä mikään tästä ole uusi havainto — bs4:n oma “Differences between parsers” -dokumentaatio sanoo jo suoraan, että html.parser on “less lenient”. Rikkinäisten esimerkkien matriisi tuo vain näkyviin täsmälliset, toistettavat tapaukset, joissa “vähemmän armollinen” muuttuu vääräksi tulokseksi.
Mitä et menetä: API ja CSS ovat tämän kirjaston parasta antia
bs4 on siis hidas, yksisäikeinen ja siinä on oletusbackendin sudenkuoppa. Silti ihmiset tarttuvat siihen, koska vaihdon “mukava” puoli on täysin todellinen — ja testit vahvistavat sen.

Ajoin 29 API-testiä, jotka kattoivat haun, CSS:n, puunavigoinnin, tekstin poiminnan ja DOM-muutokset. Kaikki 29 menivät läpi, ja jokaisen tulos laskettiin vertaamalla todellista palautusarvoa odotettuun arvoon, ei silmämääräisesti. Kaksi näistä kyvykkyyksistä ovat juuri sitä ergonomiaa, jota C-parserit eivät vain tarjoa:
- Funktiopredikaatit
find/find_all-kutsuissa. Voit kirjoittaasoup.find(lambda t: t.name == "a" and "btn" in t.get("class", []))ja ilmaista monimutkaisen ehdon yhdellä Python-rivillä — ei tarvitse tehdä “valitse kaikki, suodata sitten” -kierrosta. - Nimetty, kaksisuuntainen puunavigointi.
.parent,.next_sibling,.find_parent,.stripped_strings,.descendants— läpikäynnit ovat luettavia kuin englanti ja kulkevat molempiin suuntiin. selectolax vaatii joissain näistä useita vaiheita tai ei tarjoa niitä lainkaan.
Tässä on se konkreettinen osa “säästää kehittäjän aikaa” -väitteestä. Se ei ole markkinointia; se on 29 vihreää checkiä.
Kaksi ansaa on silti hyvä nostaa esiin, koska reilu arvio kertoo molemmat puolet. Ensimmäinen liittyy boolean-attribuutteihin: <input disabled> palauttaa bs4:ssä disabled-arvoksi tyhjän merkkijonon "" (selectolax palauttaa None). Molemmat ovat falsy-arvoja, joten if node.get("disabled") ohittaa hiljaa boolean-attribuutin, joka on oikeasti olemassa kummassakin kirjastossa — turvallinen tarkistus on "disabled" in tag.attrs. Toinen: get_text(strip=True) liittää solujen tekstin yhteen ilman erotinta stripin jälkeen, joten "...with " + "link1" muuttuu muotoon "withlink1". Anna separator=" ", kun tarvitset sanakatkoja. Kumpikaan ansa ei ole bs4-spesifinen; molemmat ovat eri kirjastojen yhteisiä kompastuskiviä.
Ja nyt se osa, joka yllättää monia: bs4:n valitseminen ei vie sinulta CSS-tukea. Sen CSS-moottori, soupsieve, on tämän vertailun kattavin toteutus. 41 tapauksen perusmatriisissa (lainattu selectolax-ympäristöstä) soupsieve sai tuloksen 41/41 — alan ainoa täydet pisteet saanut toteutus, edellä selectolax-Lexborin 39/41 ja cssselectin (lxml/parsel) 37/41. Sen jälkeen ajoin 20 lisättyä laajennettua tapausta, joita soupsieven dokumentaatio mainostaa, ja tulos oli 20/20, mukaan lukien sellaiset valitsimet, jotka Lexbor hylkää täysin: :lang(en), soupsieve-uniikin :-soup-contains('featured')-valitsimen, :is(), :where() ja :has(> a). Ainoat oikeat puutteet ovat XPath (soupsieve on vain CSS) ja parselin ::text / ::attr()-pseudoelementit, jotka ovat Scrapy-laajennuksia. Jos elät XPath-maailmassa, migraatio sattuu.
Tämän osion johtopäätös on selvä: se, mistä luovut valitsemalla BeautifulSoupin, on nopeus. Ei API-ergonomia, eikä todellakaan CSS-tuki.
Kaksi tuotantokäytön ansaa, joihin kannattaa budjetoida aikaa
Oletusbackendin lisäksi kaksi käyttäytymistä puree erityisesti pitkäikäisissä tai ei-UTF-8-työkuormissa.
Viittauskierrot: kutsu decompose() pitkissä silmukoissa
Jokainen bs4:n Tag pitää viittausta sekä vanhempaansa että lapsiinsa, mikä muodostaa viittauskierron. CPythonin viitelaskenta ei pysty vapauttamaan kiertoa itsenäisesti — se on sukupolvisen roskienkerääjän tehtävä. Jotta näkee, kuinka paljon sillä on väliä, rakensin ja poistin puun 300 kertaa GC pois päältä, ja laskin sitten muistissa vielä roikkuvat Tag-oliot:

| Tilanne | Jäljelle jääneet Tag-oliot del-kutsun jälkeen |
|---|---|
| GC pois päältä | 120,900 (300 sykliä, mitään ei vapautunut) |
| GC päällä | 26,598 (sukupolvinen GC käynnistyi kesken silmukan) |
Pakotetun gc.collect()-kutsun jälkeen | 0 (kaikki vapautuivat) |
| Ei-kierron kontrolli (merkkijonolista, GC pois päältä) | delta 0 |
Kun GC oli pois päältä, del soup ei vapauttanut mitään — kaikki 120 900 oliota jäivät muistiin, koska viittauskierto kaataa viitelaskennan. Yksi gc.collect() tyhjensi ne kaikki. Ei-kiertoinen kontrolliryhmä (pelkkä merkkijonolista, jonka tiedetään olevan kiertoton) pysyi nollassa, mikä todistaa, että kasaantuminen tuli bs4:n kierrosta eikä mittauskohinasta. bs4:n omat dokumentit sanovat, että objektit ovat “densely interconnected ... exactly the sort a garbage collector would have trouble with”, joten tämä on dokumentoitua käyttäytymistä; testi tuo mukaan säilytettyjen objektien määrän ja todisteen siitä, että collect() nollaa tilanteen.
Käytännön sääntö: putkessa, joka parsii monta suurta sivua tiukassa loopissa, jos koodisi (tai jokin korkean läpimenon asetus) kytkee GC:n pois tai ei laukaise sitä tarpeeksi usein, bs4-puut jäävät roikkumaan ja muisti kasvaa. Kutsu soup.decompose() jokaisen sivun jälkeen — bs4 tarjoaa sen juuri siksi, että kierrot katkeavat ja vapautus tapahtuu aikaisemmin. selectolaxin ja lxml:n C-puut eivät kärsi tästä lainkaan.
Koodaus: UnicodeDammit on bs4:n hiljainen etu
bs4:n mukana tulee komponentti, jota nopeat parserit eivät tarjoa: UnicodeDammit, joka tunnistaa dokumentin koodauksen ja muuntaa sen automaattisesti Unicodeksi. Annoin sille 8 tapauksen “ilmoitettu vs. todellinen charset” -matriisin:

| Tapaus | Todellinen koodaus | UnicodeDammit arveli | Palautuiko? |
|---|---|---|---|
| utf8_no_decl | utf-8 | utf-8 | Kyllä |
| utf16_bom | utf-16 | utf-16le | Kyllä |
| gbk_chinese | gbk | gb18030 | Kyllä (superset) |
| shiftjis | shift_jis | cp932 | Kyllä (superset) |
| latin1_declared_utf8 | latin-1 (ilmoitettu utf-8) | iso-8859-1 | Kyllä (ohitti valheen) |
| latin1_no_decl | latin-1 | cp720 | Ei |
| cp1252_no_decl | cp1252 | cp862 | Ei |
| utf8_declared_latin1 | utf-8 (ilmoitettu latin-1) | iso-8859-1 | Ei (seurasi valhetta) |
Viisi kahdeksasta palautui oikein. UTF-8, BOM:illinen UTF-16, GBK, Shift-JIS ja jopa väärin merkitty latin-1 tulivat kaikki takaisin oikein, ja superset-arviot (GBK→gb18030, Shift-JIS→cp932) dekoodaavat silti hyvin. Kaksi epäonnistumistapaa kannattaa tuntea: lyhyet latin-1/cp1252-bittinäytteet tulkitaan joskus DOS-koodisivuiksi, koska tilastollinen tunnistus ei ole lyhyillä syötteillä luotettava ja DOS:n laatikkopiirto-merkkejä löytyy samoista pisteistä kuin latin-1:n koodeja; ja kun <meta charset>-ilmoitus on yksinkertaisesti väärä, UnicodeDammit uskoo ilmoitusta. bs4:n dokumentaatio huomauttaa molemmista — näyte voi olla “so short that Unicode, Dammit can't get a lock on it”, ja enemmän dataa tarkoittaa parempaa arvausta.
selectolaxiin verrattuna, joka korruptoi non-UTF-8-bitit hiljaa ja odottaa sinun dekoodaavan ne itse, tämä on aito etu: bs4 ainakin yrittää tunnistaa koodauksen ja onnistuu usein. Mutta se ei ole tae. Kun koodaus on tiedossa, ohita arvaaminen ja ole eksplisiittinen: BeautifulSoup(bytes, from_encoding="...").
Eroavatko backendit oikeasti koskaan todellisilla sivuilla?
Rikkinäinen HTML-matriisi näyttää backendien eroavan tarkoituksella rikotulla syötteellä. Luonnollinen seuraava kysymys on, onko sillä väliä oikeassa maailmassa, joten ajoin kaikki kolme backendia 11 oikean, haetun sivun läpi — BBC, Wikipedia, Craigslist, MDN, old.reddit, Python docs, Hacker News, Books to Scrape, webscraper.io, whitehouse.gov ja JS-renderöity quotes-sivu — ja vertasin linkki-, otsikko- ja kuvamääriä.
Kaikki kolme olivat samaa mieltä kaikilla 11 sivulla. Ei yhtään eroa. Se tarkoittaa, että trap-osiossa nähty backend-erimielisyys näkyy vain tarkoituksella rikotussa HTML:ssä; kun moderni tuotantosivu on rakenteeltaan riittävän kunnossa — vaikka se olisi “sekava” — backendin valinta ei muuta sitä, mitä poimit. Käytännössä: valtavirran, hyvin muodostetuilla sivuilla html.parser on täysin kelvollinen ja säästää riippuvuuden. Vasta kun scrapat näkyvästi epästandardia, käsin kirjoitettua tai ikivanhaa HTML:ää, backendin valinta alkaa muuttaa tulosta, ja silloin kannattaa vaihtaa lxml:ään tai html5libiin.
Yksi sivuhuomio tuosta ajosta, koska se on oikea reunatapaus. MDN-sivulla on <template>-elementti, ja kaikki bs4-backendit palauttivat 508 linkkiä — eli bs4 litistää <template>-sisällön pääpuuhun. Tässä bs4 on samalla puolella kuin lxml, ja vastakkaisella puolella kuin selectolax-Lexbor, joka noudattaa tiukasti HTML5-spesifikaatiota (<template> on inertti DocumentFragment) ja palauttaa 497, pudottaen hiljaa 11 linkkiä templateen sisälle. bs4 siis nappaa datan <template>-elementin sisältä — hyödyllistä, mutta samalla tapa poimia “haamisisältöä”, jota selain ei koskaan renderöisi. Kumpikaan käytös ei ole väärin; ne ovat eri spesifikaatiotulkintoja, ja sinun kannattaa tietää, kumman saat.
Mihin BeautifulSoup sopii — ja mihin ei
Sen sijaan, että puristaisi kaiken yhteen 0–100-pisteeseen, mikä peittäisi juuri ne kompromissit, joilla on oikeasti väliä, tässä on dimensioittainen arviointitaulukko, ja jokaisella rivillä on oma huomionsa:
| Ulottuvuus | Mitä testit löysivät | Lukijan huomio |
|---|---|---|
| Asennus / ensimmäinen ajo | Pelkkä wrapperi, ei selainta tai asetuksia; html.parserissa ei riippuvuuksia; kaikille valmiit wheelit | lxml-backend tarvitsee C-riippuvuuden |
| Nopeus vs C-parserit | 12–17x hitaampi (html.parser) / 10.5–14x (lxml-backend), kaikissa kokoluokissa | Yksi testialusta; selectolax-dataa käytettiin uudelleen |
| CSS-kyselyiden läpimeno | ~6–7.5x hitaampi 100k solmulla; lxml-backend ei pelasta | Käytetty uudelleen; maksaa Python Tag -veron |
| Muisti | 1.5–1.75x selectolax/lxml; raskain | Käytetty uudelleen; mitattu RSS:nä |
| Importin cold start | 2.36x hitaampi (33.4 vs 14.1 ms) | Käytetty uudelleen; pieni yksittäinen kustannus |
| Säie-skalaus | bs4-lxml ~3.9x hitaampi 4 säikeellä (pitää GIL:n) | Yksi havainto; käytä multiprocessingia |
| API-ergonomia | 29/29 testiä; funktiopredikaatti-find + kaksisuuntainen navigointi | Tyhjä merkkijono boolean-attribuuteissa ja strip-sanakatkojen ansat |
| CSS-tuki | soupsieve on vahvin: 41/41 perus + 20/20 laajennettu; tukee :lang | Ei XPathia, ei ::text-elementtejä |
| 3 backendin sietokyky | lxml/html5lib 15/15; html.parser 12/15 | Erot vain rikkinäisessä HTML:ssä |
| Oikeiden sivujen yhtenäisyys | 3 backendia samaa mieltä 11/11; kaikki litistävät <template>-sisällön (508) | Hyvin muodostetuilla sivuilla backendillä ei ole väliä |
| Viittauskierrot / GC | Puu on kierto; 300 kierrosta jätti 120,900 objektia, collect nollasi ne | Pitkissä loopissa tarvitaan decompose() |
| Koodaus | UnicodeDammit palauttaa 5/8; arvioi väärin lyhyitä näytteitä, uskoo huonoja ilmoituksia | Yksi havainto |
| Ylläpito | Aktiivinen (4.15.0, kesäkuu 2026); MIT | Kotisivu crummy/Launchpadissa, ei GitHubissa |
Kenelle BeautifulSoup siis sopii? Kaikille, jotka arvostavat luettavaa API:a ja armollista parsintaa enemmän kuin raakaa läpimenoa — prototyypeille, kertaluonteisiin scrapeihin, sisäisiin työkaluihin, tiimeille, joissa kehittäjäaika on kalliimpaa kuin ajonaika. Kenen kannattaa katsoa muualle? Miljoonan sivun putket, joissa nopeusvero kasautuu oikeaksi rahaksi, työkuormat, jotka tarvitsevat säietason rinnakkaisuutta, ja kaikki, jotka ovat sidottuja XPathiin.
Pieni huomio siitä, mihin tämä sijoittuu oikeassa scraping-pinossa, ja missä oma työkalumme tulee kuvaan. BeautifulSoup olettaa, että sinulla on jo HTML hallussasi. Se ei hae sivuja, se ei renderöi JavaScriptiä, eikä se tee mitään bot-suojauksille tai CAPTCHA-haasteille — se on täysin eri ja nykyverkossa oikeasti vaikea tehtävä. Tässä kohtaa AI-scraping-API istuu eri kerrokseen: Thunderbit:n kehittäjäpino — REST API, MCP-serveri ja CLI — hoitaa haun, JS-renderöinnin ja bot-ongelman, ja palauttaa joko siistin Markdownin (POST /distill) tai skeemaan sopivan jäsennellyn JSONin (POST /extract) ilman että kirjoitat valitsimia lainkaan. Ne eivät ole kilpailijoita; ne täydentävät toisiaan. bs4 parsii HTML:n, joka sinulla jo on; Thunderbitin API, MCP ja CLI hankkivat HTML:n, johon et muuten pääse helposti käsiksi. Jos pullonkaulasi on parsinta, bs4 on hyvä vastaus. Jos pullonkaulasi on hankinta, se on toinen kerros.
Kokeile Thunderbitiä verkkodatan poimintaan
Lopputulos
BeautifulSoup antaa sinulle ystävällisimmän API:n, vahvimman rikkinäisen HTML:n sietokyvyn ja tämän vertailun kattavimman CSS-moottorin — mutta hinta on noin kertaluokkaa oleva nopeusvero ja raskain muistijalanjälki. Siinä koko diili, suoraan sanottuna. Oletusarvoinen html.parser-backend on ainoa oikea ansa: se sotkee hiljaa sulkemattomat taulukot ja listat, joten anna aina "lxml" tai "html5lib", kun syöte voi olla ruma. Säikeillä se ei nopeudu — multiprocessingilla se nopeutuu. Ja pitkäikäisissä silmukoissa kutsu jokaisen sivun jälkeen decompose(), jotta viittauskierrot eivät pääse kasaantumaan.
Kaksi rajoitusta lopuksi. Kaikki tässä mitattiin yhdellä alustalla (macOS arm64, Python 3.14, valmiit wheelit), ja ajoaikojen kertoimet on lainattu selectolax-ympäristöstä (sama bench, tilanne 2026-07-13) eikä ajettu uudestaan — joten ne perivät tämän yhden alustan rajoitteen, ja Linux x86_64- tai lähdekoodista käännetty ympäristö voisi siirtää tarkkoja lukuja. Eikä mikään näistä tuloksista ole uusi löydös: bs4 on 20 vuotta vanha kirjasto, joten jokainen testattu käyttäytyminen on joko dokumentoitu tai julkisesti tallennettu. Arvo ei ole uutispommi. Arvo on siinä, että kompromisseille annetaan oikeat numerot, kun dokumentaatio kuvaa ne vain laadullisesti.
Usein kysytyt kysymykset
Onko BeautifulSoup hidas?
Kyllä, mitattavasti. Parse + extract -tehtävässä se on noin 12–17x hitaampi kuin C-parseri kuten selectolax-Lexbor oletuksella html.parser, ja 10.5–14x hitaampi lxml-backendillä, koska se rakentaa Python-olion jokaiselle solmulle. Onko sillä väliä, riippuu mittakaavasta: 1 MB sivulla ero on 232 ms vs. 15 ms — muutamalle tuhannelle sivulle käytännössä näkymätön, mutta miljoonan sivun putkessa ratkaiseva.
Minkä BeautifulSoup-parserin minun pitäisi valita — html.parser, lxml vai html5lib?
Hyvin muodostetuille valtavirran sivustoille oletus html.parser on täysin kelvollinen eikä tuo lisäriippuvuuksia. Mutta se ei toteuta HTML5:n optional-end-tag-sääntöjä, joten sulkemattomissa taulukoissa tai listoissa se vuotaa viereistä tekstiä yhteen ilman virheilmoitusta. Kun syöte voi olla rikkinäinen, käsin kirjoitettu tai vanha, anna "lxml" tai "html5lib" eksplisiittisesti — molemmat saivat siistin 15/15 tuloksen rikkinäisen HTML:n matriisissa, jossa html.parser sai 12/15.
Voiko BeautifulSoup parsoa rinnakkain säikeillä?
Ei. bs4:n puunrakennus on puhdasta Pythonia ja pitää GIL:n, joten säikeiden lisääminen tekee siitä hitaamman, ei nopeamman — testissä neljä säiettä ajoi 1 MB parsinnan noin 3.9x hitaammin kuin yksi säie. Jos haluat rinnakkaistaa bs4:n, käytä multiprocessingia (ProcessPoolExecutor). C-ytimiset kirjastot kuten selectolax ja lxml ovat niitä, joista säietason rinnakkaisuus oikeasti hyötyy.
Käsitteleekö BeautifulSoup rikkinäistä HTML:ää hyvin?
Yleisesti kyllä — useilla rikkinäisillä esimerkeillä (väärin sisäkkäiset tagit, puuttuvat rungot, lainausmerkeettömät attribuutit ja muuta) kaikki kolme backendia toipuivat siististi. Ainoa heikko kohta on oletus html.parser ja optional-end-tags: sulkemattomat <td>/<li>-elementit sisäkkäistyvät sulkeutumisen sijaan ja sotkevat poimitun tekstin. Vaihda lxml- tai html5lib-backendiin, niin tuo ongelmaluokka poistuu.
BeautifulSoup vs lxml — kumpi on parempi?
Ne ovat eri työkaluja. lxml on paljon nopeampi sekä puunrakennuksessa että hauissa, ja tukee XPathia. BeautifulSoup taas käärii lxml:n (ja muut) paljon ystävällisempään API:in, ja sillä on itse asiassa laajempi CSS-tuki soupsieven kautta. Älä kuitenkaan odota, että lxml-backend tekisi bs4:stä lxml-nopean — backend nopeuttaa vain parsintaa, kun taas kyselyt ja läpikäynti maksavat edelleen bs4:n solmukohtaisen Python-oliokustannuksen, joten suurissa massavalinnoissa se jää noin 6–7.5x hitaammaksi.
Kokeile Thunderbitiä verkkodatan poimintaan Get Started Free


