Joka muutaman kuukauden välein putkahtaa esiin nopeampi HTML-parseri, benchmarkit leviävät somessa, ja joku ehtii jo julistaa vanhat työkalut vanhentuneiksi. Sitten yrität valita jokaisen kappaleen, joka sisältää tietyn sanan, tai hakea osuman saaneen solmun vanhemman, ja muistat, miksi lxml on yhä auki toisessa välilehdessäsi.
lxml on 20 vuotta vanha libxml2 -sidonta. Se ei ole erityisen näyttävä. Se ei ole uusi. Ja yhdessä tietyssä tehtävässä — kaikessa, mikä vaatii oikeaa XPathia — mikään muu valtavirran Python-maailmassa ei oikeasti vedä sille vertoja. Tämä on käytännönläheinen arvio siitä, mitä se tekee, missä se voittaa hiljaa ja missä pari oletusta voi purra, jos et tiedä niiden olemassaolosta.
lxml yhdessä kappaleessa: mitä se oikeasti on
lxml on Python-sidonta C-kirjastoihin libxml2 ja libxslt. Se on parseri ja serializeri, ei scrapperi eikä selain — se muuttaa merkintäkielen puuksi, jota voit kysellä ja muokata, ja muuntaa puun takaisin biteiksi. Se tarjoaa ElementTree-yhteensopivan API:n, täyden XPath 1.0 -moottorin, XSLT 1.0:n ja skeemavalidoinnin, ja sitä ylläpitää Stefan Behnel tunnuksella "the most feature-rich and easy-to-use library for processing XML and HTML in the Python language".
Näin se asettuu GitHub- ja PyPI-tilanteen perusteella, kun otos haettiin 2026-07-14:
| Kenttä | Arvo |
|---|---|
| Repo | lxml/lxml |
| Tähtiä | 3,043 |
| Forkkeja | 620 |
| Avoimia issueita | 16 |
| Lisenssi | BSD-3-Clause |
| Luotu | 2011-02-11 |
| Viimeisin push | 2026-07-02 |
| PyPI stable | 6.1.1 (2026-05-18) |
| Mukana tuleva moottori | libxml2 2.14.6 + libxslt 1.1.43 |
Yksi asia kannattaa sanoa heti alkuun, ettei tätä luulla hypetykseksi: tässä arviossa ei ole salaisuuksia. lxml on niin vanha, että jokainen tässä kuvattu käyttäytyminen löytyy jostain lxml:n dokumentaatiosta, libxml2:n changelogista tai launchpad-ketjusta. En löytänyt mitään dokumentoimatonta kikkaa enkä aio sellaista keksiä. Tämän arvion arvo on siinä, että se on jäsennelty, mitattu ja rakennettu lxml:n ympärille — ei siinä, että kyseessä olisi uutinen.
Testiasetelma (ja miksi ajoitusluvut on lainattu)
Tähän arvioon menee kahdenlaisia tietoja, ja ne tulevat kahdesta eri paikasta — sanon sen suoraan.
Kyvykkyystestit — XPath-käyttäytyminen, kaksi parseri-API:a, nimiavaruudet, enkoodaus, solmun elinkaari — ajoin tuoreesti yhdellä koneella: macOS arm64, Python 3.14.2, lxml 6.1.1, libxml2 2.14.6. Jokainen artifacts/raw/*.json-tiedostojen numero on skriptin laskema, ei käsin kirjoitettu. Kyvykkyystestit ovat deterministisiä totuusarvoja ja enum-arvoja, joten yksi ajo on vakaa — koneen kuormitus ei muuta sitä, palauttaako //a/@href attribuuttijonon.
Ajoitus- ja muistijäljen luvut eivät ole tästä paketista. Ne on käytetty sellaisenaan aiemmasta selectolax-benchmark-paketista — sama kone, sama virtuaaliympäristö, sama lxml- ja libxml2-rakenne, benchmarkit tilanteessa 2026-07-13 — enkä ajanut niitä uudelleen tässä. Tämä on tarkoituksellista. Ajoitusbenchmarkien pyörittäminen samalla kun ajetaan kyvykkyysskriptejä kutsuisi CPU-kilpailua, joka sumentaisi lainatut luvut, ja olisi myös turhaa työtä: lxml oli jo siinä paketissa täysin mitattu vertailukirjasto. Saman benchmarkin uudelleenkäyttö pitää vertailun aidosti samassa mittakaavassa sen sijaan, että mukaan tulisi toinen, hieman erilainen mittaus. Kun siis näet alla millisekuntiarvon, lue se muodossa "sama testialusta, tilanteessa 2026-07-13", ei "mittasin tämän uudelleen tänään".
Löydöksissä on luottamusmerkintä: single-observation deterministisille kyvykkyystesteille, triple-run lainatuille ajoitusjakaumille ja hypothesis kohdissa, joissa ehdotan mekanismia, jota en erikseen eristänyt.
XPath: se yksi asia, jota selectolax ja BeautifulSoup eivät vain osaa
Tämä on jutun pääotsikko, joten aloitetaan siitä.

Ajoin lxml:n xpath()-metodin läpi etukäteen määritellyn 37 kohdan matriisin — jokaiselle tapaukselle oli kirjoitettu odotettu tulos lähdekoodiin ennen testin ajoa, joten en voinut vahingossa antaa itselleni lisäpisteitä. Kymmenen akselia, yhdeksän predikaattityyliä, kymmenen sisäänrakennettua funktiota, kolme skalaariarvon tyyppiä ja viisi tahallista ansatapausta, joissa käytettiin XPath 2.0 -syntaksia, jonka lxml:n 1.0-moottorin kuuluukin hylätä.
| Kategoria | Kattavuus | Tulos |
|---|---|---|
| Akselit | child / descendant / parent / ancestor / following-sibling / preceding-sibling / self / attribute / following / preceding | 10/10 läpi |
| Predikaatit | [1] / last() / position()<n / attribuutin yhtäsuuruus / attribuutin olemassaolo / and / or / sisäkkäinen [.//a] / not() | 9/9 läpi |
| Funktiot | text() / contains() / starts-with() / count() / string-length() / normalize-space() / concat() / substring() / name() / string() | 10/10 läpi |
| Paluutyypit | boolean / number -skalaarit | 3/3 läpi |
| Ansatapaukset | matches() / sekvenssit / if-then-else / except / syntaksivirhe | 5/5 hylättiin oikein |
Tulos on 37/37, ja ansasarakkeessa on se olennaisin kohta. matches(), sekvenssilausekkeet, if/then/else ja except ovat XPath 2.0 -syntaksia, eikä libxml2:n 1.0-moottori tue niitä puolivillaisesti — se nostaa XPathEvalError-poikkeuksen ja kieltäytyy sen sijaan, että palauttaisi hiljaa väärän node-setin. Tämä on siis täydet pisteet sen jälkeen, kun sitä yritettiin rikkoa, ei pehmeillä kysymyksillä kerätty täydellinen tulos. Kaikki tässä näkyvä vastaa täsmälleen lxml:n XPath-dokumentaatiota, ja juuri se on pointti.
Myönnän yhden asian, jonka testikehys sai väärin — se on versio "37/37":sta, johon voi oikeasti luottaa. Ensimmäinen odotettu joukko lauseelle //div[.//a[@href]] ennusti kaksi osumaa; ajo palautti yhden. Oletin noin kolmekymmentä sekuntia, että lxml oli väärässä, mutta tarkistin fixturet ja huomasin, että toinen elementti olikin <footer>, ei <div> — odotus oli väärä, ei moottori. Korjasin odotetun joukon ja jätin virheen lähdekoodikommenttiin. Se on oikea syy-seurausjärjestys: epäile ensin omaa testiäsi, ennen kuin alat syyttää 20 vuotta vanhaa C-kirjastoa.
XPath vs CSS: mitä CSS:llä ei kirjaimellisesti voi ilmaista
Abstrakti väite "XPath on tehokkaampi" ansaitsee konkreettisen luvun, joten mittasin eron. lxml tarjoaa sekä .xpath()- että .cssselect()-metodit (jälkimmäinen kääntää CSS:n XPathiksi taustalla). Otin kymmenen valintakohdetta ja tarkistin, mitkä niistä CSS pystyy oikeasti ilmaisemaan.

| Kohde | XPath | CSS (cssselect) |
|---|---|---|
Suodatus tekstisisällön perusteella (contains(text(),"bargain")) | Kyllä | Ei tekstipredikaattia |
Valitse vanhempi lapsen perusteella (//b/parent::p) | Kyllä | Ei parent-valitsinta |
Palauta attribuutin arvo (//a/@href) | Kyllä | Vain elementit |
Palauta tekstisolmu (//p/text()) | Kyllä | Ei tekstisolmuja |
Ancestor-akseli (//td/ancestor::div) | Kyllä | Ei ylöspäin navigointia |
Suodata vanhempi lapsimäärän perusteella (//ul[count(li)=4]) | Kyllä | Ei count-predikaattia |
Suodata tekstin pituuden perusteella (string-length(text())>5) | Kyllä | Ei pituuspredikaattia |
nth-child / last-child / vierekkäinen sisarus | Kyllä | Kyllä (3 perusasiaa) |
Seitsemälle kymmenestä kohteesta ei ole lainkaan CSS-vastinetta. Tekstisisällön suodatus, ylöspäin siirtyminen vanhempiin ja esi-isiin, attribuuttiarvon tai paljaan tekstisolmun palauttaminen sekä laskentaan perustuvat predikaatit — CSS ei kykene mihinkään näistä. Ainoastaan kolme (nth-child, last-child, vierekkäinen sisarus) toimivat molemmissa. Se on mitattu vastaus kysymykseen "mitä oikeasti saan, kun tartun lxml:ään." selectolax on vain CSS-pohjainen eikä tarjoa lainkaan xpath()-metodia, joten nuo seitsemän kyselytyyppiä muuttuvat siellä joko monivaiheisiksi Python-silmukoiksi tai jäävät kokonaan tekemättä. Jos scraper-logiikkasi nojaa johonkin näistä, valinta on sitä myöten selvä.
(Ja kyllä, testikehys nappasi minut tästäkin kiinni toisen kerran: ennustin tyhjää joukkoa ehdolle string-length(text())>5, mutta kaksi kuusimerkkistä merkkijonoa osui. Korjasin odotuksen, en työkalua.)
Kolme tiukkuuden vaihdetta: etree, recover ja lxml.html
XPath on syy valita lxml. Kolmiportainen tiukkuussäätö on syy pitää siitä kiinni.

Useimmat parserit tarjoavat yhden käytöksen rikkinäiselle syötteelle. lxml tarjoaa kolme, ja ne ovat niin ennustettavia, että syötin niiden läpi kuusi erilaista virheellisen merkinnän luokkaa ja esirekisteröin, miten jokaisen polun pitäisi käyttäytyä.
| Virheellinen syöte | lxml.etree (tiukka) | etree + recover=True | lxml.html (salliva) |
|---|---|---|---|
Sulkematon tagi <root><a>x</root> | heittää virheen | korjaa | hyväksyy |
Väärin sisäkkäin <b><i></b></i> | heittää virheen | korjaa | hyväksyy |
Määrittelemätön entiteetti | heittää virheen | korjaa | hyväksyy |
Paljas & (Tom & Jerry) | heittää virheen | korjaa | hyväksyy |
Useita juuria <a>1</a><b>2</b> | heittää virheen | korjaa | hyväksyy |
| Hyvin muodostettu XML | hyväksyy | hyväksyy (0 virhettä) | hyväksyy |
Boolen attribuutti <input disabled> | heittää virheen | korjaa | hyväksyy |
Seitsemän seitsemästä meni juuri niin kuin odotettiin. lxml.etree nostaa XMLSyntaxError-poikkeuksen kaikista kuudesta virheellisestä muodosta. Kun samaan parseriin lisää recover=True, se nielee virheet ja rakentaa käyttökelpoisen puun uudelleen — ja tämä on aliarvostettu osa — parser.error_log listaa sen jälkeen kaikki virheet, jotka se joutui ohittamaan. lxml.html hyväksyy kaiken valittamatta.
Luokittelija, joka päättää "heittääkö virheen / korjaako / hyväksyykö", perustuu ajonaikaiseen error_log-pituuteen, ei kovakoodattuun oletukseen. Siksi hyvin muodostettu dokumentti recover=True-tilassa merkitään oikein "hyväksyy"-tilaan (tyhjä loki) eikä "korjaa"-tilaan. Ensimmäinen versio tästä luokittelijasta merkitsi kaiken recover=True-tuloksen korjaukseksi ja luokitteli puhtaan syötteen väärin; todellisen error_log-tiedon lukeminen korjasi tämän.
Mitä tästä saa käytännössä: jos syötteen pitäisi olla ehjä ja rikkonainen lähde halutaan epäonnistuvan näkyvästi, käytä lxml.etree-parseria. Jos käsissä on likainen, oikean maailman HTML ja siitä pitää vain päästä läpi, käytä lxml.html:ää. Ja välimalli, johon useimmat työkalut eivät pysty — "ole salliva, mutta kerro minulle tarkasti, mikä oli rikki, jotta voin kirjata sen" — hoituu recover=True:lla ja error login lukemisella. selectolaxissa on vain salliva vaihde, ei muuta: ei tiukkaa tilaa eikä error logia.
iterparse: suoratoistovaihde, jota selectolaxissa ei ole lainkaan
Tämä on kyvykkyyslinja, ei nopeussäätö. selectolax lukee vain koko merkkijonon — sillä ei ole inkrementaalista rajapintaa. lxml:n iterparse palauttaa elementtejä niiden sulkeutuessa, ja yhdessä klassisen fast_iter-mallin kanssa (kutsu elem.clear() ja poista aiemmat sisarukset sitä mukaa) se pitää muistin tasaisena riippumatta siitä, kuinka suuri dokumentti on.

Mittaoin muistikäyttäytymisen suoraan — huippu-RSS ru_maxrss-arvolla, jokainen kohde omassa tuoreessa prosessissaan, 300 000 <record>-elementillä, jotka olivat kooltaan yhteensä noin 15 MB.
| Tila | Huippu-RSS:n muutos | Huomio |
|---|---|---|
iterparse + clear (fast_iter) | ~1-2 MB | vapautuu matkan varrella; tasainen määrästä riippumatta |
iterparse ilman clearia | ~386 MB | pitää viitteet; yhtä raskas kuin täysi lataus |
etree.parse (täysi lataus, vertailukohta) | ~386 MB | tunnetusti raskas; osoittaa, että mittari näkee mittaluokan |
Rajoitettu tila pitää huippu-RSS:n noin 1–2 MB:ssa verrattuna täyden latauksen ~386 MB:iin — eli mittaluokassa 0,3–0,4 % — ja ensimmäinen record-tapahtuma käynnistyy ennen kuin tiedoston lukeminen on edes valmis, joten kyse on aidosta suoratoistosta, ei feikistä. Opettavin rivi on keskimmäinen. Aja sama iterparse-silmukka, mutta jätä clear() pois, ja muisti kipuaa jälleen noin 386 MB:iin, koska pidät viitteitä kaikkeen. Voitto syntyy clear()-kutsusta, ei iterparsesta itsestään. Täysi lataus -vertailukohta, joka lukee paljon korkeammalle kuin rajoitettu tila, vahvistaa myös sen, että RSS-mittari todella näkee koko mittakaavaeron eikä lue sokkona. (Tämä muistimittaus on yksi tässä paketissa ajamistani testeistä — se on footprint-mittaus, erillinen lainatuista ajoitusluvuista.)
Käytännössä tämä tarkoittaa: monen gigatavun XML-vienti, joka ei mahdu RAM-muistiin, ei tarjoa selectolax-polkuja lainkaan. Vaihtoehto on lxml:n suoratoistoparseri tai toinen ohjelmointikieli.
Nimiavaruudet: RSS, SVG ja oletusnimiavaruuden ansa
Kaksitoista nimiavaruustapausta, jotka kattavat RSS:n kolmessa nimiavaruudessa, SVG:n oletusnimiavaruudella ja xlinkillä sekä oletusnimiavaruudellisen XML:n. Kaikki kaksitoista menivät läpi.
lxml poimii RSS-syötteestä //dc:creator/text()-lauseella täsmälleen ["Alice", "Bob"], ratkaisee //atom:link/@href- ja //content:encoded-polut kolmen eri nimiavaruuden yli samassa dokumentissa, käsittelee SVG:n toisessa nimiavaruudessa //s:rect- ja //s:use/@xlink:href-haut, jakaa Clark-notaation {uri}local-nimet QName:lla ja tarjoaa nsmap-tiedot tarkasteltavaksi. Tämä on ylläpidettyä ja dokumentoitua käyttäytymistä, ja se on kokonainen ulottuvuus, johon selectolax ei koske, koska selectolax on HTML5-pohjainen eikä käsittele mielivaltaisia XML-nimiavaruuksia.
Yksi dokumentoitu ansa kannattaa painaa mieleen. XPathissa ei ole käsitettä oletusnimiavaruudesta. Jos osoitat //book-polun dokumenttiin, jossa on xmlns="urn:...", osumia tulee nolla — tyhjä etuliite on XPathissa määrittelemätön, kuten lxml:n dokumentaatio sanoo. Sinun täytyy sitoa keinotekoinen etuliite (//c:book ja namespaces={"c": "urn:..."}, joka löysi kaikki kolme) tai käyttää muotoa //*[local-name()='book'] (myös kolme). Ei bugi — vaan XPath-spesifikaatio, toteutettuna uskollisesti. Se vain yllättää joka kerta.
Oikeat likaiset sivut: tarkkuus 11 aidossa scrapessa
Synteettiset testit ovat siistejä; web ei ole. Käytin uudelleen yksitoista oikeaa tallennettua sivua selectolax-paketin fixturejoukosta (tilanne 2026-07-10, read-only) ja ajoin ne lxml.html:n läpi lxml:n ollessa tarkastelun kohteena.
| Fixture | Koko | Linkit | libxml2:n korjaamat virheet | Tiukka XML |
|---|---|---|---|---|
| news_bbc.html | 398 KB | 255 | 4 | ok |
| docs_mdn_array.html | 243 KB | 508 | 0 | heitti virheen |
| wiki_scraping.html | 227 KB | 460 | 0 | heitti virheen |
| gov_whitehouse.html | 289 KB | 154 | 0 | heitti virheen |
| oldstyle_craigslist.html | 561 KB | 351 | 0 | heitti virheen |
| forum_reddit.html | 129 KB | 318 | 0 | heitti virheen |
| docs_python.html | 80 KB | 341 | 2 | heitti virheen |
| ecommerce_books.html | 51 KB | 94 | 0 | heitti virheen |
| news_hackernews.html | 35 KB | 229 | 0 | heitti virheen |
| ecommerce_webscraper_allinone.html | 16 KB | 35 | 0 | heitti virheen |
| spa_quotes_js.html | 6 KB | 5 | 0 | heitti virheen |
Kaikki yksitoista parsittiin lxml.html:llä, ja linkki-, otsikko- ja kuvamäärät täsmäsivät kaikissa yksitoista aiemmin käytetyn selectolax-paketin lxml-lukuihin — cross-check: true. Juuri tuo täsmäys kertoo, että uudelleenkäyttö on oikeasti vertailukelpoista eikä kaksi eri mittausta vain saman nimilapun alla.
Sivulöydös: tiukka XML-parseri heitti virheen kymmenessä yhdestätoista sivusta. Oikean maailman web-sivut eivät juuri koskaan ole hyvin muodostettua XML:ää, ja juuri siksi libxml2:n HTML-recovery-tila on olemassa nielemään niitä. Ainoa poikkeus oli BBC News, joka oli Next.js:llä renderöity ja tarpeeksi hyvin muodostettu selviämään tiukasta XML-parsinnasta. Kaikkea, mikä on nimetty "HTML:ksi", ei tarvitse ajaa recovery-polulla.
Yksi laskentahuomio, johon on helppo kompastua. Kohteessa docs_python.html //a[@href] (attribuutin olemassaolo) antoi tulokseksi 343, kun taas selectolax-paketin if n.get("href") (totuusarvoinen arvo) antoi 341. Kaksi ylimääräistä ovat tyhjiä href=""-linkkejä. Tämä on laskentatavan ero — attribuutti on olemassa vs. attribuutti ei ole tyhjä — ei lxml:n käyttäytymisero, ja luvut täsmäävät, kun predikaatti sovitetaan samaan logiikkaan. Hyvä tietää scrapatessa: lasketaanko tyhjät hrefit mukaan, on suodattimesi valinta, ei parserin.
Syvyysrajoitus, joka näyttää bugilta (mutta ei ole)
selectolax-paketissa oli kirjattu, että lxml pudotti syvimmän sisällön 1 000- ja 5 000-tason sisäkkäisillä <div>-rakenteilla, ja sitä kuvattiin muodossa "lxml menettää hiljaisesti syvimmän sisällön." Halusin mekanismin, joten ajoin oletusparserin ja vertasin sitä huge_tree=True:een.

| Pyydetty syvyys | Oletusparseri pääsee | huge_tree=True pääsee |
|---|---|---|
| 300 | 253 (pudottaa loput) | 299 (palautuu) |
| 1000 | 253 (pudottaa loput) | 999 (palautuu) |
| 5000 | 253 (pudottaa loput) | 2045 (silti pudottaa) |
Oletusparseri katkaisee noin 253 tasoon ja pudottaa kaiken syvemmän hiljaa. Se ei ole bugi — se on libxml2:n DoS-suoja, noin 256-tasoinen sisäkkäisyyskatto, joka estää vihamielistä dokumenttia rikkomasta pinoa, ja se on dokumentoitu lxml:n launchpad-ketjussa liittyen XML_PARSE_HUGE-asetukseen. Kun laitat huge_tree=True, syvyydet 300 ja 1 000 palautuvat kokonaan. Syvyydessä 5 000 päästään kuitenkin vain tasolle 2 045, vaikka huge_tree on päällä — libxml2:ssa on toinen, kovempi rekursion yläraja, joka on asetettavan rajan yläpuolella, eikä huge_tree poista sitä.
Toimintakehotus on siis selkeä: kun parsit syvää merkintää lähteestä, johon luotat, käytä lxml.html.HTMLParser(huge_tree=True). Tämän paketin lisäarvo aiemman havainnon päälle on mekanismi (turvaraja, ei datan vioittuminen), korjaus (huge_tree) ja tieto siitä, että on olemassa toinenkin katto, johon korjaus ei yllä.
DOMin luku/ kirjoitus, serialisointi, enkoodaus
lxml on täysi luku- ja kirjoitustekninen puu, ei pelkkä lukija, ja varmistin muokkauspinnan tapauskohtaisesti. Kaikki kahdeksan DOM-operaatiota menivät läpi: SubElement, insert, remove, replace, strip_tags (poista tagit, säilytä niiden teksti), strip_elements (poista tagit ja niiden teksti), drop_tree (vain lxml.html:ssä) sekä text/tail-kaksoissolmumalli, joka hämmentää aloittelijoita — <p>head<b>bold</b>tail</p>-esimerkissä p.text on "head", b.text on "bold" ja b.tail on "tail".
Serialisointi meni viidestä viiteen: tostring XML- ja HTML-tilassa (HTML jättää tyhjät elementit oikein sulkematta itseään), pretty_print, C14N-kanonisointi (method="c14n", toinen lxml:n erikoisuuksista) ja puhdas round-trip.
Enkoodaus on kohta, jossa lxml erottuu hiljaa muista. Syötä sille muuta kuin UTF-8-bittejä — esimerkiksi "<p>café éè</p>".encode("latin-1") ja lxml.html.fromstring — ja se palauttaa café éè-tekstin ehjänä, ilman U+FFFD-korvausmerkkejä ja ilman kadonneita bittejä. Tämä toistaa suoraan sen roolin, joka sillä oli selectolax-paketissa "puhtaana referenssinä", jossa sama syöte korruptoitui hiljaa kahdessa muussa moottorissa (Lexbor tuotti korvausmerkkejä, Modest pudotti bittejä kokonaan). lxml:n libxml2-pohjainen merkistötunnistus on tässä yksinkertaisesti vakaampi.
Kääntöpuoli on tiukkuus siinä, miten enkoodaus ilmoitetaan. XML-deklaroinnin encoding="latin-1" nostaa virheen XMLSyntaxError: Unsupported encoding: latin-1, kun taas IANA-kanoninen encoding="ISO-8859-1" parsitaan ongelmitta ja palauttaa café. libxml2 hyväksyy vain kanoniset enkoodausnimet, ei aliaksia — yksityiskohta, joka on dokumentoitu jo launchpadin bugissa #613302. Ärsyttävä, jos et tiedä sitä; triviaali, kun tiedät.
Lopuksi solmun elinkaari. Ajoin kolme vanhentuneen kahvan skenaariota eristetyissä aliprocesseissa (kovana kaatumisena näkyisi ei-nolla exit): pidetty solmu puun ollessa kerätty roskana, kahvan lukeminen drop_tree()-toiminnon jälkeen ja solmun käyttäminen remove()-operaation jälkeen. Yhdessäkään ei tullut segfaultia — lxml pitää solmun viitteen puuhun elossa estääkseen käytön vapautuksen jälkeen. Sama puhdas terveystodistus tuli selectolaxille tässä testissä.
Nopeus ja muisti (lainattuina, ja siitä rehellisesti)
Kaikki tämän osion luvut on lainattu selectolax-paketista, tilanteessa 2026-07-13. Tämä paketti ei tuottanut omia ajoituslukujaan, ja sanon sen mieluummin kahdesti kuin annan sinun luulla, että mittasin jotakin uudelleen.
| Ulottuvuus | lxml-arvo | Tulkinta |
|---|---|---|
| Pelkkä parse p50 (10 MB) | 77.9 ms | noin 33–34 % nopeampi kuin selectolax-Lexbor |
| Täysi parse + extract p50 (1 MB / 10 MB) | 14.18 ms / 172.9 ms | käytännössä tasoissa Lexborin kanssa pienissä kooissa |
| 100k-solmun CSS-läpimeno | 3,002,646 solmua/s | kolmen C-moottorin nopein taso |
| 10 MB RSS-muutos | 128.9 MB | kuudesta parserista kevein, noin 1,7× kevyempi kuin BeautifulSoup |
| Importin kylmäkäynnistys | 14.1 ms | noin 2,3× nopeampi kuin parsel-tyyppiset importit |
Pelkkä parse- ja läpimenoaika ovat vahvoja, ja lxml on mitatuista kuudesta parserista kaikkein vähämuistisin. Säikeistyskuvaan liittyy kuitenkin varaus. Lainattu data näyttää neljän säikeen wall-clock-parannukseksi vain 1.21×, ja se on merkitty epävarmaksi — mutta kyse on jaetun oletusparserin polusta. lxml:n FAQ sanoo selvästi, että GIL vapautuu parsinnassa vain silloin, kun jokaisella säikeellä on oma parseri (tai kopioitu oletus); jaettu parseri sarjallistaa pääsyn. Varmistin rakenteellisesti, että oikeaa käyttöä varten API-pinta on olemassa (XMLParser.copy(), get/set_default_parser ja XPathEvaluatorin sisäinen lukko), mutta en mitannut säikeittäisen parserin nopeutuskerrointa — se olisi uusi ajoitusmittaus, eikä tämä paketti tuota niitä. Lue siis "1.21×" muodossa "naivilla jaetulla polulla", ei lxml:n säikeistyksen ylärajana.
Yksi tähdellä merkitty huomio kaikessa tässä: nämä ovat yhden alustan lukuja, macOS arm64. Väite siitä, että lxml:n pelkkä parse päihittää Lexborin, on ristiriidassa sen yleisen käsityksen kanssa, että Lexbor-pohjainen parseri on nopein, joten se kaipaa aidosti Linux x86_64 -tarkistusta ennen kuin kukaan pitää sitä lopullisena totuutena.
Lisensointi: tylsä voitto
lxml toimitetaan BSD-3-Clause-lisenssillä, ja sen mukana tulevat C-kirjastot — libxml2 ja libxslt — ovat molemmat MIT-lisenssillä. Tämä on täysin salliva ketju ilman copyleftia missään kohdassa, mikä on tärkeää heti kun alat jakaa tuotetta eteenpäin. Vertailun vuoksi selectolax-wheel sisältää LGPL-2.1-lisensoidun Modestin ja Apache-2.0-lisensoidun Lexborin, joten lxml on selkeämpi vaihtoehto suljettuun tuotteeseen paketoimiseen.
Myös asennuksessa on käytännön etu: lxml julkaisee valmiiksi kootut wheelit, jotka linkittävät libxml2:n ja libxslt:n staattisesti, joten pip install lxml ei yleensä vaadi järjestelmän libxml2:a eikä kääntäjää koneellesi — eri kokemus kuin lähdekoodista rakentaminen.
Mihin lxml sopii — ja missä AI-pohjainen ekstraktiokerros ottaa vallan
On aika tehdä raja selväksi, koska tässä kohtaa tehdään helposti luokkavirhe. lxml on parsimiskirjasto. Se antaa sinulle puun ja erinomaisen kyselymoottorin, mutta kaikki puun ympärillä on yhä sinun vastuullasi: sivun haku, JavaScriptin renderöinti, anti-bot-suojien kiertäminen, XPathien kirjoittaminen ja ylläpito sekä tuloksen jäsentely. Se on eri kerros kuin hostattu extraction-palvelu, eivätkä nämä ole niinkään kilpailijoita kuin naapureita.
Kehittäjälle, joka ei halua omistaa koko fetch-render-select-maintain-pinosta, tuo ylempi kerros on juuri se paikka, jossa esimerkiksi Thunderbit elää — ja tälle yleisölle kyse on API:sta, MCP-palvelimesta ja CLI:stä, ei selainlaajennuksesta. Thunderbit Open API tarjoaa POST /distill-rajapinnan, jolla sivu muutetaan puhtaaksi Markdowniksi, sekä POST /extract-rajapinnan, jolla poimitaan rakenteinen data JSON Schemaa vasten, mukana renderMode-vaihto ja eräajot suuremmille määrille. Sama moottori on saatavilla MCP-palvelimena (thunderbit_suggest_fields, thunderbit_distill, thunderbit_extract) agenteille ja koodausavustajille sekä CLI:nä, jota voi ajaa suoraan terminaalista komennolla npx @thunderbit/thunderbit-cli. Se hoitaa JS-renderöinnin, anti-botin ja CAPTCHA:t valmiiksi ja palauttaa skeemaa vastaavan JSONin — eli kyse on parsimisen yläpuolella olevasta kerroksesta, ei sen korvaamisesta.
Kokeile Thunderbitiä verkkodatan poimintaan
Ajatus on yksinkertainen valinta. Ota lxml käyttöön, kun hallitset putken itse ja haluat kirurgisen tarkan XPath-kontrollin ymmärtämääsi puuhun. Ota AI-pohjainen extraction-API käyttöön, kun et halua ylläpitää valitsimia ja renderöintiä lainkaan. Moni oikea järjestelmä käyttää molempia — lxml:ää rakenteisiin syötteisiin, joita se hallitsee itse, ja extraction-palvelua sotkuisiin pitkän hännän sivuihin, joita se ei hallitse.
Mitä tämä arvio ei testannut
Tämä on alustava arvio, ei lopullinen pistetaulukko, joten tässä mitä se ei kata.
Kaikki ajoitus- ja muistiluvut on lainattu, yhden alustan lukuja (macOS arm64, Python 3.14) ja ne perivät paketin omat varaukset — tulos "lxml on nopeampi pelkässä parsinnassa" on vastoin yleistä konsensusta ja tarvitsee Linux x86_64 -uudelleentarkistuksen. Säikeittäisen parserin nopeutusta ei testattu (se vaatisi uutta ajoitusmittausta). Mittasin iterparse-muistin 300k recordilla, mutta en gigatavuluokan oikeaa XML:ää, en iterparsea HTML:llä verrattuna XML:ään enkä monen tunnin soak-ajoa. lxml:n XSLT 1.0, RelaxNG / XMLSchema / DTD-validointi ja EXSLT-laajennukset eivät ole tässä lainkaan testattuja — laaja kyvykkyysalue, mutta parsimisen ja valinnan ydintä laajempi. Havaitsin toisen syvyyskaton tasolla 2 045, mutta en selvittänyt libxml2:n tarkkaa rekursiovakiota. Vain vakaa 6.1.1 testattiin, ei 7.0.0-alfa. Windows, lähdekoodista rakennettavat versiot ja free-threaded 3.14t -kooste jäivät kaikki testaamatta. Ja itse XPathissa käsittelin sisäänrakennettuja funktioita, mutta en XPath-muuttujia, omia Python-laajennusfunktioita tai esikäännettyjen etree.XPath-olioiden uudelleenkäyttöä.
Lopullinen arvio
lxml ei ole nopea uusi tulokas, ja juuri siksi se on suositus. Se on kahden vuosikymmenen ikäinen libxml2-sidonta, jossa on täysi XPath 1.0 -moottori, jolle mikään valtavirran Python-vaihtoehto ei vedä vertoja, kolme ennustettavaa parsimisen tiukkuusvaihdetta, joiden keskellä on error logi, oikea suoratoistoparseri muistissa liian suurille dokumenteille, toimiva moni-nimiavaruus- ja enkoodauskäsittely sekä täysin salliva lisenssi. Pari terävää kohtaa — noin 253 tason syvyyskatto ja jaetun parserin säikeistysluku — ovat dokumentoituja, säädettäviä ja nyt myös selitettyjä.
Jos omistat scraping-putkesi ja nojaat XPathiin, lxml on edelleen parseri, johon kannattaa tarttua. Jos et halua ylläpitää valitsimia ja renderöintiä itse, siihen on AI-pohjainen extraction-kerros kuten Thunderbit API, MCP ja CLI — selkeä työnjako, ei kilpailuasetelma. Joka tapauksessa pidä näitä lukuja alustavina ja tarkista ajoitus omalla alustallasi ennen kuin lainaat niitä suunnitteludokumenttiin.
Kokeile Thunderbitiä verkkodatan poimintaan Get Started Free
Usein kysytyt kysymykset
Onko lxml web scraper?
Ei. lxml on parseri ja serializeri — Python-sidonta libxml2/libxslt:hen, joka muuttaa merkinnän muokattavaksi ja kyseltäväksi puuksi. Se ei hae sivuja, renderöi JavaScriptiä eikä hoida anti-bot-suojia; sinun täytyy tarjota pyyntökerros (requests, httpx, headless-browser tai scraping-palvelu) ja syöttää bitit lxml:lle.
Milloin lxml:ää kannattaa käyttää BeautifulSoupin tai selectolaxin sijaan? Tarttu lxml:ään, kun tarvitset XPathia. BeautifulSoup voi käyttää lxml:ää taustaparserina, mutta ei tarjoa natiivia XPathia, ja selectolax on CSS-pohjainen sekä nopea omassa kapeassa nurkassaan. Jos valintalogiikkasi tarvitsee tekstisisällön suodatusta, siirtymistä vanhempiin tai esi-isiin, attribuutti- tai tekstisolmun poimintaa tai laskentapredikaatteja, lxml:n XPath-moottori on ainoa valtavirran Python-vaihtoehto, joka ilmaisee ne suoraan.
Miksi lxml pudottaa syvälle sisäkkäisen sisällön hiljaa?
Sen oletusparseri rajoittaa sisäkkäisyyden noin 253 tasoon — libxml2:n DoS-suoja vihamielisiä dokumentteja vastaan, ei bugi. Aseta huge_tree=True (esimerkiksi lxml.html.HTMLParser(huge_tree=True)), niin se palauttaa syvyydet 300 ja 1 000 täysin. Huomaa kuitenkin toinen, kovempi rekursion raja noin tasolla 2 045, jota huge_tree ei poista.
Vapauttaako lxml GILin monisäikeisessä parsinnassa? Vain oikeissa oloissa. lxml:n FAQ kertoo, että GIL vapautuu parsinnassa, kun jokaisella säikeellä on oma parseri tai kopioitu oletusparseri; jaettu parseri sarjallistaa pääsyn sen sijaan. Uudelleenkäytetty 4 säikeen 1.21× nopeutus kuvaa naivia jaetun parserin polkua, ei säikeittäisen parserin kattoa, jota ei tässä mitattu.
Onko lxml yhä ylläpidetty vuonna 2026? Kyllä. Vakaa julkaisu 6.1.1 ilmestyi 2026-05-18, repositoryyn tuli viimeisin push 2026-07-02, ja työn alla on 7.0.0-alfa. Noin 3 000 GitHub-tähden ja aktiivisesti ylläpidetyn libxml2:n ansiosta kyse on edelleen ajantasaisesta ja hyvin tuetusta kirjastosta, ei perinnöstä.


