Hver noen måneder dukker det opp en raskere HTML-parser, benchmarkene går sin vante runde, og noen erklærer den gamle garde for utdatert. Så skal du velge ut hvert avsnitt som inneholder et bestemt ord, eller hente foreldrenoden til et treff, og da husker du hvorfor lxml fortsatt ligger åpen i den andre fanen din.
lxml er en 20 år gammel binding til libxml2. Den er ikke spennende. Den er ikke ny. Og til én helt spesifikk jobb — alt som trenger ekte XPath — er det faktisk ingenting annet i det vanlige Python-miljøet som matcher. Dette er en praktisk gjennomgang av hva den gjør, hvor den stille og rolig vinner, og de få stedene der standardoppsettet kan bite deg hvis du ikke vet at de finnes.
lxml på ett avsnitt: Hva det faktisk er
lxml er en Python-binding til C-bibliotekene libxml2 og libxslt. Det er en parser og serializer, ikke en scraper og ikke en nettleser — den gjør markup om til et tre du kan spørre mot og redigere, og gjør treet om til bytes igjen. Du får et ElementTree-kompatibelt API, en fullverdig XPath 1.0-motor, XSLT 1.0 og skjema-validering, vedlikeholdt av Stefan Behnel under slagordet "the most feature-rich and easy-to-use library for processing XML and HTML in the Python language".
Her er statusen, basert på et GitHub- og PyPI-øyeblikksbilde hentet 2026-07-14:
| Felt | Verdi |
|---|---|
| Repo | lxml/lxml |
| Stjerner | 3,043 |
| Forks | 620 |
| Åpne saker | 16 |
| Lisens | BSD-3-Clause |
| Opprettet | 2011-02-11 |
| Siste push | 2026-07-02 |
| Stabil på PyPI | 6.1.1 (2026-05-18) |
| Innebygd motor | libxml2 2.14.6 + libxslt 1.1.43 |
En ting det er greit å få ut av veien før noen beskylder meg for hype: det finnes ingen hemmeligheter i denne gjennomgangen. lxml er gammel nok til at hver eneste oppførsel her er dokumentert et sted i lxml-dokumentasjonen, en libxml2-changelog eller en Launchpad-tråd. Jeg fant ingen eksklusiv, udokumentert triksing, og jeg kommer ikke til å finne på noe. Verdien i det som følger, er at det er systematisert, kvantifisert og organisert rundt lxml som tema — ikke at det er nyheter.
Testoppsettet (og hvorfor tidsmålingene er lånt)
To typer data går inn i denne gjennomgangen, og de kommer fra to ulike steder, så jeg er tydelig på hva som er hva.
Funksjonstester — XPath-oppførsel, de to parser-API-ene, namespaces, koding, nodelivssyklus — kjørte jeg på nytt på én maskin: macOS arm64, Python 3.14.2, lxml 6.1.1, libxml2 2.14.6. Hvert tall i artifacts/raw/*.json-filene er beregnet av et script, ikke skrevet inn for hånd. Funksjonstester er deterministiske booleans og enum-er, så én kjøring er stabil — maskinbelastning endrer ikke om //a/@href returnerer en attributtstreng.
Tids- og minne-tallene er ikke fra denne pakken. De er brukt ordrett fra den tidligere selectolax-benchmarkpakken — samme maskin, samme virtuelle miljø, samme lxml- og libxml2-bygg, benchmarks per 2026-07-13 — og jeg kjørte dem ikke på nytt her. Det er bevisst. Å kjøre tidsbenchmarks samtidig med en bunke funksjonsscripts inviterer til CPU-konkurranse som forurenser de gjenbrukte tallene, og det ville dessuten vært dobbeltarbeid: lxml var allerede et fullt målt kontrollbibliotek i den pakken. Ved å gjenbruke samme testoppsett holder vi alt sammenlignbart, i stedet for å introdusere en andre, litt annerledes måling. Når du ser et millisekundtall under, skal du lese det som "samme testoppsett, per 2026-07-13," ikke "jeg målte dette på nytt i dag."
Funnene har en tillitsmerking: single-observation for de deterministiske funksjonstestene, triple-run for de gjenbrukte tidsfordelingene, hypothesis der jeg foreslår en mekanisme jeg ikke isolerte.
XPath: Det ene selectolax og BeautifulSoup bare ikke har
Dette er hovedpoenget, så jeg starter her.

Jeg kjørte lxmls xpath() gjennom en forhåndsregistrert matrise med 37 punkter — forventet resultat for hvert tilfelle var skrevet inn i kildekoden før testen startet, så jeg kunne ikke ubevisst gi den en mildere vurdering. Ti akser, ni predicate-stiler, ti innebygde funksjoner, tre skalare returtyper og fem bevisste feller med XPath 2.0-syntaks som lxmls 1.0-motor skal avvise.
| Kategori | Dekning | Resultat |
|---|---|---|
| Akser | child / descendant / parent / ancestor / following-sibling / preceding-sibling / self / attribute / following / preceding | 10/10 bestått |
| Predikater | [1] / last() / position()<n / attributtlikhet / attributt-eksistens / and / or / nestet [.//a] / not() | 9/9 bestått |
| Funksjoner | text() / contains() / starts-with() / count() / string-length() / normalize-space() / concat() / substring() / name() / string() | 10/10 bestått |
| Returtyper | boolean / number-skalare | 3/3 bestått |
| Felle-tilfeller | matches() / sekvenser / if-then-else / except / syntaksfeil | 5/5 riktig avvist |
Scoren er 37/37, og fellekolonnen er den viktige delen. matches(), sekvensuttrykk, if/then/else og except er alle XPath 2.0-syntaks, og libxml2s 1.0-motor støtter dem ikke halvveis — den kaster XPathEvalError og stopper, i stedet for å returnere feil node-sett i stillhet. Så dette er en perfekt score etter at jeg prøvde å ødelegge det, ikke en perfekt score satt sammen av enkle spørsmål. Hver oppførsel her er akkurat det lxmls XPath-dokumentasjon beskriver, og det er poenget.
Jeg må innrømme én ting harnessen tok feil om, fordi det er den eneste typen "37/37" du faktisk kan stole på. Min første forventede mengde for //div[.//a[@href]] mente det skulle bli to treff; kjøringen ga ett. Jeg antok i omtrent tretti sekunder at lxml tok feil, sjekket så fixture-filen og oppdaget at det andre elementet var en <footer>, ikke en <div> — det var forventningen min som var feil, ikke motoren. Jeg rettet den forventede mengden og lot feilen bli stående i en kommentert kildekode-linje. Det er riktig rekkefølge å plassere skyld i: mistenk din egen test før du mistenker et 20 år gammelt C-bibliotek.
XPath vs CSS: Det du bokstavelig talt ikke kan uttrykke i CSS
Det abstrakte argumentet om at "XPath er kraftigere" fortjener et konkret tall, så jeg kvantifiserte gapet. lxml gir deg både .xpath() og .cssselect() (den siste oversetter CSS til XPath under panseret). Jeg tok ti utvalgsmål og sjekket hvilke av dem CSS faktisk kan uttrykke.

| Mål | XPath | CSS (cssselect) |
|---|---|---|
Filtrer på tekstinnhold (contains(text(),"bargain")) | Ja | Nei, ingen tekstpredicate |
Velg forelder fra barn (//b/parent::p) | Ja | Nei, ingen parent-selector |
Returner en attributtverdi (//a/@href) | Ja | Kun elementer |
Returner en tekstnode (//p/text()) | Ja | Ingen tekstnoder |
Ancestor-akse (//td/ancestor::div) | Ja | Ingen navigasjon oppover |
Filtrer forelder etter antall barn (//ul[count(li)=4]) | Ja | Ingen count-predicate |
Filtrer på tekstlengde (string-length(text())>5) | Ja | Ingen lengdepredicate |
nth-child / last-child / adjacent sibling | Ja | Ja (3 grunnleggende) |
Sju av ti mål har ingen CSS-ekvivalent i det hele tatt. Filtrering på tekstinnhold, oppover-navigasjon til foreldre og forfedre, å hente en attributtverdi eller en ren tekstnode som resultat, predicates basert på telling — CSS kan ikke uttrykke noe av det. Bare tre (nth-child, last-child, adjacent sibling) fungerer i begge. Det er det kvantifiserte svaret på "hva får jeg egentlig ved å velge lxml." selectolax er CSS-only og har ikke engang xpath()-metode, så de sju spørringstypene blir der til flertrinns Python-løkker eller skjer ikke i det hele tatt. Hvis scrapestrategien din bygger på noen av dem, er valget ditt gjort.
(Og ja, harnessen tok meg en gang til her: jeg forventet tomt sett for string-length(text())>5, men to strenger med seks tegn matchet. Rettet forventningen, ikke verktøyet.)
Tre nivåer av strenghet: etree vs recover vs lxml.html
XPath er grunnen til å velge lxml. De tre strenghetsnivåene er grunnen til å beholde det.

De fleste parsere gir deg én oppførsel ved ødelagt input. lxml gir deg tre, og de er forutsigbare nok til at jeg kjørte seks klasser med ugyldig markup gjennom hver av dem og forhåndsregistrerte hvordan hver vei skulle oppføre seg.
| Ugyldig input | lxml.etree (streng) | etree + recover=True | lxml.html (tolerant) |
|---|---|---|---|
Uavsluttet tag <root><a>x</root> | kaster | gjenoppretter | aksepterer |
Feil nestet <b><i></b></i> | kaster | gjenoppretter | aksepterer |
Udefinert entity | kaster | gjenoppretter | aksepterer |
Løst & (Tom & Jerry) | kaster | gjenoppretter | aksepterer |
Flere røtter <a>1</a><b>2</b> | kaster | gjenoppretter | aksepterer |
| Velutformet XML | aksepterer | aksepterer (0 feil) | aksepterer |
Boolsk attributt <input disabled> | kaster | gjenoppretter | aksepterer |
Syv av syv matchet forventningen som var registrert på forhånd. lxml.etree kaster XMLSyntaxError på alle de seks ugyldige klassene. Legg recover=True til samme parser, og den svelger feilene og rekonstruerer et brukbart tre — og dette er den undervurderte delen — parser.error_log lister deretter opp hver eneste feil den svelget. lxml.html aksepterer alt uten å klage.
Klassifikatoren som avgjør "kastet vs gjenopprettet vs akseptert" styres av lengden på error_log ved kjøring, ikke av hardkodede verdier, og det er grunnen til at et velutformet dokument kjørt med recover=True blir korrekt merket som "aksepterer" (tom logg) i stedet for "gjenoppretter." Min første versjon av klassifikatoren merket alt med recover=True som "gjenoppretter" og feilkodet det rene inputet; å lese den faktiske error_log-en rettet det.
Hva dette gir deg i praksis: streng validering når en ødelagt feed skal feile tydelig, bruk lxml.etree. Skitten, ekte HTML du bare må komme deg gjennom, bruk lxml.html. Og mellomtilfellet de fleste verktøy ikke klarer — "vær tolerant, men si meg nøyaktig hva som var ødelagt så jeg kan logge det" — bruk recover=True og les feilloggen. selectolax har den tolerante modusen og ellers ingenting, ingen streng modus og ingen feillogg.
iterparse: Strømmemodusen selectolax ikke har i det hele tatt
Dette er en kapabilitetslinje, ikke en hastighetsknapp. selectolax leser bare inn en hel streng — det finnes ingen inkrementell grensesnitt. lxmls iterparse gir elementer idet de lukkes, og sammen med det klassiske fast_iter-mønsteret (kall elem.clear() og slett foregående søsken mens du går) holder det minnet flatt uansett hvor stort dokumentet blir.

Jeg målte minneoppførselen direkte — topp-RSS via ru_maxrss, hvert testobjekt i en egen fersk prosess, på 300,000 <record>-elementer som til sammen var omtrent 15 MB.
| Modus | Topp RSS-endring | Notater |
|---|---|---|
iterparse + clear (fast_iter) | ~1-2 MB | frigjøres fortløpende; flatt uansett antall |
iterparse uten clear | ~386 MB | beholder referanser; like tungt som full lasting |
etree.parse (full lasting, referanse) | ~386 MB | kjent tung; viser at måleren leser størrelsesorden |
Den avgrensede modusen holder topp-RSS-endringen nede på omtrent 1-2 MB mot full lasting på ~386 MB — en forskjell på 0,3-0,4 % i størrelsesorden — og den første record-hendelsen kommer før filen en gang er ferdig lest, så dette er genuint inkrementelt, ikke falsk strømming. Den mest læringsrike linjen er den midterste. Kjør samme iterparse-løkke, men hopp over clear(), og minnet stiger rett tilbake til ~386 MB, fordi du holder referanser til alt. Gevinsten ligger i clear(), ikke i iterparse alene. Referansepunktet for full lasting, som ligger mye høyere enn den avgrensede modusen, bekrefter også at RSS-måleren faktisk ser størrelsesforskjellen og ikke måler i blinde. (Denne minnetesten er en av dem jeg kjørte i denne pakken — det er en fotavtrykkmåling, forskjellig fra de lånte tidsmålingene.)
Den virkelige versjonen av dette: en XML-eksport på flere gigabyte som ikke får plass i RAM har ingen selectolax-løsning overhodet. Da er det lxmls strømmepser eller et annet språk.
Namespaces: RSS, SVG og fellen med standard-namespace
Tolv namespace-tilfeller, som dekker RSS over tre namespaces, SVG med standard-namespace pluss xlink, og XML med standard-namespace. Alle tolv bestod.
lxml henter //dc:creator/text() ut av en RSS-feed som nøyaktig ["Alice", "Bob"], løser //atom:link/@href og //content:encoded på tvers av tre separate namespaces i samme dokument, håndterer //s:rect og //s:use/@xlink:href i SVGs andre namespace, splitter Clark-notasjonens {uri}lokalnavn med QName, og introspekterer via nsmap. Dette er dokumentert og vedlikeholdt oppførsel, og det er en hel dimensjon selectolax ikke rører, fordi selectolax er HTML5-only og ikke behandler vilkårlige XML namespaces.
Det finnes én dokumentert felle det er verdt å huske. XPath har ikke noe konsept om standard namespace. Pek //book mot et dokument som deklarerer xmlns="urn:...", og du får null treff — det tomme prefikset er udefinert for XPath, slik lxml-dokumentasjonen forklarer. Du må enten binde et kunstig prefiks (//c:book med namespaces={"c": "urn:..."}, som fant alle tre) eller falle tilbake til //*[local-name()='book'] (også tre). Ikke en feil — det er XPath-spesifikasjonen, implementert korrekt. Den overrasker bare alle én gang.
Virkelig skittene sider: Fidelity på 11 ekte scraper
Syntetiske tester er rene; nettet er det ikke. Jeg gjenbrukte elleve ekte, lagrede sider fra fixture-settet i selectolax-pakken (per 2026-07-10, skrivebeskyttet) og kjørte lxml.html gjennom dem med lxml som objekt.
| Fixture | Størrelse | Lenker | libxml2-gjenopprettede feil | Streng XML |
|---|---|---|---|---|
| news_bbc.html | 398 KB | 255 | 4 | ok |
| docs_mdn_array.html | 243 KB | 508 | 0 | kastet |
| wiki_scraping.html | 227 KB | 460 | 0 | kastet |
| gov_whitehouse.html | 289 KB | 154 | 0 | kastet |
| oldstyle_craigslist.html | 561 KB | 351 | 0 | kastet |
| forum_reddit.html | 129 KB | 318 | 0 | kastet |
| docs_python.html | 80 KB | 341 | 2 | kastet |
| ecommerce_books.html | 51 KB | 94 | 0 | kastet |
| news_hackernews.html | 35 KB | 229 | 0 | kastet |
| ecommerce_webscraper_allinone.html | 16 KB | 35 | 0 | kastet |
| spa_quotes_js.html | 6 KB | 5 | 0 | kastet |
Alle elleve ble parsers med lxml.html, og antall lenker, overskrifter og bilder matchet de gjenbrukte lxml-tallene fra selectolax-pakken på alle elleve — kryssjekk true. Den enigheten er det som forteller meg at gjenbruken faktisk er epler mot epler og ikke to ulike målinger med samme etikett.
Sidefunn: den strenge XML-parseren kastet på ti av de elleve sidene. Ekte nettsider er i overveldende grad ikke velutformet XML, og det er nettopp derfor libxml2s HTML-gjenopprettingsmodus finnes. Det eneste unntaket var BBC News, gjengitt av Next.js og velutformet nok til å overleve streng XML-parsing. Ikke alt som er merket "HTML" trenger gjenopprettingsmodus.
En tellefelle som er lett å gå i. På docs_python.html ga //a[@href] (attributt-eksistens) 343, mens selectolax-pakkens if n.get("href") (sannhetsverdi) ga 341. De to ekstra er tomme href=""-lenker. Det er en forskjell i tellekonvensjon — attributt finnes versus attributtet er ikke-tomt — ikke en forskjell i lxml-oppførsel, og tallene går opp når du justerer predikatet. Greit å vite når du scraper: om tomme href-er skal telles, er ditt filtervalg, ikke parserens.
Dybdegrensen som ser ut som en feil (men ikke er det)
selectolax-pakken hadde registrert at lxml mistet den dypeste innholdsdelen i markup med 1,000 og 5,000 nivåer nestede <div>-er, og beskrev det som at "lxml mister stille og rolig det dypeste innholdet." Jeg ville vite mekanismen, så jeg kjørte standardparseren mot huge_tree=True.

| Ønsket dybde | Standardparser når | huge_tree=True når |
|---|---|---|
| 300 | 253 (dropper resten) | 299 (gjenopprettet) |
| 1000 | 253 (dropper resten) | 999 (gjenopprettet) |
| 5000 | 253 (dropper resten) | 2045 (fortsatt dropper) |
Standardparseren kutter ved omtrent 253 nivåer og slipper stille unna alt dypere. Det er ikke en bug — det er libxml2s DoS-beskyttelse, en nesting-grense på rundt 256 nivåer som hindrer at et fiendtlig dokument sprenger stacken, og dette er dokumentert i lxml Launchpad-tråden om XML_PARSE_HUGE. Sett huge_tree=True, og dybdene 300 og 1,000 kommer helt tilbake. Dybde 5,000 når derimot bare 2,045 selv med huge_tree på — det finnes en andre, hardere libxml2-grense for rekursjon over den konfigurerbare, og huge_tree fjerner ikke den.
Så handlingspunktet er konkret: når du parser dyp markup fra en kilde du stoler på, bruk lxml.html.HTMLParser(huge_tree=True). Det denne pakken legger til over den gjenbrukte observasjonen, er mekanismen (en sikkerhetsgrense, ikke datakorrupsjon), fiksen (huge_tree), og det at det finnes en andre grense som fiksen ikke når.
Lese/skrive DOM, serialisering, koding
lxml er et fullverdig lese-/skrivetre, ikke bare en lese-only-ekstraktor, og jeg verifiserte redigeringsflaten punkt for punkt. Alle åtte DOM-operasjoner bestod: SubElement, insert, remove, replace, strip_tags (fjern tagger, behold teksten), strip_elements (fjern tagger og teksten deres), drop_tree (en lxml.html-spesifikk funksjon) og den doble tekst-/tail-modellen som feller nybegynnere — i <p>head<b>bold</b>tail</p> er p.text "head", b.text "bold", og b.tail "tail".
Serialisering bestod fem av fem: tostring i XML- og HTML-modus (HTML lar korrekt void elements være useluttede), pretty_print, C14N-kanonisering (method="c14n", også dette lxml-spesifikt), og en ren round-trip.
Koding er der lxml stille og rolig skiller seg ut. Gi den bytes som ikke er UTF-8 — "<p>café éè</p>".encode("latin-1") gjennom lxml.html.fromstring — og den gjenoppretter café éè intakt, uten U+FFFD-erstatningstegn og uten tapte bytes. Det gjenskaper direkte rollen dens som "ren referanse" i selectolax-pakken, der samme input ble korrumpert i stillhet av de to andre motorene (Lexbor produserte erstatningstegn, Modest droppet bytes helt). lxmls libxml2-baserte tegnsettgjenkjenning er ganske enkelt jevnere her.
Den andre siden er strenghet på hvordan du deklarerer en koding. encoding="latin-1" i en XML-deklarasjon kaster XMLSyntaxError: Unsupported encoding: latin-1, mens den IANA-korrekte encoding="ISO-8859-1" parses fint og returnerer café. libxml2 godtar bare kanoniske kodenavn, ikke aliaser — en detalj dokumentert allerede i launchpad #613302. Irriterende hvis du ikke vet det, trivielt når du gjør det.
Til slutt, nodelivssyklus. Jeg kjørte tre scenarier med gamle håndtak i isolerte subprocesser (en hard krasj ville vise seg som en ikke-null avslutningskode): holde en node etter at treet er blitt garbage-collected, lese et håndtak etter drop_tree(), og bruke en node etter remove(). Ingen segfaults i noen av dem — lxml holder nodens referanse til treet i live for å unngå use-after-free. Samme gode vurdering fikk selectolax i denne testen.
Hastighet og minne (lånt, og ærlig om det)
Alt i denne delen er gjenbrukt fra selectolax-pakken, per 2026-07-13. Denne pakken produserte null egne tidsmålinger, og jeg sier heller det to ganger enn at du skal tro jeg målte noe på nytt.
| Dimensjon | lxml-verdi | Tolkning |
|---|---|---|
| Ren parsing p50 (10 MB) | 77.9 ms | ~33-34% raskere enn selectolax-Lexbor |
| Full parsing + ekstraksjon p50 (1 MB / 10 MB) | 14.18 ms / 172.9 ms | omtrent lik Lexbor på små størrelser |
| 100k-node CSS-throughput | 3,002,646 noder/s | raskeste nivå blant de tre C-motorene |
| 10 MB RSS-endring | 128.9 MB | lettest av seks parsere, ~1,7x lettere enn BeautifulSoup |
| Kald importstart | 14.1 ms | ~2.3x raskere enn parsel-lignende imports |
Tallene for ren parsing og throughput er sterke, og lxml er den mest minneøkonomiske av de seks parserne som ble målt. Tråd-bildet trenger likevel en reservasjon. Gjenbrukte data viser en 4-tråds wall-clock-boost på bare 1.21x, markert som inkonklusiv — men det er med delt standardparser. lxmls FAQ er eksplisitt på at GIL frigjøres under parsing bare når hver tråd bruker sin egen parser (eller en kopiert standardparser); en delt parser serialiserer tilgangen. Jeg verifiserte API-flaten strukturelt for å gjøre det riktig (XMLParser.copy() finnes, get/set_default_parser finnes, XPathEvaluator har en intern lås), men jeg målte ikke hastighetsgevinsten per tråd-parser — det ville vært en ny tidsmåling, og denne pakken produserer ikke slike. Så les "1.21x" som "under den naive delte stien," ikke som lxmls trådgrense.
Og én fotnote på alt dette: dette er tall fra én plattform, macOS arm64. Påstanden om at lxmls rene parsing slår Lexbor går på tvers av den vanlige konsensusen om at Lexbor-baserte parseren er raskest, så den bør faktisk etterprøves på Linux x86_64 før noen behandler den som endelig.
Lisensiering: Den kjedelige gevinsten
lxml leveres under BSD-3-Clause, og C-bibliotekene den pakker med — libxml2 og libxslt — er begge MIT. Det er en fullt permissiv kjede uten noe copyleft noe sted, og det betyr noe i det øyeblikket du redistribuerer. Til sammenligning pakker selectolax-hjulet inn LGPL-2.1 Modest og Apache-2.0 Lexbor, så lxml er den ryddigere historien hvis du skal shippe i et lukket produkt.
Det finnes også en praktisk installasjonsfordel: lxml publiserer forhåndsbygde wheels som statisk linker libxml2 og libxslt, så pip install lxml trenger som regel verken systemlibxml2 eller kompilator på maskinen din — en annen opplevelse enn å bygge fra kildekode.
Hvor lxml passer inn — og hvor et AI-ekstraksjonslag tar over
Det er på tide å være tydelig på grensen, for dette er en lett kategorifeil å gjøre. lxml er et parserbibliotek. Det gir deg et tre og en svært god spørringsmotor, og alt rundt det treet er fortsatt ditt ansvar: hente siden, rendere JavaScript, komme forbi anti-bot-beskyttelse, skrive og vedlikeholde XPath-en og strukturere resultatet. Det er et annet lag enn en hostet ekstraksjonstjeneste, og de to er mer naboer enn rivaler.
For en utvikler som helst ikke vil eie hele fetch-render-select-vedlikehold-stakken, er det øvre laget der noe som Thunderbit passer inn — og for dette publikummet er det API-et, MCP-serveren og CLI-en, ikke nettleserutvidelsen. Thunderbit Open API eksponerer POST /distill for å gjøre en side om til ren Markdown og POST /extract for å trekke ut strukturert data mot et JSON Schema, med en renderMode-bryter og batch-jobber for volum. Den samme motoren er tilgjengelig som en MCP-server (thunderbit_suggest_fields, thunderbit_distill, thunderbit_extract) for agenter og kodeassistenter, og som en CLI du kan kjøre rett fra terminalen via npx @thunderbit/thunderbit-cli. Den håndterer JS-rendering, anti-bot og CAPTCHA-er ut av boksen og returnerer JSON som matcher skjemaet — altså laget over parsing, ikke en erstatning for det.
Prøv Thunderbit for webdatauttrekk
Tanken er enkel. Velg lxml når du eier pipelinen og vil ha kirurgisk XPath-kontroll over et tre du forstår. Velg et AI-ekstraksjons-API når du helst vil slippe å vedlikeholde selektorer og rendering helt. Mange ekte systemer bruker begge deler — lxml for de strukturerte feedene de kontrollerer, en ekstraksjonstjeneste for de rotete langhalepages de ikke gjør.
Hva denne gjennomgangen ikke testet
Dette er en foreløpig gjennomgang, ikke en endelig karakter, så her er det som ikke er dekket.
Alle tids- og minnetall er gjenbrukt, fra én plattform (macOS arm64, Python 3.14), og arver forbeholdene fra den pakken — resultatet om at "lxml er raskere på ren parsing" går mot vanlig konsensus og bør etterprøves på Linux x86_64. Trådgevinsten per tråd-parser er ikke testet (det ville kreve nye tidsmålinger). Jeg målte iterparse-minne på 300k records, men ikke ekte XML i gigabyte-klassen, ikke iterparse på HTML versus XML og ikke en fler-timers soak-test. lxmls XSLT 1.0, RelaxNG / XMLSchema / DTD-validering og EXSLT-utvidelser er helt utestet her — et stort funksjonsområde, men utenfor selve parsing- og utvalgskjernen. Jeg observerte den andre dybdegrensen ved 2,045, men fastslo ikke den nøyaktige libxml2-konstanten for rekursjon. Bare stabile 6.1.1 ble testet, ikke 7.0.0 alpha. Windows, kildebygg og den frie trådede 3.14t-builden er alle utestet. Og innen XPath selv dekket jeg innebygde funksjoner, men ikke XPath-variabler, egendefinerte Python-utvidelsesfunksjoner eller gjenbruk av forhåndskompilerte etree.XPath-objekter.
Dommen
lxml er ikke den raske nye tingen, og det er akkurat derfor den anbefales. Det er en to tiår gammel libxml2-binding med en full XPath 1.0-motor ingen mainstream Python-alternativ matcher, tre forutsigbare nivåer av parserstrenghet med en feillogg i midten, en ekte strømmepser for dokumenter som ikke får plass i minnet, korrekt håndtering av flere namespaces og kodinger, og en fullt permissiv lisens. De få skarpe kantene — grensen rundt 253 nivåer og tallet for delt-parser i trådbruk — er dokumentert, konfigurerbare og nå forklart.
Hvis du eier scraper-pipelinen din og bruker XPath tungt, er lxml fortsatt parseren du bør velge. Hvis du helst vil slippe å vedlikeholde selektorer og rendering, er det det et AI-ekstraksjonslag som Thunderbit API, MCP og CLI er til for — en ren arbeidsdeling, ikke en konkurranse. Uansett: behandl disse tallene som foreløpige og sjekk tidsmålingene på din egen plattform før du siterer dem i et design-dokument.
Prøv Thunderbit for webdatauttrekk Get Started Free
Vanlige spørsmål
Er lxml en webscraper?
Nei. lxml er en parser og serializer — en Python-binding til libxml2/libxslt som gjør markup om til et redigerbart, søkbart tre. Den henter ikke sider, renderer ikke JavaScript og håndterer ikke anti-bot-beskyttelse; du leverer request-laget (via requests, httpx, en headless nettleser eller en scraping-tjeneste) og sender bytes til lxml.
Når bør jeg bruke lxml i stedet for BeautifulSoup eller selectolax? Velg lxml når du trenger XPath. BeautifulSoup kan faktisk bruke lxml som backend-parser, men tilbyr ingen innebygd XPath, og selectolax er CSS-only og raskere i sin smale nisje. Hvis utvalg-logikken din trenger filtrering på tekstinnhold, navigasjon til foreldre eller forfedre, uttrekk av attributter/tekstnoder eller count-predicates, er lxmls XPath-motor det eneste vanlige Python-valget som uttrykker dette direkte.
Hvorfor slipper lxml stille og rolig innhold som er dypt nestet?
Standardparseren har en grense på omtrent 253 nivåer — libxml2s DoS-beskyttelse mot fiendtlige dokumenter, ikke en bug. Sett huge_tree=True (for eksempel lxml.html.HTMLParser(huge_tree=True)) og den gjenoppretter dybder på 300 og 1,000 fullt ut. Merk at det finnes en andre, hardere rekursjonsgrense rundt 2,045 nivåer som huge_tree ikke fjerner.
Frigjør lxml GIL for multitrådet parsing? Bare under riktige betingelser. lxmls FAQ sier at GIL frigjøres under parsing når hver tråd bruker sin egen parser eller en kopiert standardparser; en delt parser serialiserer tilgangen i stedet. Den gjenbrukte 4-tråds gevinsten på 1.21x reflekterer den naive delte parser-stien, ikke grensen per tråd-parser, som ikke ble målt her.
Er lxml fortsatt vedlikeholdt i 2026? Ja. Stabil versjon 6.1.1 kom 2026-05-18, repoet fikk siste push 2026-07-02, og det jobbes med en 7.0.0 alpha. Med rundt 3,000 GitHub-stjerner og et aktivt vedlikeholdt libxml2 under panseret, er dette fortsatt et aktuelt og godt støttet bibliotek — ikke et legacy-verktøy.


