Hver få måneder dukker der en hurtigere HTML-parser op, benchmark-resultaterne går deres runde, og nogen erklærer den gamle garde for forældet. Så skal du pludselig vælge hver eneste paragraf, der indeholder et bestemt ord, eller hente parent til en matchet node, og så husker du, hvorfor lxml stadig er åben i fanen ved siden af.
lxml er en 20 år gammel binding til libxml2. Den er ikke spændende. Den er ikke ny. Og til én helt bestemt opgave — alt, der kræver rigtig XPath — kan intet andet i mainstream Python reelt måle sig. Her får du en praktisk anmeldelse af, hvad den kan, hvor den stille vinder, og de få steder, hvor standardindstillingerne kan bide dig, hvis du ikke ved, at de findes.
lxml på én paragraf: Hvad det faktisk er
lxml er en Python-binding til C-bibliotekerne libxml2 og libxslt. Det er en parser og serializer, ikke en scraper og ikke en browser — den omsætter markup til et træ, du kan forespørge i og redigere, og omsætter træet tilbage til bytes. Den giver dig et ElementTree-kompatibelt API, en fuld XPath 1.0-motor, XSLT 1.0 og skemavalidering, vedligeholdt af Stefan Behnel under sloganet "the most feature-rich and easy-to-use library for processing XML and HTML in the Python language".
Her er status, baseret på et GitHub- og PyPI-snapshot hentet den 2026-07-14:
| Felt | Værdi |
|---|---|
| Repo | lxml/lxml |
| Stjerner | 3.043 |
| Forks | 620 |
| Åbne issues | 16 |
| Licens | BSD-3-Clause |
| Oprettet | 2011-02-11 |
| Sidste push | 2026-07-02 |
| Stabil version på PyPI | 6.1.1 (2026-05-18) |
| Medfølgende engine | libxml2 2.14.6 + libxslt 1.1.43 |
En ting skal lige på plads, før nogen beskylder mig for hype: der er ingen hemmeligheder i denne anmeldelse. lxml er så gammel, at enhver adfærd her er dokumenteret et sted i lxml-dokumentationen, en libxml2-changelog eller en tråd på Launchpad. Jeg fandt ingen eksklusiv, udokumenteret genvej, og jeg opfinder heller ingen. Pointen her er, at det hele er systematiseret, kvantificeret og organiseret omkring lxml som emne — ikke at det er nyheder.
Testopsætningen (og hvorfor timings er lånte)
To kategorier data indgår i denne anmeldelse, og de kommer fra to forskellige steder, så jeg siger det lige åbent.
Kapacitets-testene — XPath-adfærd, de to parser-API'er, namespaces, encoding og node-livscyklus — kørte jeg frisk på én maskine: macOS arm64, Python 3.14.2, lxml 6.1.1, libxml2 2.14.6. Hvert tal i artifacts/raw/*.json er beregnet af et script, ikke tastet ind manuelt. Kapacitets-tests er deterministiske booleans og enums, så én kørsel er stabil — maskinbelastning ændrer ikke, om //a/@href returnerer en attributstreng.
Timing- og memory-tal er ikke fra denne pakke. De er genbrugt ordret fra den tidligere selectolax-benchmarkpakke — samme maskine, samme virtuelle miljø, samme lxml- og libxml2-build, benchmarks pr. 2026-07-13 — og jeg kørte dem ikke igen her. Det er bevidst. Hvis man genkører timing-benchmarks samtidig med en bunke kapacitets-scripts, risikerer man CPU-kontention, som forurener de genbrugte tal, og det ville være dobbeltarbejde: lxml var allerede et fuldt målt kontrolbibliotek i den pakke. Genbrug af de samme benchmarks holder alt på samme grundlag i stedet for at indføre en anden, subtilt forskellig måling. Så når du ser et millisekundtal nedenfor, så læs det som "samme testopsætning, pr. 2026-07-13," ikke "jeg genmålte det i dag."
Resultaterne bærer en tillidsmarkør: single-observation for de deterministiske kapacitets-tests, triple-run for de genbrugte timing-distributioner og hypothesis, hvor jeg foreslår en mekanisme, jeg ikke isolerede.
XPath: Det ene, selectolax og BeautifulSoup bare ikke har
Det her er hovedpointen, så jeg starter her.

