lxml im Test: Die XPath-Engine, die viele Python-Parser bis heute überragt

Zuletzt aktualisiert am July 17, 2026
lxml im Test: Die XPath-Engine, die viele Python-Parser bis heute überragt
KI-Zusammenfassung
Dieses Review ordnet lxml als langjährigen Python-Binding rund um libxml2 und libxslt ein — mit einem entscheidenden Vorteil, den neuere Parser oft noch nicht bieten: einer echten XPath-Engine. Der Artikel testet XPath-Abdeckung, Strenge-Modi der Parser, Streaming beim Speicherverbrauch, die Ausdrucksstärke von CSS gegenüber XPath sowie libxml2s Tiefenbegrenzungen. Dabei zeigt sich lxml als schnell, speichereffizient und außergewöhnlich leistungsfähig für XML- und HTML-Workloads, die Achsen, Prädikate, Funktionen, Streaming oder robuste Recovery-Modi benötigen. Außerdem erklärt das Review die sicherheitsbedingten Standardwerte für Baumtiefe und wann huge_tree diese Grenze verschiebt.

Alle paar Monate taucht ein schnellerer HTML-Parser auf, die Benchmark-Runden drehen sich, und plötzlich erklärt jemand die alte Garde für erledigt. Spätestens wenn du dann jeden Absatz auswählen willst, der ein bestimmtes Wort enthält, oder den Parent eines gefundenen Knotens brauchst, merkst du wieder, warum lxml in deinem anderen Tab noch immer offen ist.

lxml ist ein 20 Jahre alter Python-Binding für libxml2. Es ist weder spektakulär noch neu. Und genau bei einem Anwendungsfall — allem, was echtes XPath braucht — spielt es in der Mainstream-Python-Welt sonst kaum jemand auf demselben Niveau. Das hier ist ein praxisnaher Test: was lxml kann, wo es still und heimlich punktet und an welchen Stellen dich die Default-Einstellungen erwischen, wenn du sie nicht kennst.

lxml in einem Absatz: Was es wirklich ist

lxml ist ein Python-Binding für die C-Bibliotheken libxml2 und libxslt. Es ist ein Parser und Serializer, kein Scraper und kein Browser — es wandelt Markup in einen Baum um, den du abfragen und bearbeiten kannst, und verwandelt diesen Baum wieder in Bytes. Du bekommst eine ElementTree-kompatible API, eine vollständige XPath 1.0-Engine, XSLT 1.0 und Schema-Validierung. Betreut wird das Projekt von Stefan Behnel, mit dem Motto "the most feature-rich and easy-to-use library for processing XML and HTML in the Python language".

Hier der Stand auf Basis eines GitHub- und PyPI-Snapshots vom 2026-07-14:

FeldWert
Repolxml/lxml
Stars3.043
Forks620
Offene Issues16
LizenzBSD-3-Clause
Erstellt2011-02-11
Letzter Push2026-07-02
PyPI Stable6.1.1 (2026-05-18)
Gebündelte Enginelibxml2 2.14.6 + libxslt 1.1.43

Eines vorweg, damit hier niemand Marketing vermutet: In diesem Review steckt kein Geheimwissen. lxml ist alt genug, dass sich jedes Verhalten irgendwo in den lxml-Dokumenten, in einem libxml2-Changelog oder in einem Launchpad-Thread nachlesen lässt. Ich habe keinen exklusiven, undokumentierten Trick gefunden — und ich erfinde auch keinen. Der Wert dieses Textes liegt in der Systematik: alles ist geordnet, messbar gemacht und auf lxml als Thema zugeschnitten.

Testaufbau (und warum die Timing-Werte geliehen sind)

In dieses Review fließen zwei Arten von Daten ein, und sie stammen aus zwei verschiedenen Quellen. Das sage ich bewusst offen.

