lxml beoordeeld: de XPath-engine die nog steeds elke Python-parser overklast

Laatst bijgewerkt op July 17, 2026
lxml beoordeeld: de XPath-engine die nog steeds elke Python-parser overklast
AI Samenvatting
Deze review van lxml positioneert de bibliotheek als de al jarenlang gebruikte Python-binding rond libxml2 en libxslt, met één groot voordeel dat nieuwere parsers zelden evenaren: een echte XPath-engine. Het artikel test XPath-dekking, parser-striktheidsmodi, streaming-geheugen, CSS-versus-XPath-uitdrukking en de dieptelimieten van libxml2. Het laat zien dat lxml snel, geheugenefficiënt en bijzonder capabel is voor XML- en HTML-workloads die axes, predicates, functies, streaming of robuuste recover-modi nodig hebben. De review legt ook de beveiligingsgerichte standaard voor boomdiepte uit en wanneer huge_tree die grens verlegt.

Elke paar maanden duikt er weer een snellere HTML-parser op, staan de benchmarks overal en roept iemand dat de oude garde achterhaald is. Tot je vervolgens elk alinea-element wilt selecteren dat een bepaald woord bevat, of de parent van een gematchte node wilt ophalen, en je weer beseft waarom lxml nog altijd openstaat in je andere tabblad.

lxml is een 20 jaar oude binding voor libxml2. Spannend is het niet. Nieuw ook niet. Maar voor één heel specifiek doel — alles wat echte XPath nodig heeft — kan niets anders in mainstream Python hier serieus tegenop. Dit is een praktische review van wat lxml doet, waar het stilletjes wint, en de paar plekken waar de standaardinstellingen je kunnen verrassen als je niet weet dat ze er zijn.

De testopstelling (en waarom de timingcijfers geleend zijn)

Voor deze review zijn twee soorten data gebruikt, en die komen uit twee verschillende bronnen. Dat zeg ik er meteen graag duidelijk bij.

De capability-tests — XPath-gedrag, de twee parser-API's, namespaces, encoding, node-lifecycle — heb ik opnieuw gedraaid op één machine: macOS arm64, Python 3.14.2, lxml 6.1.1, libxml2 2.14.6. Elk getal in die artifacts/raw/*.json-bestanden is door een script berekend, niet handmatig ingevuld. Capability-tests zijn deterministische booleans en enums, dus één run is stabiel — machinebelasting verandert niet of //a/@href een attribuutstring teruggeeft.

De timing- en geheugencijfers zijn niet van deze pack. Die zijn letterlijk overgenomen uit de eerdere selectolax-benchmarkpack — dezelfde machine, dezelfde virtuele omgeving, dezelfde lxml- en libxml2-build, benchmarks van 2026-07-13 — en ik heb ze hier niet opnieuw gedraaid. Dat is bewust. Timingbenchmarks tegelijk draaien met een batch capability-scripts introduceert CPU-contention die de hergebruikte cijfers zou vervuilen, en het zou dubbel werk zijn: lxml was in die pack al een volledig gemeten controlebibliotheek. Door dezelfde benchmarks te hergebruiken blijft alles echt appels-met-appels in plaats van een tweede, subtiel andere meting te introduceren. Dus wanneer je hieronder een milliseconde-cijfer ziet, lees het dan als "zelfde testopstelling, van 2026-07-13", niet als "ik heb dit vandaag opnieuw getimed".

Resultaten krijgen een vertrouwenslabel: single-observation voor de deterministische capability-tests, triple-run voor de hergebruikte timingverdelingen, en hypothesis wanneer ik een mechanisme voorstel dat ik niet afzonderlijk heb geïsoleerd.

lxml in één alinea: wat het echt is

lxml is een Python-binding voor de C-bibliotheken libxml2 en libxslt. Het is een parser en serializer, geen scraper en geen browser — het zet markup om in een boomstructuur die je kunt doorzoeken en aanpassen, en zet die boom daarna weer terug naar bytes. Je krijgt een API die compatibel is met ElementTree, een volledige XPath 1.0-engine, XSLT 1.0 en schema-validatie, onderhouden door Stefan Behnel onder de slogan "de meest feature-rijke en gebruiksvriendelijke bibliotheek voor het verwerken van XML en HTML in de Python-taal".

