lxml, anmeldt: XPath-motoren, der stadig overgår alle Python-parsere

Sidst opdateret den July 17, 2026
lxml, anmeldt: XPath-motoren, der stadig overgår alle Python-parsere
AI-resumé
Denne anmeldelse af lxml beskriver biblioteket som den langtidsholdbare Python-binding til libxml2 og libxslt med én stor fordel, som nyere parsere stadig sjældent matcher: en rigtig XPath-motor. Artiklen tester XPath-dækning, parser-strenghed, streaming-adfærd, CSS versus XPath og libxml2s dybdegrænser. Den viser lxml som hurtig, memory-effektiv og usædvanligt kapabel til XML- og HTML-opgaver, der kræver akser, predikater, funktioner, streaming eller robust recovery. Samtidig forklares de sikkerhedsorienterede standardgrænser for trædybde, og hvornår huge_tree ændrer dem.

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:

FeltVærdi
Repolxml/lxml
Stjerner3.043
Forks620
Åbne issues16
LicensBSD-3-Clause
Oprettet2011-02-11
Sidste push2026-07-02
Stabil version på PyPI6.1.1 (2026-05-18)
Medfølgende enginelibxml2 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.

lxml XPath coverage moat with axes predicates and functions

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.

KategoriDækningResultat
Akserchild / descendant / parent / ancestor / following-sibling / preceding-sibling / self / attribute / following / preceding10/10 bestået
Predikater[1] / last() / position()<n / attribut-lighed / attribut-eksistens / and / or / indlejret [.//a] / not()9/9 bestået
Funktionertext() / contains() / starts-with() / count() / string-length() / normalize-space() / concat() / substring() / name() / string()10/10 bestået
Returtyperboolean / number-skalare3/3 bestået
Fældermatches() / sequences / if-then-else / except / syntaksfejl5/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.

XPath expresses seven of ten tasks CSS cannot express

MålXPathCSS (cssselect)
Filtrér efter tekstindhold (contains(text(),"bargain"))JaNej, ingen tekst-predicate
Vælg parent ud fra child (//b/parent::p)JaNej, ingen parent-selector
Returnér en attributværdi (//a/@href)JaKun elementer
Returnér en tekstnode (//p/text())JaIngen tekstnoder
Ancestor-akse (//td/ancestor::div)JaIngen navigation opad
Filtrér parent efter antal children (//ul[count(li)=4])JaIngen count-predicate
Filtrér efter tekstlængde (string-length(text())>5)JaIngen længde-predicate
nth-child / last-child / adjacent siblingJaJa (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.

lxml strictness gears: etree, recover, and lxml.html

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 inputlxml.etree (striks)etree + recover=Truelxml.html (tolerant)
Uden lukning <root><a>x</root>kaster fejlgendanneraccepterer
Forkert indlejring <b><i></b></i>kaster fejlgendanneraccepterer
Udefineret entity &nbsp;kaster fejlgendanneraccepterer
Løst & (Tom & Jerry)kaster fejlgendanneraccepterer
Flere roots <a>1</a><b>2</b>kaster fejlgendanneraccepterer
Velformet XMLacceptereraccepterer (0 fejl)accepterer
Boolsk attribut <input disabled>kaster fejlgendanneraccepterer

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.

lxml iterparse streams 300K records with about 1-2 MB RSS

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.

TilstandPeak RSS-deltaNoter
iterparse + clear (fast_iter)~1-2 MBfrigøres undervejs; fladt uanset antal
iterparse uden clear~386 MBholder referencer; lige så tung som fuld indlæsning
etree.parse (fuld indlæsning, anker)~386 MBkendt 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.

FixtureStørrelseLinkslibxml2 gendannede fejlStriks XML
news_bbc.html398 KB2554ok
docs_mdn_array.html243 KB5080fejl
wiki_scraping.html227 KB4600fejl
gov_whitehouse.html289 KB1540fejl
oldstyle_craigslist.html561 KB3510fejl
forum_reddit.html129 KB3180fejl
docs_python.html80 KB3412fejl
ecommerce_books.html51 KB940fejl
news_hackernews.html35 KB2290fejl
ecommerce_webscraper_allinone.html16 KB350fejl
spa_quotes_js.html6 KB50fejl

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.

lxml default depth guard around 253 levels and huge_tree to 2045

Anmodet dybdeStandardparser nårhuge_tree=True når
300253 (dropper resten)299 (gendannet)
1000253 (dropper resten)999 (gendannet)
5000253 (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.

Dimensionlxml-værdiLæ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 msomtrent på niveau med Lexbor ved små størrelser
100k-nodes CSS-throughput3.002.646 noder/shurtigste niveau af de tre C-engines
10 MB RSS-delta128,9 MBlettest af seks parsere, ~1,7x mere memory-effektiv end BeautifulSoup
Kold importstart14,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.

Ke
Ke
CTO hos Thunderbit | Senior Data Scientist og ML-ekspert Med næsten et årtis erfaring inden for machine learning og data science er Ke Shen tidligere studerende fra Columbia University og tidligere Senior Data Scientist hos Walmart Labs. Med dyb, anerkendt ekspertise i Python, R, Java og statistik deler han afprøvede indsigter i, hvordan komplekse AI-algoritmer føres fra teori til produktionsklar arkitektur.
Topics
Web Scraping-værktøjerAI Web Scraper

Prøv Thunderbit

Udtræk leads og andre data med kun 2 klik. Drevet af AI.

Hent Thunderbit Det er gratis
Udtræk data med AI
Overfør nemt data til Google Sheets, Airtable eller Notion
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week