Die Funktionstests — XPath-Verhalten, die beiden Parser-APIs, Namespaces, Encoding, Knoten-Lifecycle — habe ich frisch auf einer Maschine ausgeführt: macOS arm64, Python 3.14.2, lxml 6.1.1, libxml2 2.14.6. Alle Werte in den artifacts/raw/*.json-Dateien stammen aus einem Skriptlauf und wurden nicht von Hand eingetragen. Capability-Tests sind deterministische Booleans und Enums; ein einzelner Lauf reicht, weil Last auf der Maschine nicht ändert, ob //a/@href einen Attribut-String zurückgibt.

Die Timing- und Speicherwerte stammen nicht aus diesem Pack. Sie wurden unverändert aus dem früheren selectolax-Benchmark-Pack übernommen — gleiche Maschine, gleiche virtuelle Umgebung, gleicher lxml- und libxml2-Build, Benchmarks vom Stand 2026-07-13 — und hier nicht neu gemessen. Das ist Absicht. Timing-Benchmarks parallel zu einer Reihe von Capability-Skripten würden CPU-Contention erzeugen und die wiederverwendeten Zahlen verfälschen; außerdem wäre es doppelte Arbeit, denn lxml war in diesem Pack bereits vollständig vermessenes Kontrollwerkzeug. Durch die Wiederverwendung bleibt der Vergleich sauber und nicht subtil unterschiedlich. Wenn du unten also einen Millisekundenwert siehst, lies ihn als „gleiches Test-Setup, Stand 2026-07-13“ — nicht als „heute neu getimt“.

Die Befunde tragen einen Vertrauenswert: single-observation für die deterministischen Funktionstests, triple-run für wiederverwendete Timing-Verteilungen und hypothesis, wenn ich einen Mechanismus vermute, aber nicht isoliert habe.

XPath: Das eine, was selectolax und BeautifulSoup einfach nicht haben

Das ist die Kernbotschaft, also fange ich genau hier an.

lxml XPath coverage moat with axes predicates and functions

Ich habe lxmls xpath() durch eine vorregistrierte 37-teilige Testmatrix gejagt — für jeden Fall stand das erwartete Ergebnis bereits im Quellcode, bevor der Test lief. So konnte ich nicht versehentlich großzügig bewerten. Zehn Achsen, neun Predicate-Stile, zehn eingebaute Funktionen, drei skalare Rückgabetypen und fünf absichtliche Stolperfallen mit XPath-2.0-Syntax, die die 1.0-Engine von lxml ablehnen sollte.

KategorieAbdeckungErgebnis
Achsenchild / descendant / parent / ancestor / following-sibling / preceding-sibling / self / attribute / following / preceding10/10 bestanden
Prädikate[1] / last() / position()<n / Attributgleichheit / Attributexistenz / and / or / verschachtelt [.//a] / not()9/9 bestanden
Funktionentext() / contains() / starts-with() / count() / string-length() / normalize-space() / concat() / substring() / name() / string()10/10 bestanden
Rückgabetypenboolean / number scalars3/3 bestanden
Stolperfallenmatches() / Sequenzen / if-then-else / except / Syntaxfehler5/5 korrekt abgelehnt

Das Ergebnis ist 37/37, und die Spalte mit den Stolperfallen ist die eigentliche Pointe. matches(), Sequenz-Ausdrücke, if/then/else und except sind XPath-2.0-Syntax, und die 1.0-Engine von libxml2 unterstützt sie nicht halbherzig, sondern lehnt sie mit XPathEvalError ab. Sie liefert also nicht stillschweigend ein falsches Node-Set zurück. Das ist also eine perfekte Punktzahl nach dem Versuch, es zu knacken — nicht nur eine schöne Statistik auf Basis harmloser Testfälle. Genau dieses Verhalten beschreibt auch die lxml-XPath-Doku, und darauf kommt es an.

Ich gestehe aber auch einen Fehler im Harness ein — und zwar den Teil von „37/37“, dem du wirklich trauen kannst. Mein erstes Expected Set für //div[.//a[@href]] ging von zwei Treffern aus; der Lauf lieferte nur einen. Ich dachte etwa dreißig Sekunden lang, lxml liege falsch, bis ich das Fixture prüfte und merkte: Das zweite Element war ein <footer> und kein <div> — also war meine Erwartung falsch, nicht die Engine. Ich habe den Expected Set korrigiert und den Fehler als Kommentar im Quellcode stehen lassen. Das ist die richtige Reihenfolge der Schuldfrage: erst die eigene Testlogik anzweifeln, dann die 20 Jahre alte C-Bibliothek.

XPath vs. CSS: Was du in CSS buchstäblich nicht ausdrücken kannst

Die abstrakte Aussage „XPath ist mächtiger“ braucht eine konkrete Zahl. Also habe ich die Lücke vermessen. lxml liefert sowohl .xpath() als auch .cssselect() (letzteres übersetzt CSS intern in XPath). Ich habe zehn Selektionsziele geprüft und festgehalten, welche davon sich mit CSS überhaupt ausdrücken lassen.

XPath expresses seven of ten tasks CSS cannot express

ZielXPathCSS (cssselect)
Nach Textinhalt filtern (contains(text(),"bargain"))JaKein Text-Prädikat
Parent aus Kind selektieren (//b/parent::p)JaKein Parent-Selektor
Attributwert zurückgeben (//a/@href)JaNur Elemente
Textknoten zurückgeben (//p/text())JaKeine Textknoten
Ancestor-Achse (//td/ancestor::div)JaKeine Navigation nach oben
Parent nach Anzahl Kinder filtern (//ul[count(li)=4])JaKein Count-Prädikat
Nach Textlänge filtern (string-length(text())>5)JaKein Längen-Prädikat
nth-child / last-child / benachbarter GeschwisterknotenJaJa (3 Basisfälle)

Sieben von zehn Zielen haben gar kein CSS-Äquivalent. Filtern nach Textinhalt, nach oben zu Parents oder Ancestors navigieren, einen Attributwert oder einen nackten Textknoten als Ergebnis zurückgeben, Zähl-Prädikate — CSS kann all das nicht. Nur drei Fälle (nth-child, last-child, benachbarter Geschwisterknoten) funktionieren in beiden Welten. Das ist die konkrete Antwort auf die Frage: „Was gewinne ich eigentlich mit lxml?“ selectolax ist CSS-only und hat überhaupt keine xpath()-Methode, also werden diese sieben Abfragetypen dort zu mehrstufigen Python-Loops oder fallen ganz weg. Wenn deine Scraping-Logik darauf aufbaut, ist die Entscheidung damit praktisch schon gefallen.

(Und ja, das Harness hat mich hier ein zweites Mal erwischt: Für string-length(text())>5 hatte ich eine leere Menge erwartet, aber zwei sechsstellige Strings haben gematcht. Erwartung angepasst, Tool war korrekt.)

Drei Strenge-Stufen: etree vs. recover vs. lxml.html

XPath ist der Grund, lxml zu wählen. Die dreistufige Strenge ist der Grund, es zu behalten.

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

Die meisten Parser geben dir bei kaputten Eingaben nur ein Verhalten. lxml gibt dir drei — und sie sind so vorhersagbar, dass ich sechs Klassen fehlerhaftes Markup durch alle drei Modi gejagt und vorher festgelegt habe, wie jeder Pfad reagieren soll.

Fehlerhafte Eingabelxml.etree (streng)etree + recover=Truelxml.html (tolerant)
Nicht geschlossenes Tag <root><a>x</root>wirftrepariertakzeptiert
Falsch verschachtelt <b><i></b></i>wirftrepariertakzeptiert
Undefinierte Entity &nbsp;wirftrepariertakzeptiert
Blankes & (Tom & Jerry)wirftrepariertakzeptiert
Mehrere Wurzeln <a>1</a><b>2</b>wirftrepariertakzeptiert
Wohlgeformtes XMLakzeptiertakzeptiert (0 Fehler)akzeptiert
Boolesches Attribut <input disabled>wirftrepariertakzeptiert

Sieben von sieben Fällen entsprachen der vorregistrierten Erwartung. lxml.etree wirft bei allen sechs fehlerhaften Klassen XMLSyntaxError. Wenn du denselben Parser mit recover=True ausführst, schluckt er die Fehler und baut einen nutzbaren Baum neu auf — und das ist der unterschätzte Teil — parser.error_log listet anschließend jeden Fehler auf, den er geschluckt hat. lxml.html akzeptiert alles kommentarlos.

Der Klassifizierer, der zwischen „wirft“, „repariert“ und „akzeptiert“ unterscheidet, hängt selbst von der Laufzeitlänge des error_log ab und nicht von fest einprogrammierten Regeln. Deshalb wird ein wohlgeformtes Dokument unter recover=True korrekt als „akzeptiert“ eingestuft (leeres Log) und nicht als „repariert“. Meine erste Version des Klassifizierers hatte jedes recover=True-Ergebnis als „repariert“ markiert und damit die saubere Eingabe falsch gelabelt; das Lesen des echten error_log hat das behoben.

Was dir das praktisch bringt: strenge Validierung, wenn ein kaputter Feed laut scheitern soll — dann nimm lxml.etree. Schmutziges HTML aus der echten Welt, das du einfach durchbekommen willst — dann nimm lxml.html. Und für den Mittelweg, den die meisten Tools nicht sauber können — „sei tolerant, aber sag mir genau, was kaputt war, damit ich es loggen kann“ — nimm recover=True und lies das Error-Log. selectolax hat nur den toleranten Modus und sonst nichts: weder strengen Modus noch Error-Log.

iterparse: Die Streaming-Funktion, die selectolax gar nicht hat

Hier geht es nicht um Geschwindigkeit, sondern um Fähigkeit. selectolax kann nur einen kompletten String einlesen — eine inkrementelle Schnittstelle gibt es nicht. lxmls iterparse liefert Elemente, sobald sie geschlossen werden, und zusammen mit dem klassischen fast_iter-Muster (also elem.clear() plus vorherige Geschwister löschen) bleibt der Speicherverbrauch konstant, egal wie groß das Dokument wird.

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

Ich habe das Speicherverhalten direkt gemessen — Peak-RSS über ru_maxrss, jede Variante in einem frischen Prozess, auf 300.000 <record>-Elementen mit insgesamt rund 15 MB.

ModusPeak-RSS-DeltaHinweis
iterparse + clear (fast_iter)~1-2 MBwird laufend freigegeben; flach unabhängig von der Menge
iterparse ohne clear~386 MBhält Referenzen fest; so schwer wie ein Komplett-Load
etree.parse (voller Load, Referenz)~386 MBerwartbar schwer; zeigt, dass der Zähler die Größenordnung korrekt erkennt

Der begrenzte Modus hält das Peak-RSS-Delta bei etwa 1-2 MB gegenüber rund 386 MB beim Voll-Load — also nur 0,3-0,4 % der Größenordnung — und das erste Record-Event feuert noch bevor die Datei komplett gelesen ist. Das ist echtes inkrementelles Streaming, kein Fake. Der lehrreichste Wert ist der mittlere: Derselbe iterparse-Loop ohne clear() lässt den Speicher wieder auf etwa 386 MB anwachsen, weil du alles referenzierst. Der Gewinn steckt also in clear(), nicht in iterparse allein. Dass der Voll-Load deutlich höher liegt als der begrenzte Modus, bestätigt außerdem, dass der RSS-Zähler den Größenunterschied wirklich sieht und nicht blind misst. (Dieser Speichertest stammt aus diesem Pack — es ist eine Footprint-Messung und damit etwas anderes als die geliehenen Timing-Werte.)

Die Praxisversion davon: Ein XML-Export im Multi-Gigabyte-Bereich, der nicht in den RAM passt, hat mit selectolax schlicht keinen gangbaren Weg. Dann bleibt nur lxmls Streaming-Parser oder eine andere Sprache.

Namespaces: RSS, SVG und die Default-Namespace-Falle

Zwölf Namespace-Fälle, darunter RSS über drei Namespaces, SVG mit Default-Namespace plus xlink und XML mit Default-Namespace. Alle zwölf bestanden.

lxml zieht //dc:creator/text() aus einem RSS-Feed exakt als ["Alice", "Bob"], löst //atom:link/@href und //content:encoded über drei getrennte Namespaces im selben Dokument auf, verarbeitet //s:rect und //s:use/@xlink:href in SVGs zweitem Namespace, zerlegt Clark-Notation {uri}local-Namen mit QName und erlaubt Inspektion via nsmap. Das ist dokumentiertes, gepflegtes Verhalten — und genau eine Dimension, die selectolax gar nicht abdeckt, weil selectolax HTML5-only ist und keine beliebigen XML-Namespaces verarbeitet.

Es gibt allerdings eine dokumentierte Falle, die man sich merken sollte: XPath kennt kein Konzept für einen Default-Namespace. Wenn du //book auf ein Dokument mit xmlns="urn:..." anwendest, bekommst du null Treffer — der leere Präfix ist für XPath undefiniert, wie die lxml-Dokumente beschreiben. Du musst entweder einen künstlichen Präfix binden (//c:book mit namespaces={"c": "urn:..."}, was alle drei Treffer fand) oder auf //*[local-name()='book'] ausweichen (ebenfalls drei Treffer). Kein Bug — das ist die XPath-Spezifikation, korrekt umgesetzt. Überraschend ist es trotzdem, und zwar genau einmal.

Echte schmutzige Seiten: Treue auf 11 realen Scrapes

Synthetische Tests sind sauber; das Web nicht. Ich habe elf reale, gespeicherte Seiten aus dem Fixture-Set des selectolax-Packs wiederverwendet (Stand 2026-07-10, nur lesend) und lxml.html mit lxml als Untersuchungsobjekt darauf angesetzt.

FixtureGrößeLinksVon libxml2 reparierte FehlerStrenges XML
news_bbc.html398 KB2554ok
docs_mdn_array.html243 KB5080wirft
wiki_scraping.html227 KB4600wirft
gov_whitehouse.html289 KB1540wirft
oldstyle_craigslist.html561 KB3510wirft
forum_reddit.html129 KB3180wirft
docs_python.html80 KB3412wirft
ecommerce_books.html51 KB940wirft
news_hackernews.html35 KB2290wirft
ecommerce_webscraper_allinone.html16 KB350wirft
spa_quotes_js.html6 KB50wirft

Alle elf Seiten wurden mit lxml.html geparst, und die Link-, Überschriften- und Bildanzahlen stimmten auf allen elf Seiten mit den wiederverwendeten lxml-Zahlen aus dem selectolax-Pack überein — Cross-Check: true. Genau diese Übereinstimmung zeigt mir, dass die Wiederverwendung fair ist und nicht zwei verschiedene Messungen denselben Namen tragen.

Die Nebenbeobachtung: Der strenge XML-Parser ist bei zehn der elf Seiten gescheitert. Reale Webseiten sind überwiegend nicht wohlgeformtes XML — genau deshalb gibt es in libxml2 den HTML-Recovery-Modus. Die einzige Ausnahme war BBC News, offenbar von Next.js gerendert und gerade noch wohlgeformt genug, um strenges XML zu überleben. Nicht alles, was als „HTML“ etikettiert ist, braucht also automatisch den Recovery-Pfad.

Ein Zählhinweis, bei dem man leicht stolpert: Auf docs_python.html ergab //a[@href] (Attribut existiert) 343 Treffer, während im selectolax-Pack if n.get("href") (truthy Wert) nur 341 Treffer ergab. Die zwei zusätzlichen Treffer sind leere href=""-Links. Das ist ein Unterschied in der Zählkonvention — Attribut vorhanden versus Attribut nicht leer — kein Unterschied im Verhalten von lxml. Fürs Scraping ist das wichtig: Ob leere hrefs mitzählen, entscheidet dein Filter, nicht der Parser.

Die Tiefenbegrenzung, die wie ein Bug aussieht (aber keiner ist)

Im selectolax-Pack war dokumentiert, dass lxml bei 1.000 und 5.000 Ebenen tief verschachteltem <div>-Markup den tiefsten Inhalt verliert — formuliert als „lxml verliert stillschweigend den tiefsten Inhalt“. Ich wollte den Mechanismus verstehen und habe den Default-Parser gegen huge_tree=True getestet.

lxml default depth guard around 253 levels and huge_tree to 2045

Angeforderte TiefeDefault-Parser erreichthuge_tree=True erreicht
300253 (Rest fällt weg)299 (wiederhergestellt)
1000253 (Rest fällt weg)999 (wiederhergestellt)
5000253 (Rest fällt weg)2045 (immer noch abgeschnitten)

Der Default-Parser schneidet bei rund 253 Ebenen ab und wirft alles darunter stillschweigend weg. Das ist kein Bug, sondern eine DoS-Schutzmaßnahme von libxml2: ein Nesten-Limit von ungefähr 256 Ebenen verhindert, dass ein bösartiges Dokument den Stack sprengt. Das ist auch im lxml-Launchpad-Thread zu XML_PARSE_HUGE dokumentiert. Mit huge_tree=True kommen die Tiefen 300 und 1.000 vollständig zurück. Bei 5.000 Ebenen erreicht der Parser selbst mit huge_tree jedoch nur 2.045 — oberhalb des konfigurierbaren Limits gibt es noch eine zweite, härtere Rekursionsgrenze in libxml2, und huge_tree hebt die nicht auf.

Die konkrete Konsequenz: Wenn du tief verschachteltes Markup aus einer vertrauenswürdigen Quelle parsest, nimm lxml.html.HTMLParser(huge_tree=True). Der Mehrwert dieses Packs gegenüber der wiederverwendeten Beobachtung ist der Mechanismus (Sicherheitsgrenze statt Datenverlust), die Lösung (huge_tree) und der Hinweis, dass es eine zweite, nicht entfernbare Grenze gibt.

DOM lesen und schreiben, Serialisierung, Encoding

lxml ist ein vollständiger Lese-/Schreibbaum und kein reiner Extraktor. Ich habe die Bearbeitungsoberfläche Fall für Fall geprüft. Alle acht DOM-Operationen bestanden: SubElement, insert, remove, replace, strip_tags (Tags entfernen, Text behalten), strip_elements (Tags und ihren Text entfernen), drop_tree (eine lxml.html-Exklusivität) sowie das Text-/Tail-Dualslot-Modell, das Einsteiger oft stolpern lässt — in <p>head<b>bold</b>tail</p> ist p.text "head", b.text "bold" und b.tail "tail".

Die Serialisierung bestand fünf von fünf: tostring im XML- und HTML-Modus (HTML lässt Void-Elemente korrekt nicht selbstschließend), pretty_print, C14N-Kanonisierung (method="c14n", ebenfalls eine lxml-Exklusivität) und ein sauberer Roundtrip.

Beim Encoding trennt sich lxml unauffällig von der Masse. Gibst du ihm Nicht-UTF-8-Bytes — etwa "<p>café éè</p>".encode("latin-1") über lxml.html.fromstring — dann rekonstruiert es café éè unverfälscht, ohne U+FFFD-Ersatzzeichen und ohne verlorene Bytes. Das reproduziert direkt seine Rolle als „saubere Referenz“ im selectolax-Pack, wo derselbe Input bei den anderen beiden Engines still beschädigt wurde (Lexbor erzeugte Ersatzzeichen, Modest verwarf Bytes). lxmls auf libxml2 basierende Charset-Erkennung ist hier schlicht robuster.

Die Kehrseite ist Strenge bei der Art, wie du ein Encoding deklarierst. encoding="latin-1" in einer XML-Deklaration wirft XMLSyntaxError: Unsupported encoding: latin-1, während das IANA-kanonische encoding="ISO-8859-1" sauber geparst wird und café zurückgibt. libxml2 akzeptiert nur kanonische Encoding-Namen, keine Aliase — ein Detail, das schon in launchpad #613302 dokumentiert ist. Nervig, wenn man es nicht kennt; trivial, wenn man es weiß.

Zum Schluss noch der Knoten-Lifecycle. Ich habe drei Szenarien mit „verwaisten“ Handles in isolierten Subprozessen getestet (ein harter Crash hätte sich als nicht-null Exit gezeigt): einen Knoten nach der Garbage Collection seines Baums behalten, einen Handle nach drop_tree() lesen und einen Knoten nach remove() weiterverwenden. In keinem Fall gab es einen Segfault — lxml hält die Referenz eines Knotens auf seinen Baum am Leben und verhindert so Use-after-free. Diese saubere Bilanz bekam selectolax in demselben Test ebenfalls.

Geschwindigkeit und Speicher (geliehen, und offen dazu)

Alles in diesem Abschnitt stammt aus dem selectolax-Pack, Stand 2026-07-13. Dieses Pack erzeugt keine eigenen Timing-Werte, und ich sage das lieber zweimal, als dass du denkst, ich hätte irgendetwas neu getimt.

Dimensionlxml-WertEinordnung
Pure Parse p50 (10 MB)77,9 msca. 33-34 % schneller als selectolax-Lexbor
Voller Parse + Extract p50 (1 MB / 10 MB)14,18 ms / 172,9 msbei kleinen Größen ungefähr auf Augenhöhe mit Lexbor
100k-Node CSS-Throughput3.002.646 Nodes/sschnellste Liga der drei C-Engines
10-MB-RSS-Delta128,9 MBsparsamster von sechs Parsern, ca. 1,7x schlanker als BeautifulSoup
Kaltstart beim Import14,1 msca. 2,3x schneller als parsel-artige Imports

Die Pure-Parse- und Throughput-Werte sind stark, und lxml ist von den sechs gemessenen Parsern der sparsamste im Speicher. Bei Threads braucht das Bild aber eine Einschränkung. Die wiederverwendeten Daten zeigen für vier Threads nur einen Wall-Clock-Speedup von 1,21x, also ohne klare Aussage — allerdings auf dem Pfad mit gemeinsam genutztem Default-Parser. Die lxml-FAQ stellt ausdrücklich klar, dass der GIL beim Parsen nur dann freigegeben wird, wenn jeder Thread seinen eigenen Parser nutzt (oder eine Kopie des Default-Parsers); ein geteilter Parser serialisiert den Zugriff. Ich habe die API-Oberfläche für den korrekten Weg strukturell überprüft (XMLParser.copy() existiert, get/set_default_parser existieren, XPathEvaluator bringt ein internes Lock mit), aber ich habe den Speedup pro Thread-Parser nicht gemessen — das wäre eine neue Timing-Messung, und dieses Pack erzeugt die nicht. Lies die 1,21x also als Wert auf dem naiven Shared-Parser-Pfad, nicht als Thread-Grenze von lxml.

Und ein Sternchen gehört zu allen Zahlen: Das sind Single-Platform-Werte auf macOS arm64. Die Aussage, dass lxml beim reinen Parsen schneller als Lexbor ist, läuft der üblichen Erwartung zuwider, wonach der Lexbor-basierte Parser der schnellste ist. Das sollte also unbedingt auf Linux x86_64 gegengeprüft werden, bevor irgendjemand das als endgültig behandelt.

Lizenz: Der langweilige, aber echte Gewinn

lxml wird unter BSD-3-Clause ausgeliefert, und die eingebetteten C-Bibliotheken — libxml2 und libxslt — stehen beide unter MIT. Das ist eine durchgehend permissive Kette ohne Copyleft an irgendeiner Stelle, und genau das wird wichtig, sobald du etwas weitergibst. Zum Vergleich: Das selectolax-Wheel bündelt LGPL-2.1-Modest und Apache-2.0-Lexbor. Für ein geschlossenes Produkt ist lxml daher die deutlich unkompliziertere Geschichte.

Es gibt auch einen praktischen Installationsvorteil: lxml veröffentlicht vorgebaute Wheels, die libxml2 und libxslt statisch mitbringen. pip install lxml braucht deshalb in der Regel weder ein System-libxml2 noch einen Compiler auf deinem Rechner — ganz anders als ein Build aus dem Quellcode.

Wo lxml hingehört — und wo eine AI-Extraktionsschicht übernimmt

Hier lohnt sich eine klare Grenze, weil die Verwechslung schnell passiert. lxml ist eine Parsing-Bibliothek. Sie liefert dir einen Baum und eine hervorragende Query-Engine; alles um diesen Baum herum bleibt dein Job: die Seite abrufen, JavaScript rendern, Anti-Bot-Hürden umgehen, die XPath-Logik schreiben und pflegen, und das Ergebnis strukturieren. Das ist eine andere Schicht als ein gehosteter Extraktionsdienst — nicht Rivalen, eher Nachbarn.

Für Entwickler, die Fetch-/Render-/Select-/Maintain-Stack nicht selbst tragen wollen, beginnt genau dort eine Lösung wie Thunderbit — und für diese Zielgruppe sind API, MCP-Server und CLI interessanter als die Browser-Erweiterung. Die Thunderbit Open API stellt POST /distill bereit, um eine Seite in sauberes Markdown zu verwandeln, und POST /extract, um strukturierte Daten anhand eines JSON Schema zu extrahieren. Dazu gibt es einen renderMode-Schalter und Batch-Jobs für größere Mengen. Dieselbe Engine steht außerdem als MCP-Server (thunderbit_suggest_fields, thunderbit_distill, thunderbit_extract) für Agents und Coding-Assistenten zur Verfügung sowie als CLI, das du direkt im Terminal über npx @thunderbit/thunderbit-cli starten kannst. Es übernimmt JS-Rendering, Anti-Bot und CAPTCHAs out of the box und gibt schema-konformes JSON zurück — also die Schicht über dem Parsen, nicht den Ersatz dafür.

Thunderbit für Web-Datenextraktion testen

Die Einordnung ist einfach: Greif zu lxml, wenn du die Pipeline selbst kontrollierst und präzise XPath-Steuerung über einen bekannten Baum willst. Greif zu einer AI-Extraktions-API, wenn du Selektoren und Rendering lieber gar nicht selbst pflegen möchtest. In der Praxis nutzen viele Systeme beides — lxml für die strukturierten Feeds, die sie kontrollieren, und einen Extraktionsdienst für die unordentlichen Long-Tail-Seiten, die sie nicht beherrschen.

Was dieses Review nicht getestet hat

Das hier ist ein vorläufiges Review und keine endgültige Scorecard. Deshalb offen, was fehlt.

Alle Timing- und Speicherwerte sind wiederverwendet, stammen von nur einer Plattform (macOS arm64, Python 3.14) und übernehmen die Vorbehalte des ursprünglichen Packs — insbesondere ist das Ergebnis „lxml ist beim reinen Parsen schneller“ gegen die gängige Erwartung und braucht einen Linux-x86_64-Gegencheck. Der Threading-Speedup pro Thread-Parser wurde nicht getestet (dafür wären neue Timings nötig). Ich habe das iterparse-Speicherverhalten auf 300k Records gemessen, aber nicht mit echtem XML im GB-Bereich, nicht mit iterparse auf HTML versus XML und nicht über einen mehrstündigen Soak-Test. lxmls XSLT 1.0, die RelaxNG-/XMLSchema-/DTD-Validierung und EXSLT-Erweiterungen wurden hier überhaupt nicht getestet — ein großer Funktionsumfang, aber außerhalb des Parsing-/Selection-Kerns. Die zweite Tiefenbegrenzung bei 2.045 habe ich beobachtet, aber die exakte libxml2-Rekursionskonstante nicht ermittelt. Getestet wurde nur die stabile Version 6.1.1, nicht die 7.0.0-alpha. Windows, Source-Builds und der frei-threaded 3.14t-Build wurden ebenfalls nicht geprüft. Und innerhalb von XPath selbst habe ich zwar eingebaute Funktionen abgedeckt, aber keine XPath-Variablen, keine benutzerdefinierten Python-Extension-Funktionen und keine Wiederverwendung vorab kompilierter etree.XPath-Objekte.

Fazit

lxml ist nicht das neue, schnelle Tool — und genau deshalb ist es hier die Empfehlung. Es ist ein zwei Jahrzehnte alter libxml2-Binding mit einer vollständigen XPath-1.0-Engine, die keine Mainstream-Python-Alternative sauber erreicht, mit drei klaren Strengestufen beim Parsen samt Error-Log in der Mitte, einem echten Streaming-Parser für Dokumente, die nicht in den Speicher passen, korrekter Behandlung von Multi-Namespaces und Encodings sowie einer vollständig permissiven Lizenz. Die wenigen scharfen Kanten — die Tiefenbegrenzung um ~253 Ebenen und der Threading-Wert beim Shared-Parser — sind dokumentiert, konfigurierbar und jetzt auch erklärt.

Wenn du deine Scraping-Pipeline selbst verantwortest und XPath intensiv nutzt, ist lxml immer noch der Parser, zu dem du greifen solltest. Wenn du Selektoren und Rendering lieber nicht pflegen willst, ist dafür eine AI-Extraktionsschicht wie die Thunderbit API, MCP und CLI da — klare Arbeitsteilung, kein Konkurrenzkampf. In jedem Fall solltest du diese Zahlen als vorläufig ansehen und die Timings auf deiner eigenen Plattform noch einmal prüfen, bevor du sie in einem Design-Dokument zitierst.

Thunderbit für Web-Datenextraktion testen Get Started Free

FAQs

Ist lxml ein Web Scraper? Nein. lxml ist ein Parser und Serializer — ein Python-Binding zu libxml2/libxslt, das Markup in einen editier- und abfragbaren Baum umwandelt. Es lädt keine Seiten, rendert kein JavaScript und übernimmt keine Anti-Bot-Mechanismen; dafür brauchst du die Request-Schicht (z. B. über requests, httpx, einen Headless Browser oder einen Scraping-Dienst) und übergibst dann die Bytes an lxml.

Wann sollte ich lxml statt BeautifulSoup oder selectolax verwenden? Greif zu lxml, wenn du XPath brauchst. BeautifulSoup kann lxml zwar als Backend-Parser nutzen, bietet aber kein natives XPath. selectolax ist CSS-only und in seinem engen Spezialfall schneller. Wenn deine Selektionslogik Textinhalt, Parent-/Ancestor-Navigation, das Extrahieren von Attributen oder Textknoten oder Count-Prädikate braucht, ist lxmls XPath-Engine die einzige Mainstream-Python-Option, die das direkt ausdrücken kann.

Warum lässt lxml tief verschachtelten Inhalt scheinbar stillschweigend verschwinden? Der Default-Parser begrenzt die Verschachtelung auf etwa 253 Ebenen — das ist libxml2s DoS-Schutz gegen bösartige Dokumente und kein Bug. Setze huge_tree=True (zum Beispiel lxml.html.HTMLParser(huge_tree=True)), dann werden Tiefen von 300 und 1.000 vollständig wiederhergestellt. Beachte aber die zweite, härtere Rekursionsgrenze um 2.045 Ebenen, die huge_tree nicht aufhebt.

Gibt lxml den GIL beim parallelen Parsen frei? Nur unter den richtigen Bedingungen. Die lxml-FAQ sagt, dass der GIL beim Parsen freigegeben wird, wenn jeder Thread seinen eigenen Parser oder eine Kopie des Default-Parsers nutzt; ein geteilter Parser serialisiert den Zugriff stattdessen. Der wiederverwendete 4-Thread-Speedup von 1,21x beschreibt also den naiven Shared-Parser-Pfad und nicht das Potenzial mit einzelnen Thread-Parsern, das hier nicht gemessen wurde.

Wird lxml im Jahr 2026 noch gepflegt? Ja. Die stabile Version 6.1.1 erschien am 2026-05-18, das Repository wurde zuletzt am 2026-07-02 gepusht, und eine 7.0.0-alpha ist bereits in Arbeit. Mit rund 3.000 GitHub-Stars und einer aktiv gepflegten libxml2 darunter ist das keine Legacy-Bibliothek, sondern weiterhin eine aktuelle, gut unterstützte Lösung.

Ke
Ke
CTO bei Thunderbit | Senior Data Scientist & ML-Experte Mit fast zehn Jahren Erfahrung in Machine Learning und Data Science ist Ke Shen Absolvent der Columbia University und ehemaliger Senior Data Scientist bei Walmart Labs. Mit tiefgreifender, von Fachkollegen anerkannter Expertise in Python, R, Java und Statistik teilt er praxiserprobte Einblicke dazu, wie sich komplexe KI-Algorithmen von der Theorie in eine produktionsreife Architektur überführen lassen.
Inhaltsverzeichnis
Thunderbit · KI-Webdaten-Agent

Daten von jeder Seite in 1 Klick extrahieren

Vertrauen von über 250.000 Nutzern
Kostenloser Plan verfügbar
Daten mit KI extrahieren
Daten einfach in Google Sheets, Airtable oder Notion übertragen
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week