Zo staat het er, op basis van een GitHub- en PyPI-snapshot van 2026-07-14:

VeldWaarde
Repolxml/lxml
Sterren3,043
Forks620
Open issues16
LicentieBSD-3-Clause
Aangemaakt2011-02-11
Laatste push2026-07-02
Stabiele PyPI-versie6.1.1 (2026-05-18)
Meegeleverde enginelibxml2 2.14.6 + libxslt 1.1.43

Eén ding wil ik alvast uit de weg hebben voordat iemand me van hype beschuldigt: deze review bevat geen geheimen. lxml bestaat al lang genoeg dat elk gedrag hier ergens in de lxml-documentatie, een libxml2-changelog of een launchpad-thread terug te vinden is. Ik heb geen exclusieve, ongedocumenteerde truc ontdekt — en ik ga die ook niet verzinnen. De waarde van wat volgt zit in de structuur, de kwantificering en de focus op lxml zelf; niet in het nieuwsgehalte.

XPath: het ene wat selectolax en BeautifulSoup simpelweg niet hebben

Dit is de hoofdreden om lxml te kiezen, dus ik begin hier.

lxml XPath coverage moat with axes predicates and functions

Ik heb lxml's xpath() door een vooraf vastgelegde matrix van 37 items gehaald — de verwachte uitkomst voor elk geval stond al in de broncode voordat de test begon, zodat ik mezelf niet per ongeluk een ruimhartiger score kon geven. Tien axes, negen predicate-stijlen, tien ingebouwde functies, drie scalaire returntypes en vijf opzettelijke valkuilen met XPath 2.0-syntax die lxml's 1.0-engine juist moet afwijzen.

