Parin kuukauden välein markkinoille ilmestyy nopeampi HTML parser, benchmarkit leviävät joka paikkaan ja joku julistaa vanhat ratkaisut 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ä.
lxml on 20 vuotta vanha libxml2 -sidonta. Se ei ole säväyttävä. Se ei ole uusi. Mutta yhdessä tietyssä tehtävässä — kaikessa, mikä tarvitsee oikeaa XPathia — mikään muu valtavirran Python-maailmassa ei oikeasti kilpaile. Tämä on käytännönläheinen arvio siitä, mitä lxml tekee, missä se voittaa huomaamatta, ja mitkä pari oletusta voivat purra, jos et tiedä niiden olevan olemassa.
lxml yhdessä kappaleessa: mitä se oikeastaan on
lxml on Python-sidonta C-kirjastoihin libxml2 ja libxslt. Se on parseri ja serialisoija, ei scrapersovellus eikä selain — se muuttaa merkinnän puuksi, jota voit kysellä ja muokata, ja muuttaa puun takaisin biteiksi. Se tarjoaa ElementTree-yhteensopivan rajapinnan, täydellisen XPath 1.0 -moottorin, XSLT 1.0:n ja skeemavalidoinnin. Kehitystä ylläpitää Stefan Behnel tunnuslauseella "the most feature-rich and easy-to-use library for processing XML and HTML in the Python language".
Tässä sen nykytilanne GitHub- ja PyPI-näkymän perusteella, joka poimittiin 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, ennen kuin kukaan väittää tätä hypetykseksi: tässä arvioissa ei ole salaisuuksia. lxml on niin vanha, että jokainen tässä mainittu käytös löytyy jostain lxml-dokumentaatiosta, libxml2:n changelogista tai Launchpad-keskustelusta. En löytänyt mitään salaista, dokumentoimatonta kikkaa, enkä aio sellaista keksiä. Tämän tekstin arvo on siinä, että se on jäsennelty, mitattu ja koottu lxml:n ympärille — ei siinä, että se olisi uusi.
Testiasetelma (ja miksi ajoitusluvut on lainattu)
Tähän arvioon kuuluu kahdenlaisia tuloksia, ja ne tulevat kahdesta eri paikasta, joten kerron suoraan kumpi on kumpaa.
Ominaisuustestit — XPath-käyttäytyminen, kaksi parseri-APIa, nimetilojen käsittely, enkoodaus, node-elinkaari — ajettiin tuoreesti yhdellä koneella: macOS arm64, Python 3.14.2, lxml 6.1.1, libxml2 2.14.6. Jokainen artifacts/raw/*.json-tiedostojen luku on skriptin laskema, ei käsin kirjoitettu. Ominaisuustestit ovat deterministisiä booleaneja ja enum-arvoja, joten yksi ajo on vakaa — koneen kuormitus ei muuta sitä, palauttaako //a/@href attribuuttijonon.
Ajoitus- ja muistijälkiluvut eivät ole tästä paketista. Ne on otettu sellaisenaan aiemmasta selectolax-benchmark-paketista — sama kone, sama virtuaaliympäristö, sama lxml- ja libxml2-rakennelma, benchmarkit tilanteessa 2026-07-13 — enkä ajanut niitä uudelleen tässä. Se on tarkoituksellista. Ajoitustestien pyörittäminen samalla kun ajetaan ominaisuusskriptejä voi aiheuttaa CPU-kilpailua ja vääristää lainattuja lukuja, ja se olisi myös päällekkäistä työtä: lxml oli jo tuossa paketissa täysin mitattu vertailukirjasto. Kun käytetään samaa benchmark-pohjaa, vertailu pysyy aidosti vertailukelpoisena eikä synny toista, hieman eri tavalla mitattua datapistettä. Kun alla näkyy millisekuntiluku, lue se siis muodossa "sama testilaitteisto, tilanteessa 2026-07-13", ei "mittasin tämän tänään uudelleen".
Löydöksissä on luottamusmerkintä: single-observation deterministisille ominaisuustesteille, triple-run lainatuille ajoitusjakaumille ja hypothesis, kun esitetään mekanismi, jota ei erikseen eristetty.
XPath: se yksi asia, jota selectolax ja BeautifulSoup eivät yksinkertaisesti tarjoa
Tämä on koko jutun pääväite, joten aloitetaan siitä.

Ajoin lxml:n xpath()-metodin läpi 37 kohdan ennalta rekisteröidyn matriisin — odotettu tulos jokaiselle tapaukselle oli kirjattu lähteeseen ennen testin käynnistymistä, joten en voinut vahingossa antaa pisteitä löysästi. Kymmenen akselia, yhdeksän predikaattityyliä, kymmenen sisäänrakennettua funktiota, kolme skalaaripalautustyyppiä ja viisi tahallista ansatapausta, joissa käytettiin vain XPath 2.0:ssa sallittua syntaksia, jonka lxml:n 1.0-moottorin pitäisi hylätä.
| Kategoria | Kattavuus | Tulos |
|---|---|---|
| Akselit | child / descendant / parent / ancestor / following-sibling / preceding-sibling / self / attribute / following / preceding | 10/10 läpäisy |
| Predikaatit | [1] / last() / position()<n / attribuutin yhtäsuuruus / attribuutin olemassaolo / and / or / sisäkkäinen [.//a] / not() | 9/9 läpäisy |
| Funktiot | text() / contains() / starts-with() / count() / string-length() / normalize-space() / concat() / substring() / name() / string() | 10/10 läpäisy |
| Palautustyypit | boolean / number -skalaarit | 3/3 läpäisy |
| Ansatapaukset | matches() / sekvenssit / if-then-else / except / syntaksivirhe | 5/5 hylätty oikein |
Pisteet ovat 37/37, ja ansasarake on se oleellinen osa. matches(), sekvenssiekspressiot, if/then/else ja except ovat XPath 2.0 -syntaksia, eikä libxml2:n 1.0-moottori tue niitä puolittain — se nostaa XPathEvalError-virheen ja kieltäytyy, sen sijaan että palauttaisi hiljaa väärän node-joukon. Eli tämä on täydet pisteet sen jälkeen, kun sitä yritettiin rikkoa, ei täydet pisteet helppoja tehtäviä keräämällä. Jokainen tässä näkyvä käytös vastaa täsmälleen lxml XPath -dokumentaatiota, mikä on juuri pointti.
Myönnän yhden asian, jonka testiharness teki aluksi väärin — eli sen version "37/37":stä, johon voi oikeasti luottaa. Ensimmäinen odotettu joukko polulle //div[.//a[@href]] ennusti kaksi osumaa; ajo palautti yhden. Oletin hetken noin kolmekymmentä sekuntia, että lxml oli väärässä, kunnes tarkistin fixture-tiedoston ja huomasin, että toinen elementti olikin <footer>, ei <div> — odotukseni oli väärä, ei moottori. Korjasin odotetun joukon ja jätin virheen lähdekoodin kommenttiin. Se on oikea syyllisyysjärjestys: epäile ensin omaa testiäsi, älä 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()-metodin (jälkimmäinen kääntää CSS:n XPathiksi taustalla). Otin kymmenen valintatavoitetta ja katsoin, mitkä niistä CSS osaa oikeasti ilmaista.

| Tavoite | XPath | CSS (cssselect) |
|---|---|---|
Suodata tekstisisällön perusteella (contains(text(),"bargain")) | Kyllä | Ei tekstipredikaattia |
Valitse vanhempi lapsesta (//b/parent::p) | Kyllä | Ei parent-valitsinta |
Palauta attribuutin arvo (//a/@href) | Kyllä | Vain elementtejä |
Palauta tekstisolmu (//p/text()) | Kyllä | Ei tekstisoluja |
Ancestor-akseli (//td/ancestor::div) | Kyllä | Ei ylöspäin navigointia |
Suodata vanhempi lapsien mää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ä tavoitteesta ei ole CSS-vastinetta lainkaan. Tekstisisällön suodatus, ylöspäin kulkeva navigointi vanhempiin ja esi-isiin, attribuuttiarvon tai paljaan tekstisolmun palauttaminen tuloksena, lukumäärään perustuvat predikaatit — CSS ei yksinkertaisesti pysty näihin. Vain kolme (nth-child, last-child, vierekkäinen sisarus) toimii molemmissa. Tässä on numeerinen vastaus kysymykseen "mitä oikeasti saan käyttämällä lxml:ää". selectolax tukee vain CSS:ää eikä siinä ole lainkaan xpath()-metodia, joten nuo seitsemän kyselytyyppiä muuttuvat siellä joko monivaiheisiksi Python-silmukoiksi tai jäävät kokonaan tekemättä. Jos scrapauslogiikkasi nojaa niihin, päätös on jo käytännössä tehty.
(Ja kyllä, harness nalkkasi minut tässäkin toisen kerran: ennustin tyhjän joukon kohdalle string-length(text())>5, mutta kaksi kuusimerkkistä merkkijonoa osui mukaan. Korjasin odotuksen, en työkalua.)
Kolme tiukkuusvaihdetta: etree, recover ja lxml.html
XPath on syy valita lxml. Kolmiportainen tiukkuuden hallinta on syy pitää se valittuna.

Useimmat parserit tarjoavat yhden käyttäytymisen rikkinäiselle syötteelle. lxml tarjoaa kolme, ja ne ovat niin ennustettavia, että syötin kuusi eri virheellisen merkinnän luokkaa kunkin läpi ja rekisteröin etukäteen, miten jokaisen pitäisi käyttäytyä.
| Virheellinen syöte | lxml.etree (tiukka) | etree + recover=True | lxml.html (salliva) |
|---|---|---|---|
Sulkematon tagi <root><a>x</root> | nostaa virheen | palauttaa rakenteen | hyväksyy |
Väärin sisäkkäinen <b><i></b></i> | nostaa virheen | palauttaa rakenteen | hyväksyy |
Määrittelemätön entiteetti | nostaa virheen | palauttaa rakenteen | hyväksyy |
Paljas & (Tom & Jerry) | nostaa virheen | palauttaa rakenteen | hyväksyy |
Useita juuria <a>1</a><b>2</b> | nostaa virheen | palauttaa rakenteen | hyväksyy |
| Hyvin muodostettu XML | hyväksyy | hyväksyy (0 virhettä) | hyväksyy |
Boolean-attribuutti <input disabled> | nostaa virheen | palauttaa rakenteen | hyväksyy |
Seitsemän seitsemästä osui ennalta rekisteröityyn odotukseen. lxml.etree nostaa XMLSyntaxError-virheen kaikissa kuudessa virheellisessä luokassa. Kun samaan parseriin lisätään recover=True, se nielee virheet ja rakentaa käyttökelpoisen puun — ja tämä on aliarvostettu osa — parser.error_log listaa tällöin kaikki virheet, jotka se ohitti. lxml.html hyväksyy kaiken valittamatta.
Luokittelija, joka päättää "nosti / palautti / hyväksyi", perustuu ajonaikaiseen error_log-pituuteen, ei kovakoodattuun sääntöön. Siksi hyvin muodostettu dokumentti recover=True-tilassa luokitellaan oikein "hyväksyy"-tapaukseksi (tyhjä loki) eikä "palauttaa"-tapaukseksi. Ensimmäinen versioni tästä luokittelijasta merkitsi kaikki recover=True-ajot palautuksiksi ja leimasi myös siistin syötteen väärin; todellisen error_log-sisällön lukeminen korjasi sen.
Mitä tästä on käytännössä hyötyä: jos syöte pitää validoida tiukasti ja rikkinäisen syötteen halutaan kaatuvan äänekkäästi, käytä lxml.etree-parseria. Jos kyseessä on likainen, oikean maailman HTML, joka pitää vain saada läpi, käytä lxml.html-parseria. Ja siihen välimuotoon, jota useimmat työkalut eivät osaa — "ole salliva, mutta kerro tarkasti mikä oli rikki, jotta voin kirjata sen" — käytä recover=True-tilaa ja lue virheloki. selectolaxissa on vain salliva vaihde, ei muuta: ei tiukkaa tilaa eikä virhelokia.
iterparse: suoratoistovaihde, jota selectolaxilla ei ole lainkaan
Tämä on ominaisuuslinja, ei nopeussäätö. selectolax lukee vain koko merkkijonon — siinä ei ole inkrementaalista rajapintaa. lxml:n iterparse tuottaa elementit niiden sulkeutuessa, ja yhdistettynä klassiseen fast_iter-malliin (kutsu elem.clear() ja poista edeltävät sisarukset sitä mukaa) se pitää muistin tasaisena riippumatta siitä, kuinka suuri dokumentti on.

Mittaustulos oli suora muistiluku — huippu-RSS ru_maxrss:n kautta, kukin kohde omassa puhtaassa prosessissaan, 300 000 <record>-elementillä, joiden yhteiskoko oli noin 26,7 MB (26,744,801 tavua).
| Tila | Huippu-RSS:n muutos | Huomio |
|---|---|---|
iterparse + clear (fast_iter) | ~1-2 MB | vapauttaa matkan varrella; tasainen riippumatta määrästä |
iterparse ilman cleariä | ~386 MB | pitää viittaukset; yhtä raskas kuin täysi lataus |
etree.parse (täysi lataus, vertailu) | ~386 MB | tunnetusti raskas; osoittaa mittarin lukevan mittakaavan |
Rajattu tila pitää huippu-RSS:n suunnilleen 1–2 MB:ssa täyteen lataukseen verrattuna, kun täysi lataus vie noin 386 MB — ero on suuruusluokaltaan 0,3–0,4 % — ja ensimmäinen record-tapahtuma laukeaa ennen kuin tiedosto on edes kokonaan luettu, joten kyseessä on aidosti inkrementaalinen suoratoisto, ei näennäinen streamaus. Opettavin rivi on keskimmäinen. Aja sama iterparse-silmukka mutta jätä clear() pois, ja muisti nousee takaisin noin 386 MB:iin, koska pidät kiinni kaikista viitteistä. Voitto syntyy clear()-kutsusta, ei iterparse-toiminnosta yksinään. Täyden latauksen vertailuarvo, joka on selvästi korkeampi kuin rajattu tila, vahvistaa myös, että RSS-mittari pystyy oikeasti näkemään mittakaavaeron eikä vain arvaile sitä. (Tämä muistimittaus on tämän paketin oma ajo — footprint-mittaus, eri asia kuin lainatut ajoitusluvut.)
Käytännössä tämä tarkoittaa: monen gigatavun XML-vienti, joka ei mahdu RAMiin, ei ole selectolaxin kanssa mahdollinen. Tarvitset lxml:n suoratoistoparsijan tai kokonaan toisen ohjelmointikielen.
Nimitilat: RSS, SVG ja oletusnimitilan miina
Kaksitoista nimitilatapausta, jotka kattavat RSS:n kolmen nimitilan läpi, SVG:n oletusnimitilan ja xlinkin yhdistelmän sekä oletusnimitilaisen XML:n. Kaikki kaksitoista läpäistiin.
lxml poimii //dc:creator/text()-polulla RSS-syötteestä täsmälleen arvon ["Alice", "Bob"], ratkaisee //atom:link/@href- ja //content:encoded-polut kolmen erillisen nimitilan läpi samassa dokumentissa, käsittelee SVG:n toisen nimitilan //s:rect- ja //s:use/@xlink:href-muodot, pilkkoo Clark-merkinnän {uri}local-nimet QName:lla ja tarjoaa introspektiota nsmap-rakenteen kautta. Tämä on ylläpidetty ja dokumentoitu käytös, ja se on kokonainen ulottuvuus, johon selectolax ei koske, koska selectolax on HTML5-pohjainen eikä käsittele mielivaltaisia XML-nimitiloja.
On kuitenkin yksi dokumentoitu miina, joka kannattaa painaa mieleen. XPathissa ei ole käsitettä oletusnimitilasta. Jos osoitat //book-polun dokumenttiin, jossa on xmlns="urn:...", saat nolla osumaa — tyhjä etuliite ei ole XPathissa määritelty, kuten lxml-dokumentaatio kertoo. Sinun on joko sidottava keinotekoinen etuliite (//c:book ja namespaces={"c": "urn:..."}, joka löysi kaikki kolme) tai palattava muotoon //*[local-name()='book'] (myös kolme osumaa). Ei bugi — tämä on XPath-spesifikaation mukaista toimintaa. Se vain yllättää joka kerta.
Oikeat sotkuiset sivut: tarkkuus 11 aidolla scrapella
Synteettiset testit ovat siistejä; web ei ole. Käytin uudelleen yksitoista oikeaa talletettua sivua selectolax-paketin fixturejoukosta (tilanne 2026-07-10, read-only) ja ajoin ne lxml.html:n läpi lxml:n ollessa tarkastelun kohde.
| Fixture | Koko | Linkit | libxml2:n palauttamat virheet | Strict XML |
|---|---|---|---|---|
| news_bbc.html | 398 KB | 255 | 4 | ok |
| docs_mdn_array.html | 243 KB | 508 | 0 | nosti virheen |
| wiki_scraping.html | 227 KB | 460 | 0 | nosti virheen |
| gov_whitehouse.html | 289 KB | 154 | 0 | nosti virheen |
| oldstyle_craigslist.html | 561 KB | 351 | 0 | nosti virheen |
| forum_reddit.html | 129 KB | 318 | 0 | nosti virheen |
| docs_python.html | 80 KB | 341 | 2 | nosti virheen |
| ecommerce_books.html | 51 KB | 94 | 0 | nosti virheen |
| news_hackernews.html | 35 KB | 229 | 0 | nosti virheen |
| ecommerce_webscraper_allinone.html | 16 KB | 35 | 0 | nosti virheen |
| spa_quotes_js.html | 6 KB | 5 | 0 | nosti virheen |
Kaikki yksitoista parsittiin lxml.html:llä, ja linkki-, otsikko- ja kuvamäärät vastasivat kaikissa yhdestätoista selectolax-paketin lxml-lukuja — ristivarmistus true. Juuri tuo yhteismitallisuus kertoo, että uudelleenkäyttö on oikeasti samaa mittaustapaa eikä kahta eri mittausta samalla nimellä.
Sivuhuomio: tiukka XML-parseri nosti virheen kymmenessä yhdestätoista sivusta. Oikeat verkkosivut eivät ole läheskään koskaan hyvin muodostettua XML:ää, ja juuri siksi libxml2:n HTML-palautustila on olemassa nielemään ne. Ainoa poikkeus oli BBC News, joka oli Next.js:n renderöimä ja tarpeeksi hyvin muodostettu selvitäkseen tiukasta XML-parsinnasta. Kaikki HTML-etiketillä kutsuttu ei tarvitse palautustilaa.
Yksi laskentahuomio, johon on helppo kompastua. Tiedostossa docs_python.html //a[@href] (attribuutin olemassaolo) antoi laskennaksi 343, kun taas selectolax-paketin if n.get("href") (totuusarvona testattu arvo) antoi 341. Kaksi ylimääräistä ovat tyhjiä href=""-linkkejä. Tämä on laskentatavan ero — attribuutti on olemassa vs. attribuutti on ei-tyhjä — ei lxml:n käyttäytymiserot, ja luvut sovittuvat yhteen heti kun predikaatti on sama. Hyvä tietää scrapatessa: se, lasketko tyhjät hrefit mukaan, on suodattimesi päätös, ei parserin.
Syvyysraja, joka näyttää bugilta, mutta ei ole sitä
selectolax-paketissa oli jo havaittu, että lxml tiputti syvimmän sisällön 1 000- ja 5 000-tason sisäkkäisestä <div>-rakenteesta, ja sitä kuvattiin sanoin "lxml silently loses the deepest content". Halusin selvittää mekanismin, joten ajoin oletusparserin ja huge_tree=True-tilan rinnakkain.

| Pyydetty syvyys | Oletusparseri yltää | huge_tree=True yltää |
|---|---|---|
| 300 | 253 (tiputtaa loput) | 299 (palautuu) |
| 1000 | 253 (tiputtaa loput) | 999 (palautuu) |
| 5000 | 253 (tiputtaa loput) | 2045 (edelleen tiputtaa) |
Oletusparseri katkaisee noin 253 tason kohdalla ja pudottaa kaiken syvemmällä olevan hiljaa pois. Se ei ole bugi — se on libxml2:n DoS-suoja, noin 256 tason sisäkkäisyysraja, joka estää vihamielistä dokumenttia kaatamasta pinoa, ja se on dokumentoitu lxml Launchpad -keskustelussa liittyen XML_PARSE_HUGE-asetukseen. Kun asetat huge_tree=True, syvyydet 300 ja 1 000 palautuvat täysin. Mutta syvyydessä 5 000 päästään huge_tree-tilassakin vain tasolle 2 045 — libxml2:ssa on toinen, kovempi rekursioraja, joka on asetettavaa rajaa korkeammalla, eikä huge_tree poista sitä.
Toimenpide on siis selvä: kun parsit syvää merkintää lähteestä, johon luotat, käytä lxml.html.HTMLParser(huge_tree=True)-asetusta. Tämän paketin tuoma lisäarvo aiempaan havaintoon on mekanismi (turvaraja, ei datan korruptio), korjaus (huge_tree) ja tieto siitä, että on olemassa toinen raja, johon korjaus ei yllä.
Lue/muokkaa DOMia, serialisointi, enkoodaus
lxml on täysi luku- ja kirjoituspuiden käsittelijä, ei pelkkä lukupohjainen poimija, ja tarkistin muokkauspuolen kohta kohdalta. Kaikki kahdeksan DOM-operaatiota läpäistiin: 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ä tekstin ja tailin kaksoiskenttämalli, joka hämmentää aloittelijoita — <p>head<b>bold</b>tail</p>-rakenteessa p.text on "head", b.text on "bold" ja b.tail on "tail".
Serialisointi meni viidestä viisi: tostring XML- ja HTML-tiloissa (HTML jättää tyhjäelementit oikein ilman itseään sulkevaa muotoa), pretty_print, C14N-kanonisointi (method="c14n", toinen lxml:n erikoisominaisuus) sekä puhdas round-trip.
Enkoodauksessa lxml erottuu hiljaa muista. Syötä sille ei-UTF-8-bittejä — esimerkiksi "<p>café éè</p>".encode("latin-1") lxml.html.fromstring-funktion läpi — ja se palauttaa café éè-tekstin ehjänä, ilman U+FFFD-vaihtomerkkejä ja ilman pudonneita tavuja. Tämä toistaa suoraan sen roolin, joka sillä oli selectolax-paketin "clean reference" -vertailukohteena, jossa sama syöte korruptoitui hiljaa muissa kahdessa moottorissa (Lexbor tuotti vaihtomerkkejä, Modest pudotti tavuja suoraan). lxml:n libxml2-pohjainen merkistön tunnistus on tässä yksinkertaisesti vakaampi.
Kääntöpuolena on tiukkuus siinä, miten enkoodaus ilmoitetaan. XML-deklaration encoding="latin-1" nostaa virheen XMLSyntaxError: Unsupported encoding: latin-1, kun taas IANA-kanoninen encoding="ISO-8859-1" toimii ja palauttaa café. libxml2 hyväksyy vain kanoniset enkoodausnimet, ei aliasnimiä — yksityiskohta, joka on dokumentoitu jo Launchpad-ongelmassa #613302. Ärsyttävää, jos et tiedä sitä; mitätön asia, kun tiedät.
Viimeisenä node-elinkaari. Ajoin kolme vanhentuneen kahvan tilannetta erillisissä aliprosesseissa (kova kaatuminen näkyisi ei-nollana exit-koodina): node säilytettynä sen jälkeen, kun puu on kerätty roskina, kahvan lukeminen drop_tree()-toiminnon jälkeen ja noden käyttäminen remove()-kutsun jälkeen. Yhdessäkään ei tullut segfaultia — lxml pitää noden viitteen puuhunsa elossa estääkseen käytön vapautetun muistin jälkeen. Sama puhdas terveystodistus tuli selectolaxille tässä testissä.
Nopeus ja muisti (lainattuna ja rehellisesti)
Kaikki tämän osion luvut on lainattu selectolax-paketista, tilanteessa 2026-07-13. Tämä paketti ei tuottanut yhtään omaa ajoituslukua, ja sanon sen mieluummin kahdesti kuin annat ymmärtää, että mittasin ne uudelleen.
| Ulottuvuus | lxml-arvo | Tulkinta |
|---|---|---|
| Pelkkä parse p50 (10 MB) | 77.9 ms | noin 33–34 % nopeampi kuin selectolax-Lexbor |
| Täysi parse + poiminta p50 (1 MB / 10 MB) | 14.18 ms / 172.9 ms | suunnilleen tasoissa Lexborin kanssa pienessä koossa |
| 100k-solmun CSS-läpivienti | 3,002,646 solmua/s | kolmen C-moottorin nopein taso |
| 10 MB RSS-muutos | 128.9 MB | kuudesta parserista kevyin, noin 1,7x kevyempi kuin BeautifulSoup |
| Kylmäimportin aloitus | 14.1 ms | noin 2,3x nopeampi kuin parsel-tyyliset importit |
Pelkkä parsaus ja läpivientinopeus ovat vahvoja, ja lxml on mitatuista kuudesta parserista muistitaloudellisin. Säikeistyskuvassa on kuitenkin tärkeä varaus. Uudelleenkäytetty data näyttää neljän säikeen wall-clock-kiihdytykseksi vain 1,21x, ja se on merkitty epäselväksi — mutta kyse on jaetun oletusparserin polusta. lxml FAQ sanoo selvästi, että GIL vapautetaan parsinnassa vain, jos jokainen säie käyttää omaa parseriaan (tai kopioitua oletusparseria); jaettu parseri sarjoittaa pääsyn. Varmistin API-pinnan rakenteellisesti oikeaa toteutusta varten (XMLParser.copy() on olemassa, get/set_default_parser ovat olemassa, XPathEvaluator sisältää sisäisen lukon), mutta en mitannut per-säie-parserin nopeutuskerrointa — se olisi uusi ajoitusmittaus, eikä tämä paketti tuota sellaisia. Lue siis "1.21x" merkityksessä "naiivilla jaetulla polulla", ei lxml:n säikeistyskattona.
Ja yksi tähdellinen huomio kaikkeen tähän: nämä ovat yhden alustan lukuja, macOS arm64. Väite, että lxml:n pelkkä parse voittaa Lexborin, menee tavallista konsensusta vastaan, jonka mukaan Lexbor-pohjainen parseri on nopein, joten se tarvitsee aidosti Linux x86_64 -uudelleentarkistuksen 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-lisensoituja. Tämä on täysin salliva ketju ilman copyleftiä missään kohdassa, ja sillä on merkitystä heti, kun alat jakaa ohjelmistoa eteenpäin. Vertailun vuoksi selectolax-paketti sisältää LGPL-2.1 Modestin ja Apache-2.0 Lexborin, joten lxml on siistimpi valinta suljetun tuotteen jakeluun.
Myös käytännön asennuspuoli on eduksi: lxml julkaisee valmiiksi käännettyjä wheel-paketteja, jotka linkittävät libxml2:n ja libxslt:n staattisesti, joten pip install lxml ei yleensä tarvitse järjestelmän libxml2:a eikä kääntäjää koneellesi — kokemus on siis erilainen kuin lähdekoodista rakentaminen.
Mihin lxml sopii — ja missä AI-pohjainen extraction-kerros ottaa ohjat
On aika sanoa raja selvästi, koska tässä menee helposti luokkavirhe. lxml on parserikirjasto. Se antaa sinulle puun ja erinomaisen kyselymoottorin, ja kaikki puun ympärillä on yhä sinun vastuullasi: sivun hakeminen, JavaScriptin renderöinti, bot-suojien kiertäminen, XPathien kirjoittaminen ja ylläpito sekä tuloksen jäsentely. Se on eri kerros kuin hostattu extraction-palvelu, eikä nämä ole niinkään kilpailijoita kuin naapureita.
Kehittäjälle, joka ei halua omistaa fetch-render-select-maintain -pinon ylläpitoa, tuo ylempi kerros on paikka, jossa jotain Thunderbitin kaltaista käytetään — ja tälle yleisölle se tarkoittaa API:a, MCP-serveriä ja CLI:tä, ei selainlaajennusta. Thunderbit Open API tarjoaa POST /distill-kutsun, jolla sivu muutetaan siistiksi Markdowniksi, ja POST /extract-kutsun, jolla poimitaan strukturoitua dataa JSON Scheman avulla. Mukana on renderMode-vaihtokytkin ja batch-ajot suurempaa volyymia varten. Sama moottori on saatavilla MCP-serverinä (thunderbit_suggest_fields, thunderbit_distill, thunderbit_extract) agenteille ja koodiavustajille sekä CLI:nä, jota voi ajaa suoraan terminaalista npx @thunderbit/thunderbit-cli-komennolla. Se hoitaa JavaScript-renderöinnin, anti-bot-suojat ja CAPTCHA:t valmiiksi, ja palauttaa skeemaa vastaavaa JSONia — eli kyse on parsinnan yläpuolella olevasta kerroksesta, ei sen korvaajasta.
Kokeile Thunderbitiä verkkodatan poimintaan
Ajattelutapa on yksinkertainen haarautuma. Valitse lxml, kun omistat putken ja haluat kirurgisen tarkan XPath-hallinnan puun yli, jonka tunnet. Valitse AI-pohjainen extraction-API, kun et halua ylläpitää selektoreita ja renderöintiä lainkaan. Moni oikea järjestelmä käyttää molempia — lxml:ää hallituille, rakenteisille syötteille ja extraction-palvelua sotkuisille pitkän hännän sivuille.
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, ne ovat yhden alustan lukuja (macOS arm64, Python 3.14) ja perivät kyseisen paketin varaukset — tulos "lxml on nopeampi pelkässä parsinnassa" on konsensusta vastaan ja vaatii Linux x86_64 -uudelleentarkistuksen. Per-säie-parserin säikeistysnopeutusta ei testattu (se vaatisi uuden ajoituksen). Mittasin iterparse-muistin 300k rekordilla, mutta en gigatavuluokan oikeaa XML:ää, en iterparsea HTML:llä verrattuna XML:ään, enkä usean tunnin kuormitusta. lxml:n XSLT 1.0, RelaxNG / XMLSchema / DTD -validointi ja EXSLT-laajennukset jäivät kokonaan testaamatta täällä — laaja kyvykkyysalue, mutta parsinta- ja valintaytimen ulkopuolella. Havaitsin toisen syvyysrajan 2 045 tasolla, mutta en selvittänyt libxml2:n täsmällistä rekursiokonstanttia. Vain vakaa 6.1.1 testattiin, ei 7.0.0-alphaa. Windows, lähdekoodista tehdyt buildit ja free-threaded 3.14t -rakenne jäivät kaikki testaamatta. Ja itse XPathissa käsittelin sisäänrakennettuja funktioita, mutta en XPath-muuttujia, omia Python-laajennusfunktioita tai valmiiksi käännetyn etree.XPath-olion uudelleenkäyttöä.
Lopputulos
lxml ei ole se nopea uusi tulokas, ja juuri siksi se on suositus. Se on kaksi vuosikymmentä vanha libxml2-sidonta, jossa on täysi XPath 1.0 -moottori, jolle mikään valtavirran Python-vaihtoehto ei yllä, kolme ennustettavaa parsinnan tiukkuusvaihdetta ja virheloki keskimmäisessä, oikea suoratoistoparsija dokumenteille, jotka eivät mahdu muistiin, kunnollinen monen nimitilan ja enkoodauksen käsittely sekä täysin salliva lisenssi. Pienet terävät kohdat — noin 253 tason syvyysraja ja jaetun parserin säikeistysluku — ovat dokumentoituja, säädettäviä ja nyt myös selitettyjä.
Jos hallitset omaa scraping-putkeasi ja nojaat XPathiin, lxml on yhä se parseri, johon tarttua. Jos taas et halua ylläpitää selektoreita ja renderöintiä itse, siihen on olemassa AI-pohjainen extraction-kerros, kuten Thunderbit API, MCP ja CLI — selkeä työnjako, ei kilpailu. Joka tapauksessa nämä luvut kannattaa käsitellä alustavina ja tarkistaa ajoitukset omalla alustallasi ennen kuin siteeraat niitä suunnitteludokumentissa.
Kokeile Thunderbitiä verkkodatan poimintaan Get Started Free
Usein kysytyt kysymykset
Onko lxml web-scraper?
Ei. lxml on parseri ja serialisoija — Python-sidonta libxml2:een/libxslt:hen, joka muuttaa merkinnän muokattavaksi ja kyseltäväksi puuksi. Se ei hae sivuja, renderöi JavaScriptiä eikä käsittele bot-suojauksia; pyydät hakukerroksen itse (requests, httpx, headless-selain tai scraping-palvelu) ja annat sitten bitit lxml:lle.
Milloin minun pitäisi käyttää lxml:ää BeautifulSoupin tai selectolaxin sijaan? Valitse lxml, kun tarvitset XPathia. BeautifulSoup voi käyttää lxml:ää taustaparserina, mutta se ei tarjoa natiivia XPathia, ja selectolax on CSS-pohjainen ja kapea-alaisessa nicheessään nopeampi. Jos valintalogiikkasi tarvitsee tekstisisällön suodatusta, vanhempiin tai esi-isiin kulkemista, attribuutti- tai tekstisolmu-poimintaa tai lukumääräpredikaatteja, lxml:n XPath-moottori on ainoa valtavirran Python-vaihtoehto, joka ilmaisee ne suoraan.
Miksi lxml tiputtaa joskus syvälle sisäkkäisen sisällön hiljaa pois?
Sen oletusparseri rajoittaa sisäkkäisyyden noin 253 tasoon — libxml2:n DoS-suoja vihamielistä dokumenttia vastaan, ei bugi. Kun asetat huge_tree=True (esimerkiksi lxml.html.HTMLParser(huge_tree=True)), syvyydet 300 ja 1 000 palautuvat täysin. Huomaa kuitenkin toinen, kovempi rekursioraja noin tasolla 2 045, jota huge_tree ei poista.
Vapauttaako lxml GIL:n monisäikeisessä parsinnassa? Vain oikeissa olosuhteissa. lxml:n FAQ kertoo, että GIL vapautetaan parsinnassa silloin, kun jokainen säie käyttää omaa parseriaan tai kopioitua oletusparseria; jaettu parseri sarjoittaa pääsyn. Uudelleenkäytetty 4 säikeen nopeutus 1,21x kuvaa naiivia jaettua parseripolkua, ei per-säie-parserin kattoa, jota ei tässä mitattu.
Onko lxml yhä ylläpidetty vuonna 2026? Kyllä. Vakaa julkaisu 6.1.1 tuli ulos 2026-05-18, repo sai viimeisimmän pushin 2026-07-02, ja työn alla on 7.0.0 alpha. Noin 3 000 GitHub-tähteä ja aktiivisesti ylläpidetty libxml2 taustalla tarkoittavat, että kyseessä on edelleen ajantasainen ja hyvin tuettu kirjasto, ei vanha perintöprojekti.