Jeg sendte lxmls xpath() gennem en 37-punkts matrix, som var forhåndsregistreret — det forventede resultat for hvert tilfælde blev skrevet ind i koden, før testen kørte, så jeg ikke kunne komme til at bedømme mildt. Ti akser, ni predicate-stilarter, ti indbyggede funktioner, tre skalar-returtyper og fem bevidste fælder med XPath 2.0-syntaks, som lxmls 1.0-motor burde afvise.
| Kategori | Dækning | Resultat |
|---|---|---|
| Akser | child / descendant / parent / ancestor / following-sibling / preceding-sibling / self / attribute / following / preceding | 10/10 bestået |
| Predikater | [1] / last() / position()<n / attribut-lighed / attribut-eksistens / and / or / indlejret [.//a] / not() | 9/9 bestået |
| Funktioner | text() / contains() / starts-with() / count() / string-length() / normalize-space() / concat() / substring() / name() / string() | 10/10 bestået |
| Returtyper | boolean / number-skalare | 3/3 bestået |
| Fælder | matches() / sequences / if-then-else / except / syntaksfejl | 5/5 korrekt afvist |
Scoren er 37/37, og fældekolonnen er den vigtige. matches(), sequence-udtryk, if/then/else og except er alle XPath 2.0-syntaks, og libxml2s 1.0-motor støtter dem ikke delvist — den rejser XPathEvalError og afviser dem i stedet for stille at returnere et forkert nodessæt. Så det her er en perfekt score efter at have forsøgt at bryde den, ikke en perfekt score sammensat af lette opgaver. Hver eneste adfærd her er præcis, som lxmls XPath-dokumentation beskriver, og det er netop pointen.
Jeg indrømmer dog én ting, som harnessen havde galt, fordi det er den version af "37/37", man faktisk kan stole på. Min første forventede mængde for //div[.//a[@href]] forudsagde to hits; kørslen gav ét. Jeg antog i omkring tredive sekunder, at lxml tog fejl, men tjekkede så fixture og opdagede, at det andet element var en <footer>, ikke en <div> — min forventning var forkert, ikke motoren. Jeg rettede den forventede mængde og lod fejlen blive i en kildekommentar. Det er den rigtige rækkefølge at placere skylden i: mistænke sin egen test før det 20 år gamle C-bibliotek.
XPath vs. CSS: Det, du bogstaveligt talt ikke kan udtrykke i CSS
Det abstrakte argument om, at "XPath er mere kraftfuldt", fortjener et konkret tal, så jeg målte hullet. lxml giver dig både .xpath() og .cssselect() (sidstnævnte oversætter CSS til XPath under motorhjelmen). Jeg tog ti selektionsmål og tjekkede, hvilke af dem CSS faktisk kan udtrykke.

| Mål | XPath | CSS (cssselect) |
|---|---|---|
Filtrér efter tekstindhold (contains(text(),"bargain")) | Ja | Nej, ingen tekst-predicate |
Vælg parent ud fra child (//b/parent::p) | Ja | Nej, ingen parent-selector |
Returnér en attributværdi (//a/@href) | Ja | Kun elementer |
Returnér en tekstnode (//p/text()) | Ja | Ingen tekstnoder |
Ancestor-akse (//td/ancestor::div) | Ja | Ingen navigation opad |
Filtrér parent efter antal children (//ul[count(li)=4]) | Ja | Ingen count-predicate |
Filtrér efter tekstlængde (string-length(text())>5) | Ja | Ingen længde-predicate |
nth-child / last-child / adjacent sibling | Ja | Ja (3 grundlæggende) |
Syv ud af ti mål har slet ingen CSS-ækvivalent. Filtrering på tekstindhold, navigation op til parents og ancestors, udtræk af en attributværdi eller en ren tekstnode som resultat, predicates baseret på antal — CSS kan ikke udtrykke noget af det. Kun tre (nth-child, last-child, adjacent sibling) virker i begge. Det er det kvantificerede svar på, "hvad får jeg egentlig ved at vælge lxml?" selectolax er CSS-only og har slet ingen xpath()-metode, så de syv querytyper bliver enten til flertrins Python-loops der, eller sker slet ikke. Hvis din scraping-logik bygger på en af dem, er beslutningen truffet for dig.
(Og ja, harnessen fangede mig en anden gang her: jeg forventede et tomt sæt for string-length(text())>5, men to strenge på seks tegn matchede. Rettet forventningen, ikke værktøjet.)
Tre niveauer af strenghed: etree vs. recover vs. lxml.html
XPath er grunden til at vælge lxml. De tre niveauer af strenghed er grunden til at blive ved med at bruge det.

De fleste parsere giver dig én adfærd for ødelagt input. lxml giver dig tre, og de er så forudsigelige, at jeg sendte seks typer fejlbehæftet markup gennem hver og forhåndsregistrerede, hvordan hver sti skulle opføre sig.
| Fejlbehæftet input | lxml.etree (striks) | etree + recover=True | lxml.html (tolerant) |
|---|---|---|---|
Uden lukning <root><a>x</root> | kaster fejl | gendanner | accepterer |
Forkert indlejring <b><i></b></i> | kaster fejl | gendanner | accepterer |
Udefineret entity | kaster fejl | gendanner | accepterer |
Løst & (Tom & Jerry) | kaster fejl | gendanner | accepterer |
Flere roots <a>1</a><b>2</b> | kaster fejl | gendanner | accepterer |
| Velformet XML | accepterer | accepterer (0 fejl) | accepterer |
Boolsk attribut <input disabled> | kaster fejl | gendanner | accepterer |
Syv ud af syv matchede den forhåndsregistrerede forventning. lxml.etree kaster XMLSyntaxError på alle seks fejlklasser. Tilføj recover=True til den samme parser, og den sluger fejlene og rekonstruerer et brugbart træ — og det undervurderede er, at parser.error_log derefter opremser alle fejl, den slugte. lxml.html accepterer alt uden at klage.
Klassifikatoren, der afgør "kastede fejl vs. gendannede vs. accepterede", er selv drevet af længden på error_log ved runtime, ikke hardkodet, og derfor bliver et velformet dokument kørt med recover=True korrekt mærket som "accepterer" (tom log) i stedet for "gendanner." Min første version af den klassifikator mærkede alt med recover=True som "gendanner" og fejlklassificerede det rene input; at læse den faktiske error_log rettede det.
Hvad det giver i praksis: streng validering, hvor en ødelagt feed skal fejle højt, brug lxml.etree. Beskidt HTML fra den virkelige verden, som du bare skal igennem, brug lxml.html. Og mellemtilfældet, som de fleste værktøjer ikke kan håndtere — "vær tolerant, men fortæl mig præcis, hvad der var galt, så jeg kan logge det" — brug recover=True og læs fejl-loggen. selectolax har den tolerante gearstang og intet andet, ingen strict mode og ingen fejl-log.
iterparse: Streaming-gearet selectolax slet ikke har
Her er en kapacitetslinje, ikke en hastighedsknap. selectolax kan kun indlæse en hel streng — der er ingen inkrementel grænseflade. lxmls iterparse leverer elementer, når de lukkes, og sammen med det klassiske fast_iter-mønster (kald elem.clear() og slet forudgående søskende, mens du går frem) holder det memory fladt, uanset hvor stort dokumentet er.

Jeg målte memory-egenskaben direkte — peak RSS via ru_maxrss, hvert emne i sin egen friske proces, på 300.000 <record>-elementer på omkring 15 MB i alt.
| Tilstand | Peak RSS-delta | Noter |
|---|---|---|
iterparse + clear (fast_iter) | ~1-2 MB | frigøres undervejs; fladt uanset antal |
iterparse uden clear | ~386 MB | holder referencer; lige så tung som fuld indlæsning |
etree.parse (fuld indlæsning, anker) | ~386 MB | kendt tung; viser at måleren fanger størrelsesordenen |
Den afgrænsede tilstand holder peak RSS-delta nede på cirka 1-2 MB mod en fuld indlæsning på ~386 MB — en størrelsesforskel på 0,3-0,4 % — og den første record-hændelse udløses, før filen er færdig med at blive læst, så det er reelt inkrementelt og ikke falsk streaming. Den lærerige linje er den midterste. Kør den samme iterparse-løkke, men spring clear() over, og memory stiger lige tilbage til ~386 MB, fordi du holder referencer til alting. Gevinsten ligger i clear(), ikke i iterparse alene. Ankeret med fuld indlæsning langt højere end den afgrænsede tilstand bekræfter også, at RSS-måleren faktisk kan se størrelsesforskellen og ikke bare måler i blinde. (Denne memory-test kørte jeg i denne pakke — det er en footprint-måling, adskilt fra de lånte timing-tal.)
Den virkelige version af det her: et XML-eksport på flere gigabytes, som ikke kan være i RAM, har ingen selectolax-vej overhovedet. Det er lxmls streaming-parser eller et andet sprog.
Namespaces: RSS, SVG og fælden med default namespace
Tolv namespace-tilfælde, der dækker RSS på tværs af tre namespaces, SVG med et default namespace plus xlink og XML med default namespace. Alle tolv bestod.
lxml trækker //dc:creator/text() ud af en RSS-feed som præcis ["Alice", "Bob"], løser //atom:link/@href og //content:encoded på tværs af tre separate namespaces i det samme dokument, håndterer //s:rect og //s:use/@xlink:href i SVGs andet namespace, splitter Clark-notationens {uri}local-navne med QName og kan inspicere via nsmap. Det er den vedligeholdte og dokumenterede adfærd, og det er en hel dimension, som selectolax ikke rører ved, fordi selectolax kun er HTML5 og ikke behandler vilkårlige XML-namespaces.
Der er én dokumenteret fælde, det er værd at gemme i hukommelsen. XPath har ikke noget begreb om et default namespace. Peg //book på et dokument, der erklærer xmlns="urn:...", og du får nul hits — den tomme prefix er udefineret for XPath, som lxml-dokumentationen forklarer. Du er nødt til at binde et kunstigt prefix (//c:book med namespaces={"c": "urn:..."}, som fandt alle tre) eller falde tilbage til //*[local-name()='book'] (også tre). Ikke en bug — det er XPath-specifikationen, implementeret trofast. Den overrasker bare alle præcis én gang.
Rigtige beskidte sider: Korrekthed på 11 faktiske scrapes
Syntetiske tests er rene; nettet er det ikke. Jeg genbrugte elleve rigtige, gemte sider fra selectolax-pakkens fixture-sæt (pr. 2026-07-10, kun læsning) og lod lxml.html køre dem med lxml som emne.
| Fixture | Størrelse | Links | libxml2 gendannede fejl | Striks XML |
|---|---|---|---|---|
| news_bbc.html | 398 KB | 255 | 4 | ok |
| docs_mdn_array.html | 243 KB | 508 | 0 | fejl |
| wiki_scraping.html | 227 KB | 460 | 0 | fejl |
| gov_whitehouse.html | 289 KB | 154 | 0 | fejl |
| oldstyle_craigslist.html | 561 KB | 351 | 0 | fejl |
| forum_reddit.html | 129 KB | 318 | 0 | fejl |
| docs_python.html | 80 KB | 341 | 2 | fejl |
| ecommerce_books.html | 51 KB | 94 | 0 | fejl |
| news_hackernews.html | 35 KB | 229 | 0 | fejl |
| ecommerce_webscraper_allinone.html | 16 KB | 35 | 0 | fejl |
| spa_quotes_js.html | 6 KB | 5 | 0 | fejl |
Alle elleve blev parsed med lxml.html, og link-, overskrifts- og billedtallene matchede de genbrugte lxml-tal fra selectolax-pakken på alle elleve — krydstjek true. Den enighed er det, der fortæller mig, at genbruget faktisk er på samme grundlag og ikke to forskellige målinger med samme etiket.
Et sidefund: den strikske XML-parser fejlede på ti ud af de elleve sider. Rigtige websider er overvældende ofte ikke velformet XML, og det er netop derfor, libxml2s HTML-recovery-mode findes: for at kunne sluge dem. Den ene undtagelse var BBC News, renderet med Next.js og velformet nok til at overleve striks XML-parsing. Ikke alt, der kaldes "HTML", behøver recovery-stien.
En tælle-note, man let kan snuble over. På docs_python.html talte //a[@href] (attribut-eksistens) 343, mens selectolax-pakkens if n.get("href") (sand værdi) talte 341. De to ekstra er tomme href=""-links. Det er en forskel i tællekonvention — attribut findes versus attribut er ikke-tom — ikke en forskel i lxml-adfærd, og tallene falder på plads, når du aligner predikatet. Godt at vide, når du scraper: om tomme hrefs tæller med, er dit filtervalg, ikke parserens.
Dybdegrænsen, der ligner en bug (men ikke er det)
Selectolax-pakken havde registreret, at lxml droppede det dybeste indhold i 1.000- og 5.000-niveaus indlejret <div>-markup, og beskrev det som "lxml mister stille og roligt det dybeste indhold." Jeg ville have mekanismen, så jeg kørte standardparseren mod huge_tree=True.

| Anmodet dybde | Standardparser når | huge_tree=True når |
|---|---|---|
| 300 | 253 (dropper resten) | 299 (gendannet) |
| 1000 | 253 (dropper resten) | 999 (gendannet) |
| 5000 | 253 (dropper resten) | 2045 (dropper stadig) |
Standardparseren truncerer ved cirka 253 niveauer og dropper alt dybere. Det er ikke en bug — det er libxml2s DoS-forsvar, en nesting-grænse på cirka 256 niveauer, som forhindrer et fjendtligt dokument i at sprænge stacken, og det er dokumenteret i lxmls Launchpad-tråd om XML_PARSE_HUGE. Sæt huge_tree=True, og dybderne 300 og 1.000 kommer helt tilbage. Dybde 5.000 når dog stadig kun 2.045, selv med huge_tree slået til — der er en anden, hårdere libxml2-rekursionsgrænse over den konfigurerbare, og huge_tree fjerner den ikke.
Så handlingen er konkret: når du parser dyb markup fra en kilde, du stoler på, så brug lxml.html.HTMLParser(huge_tree=True). Det denne pakke tilføjer oven på den genbrugte observation er mekanismen (en sikkerhedskappe, ikke datakorruption), løsningen (huge_tree) og det faktum, at der findes en anden grænse, som løsningen ikke når.
Read/Write DOM, serialisering og encoding
lxml er et fuldt read/write-træ, ikke en read-only extractor, og jeg verificerede redigeringsfladen punkt for punkt. Alle otte DOM-operationer bestod: SubElement, insert, remove, replace, strip_tags (fjern tags, behold deres tekst), strip_elements (fjern tags og deres tekst), drop_tree (en lxml.html-eksklusiv) samt tekst/tail-modellen med to felter, som ofte fanger nye brugere — i <p>head<b>bold</b>tail</p> er p.text "head", b.text "bold", og b.tail "tail".
Serialisering bestod fem ud af fem: tostring i XML- og HTML-tilstand (HTML lader korrekt void elements stå uden self-closing), pretty_print, C14N-kanonisering (method="c14n", endnu en lxml-eksklusiv) og en ren round-trip.
Encoding er der, hvor lxml stille skiller sig ud. Giv den ikke-UTF-8 bytes — "<p>café éè</p>".encode("latin-1") gennem lxml.html.fromstring — og den genskaber café éè intakt, uden U+FFFD-erstatningstegn og uden tabte bytes. Det reproducerer direkte dens rolle som "clean reference" i selectolax-pakken, hvor samme input blev korrupt under de to andre motorer (Lexbor gav erstatningstegn, Modest droppede bytes helt). lxmls libxml2-baserede charset-detektion er ganske enkelt mere stabil her.
Den anden side er strengheden i, hvordan du angiver en encoding. encoding="latin-1" i en XML-deklaration kaster XMLSyntaxError: Unsupported encoding: latin-1, mens den IANA-kanoniske encoding="ISO-8859-1" parser fint og returnerer café. libxml2 accepterer kun kanoniske encodings-navne, ikke aliaser — en detalje dokumenteret tilbage i launchpad #613302. Irriterende, hvis du ikke kender det, trivielt, når du gør.
Til sidst node-livscyklus. Jeg kørte tre scenarier med stale handles i isolerede subprocesser (et hårdt crash ville vise sig som en ikke-nul exit): holde fast i en node efter at dens træ er blevet garbage-collected, læse et handle efter drop_tree() og bruge en node efter remove(). Ingen segfaults i nogen af dem — lxml holder en nodes reference til træet i live for at forhindre use-after-free. Samme pæne karakter fik selectolax i denne test.
Hastighed og memory (lånt, og ærligt om det)
Alt i dette afsnit er genbrugt fra selectolax-pakken, pr. 2026-07-13. Denne pakke producerede nul timing-tal selv, og jeg siger det hellere to gange, end at du tror, jeg har genmålt noget.
| Dimension | lxml-værdi | Læsning |
|---|---|---|
| Ren parse p50 (10 MB) | 77,9 ms | ~33-34% hurtigere end selectolax-Lexbor |
| Fuld parse + extract p50 (1 MB / 10 MB) | 14,18 ms / 172,9 ms | omtrent på niveau med Lexbor ved små størrelser |
| 100k-nodes CSS-throughput | 3.002.646 noder/s | hurtigste niveau af de tre C-engines |
| 10 MB RSS-delta | 128,9 MB | lettest af seks parsere, ~1,7x mere memory-effektiv end BeautifulSoup |
| Kold importstart | 14,1 ms | ~2,3x hurtigere end parsel-lignende imports |
Tal for ren parse og throughput er stærke, og lxml er den mest memory-besparende af de seks målte parsere. Threading-billedet kræver dog en note. Genbrugte data viser en 4-tråds wall-clock speedup på kun 1,21x, markeret som inconclusive — men det er den delte-default-parser-sti. lxml FAQ er eksplicit om, at GIL frigives under parsing kun når hver tråd bruger sin egen parser (eller en kopieret default); en delt parser serialiserer adgang. Jeg verificerede strukturelt API-fladen for at gøre det rigtigt (XMLParser.copy() findes, get/set_default_parser findes, XPathEvaluator har en intern lås), men jeg målte ikke speedup pr. tråd-parser — det ville være en ny timingmåling, og den producerer denne pakke ikke. Så læs "1,21x" som "under den naive delte sti," ikke som lxmls threading-loft.
Og en stjerne ved det hele: det er tal på én platform, macOS arm64. Påstanden om, at lxmls rene parse slår Lexbor, går imod den sædvanlige konsensus om, at Lexbor-baserede parseren er hurtigst, så den vil virkelig gerne have et Linux x86_64-tjek, før nogen behandler det som afgjort.
Licens: Den kedelige sejr
lxml udgives under BSD-3-Clause, og de C-biblioteker, det pakker med — libxml2 og libxslt — er begge MIT. Det er en helt permissiv kæde uden copyleft nogen steder, og det betyder noget, så snart du redistribuerer. Til sammenligning pakker selectolax-hjulet LGPL-2.1 Modest og Apache-2.0 Lexbor, så lxml er den renere historie, hvis du skal shippe i et lukket produkt.
Der er også en praktisk installationsfordel: lxml udgiver præbyggede wheels, som statisk linker libxml2 og libxslt, så pip install lxml kræver normalt hverken system-libxml2 eller compiler på maskinen — en anden oplevelse end at bygge fra source.
Hvor lxml passer ind — og hvor et AI-udtræk lag tager over
Det er tid til at være tydelig om grænsen, for det er en let kategorifejl at lave. lxml er et parse-bibliotek. Det giver dig et træ og en fremragende query-motor, og alt omkring det træ er stadig dit ansvar: at hente siden, rendere JavaScript, komme forbi anti-bot-forsvar, skrive og vedligeholde XPath og strukturere resultatet. Det er et andet lag end en hosted extraction-service, og de to er ikke så meget rivaler som naboer.
For en udvikler, der helst ikke vil eje fetch-render-select-maintain-stakken, er det øverste lag netop dér, noget som Thunderbit ligger — og for det her publikum er det API, MCP-serveren og CLI'en, ikke browserudvidelsen. Thunderbit Open API eksponerer POST /distill til at gøre en side til ren Markdown og POST /extract til at hente strukturerede data mod et JSON Schema, med en renderMode-switch og batchjobs til større volumen. Den samme motor findes som MCP-server (thunderbit_suggest_fields, thunderbit_distill, thunderbit_extract) til agenter og kodningsassistenter og som CLI, du kan køre direkte fra terminalen via npx @thunderbit/thunderbit-cli. Den håndterer JS-rendering, anti-bot og CAPTCHA'er out of the box og returnerer schema-matchet JSON — hvilket er laget oven på parsing, ikke en erstatning for det.
Prøv Thunderbit til webdata-udtræk
Rammesætningen er et simpelt valg. Vælg lxml, når du selv ejer pipelinen og vil have kirurgisk XPath-kontrol over et træ, du forstår. Vælg et AI-baseret extraction-API, når du helst vil slippe for at vedligeholde selectors og rendering overhovedet. Mange rigtige systemer bruger begge dele — lxml til de strukturerede feeds, de selv kontrollerer, og en extraction-service til de rodede long-tail-sider, de ikke gør.
Hvad denne anmeldelse ikke testede
Det her er en foreløbig anmeldelse, ikke et endeligt scoreboard, så her er det, den ikke dækker.
Alle timing- og memory-tal er genbrugte, på én platform (macOS arm64, Python 3.14) og arver den pakkes forbehold — resultatet om, at "lxml er hurtigere ved ren parsing," går imod konsensus og bør tjekkes igen på Linux x86_64. Speedup for per-thread-parser er ikke testet (det ville kræve nye timingmålinger). Jeg målte iterparse-memory på 300k records, men ikke GB-skala rigtig XML, ikke iterparse på HTML versus XML og ikke en multi-timers soak. lxmls XSLT 1.0, RelaxNG / XMLSchema / DTD-validering og EXSLT-udvidelser er slet ikke testet her — en stor kapabilitetsflade, men uden for parse-og-selektion-kernen. Jeg observerede den anden dybdegrænse på 2.045, men fandt ikke libxml2s præcise rekursionskonstant. Kun stabile 6.1.1 blev testet, ikke 7.0.0 alpha. Windows, source builds og den free-threaded 3.14t-build er alle utestede. Og inden for XPath dækkede jeg indbyggede funktioner, men ikke XPath-variabler, custom Python extension functions eller genbrug af forudkompilerede etree.XPath-objekter.
Konklusion
lxml er ikke det nye, hurtige værktøj, og det er netop derfor, det skal anbefales. Det er en to årtier gammel libxml2-binding med en fuld XPath 1.0-motor, som ingen mainstream Python-alternativer matcher, tre forudsigelige niveauer af parse-strenghed med en fejl-log i midten, en rigtig streaming-parser til dokumenter, der ikke kan være i hukommelsen, korrekt håndtering af flere namespaces og encodings samt en helt permissiv licens. De få skarpe kanter — cirka 253-niveauers dybdegrænse og tallet for shared-parser-threading — er dokumenterede, konfigurerbare og nu forklarede.
Hvis du selv ejer din scraping-pipeline og læner dig op ad XPath, er lxml stadig parseren, du vælger. Hvis du hellere vil slippe for at vedligeholde selectors og rendering, er det netop det, et AI extraction-lag som Thunderbit API, MCP og CLI er til — en ren arbejdsdeling, ikke en konkurrence. Under alle omstændigheder bør du behandle tallene her som foreløbige og tjekke timing på din egen platform, før du citerer dem i et design-dokument.
Prøv Thunderbit til webdata-udtræk Get Started Free
Ofte stillede spørgsmål
Er lxml en web scraper?
Nej. lxml er en parser og serializer — en Python-binding til libxml2/libxslt, der omsætter markup til et redigerbart, forespørgselsbart træ. Den henter ikke sider, renderer ikke JavaScript og håndterer ikke anti-bot-forsvar; du leverer request-laget (via requests, httpx, en headless browser eller en scraping-service) og giver bytes videre til lxml.
Hvornår skal jeg bruge lxml i stedet for BeautifulSoup eller selectolax? Vælg lxml, når du har brug for XPath. BeautifulSoup kan faktisk bruge lxml som backend-parser, men tilbyder ingen indbygget XPath, og selectolax er CSS-only og hurtigere i sin smalle niche. Hvis din selektionslogik kræver tekstindholdsfiltrering, navigation til parent eller ancestor, udtræk af attributter/tekstnoder eller count-predicates, er lxmls XPath-motor den eneste mainstream Python-mulighed, der udtrykker det direkte.
Hvorfor dropper lxml stille dybt indlejret indhold?
Standardparseren capper nesting ved cirka 253 niveauer — libxml2s DoS-forsvar mod fjendtlige dokumenter, ikke en bug. Sæt huge_tree=True (for eksempel lxml.html.HTMLParser(huge_tree=True)), så genskaber den dybder på 300 og 1.000 fuldt ud. Bemærk en anden, hårdere rekursionsgrænse omkring 2.045 niveauer, som huge_tree ikke fjerner.
Frigiver lxml GIL ved multitrådet parsing? Kun under de rigtige betingelser. lxml FAQ siger, at GIL frigives under parsing, når hver tråd bruger sin egen parser eller en kopieret default-parser; en delt parser serialiserer i stedet adgang. Den genbrugte 4-tråds speedup på 1,21x afspejler den naive delte-parser-sti, ikke loftet med per-thread-parser, som ikke blev målt her.
Er lxml stadig vedligeholdt i 2026? Ja. Den stabile udgivelse 6.1.1 kom den 2026-05-18, repoet blev sidst pushet den 2026-07-02, og der er en 7.0.0 alpha under udvikling. Med omkring 3.000 GitHub-stjerner og en aktivt vedligeholdt libxml2 nedenunder er det stadig et aktuelt og velfungerende bibliotek, ikke et legacy-stykke software.