CategorieDekkingResultaat
Axeschild / descendant / parent / ancestor / following-sibling / preceding-sibling / self / attribute / following / preceding10/10 geslaagd
Predicates[1] / last() / position()<n / attribuutequality / attribuut bestaat / and / or / genest [.//a] / not()9/9 geslaagd
Functiestext() / contains() / starts-with() / count() / string-length() / normalize-space() / concat() / substring() / name() / string()10/10 geslaagd
Returntypesboolean / number scalars3/3 geslaagd
Valstrikgevallenmatches() / sequences / if-then-else / except / syntax error5/5 correct afgewezen

De score is 37/37, en juist die valstrik-kolom is het belangrijkste. matches(), sequence-expressies, if/then/else en except zijn allemaal XPath 2.0-syntax, en libxml2's 1.0-engine ondersteunt die niet halfzacht: hij gooit een XPathEvalError en stopt, in plaats van stilzwijgend een verkeerd node-set terug te geven. Dit is dus een perfecte score nadat ik geprobeerd heb het stuk te krijgen, niet een perfecte score die alleen uit makkelijke testcases bestaat. Elk gedrag hier komt precies overeen met wat de lxml XPath-documentatie beschrijft — en dat is precies de bedoeling.

Ik geef één fout van de testharness toe, omdat dit de versie van "37/37" is waar je echt op kunt vertrouwen. Mijn eerste verwachte set voor //div[.//a[@href]] voorspelde twee treffers; de run gaf er één terug. Ik dacht ongeveer dertig seconden dat lxml fout zat, controleerde daarna de fixture en ontdekte dat het tweede element een <footer> was, geen <div> — mijn verwachting was fout, niet de engine. Ik heb de verwachte set aangepast en de vergissing in een broncommentaar laten staan. Dat is de juiste volgorde van schuld: wantrouw eerst je eigen test, pas daarna de 20 jaar oude C-bibliotheek.

XPath vs CSS: wat je letterlijk niet in CSS kunt uitdrukken

De abstracte claim "XPath is krachtiger" verdient een concreet cijfer, dus ik heb het verschil gekwantificeerd. lxml biedt zowel .xpath() als .cssselect() (die CSS onder de motorkap naar XPath vertaalt). Ik nam tien selectiedoelen en keek welke daarvan CSS echt kan uitdrukken.

XPath expresses seven of ten tasks CSS cannot express

DoelXPathCSS (cssselect)
Filter op tekstinhoud (contains(text(),"bargain"))JaGeen text-predicate
Parent selecteren vanuit child (//b/parent::p)JaGeen parent-selector
Een attribuutwaarde teruggeven (//a/@href)JaAlleen elementen
Een text-node teruggeven (//p/text())JaGeen text-nodes
Ancestor-axis (//td/ancestor::div)JaGeen opwaartse navigatie
Parent filteren op aantal children (//ul[count(li)=4])JaGeen count-predicate
Filteren op tekstlengte (string-length(text())>5)JaGeen lengte-predicate
nth-child / last-child / adjacent siblingJaJa (3 basisgevallen)

Zeven van de tien doelen hebben helemaal geen CSS-equivalent. Filteren op tekstinhoud, omhoog navigeren naar parents en ancestors, een attribuutwaarde of losse text-node als resultaat ophalen, predicates op basis van aantallen — CSS kan het niet. Alleen drie (nth-child, last-child, adjacent sibling) werken in beide. Dat is het gekwantificeerde antwoord op de vraag: "wat win ik eigenlijk met lxml?" selectolax is CSS-only en heeft helemaal geen xpath()-methode, dus die zeven querytypes worden daar multi-step Python-loops of simpelweg onuitvoerbaar. Als je scrapinglogica daarop leunt, is de keuze eigenlijk al gemaakt.

(En ja, de harness pakte me hier een tweede keer: ik voorspelde een lege set voor string-length(text())>5, maar twee strings van zes tekens matchten. De verwachting was fout, niet de tool.)

Drie striktheidsstanden: etree vs recover vs lxml.html

XPath is de reden om lxml te kiezen. De drie niveaus van striktheid zijn de reden om het te blijven gebruiken.

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

De meeste parsers geven je één gedrag voor kapotte input. lxml geeft je er drie, en ze zijn voorspelbaar genoeg dat ik zes klassen foutieve markup door elk van de drie heb gehaald en vooraf heb vastgelegd hoe elk pad moest reageren.

Foutieve inputlxml.etree (streng)etree + recover=Truelxml.html (soepel)
Ongeopende tag <root><a>x</root>geeft fouthersteltaccepteert
Verkeerd geneste tags <b><i></b></i>geeft fouthersteltaccepteert
Ongedefinieerde entiteit &nbsp;geeft fouthersteltaccepteert
Losse & (Tom & Jerry)geeft fouthersteltaccepteert
Meerdere roots <a>1</a><b>2</b>geeft fouthersteltaccepteert
Goed gevormde XMLaccepteertaccepteert (0 errors)accepteert
Booleaanse attribuut <input disabled>geeft fouthersteltaccepteert

Zeven van de zeven kwamen overeen met de vooraf vastgelegde verwachting. lxml.etree gooit op alle zes foutieve klassen een XMLSyntaxError. Voeg recover=True toe aan dezelfde parser en hij slikt de fouten in en reconstrueert een bruikbare tree — en dat is het onderschatte deel — parser.error_log somt vervolgens elke fout op die hij heeft geslikt. lxml.html accepteert alles zonder klacht.

De classifier die bepaalt of iets "afgewezen", "hersteld" of "geaccepteerd" is, wordt zelf gestuurd door de runtime-lengte van error_log, niet hardcoded. Daardoor wordt een goed gevormd document dat onder recover=True draait correct gelabeld als "accepteert" (lege log) in plaats van "herstelt". Mijn eerste versie van die classifier labelde elk recover=True-resultaat als "herstelt" en plakte daardoor een verkeerd label op schone input; door de echte error_log te lezen was dat opgelost.

Wat dit in de praktijk oplevert: voor strikte validatie waarbij een kapotte feed hard moet falen, gebruik lxml.etree. Voor rommelige echte HTML die je gewoon wilt kunnen verwerken, gebruik lxml.html. En voor het middengebied dat de meeste tools niet aankunnen — "wees soepel, maar vertel me precies wat kapot was zodat ik het kan loggen" — gebruik recover=True en lees de error log. selectolax heeft alleen de soepele stand en verder niets: geen strict mode en geen error log.

iterparse: de streamingstand die selectolax helemaal niet heeft

Dit is een capaciteitskenmerk, geen snelheidsknop. selectolax slurpt alleen een complete string in — er is geen incrementele interface. lxml's iterparse geeft elementen terug zodra ze sluiten, en in combinatie met het klassieke fast_iter-patroon (roep elem.clear() aan en verwijder oude siblings terwijl je verdergaat) blijft het geheugengebruik vlak, hoe groot het document ook is.

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

Ik heb het geheugengedrag direct gemeten — piek-RSS via ru_maxrss, elk onderwerp in een eigen schoon proces, op 300.000 <record>-elementen met samen ongeveer 15 MB.

ModusPiek-RSS deltaOpmerking
iterparse + clear (fast_iter)~1-2 MBwordt onderweg vrijgegeven; vlak ongeacht aantallen
iterparse zonder clear~386 MBhoudt referenties vast; net zo zwaar als volledige load
etree.parse (volledige load, referentie)~386 MBbekend zwaar; bewijst dat de meter de orde van grootte ziet

De begrensde modus houdt de piek-RSS-delta rond de 1-2 MB tegenover ~386 MB bij volledige load — een verschil in orde van grootte van 0,3-0,4% — en het eerste record-event vuurt al af voordat het bestand volledig is uitgelezen, dus dit is echt incrementeel en geen nep-streaming. De leerzame regel is de middelste. Draai exact dezelfde iterparse-lus maar sla clear() over, en het geheugen klimt direct terug naar ~386 MB, omdat je referenties naar alles blijft vasthouden. De winst zit dus in clear(), niet in iterparse alleen. Dat de volledige load veel hoger uitkomt dan de begrensde modus bevestigt bovendien dat de RSS-meter het verschil in orde van grootte echt kan zien in plaats van blind te meten. (Deze geheugentest heb ik in deze pack zelf uitgevoerd — het is een footprintmeting, los van de geleende timingcijfers.)

De praktische conclusie: een XML-export van meerdere gigabytes die niet in RAM past, heeft bij selectolax simpelweg geen route. Dan heb je lxml's streaming-parser nodig, of een andere taal.

Namespaces: RSS, SVG en de val van de default namespace

Twaalf namespace-cases, waaronder RSS over drie namespaces, SVG met een default namespace plus xlink, en XML met een default namespace. Alle twaalf slaagden.

lxml haalt //dc:creator/text() uit een RSS-feed precies als ["Alice", "Bob"], lost //atom:link/@href en //content:encoded op over drie verschillende namespaces in hetzelfde document, verwerkt //s:rect en //s:use/@xlink:href in SVG's tweede namespace, splitst Clark-notatie {uri}local-namen met QName en kan via nsmap introspecteren. Dit is het gedocumenteerde, onderhouden gedrag, en het is een hele dimensie waar selectolax niet aan komt, omdat selectolax HTML5-only is en geen willekeurige XML-namespaces verwerkt.

Er is één gedocumenteerde valkuil die je echt moet onthouden. XPath kent het concept default namespace niet. Wijst //book naar een document met xmlns="urn:...", dan krijg je nul treffers — het lege prefix bestaat niet voor XPath, zoals de lxml-documentatie uitlegt. Je moet een kunstmatig prefix binden (//c:book met namespaces={"c": "urn:..."}, waarmee alle drie de treffers werden gevonden) of terugvallen op //*[local-name()='book'] (ook drie). Geen bug — dit is gewoon de XPath-specificatie, correct geïmplementeerd. Maar het verrast iedereen precies één keer.

Echte rommelige pagina's: nauwkeurigheid op 11 echte scrapes

Synthetische tests zijn schoon; het web is dat niet. Ik heb elf echte vastgelegde pagina's hergebruikt uit de fixture-set van de selectolax-pack (van 2026-07-10, read-only) en daar lxml.html op losgelaten, met lxml als hoofdonderwerp.

FixtureGrootteLinkslibxml2 herstelde foutenStrikte XML
news_bbc.html398 KB2554ok
docs_mdn_array.html243 KB5080gaf fout
wiki_scraping.html227 KB4600gaf fout
gov_whitehouse.html289 KB1540gaf fout
oldstyle_craigslist.html561 KB3510gaf fout
forum_reddit.html129 KB3180gaf fout
docs_python.html80 KB3412gaf fout
ecommerce_books.html51 KB940gaf fout
news_hackernews.html35 KB2290gaf fout
ecommerce_webscraper_allinone.html16 KB350gaf fout
spa_quotes_js.html6 KB50gaf fout

Alle elf werden geparseerd met lxml.html, en de aantallen links, koppen en afbeeldingen kwamen op alle elf overeen met de hergebruikte lxml-cijfers uit de selectolax-pack — cross-check true. Die overeenkomst zegt me dat het hergebruik echt appels-met-appels is en niet twee verschillende metingen met hetzelfde label.

De nevenbevinding: de strikte XML-parser gaf op tien van de elf pagina's een fout. Echte webpagina's zijn overwegend géén goed gevormde XML, en precies daarom bestaat de HTML-recover-modus van libxml2 om ze te kunnen verwerken. De enige uitzondering was BBC News, gerenderd door Next.js en goed gevormd genoeg om strikte XML-parsing te overleven. Niet alles wat "HTML" heet, heeft de recover-route nodig.

Eén telverschil dat makkelijk misgaat. Op docs_python.html telde //a[@href] (attribuut bestaat) 343, terwijl de selectolax-pack met if n.get("href") (truthy waarde) op 341 uitkwam. De twee extra's zijn lege href=""-links. Dat is een verschil in telconventie — attribuut bestaat versus attribuut is niet-leeg — en geen verschil in lxml-gedrag; de cijfers vallen weer samen zodra je de predicate gelijk trekt. Goed om te weten bij scraping: of lege href's meetellen, is jouw filterkeuze, niet die van de parser.

De dieptelimiet die op een bug lijkt (maar het niet is)

In de selectolax-pack stond dat lxml de diepste content verloor bij 1.000 en 5.000 niveaus geneste <div>-markup, met de formulering dat "lxml stilletjes de diepste content verliest". Ik wilde het mechanisme begrijpen, dus ik draaide de standaardparser tegen huge_tree=True.

lxml default depth guard around 253 levels and huge_tree to 2045

Gevraagde diepteStandaardparser haalthuge_tree=True haalt
300253 (rest valt weg)299 (hersteld)
1000253 (rest valt weg)999 (hersteld)
5000253 (rest valt weg)2045 (valt nog steeds weg)

De standaardparser kapt af rond 253 niveaus en laat alles dieper stilletjes verdwijnen. Dat is geen bug — het is libxml2's DoS-bescherming, een nesting-cap rond ongeveer 256 niveaus die voorkomt dat een kwaadaardig document de stack opblaast, en dat staat gedocumenteerd in de lxml-launchpadthread over XML_PARSE_HUGE. Zet huge_tree=True en dieptes van 300 en 1.000 komen volledig terug. Diepte 5.000 haalt met huge_tree echter nog steeds maar 2.045, dus er zit boven die instelbare limiet nog een tweede, hardere libxml2-recursiegrens — en huge_tree heft die niet op.

De actie is dus concreet: wanneer je diepe markup verwerkt uit een bron die je vertrouwt, gebruik lxml.html.HTMLParser(huge_tree=True). Wat deze pack daarbovenop toevoegt is het mechanisme (een veiligheidslimiet, geen datacorruptie), de oplossing (huge_tree) en het feit dat er nog een tweede plafond is waar die oplossing niet aankomt.

DOM lezen en schrijven, serialisatie en encoding

lxml is een volledige read/write-boom, geen read-only extractor, en ik heb het bewerkingsoppervlak case voor case gecontroleerd. Alle acht DOM-operaties slaagden: SubElement, insert, remove, replace, strip_tags (tags verwijderen, tekst laten staan), strip_elements (tags én tekst verwijderen), drop_tree (een lxml.html-specifieke functie) en het twee-slotmodel voor tekst en tail dat beginners vaak op het verkeerde been zet — in <p>head<b>bold</b>tail</p> is p.text "head", b.text "bold" en b.tail "tail".

De serialisatie slaagde vijf van de vijf: tostring in XML- en HTML-modus (HTML laat lege elementen terecht niet zelfsluitend achter), pretty_print, C14N-canonicalisatie (method="c14n", nog een lxml-specifieke functie) en een nette round-trip.

Encoding is het punt waar lxml zich stilletjes onderscheidt. Geef het niet-UTF-8 bytes — "<p>café éè</p>".encode("latin-1") via lxml.html.fromstring — en het haalt café éè er intact uit, zonder U+FFFD-vervangingskarakters en zonder weggegooide bytes. Daarmee reproduceert het direct zijn rol als de "schone referentie" in de selectolax-pack, waar exact dezelfde input door de andere twee engines stilletjes werd beschadigd (Lexbor produceerde vervangingskarakters, Modest gooide bytes volledig weg). lxml's op libxml2 gebaseerde charset-detectie is hier gewoon stabieler.

De keerzijde is strengheid in hoe je een encoding declareert. encoding="latin-1" in een XML-declaratie geeft XMLSyntaxError: Unsupported encoding: latin-1, terwijl de IANA-canonieke encoding="ISO-8859-1" prima parseert en café teruggeeft. libxml2 accepteert alleen canonieke encodernamen, geen aliassen — een detail dat al is gedocumenteerd in launchpad #613302. Irritant als je het niet weet, triviaal zodra je het wel weet.

Tot slot node-lifecycle. Ik heb drie stale-handle-scenario's in geïsoleerde subprocessen getest (een harde crash zou zich tonen als een niet-nul exit): een node vasthouden nadat de tree door garbage collection is opgeruimd, een handle lezen na drop_tree(), en een node gebruiken na remove(). In geen van de gevallen segfaults — lxml houdt de referentie van een node naar zijn tree in stand om use-after-free te voorkomen. Ook hier kreeg lxml dezelfde schone uitslag als selectolax.

snelheid en geheugen (geleend, maar eerlijk daarover)

Alles in deze sectie is hergebruikt uit de selectolax-pack, van 2026-07-13. Deze pack levert zelf nul timingcijfers, en ik zeg dat liever twee keer dan dat je denkt dat ik iets opnieuw heb getimed.

Dimensielxml-waardeBetekenis
Pure parse p50 (10 MB)77.9 ms~33-34% sneller dan selectolax-Lexbor
Full parse + extract p50 (1 MB / 10 MB)14.18 ms / 172.9 msop kleine maten ongeveer gelijk aan Lexbor
100k-node CSS-throughput3,002,646 nodes/ssnelste klasse van de drie C-engines
10 MB RSS-delta128.9 MBzuinigste van zes parsers, ~1,7x zuiniger dan BeautifulSoup
Koude importstart14.1 ms~2,3x sneller dan parsel-achtige imports

De pure-parse- en throughputcijfers zijn sterk, en lxml is het zuinigst met geheugen van de zes gemeten parsers. Wel is er een kanttekening bij threading. De hergebruikte data laat een 4-thread wall-clock speedup zien van slechts 1.21x, gemarkeerd als inconclusief — maar dat is het pad met een gedeelde default parser. De lxml FAQ zegt expliciet dat de GIL alleen tijdens het parsen wordt vrijgegeven als elke thread zijn eigen parser gebruikt (of een gekopieerde default); een gedeelde parser serialiseert de toegang. Ik heb de API-oppervlakte om het goed te doen wel structureel gecontroleerd (XMLParser.copy() bestaat, get/set_default_parser bestaan, XPathEvaluator draagt een interne lock), maar ik heb de speedup per thread-parser niet gemeten — dat zou een nieuwe timingmeting zijn, en die doet deze pack niet. Lees "1.21x" dus als "onder het naïeve gedeelde pad", niet als de threading-grens van lxml.

En één asterisk bij alles: dit zijn single-platformcijfers, macOS arm64. De claim dat lxml's pure parse Lexbor verslaat botst met de gebruikelijke consensus dat de Lexbor-gebaseerde parser het snelst is, dus die wil echt een nieuwe check op Linux x86_64 voordat iemand het als definitief behandelt.

licentie: de saaie overwinning

lxml wordt geleverd onder BSD-3-Clause, en de C-bibliotheken die het meelevert — libxml2 en libxslt — zijn beide MIT. Dat is een volledig permissieve keten zonder copyleft, en dat telt zodra je gaat redistribueren. Ter vergelijking: de selectolax-wheel bundelt LGPL-2.1 Modest en Apache-2.0 Lexbor, dus lxml is de nettere keuze voor distributie in een gesloten product.

Ook praktisch is de installatie prettig: lxml publiceert vooraf gebouwde wheels die libxml2 en libxslt statisch linken, dus pip install lxml vereist op veel systemen geen system-lxml en geen compiler — een andere ervaring dan zelf vanaf bron bouwen.

Waar lxml past — en waar een AI-extractielaag het overneemt

Tijd om de grens scherp te trekken, want dit wordt vaak fout gecategoriseerd. lxml is een parsingbibliotheek. Je krijgt een tree en een uitstekende query-engine, en verder blijft alles rond die tree jouw verantwoordelijkheid: de pagina ophalen, JavaScript renderen, anti-botmaatregelen passeren, de XPath schrijven en onderhouden, en het resultaat structureren. Dat is een andere laag dan een gehoste extractieservice, en de twee zijn minder concurrenten dan buren.

Voor een ontwikkelaar die liever niet de hele fetch-render-select-maintain-stack beheert, is die bovenste laag precies waar iets als Thunderbit zit — en voor deze doelgroep gaat het dan om de API, MCP-server en CLI, niet om de browserextensie. De Thunderbit Open API biedt POST /distill om een pagina om te zetten naar schone Markdown en POST /extract om gestructureerde data te halen op basis van een JSON Schema, met een renderMode-schakelaar en batchjobs voor volume. Dezelfde engine is beschikbaar als MCP-server (thunderbit_suggest_fields, thunderbit_distill, thunderbit_extract) voor agents en coding assistants, en als CLI die je direct vanuit een terminal kunt draaien via npx @thunderbit/thunderbit-cli. Het handelt JS-rendering, anti-bot en CAPTCHA's standaard af en geeft JSON terug die exact op je schema past — wat de laag boven parsing is, geen vervanging ervoor.

Probeer Thunderbit voor webdata-extractie

De keuze is simpel. Kies lxml wanneer je de pipeline zelf beheert en chirurgische XPath-controle wilt over een tree die je begrijpt. Kies een AI-extractie-API wanneer je selectors en rendering liever helemaal niet zelf onderhoudt. In de praktijk gebruiken veel systemen gewoon beide — lxml voor de gestructureerde feeds die ze zelf controleren, en een extractieservice voor de rommelige lange staart van pagina's die ze niet bezitten.

Wat deze review niet heeft getest

Dit is een voorlopige review, geen definitief scorebord, dus dit valt buiten de scope.

Alle timing- en geheugencijfers zijn hergebruikt, single-platform (macOS arm64, Python 3.14) en erven de kanttekeningen van die pack — het resultaat dat "lxml sneller is bij pure parsing" druist in tegen de gangbare consensus en vraagt om een nieuwe check op Linux x86_64. De threading-speedup per thread-parser is niet getest (dat zou nieuwe timing vereisen). Ik heb iterparse-geheugen gemeten op 300k records, maar niet op echte XML van gigabytes, niet op iterparse van HTML versus XML, en niet op een meeruren-soaktest. lxml's XSLT 1.0, RelaxNG / XMLSchema / DTD-validatie en EXSLT-extensies zijn hier helemaal niet getest — een groot capabilities-gebied, maar buiten de kern van parsing en selectie. Ik heb de tweede dieptelimiet op 2.045 waargenomen, maar niet de exacte libxml2-recursieconstante vastgepind. Alleen de stabiele 6.1.1-versie is getest, niet de 7.0.0-alpha. Windows, source builds en de free-threaded 3.14t-build zijn allemaal niet getest. En binnen XPath zelf heb ik ingebouwde functies afgedekt, maar geen XPath-variabelen, custom Python-extensiefuncties of hergebruik van vooraf gecompileerde etree.XPath-objecten.

Oordeel

lxml is niet de snelle nieuwe ster, en juist daarom is het de aanbeveling. Het is een twee decennia oude libxml2-binding met een volledige XPath 1.0-engine waar geen enkele mainstream Python-alternatief aan tipt, drie voorspelbare striktheidsstanden voor parsing met een error log in het midden, een echte streaming parser voor documenten die niet in geheugen passen, correcte multi-namespace- en encoding-afhandeling en een volledig permissieve licentie. De paar scherpe randjes — de dieptelimiet rond 253 niveaus en het cijfer voor een gedeelde parser in threading — zijn gedocumenteerd, configureerbaar en inmiddels verklaard.

Als je je scrapingpipeline zelf beheert en op XPath leunt, is lxml nog steeds de parser waarvoor je kiest. Wil je liever selectors en rendering niet zelf onderhouden, dan is een AI-extractielaag zoals de Thunderbit API, MCP en CLI daarvoor bedoeld — een duidelijke taakverdeling, geen competitie. Hoe dan ook: behandel deze cijfers als voorlopig en controleer de timing op je eigen platform voordat je ze in een design doc citeert.

Probeer Thunderbit voor webdata-extractie Get Started Free

FAQ's

Is lxml een webscraper? Nee. lxml is een parser en serializer — een Python-binding voor libxml2/libxslt die markup omzet in een bewerkbare, doorzoekbare tree. Het haalt geen pagina's op, rendert geen JavaScript en handelt geen anti-botmaatregelen af; jij levert de request-laag (via requests, httpx, een headless browser of een scrapingservice) en geeft de bytes door aan lxml.

Wanneer gebruik ik lxml in plaats van BeautifulSoup of selectolax? Kies lxml wanneer je XPath nodig hebt. BeautifulSoup kan lxml als backend-parser gebruiken, maar biedt geen native XPath, en selectolax is CSS-only en sneller in zijn smalle niche. Als je selectielogica tekst-inhoud moet filteren, naar parents of ancestors moet navigeren, attribuut- of text-node-extractie nodig heeft of count-predicates gebruikt, dan is lxml's XPath-engine de enige mainstream Python-optie die dat direct uitdrukt.

Waarom laat lxml diep geneste content stilletjes vallen? De standaardparser begrenst nesting op ongeveer 253 niveaus — libxml2's DoS-bescherming tegen vijandige documenten, geen bug. Zet huge_tree=True (bijvoorbeeld lxml.html.HTMLParser(huge_tree=True)) en dieptes van 300 en 1.000 worden volledig hersteld. Let wel op een tweede, hardere recursiegrens rond 2.045 niveaus die huge_tree niet opheft.

Laat lxml de GIL los bij multithreaded parsing? Alleen onder de juiste omstandigheden. De lxml FAQ zegt dat de GIL wordt vrijgegeven tijdens het parsen wanneer elke thread zijn eigen parser gebruikt of een gekopieerde default parser; een gedeelde parser serialiseert de toegang in plaats daarvan. De hergebruikte speedup van 4 threads van 1.21x weerspiegelt het naïeve gedeelde-parserpad, niet de grens met per-thread-parsers, en die is hier niet gemeten.

Wordt lxml in 2026 nog steeds onderhouden? Ja. De stabiele release 6.1.1 verscheen op 2026-05-18, de repository kreeg zijn laatste push op 2026-07-02 en er is een 7.0.0-alpha in ontwikkeling. Met ongeveer 3.000 GitHub-sterren en een actief onderhouden libxml2 eronder blijft het een actuele, goed ondersteunde bibliotheek en geen legacy-tool.

Ke
Ke
CTO bij Thunderbit | Senior Data Scientist & ML-expert Met bijna tien jaar ervaring in machine learning en data science is Ke Shen alumnus van Columbia University en voormalig Senior Data Scientist bij Walmart Labs. Met diepgaande, door vakgenoten erkende expertise in Python, R, Java en statistiek deelt hij praktijkgerichte inzichten over hoe je complexe AI-algoritmen van theorie naar productieklare architectuur brengt.
Topics
Web Scraping ToolsAI Web Scraper
Inhoudsopgave
Thunderbit · AI webdata-agent

Extraheer gegevens van elke pagina in 1 klik

Vertrouwd door meer dan 250.000 gebruikers
Gratis abonnement beschikbaar
Extraheer gegevens met AI
Zet gegevens eenvoudig over naar Google Sheets, Airtable of Notion
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week