La fiecare câteva luni apare un parser HTML mai rapid, se publică benchmarkurile și cineva declară vechii lideri depășiți. Apoi ajungi să selectezi fiecare paragraf care conține un anumit cuvânt sau să iei părintele unui nod potrivit și îți amintești de ce lxml încă mai e deschis în celălalt tab.
lxml este un binding vechi de 20 de ani pentru libxml2. Nu e spectaculos. Nu e nou. Și pentru o sarcină foarte clară — orice are nevoie de XPath adevărat — nimic din ecosistemul Python mainstream nu se apropie cu adevărat de el. Acesta este un review practic despre ce face, unde câștigă discret și în ce două-trei locuri te poate înțepa dacă nu știi că există acele comportamente implicite.
lxml pe scurt: ce este, de fapt
lxml este un binding Python pentru bibliotecile C libxml2 și libxslt. Este un parser și un serializer, nu un scraper și nu un browser — transformă markup-ul într-un arbore pe care îl poți interoga și edita, apoi transformă arborele înapoi în bytes. Îți oferă o API compatibilă cu ElementTree, un motor complet XPath 1.0, XSLT 1.0 și validare pe bază de scheme, întreținut de Stefan Behnel sub sloganul "cea mai bogată în funcții și mai ușor de folosit bibliotecă pentru procesarea XML și HTML în limbajul Python".
Iată unde se află, conform unui snapshot GitHub și PyPI preluat la 2026-07-14:
| Câmp | Valoare |
|---|---|
| Repo | lxml/lxml |
| Stars | 3,043 |
| Forks | 620 |
| Open issues | 16 |
| Licență | BSD-3-Clause |
| Creat | 2011-02-11 |
| Ultimul push | 2026-07-02 |
| PyPI stable | 6.1.1 (2026-05-18) |
| Motor inclus | libxml2 2.14.6 + libxslt 1.1.43 |
Un lucru de clarificat din start, înainte să creadă cineva că vând hype: în acest review nu există secrete. lxml e suficient de vechi încât orice comportament de aici să fie documentat undeva în documentația lxml, într-un changelog libxml2 sau într-un thread de Launchpad. Nu am descoperit niciun truc exclusiv, nedezvăluit, și nu voi inventa unul. Valoarea de mai jos vine din faptul că este organizată sistematic, cuantificată și structurată în jurul lui lxml ca subiect — nu pentru că ar fi o noutate.
Configurația testelor (și de ce cifrele de timp sunt împrumutate)
Două categorii de date intră în acest review și provin din două locuri diferite, așa că spun de la început care sunt acestea.
Testele de capabilități — comportamentul XPath, cele două API-uri de parser, namespace-uri, encoding, ciclul de viață al nodurilor — le-am rulat proaspăt pe o singură mașină: macOS arm64, Python 3.14.2, lxml 6.1.1, libxml2 2.14.6. Fiecare număr din fișierele artifacts/raw/*.json este calculat de un script rulat, nu tastat manual. Testele de capabilități sunt booleani și enum-uri deterministe, deci o singură rulare e stabilă — încărcarea mașinii nu schimbă dacă //a/@href returnează un atribut ca string.
Cifrele de timp și footprint-ul de memorie nu sunt din acest pachet. Sunt reutilizate exact din pachetul anterior de benchmark pentru selectolax — aceeași mașină, același mediu virtual, aceeași construcție lxml și libxml2, benchmarkuri actualizate la 2026-07-13 — iar eu nu le-am rerulat aici. Asta e intenționat. Rerularea benchmarkurilor de timp în paralel cu un set de scripturi de capabilități poate introduce concurență pe CPU care ar murdări cifrele reutilizate, iar ar fi și muncă dublă: lxml era deja o bibliotecă-control complet măsurată în acel pachet. Refolosirea acelorași măsurători menține comparația strict echivalentă, în loc să introducă o a doua măsurare ușor diferită. Așadar, când vezi mai jos o valoare în milisecunde, citește-o ca „aceeași platformă de test, valabilă la 2026-07-13”, nu ca „am cronometrat asta chiar azi”.
Rezultatele au și o etichetă de încredere: single-observation pentru testele deterministe de capabilități, triple-run pentru distribuțiile de timp reutilizate, hypothesis atunci când propun un mecanism pe care nu l-am izolat complet.
XPath: singurul lucru pe care selectolax și BeautifulSoup pur și simplu nu îl au
Asta e piesa principală, deci încep de aici.

Am trecut xpath()-ul din lxml printr-o matrice de 37 de elemente, pre-înregistrată — rezultatul așteptat pentru fiecare caz era scris în cod înainte de rulare, astfel încât să nu pot acorda nota „din burtă”. Zece axe, nouă stiluri de predicate, zece funcții built-in, trei tipuri de returnare scalară și cinci cazuri-capcană deliberate care folosesc sintaxă XPath 2.0 pe care motorul 1.0 din lxml ar trebui s-o respingă.
| Categorie | Acoperire | Rezultat |
|---|---|---|
| Axe | child / descendant / parent / ancestor / following-sibling / preceding-sibling / self / attribute / following / preceding | 10/10 trecute |
| Predicate | [1] / last() / position()<n / egalitate pe atribut / existență de atribut / and / or / [.//a] imbricat / not() | 9/9 trecute |
| Funcții | text() / contains() / starts-with() / count() / string-length() / normalize-space() / concat() / substring() / name() / string() | 10/10 trecute |
| Tipuri de returnare | boolean / scalari numerici | 3/3 trecute |
| Cazuri-capcană | matches() / secvențe / if-then-else / except / eroare de sintaxă | 5/5 respinse corect |
Scorul e 37/37, iar coloana de capcane e cea care contează. matches(), expresiile de tip secvență, if/then/else și except sunt toate sintaxă XPath 2.0, iar motorul 1.0 din libxml2 nu le „acceptă pe jumătate” — ridică XPathEvalError și refuză, în loc să returneze în liniște un set greșit de noduri. Deci acesta e un scor perfect după ce am încercat să-l rup, nu un scor perfect obținut din întrebări ușoare. Tot ce se întâmplă aici corespunde exact cu ce descriu documentele XPath din lxml, care e și ideea.
Recunosc un lucru despre care harness-ul a greșit, fiindcă e genul de „37/37” în care chiar poți avea încredere. Primul meu set așteptat pentru //div[.//a[@href]] prevedea două potriviri; rularea a returnat una singură. Am presupus că lxml greșește vreo treizeci de secunde, apoi am verificat fixture-ul și am descoperit că al doilea element era un <footer>, nu un <div> — deci a fost o eroare în așteptările mele, nu în motor. Am corectat setul așteptat și am lăsat greșeala într-un comentariu din sursă. Asta e ordinea corectă a vinei: mai întâi îți suspectezi testul, nu biblioteca C veche de 20 de ani.
XPath vs CSS: ce pur și simplu nu poți exprima în CSS
Afirmația abstractă „XPath este mai puternic” merită un număr concret, așa că am cuantificat diferența. lxml îți oferă atât .xpath(), cât și .cssselect() (cea din urmă traduce CSS în XPath în spate). Am luat zece ținte de selecție și am verificat care dintre ele pot fi exprimate efectiv în CSS.

| Țintă | XPath | CSS (cssselect) |
|---|---|---|
Filtrare după conținut text (contains(text(),"bargain")) | Da | Fără predicat pe text |
Selectare părinte din copil (//b/parent::p) | Da | Fără selector de părinte |
Returnare valoare de atribut (//a/@href) | Da | Doar elemente |
Returnare nod text (//p/text()) | Da | Fără noduri text |
Axă ancestor (//td/ancestor::div) | Da | Fără navigare în sus |
Filtrare părinte după numărul de copii (//ul[count(li)=4]) | Da | Fără predicat count |
Filtrare după lungimea textului (string-length(text())>5) | Da | Fără predicat de lungime |
nth-child / last-child / sibling adiacent | Da | Da (3 de bază) |
Șapte din zece ținte nu au deloc echivalent CSS. Filtrarea după conținut text, navigarea în sus către părinți și strămoși, extragerea unei valori de atribut sau a unui nod text simplu ca rezultat, predicate bazate pe numărare — CSS nu poate exprima nimic din toate acestea. Doar trei (nth-child, last-child, sibling adiacent) funcționează în ambele. Acesta e răspunsul cuantificat la întrebarea „ce câștig, de fapt, dacă aleg lxml?”. selectolax e doar CSS și nu are deloc metodă xpath(), așa că acele șapte tipuri de interogări devin acolo bucle Python în mai mulți pași sau pur și simplu nu există. Dacă logica ta de scraping se sprijină pe oricare dintre ele, decizia e deja luată.
(Și da, harness-ul m-a prins din nou aici: am prezis un set gol pentru string-length(text())>5, dar au potrivit două șiruri de șase caractere. Am corectat așteptarea, nu instrumentul.)
Trei moduri de strictete: etree vs recover vs lxml.html
XPath este motivul pentru care alegi lxml. Controlul strictitudinii în trei trepte este motivul pentru care îl și păstrezi.

Majoritatea parserelor îți dau un singur comportament pentru input stricat. lxml îți oferă trei, iar acestea sunt suficient de previzibile încât am trecut prin ele șase clase de markup invalid și am pre-înregistrat cum ar trebui să se comporte fiecare cale.
| Input invalid | lxml.etree (strict) | etree + recover=True | lxml.html (tolerant) |
|---|---|---|---|
Tag neînchis <root><a>x</root> | ridică eroare | recuperează | acceptă |
Încadrare greșită <b><i></b></i> | ridică eroare | recuperează | acceptă |
Entity nedefinită | ridică eroare | recuperează | acceptă |
Ampersand simplu & (Tom & Jerry) | ridică eroare | recuperează | acceptă |
Mai multe rădăcini <a>1</a><b>2</b> | ridică eroare | recuperează | acceptă |
| XML bine format | acceptă | acceptă (0 erori) | acceptă |
Atribut boolean <input disabled> | ridică eroare | recuperează | acceptă |
Șapte din șapte au corespuns cu așteptarea pre-înregistrată. lxml.etree ridică XMLSyntaxError pentru toate cele șase clase de markup invalid. Dacă adaugi recover=True aceluiași parser, acesta înghite erorile și reconstruiește un arbore utilizabil — iar asta e partea subestimată — parser.error_log enumeră apoi toate erorile pe care le-a înghițit. lxml.html acceptă totul fără să se plângă.
Clasificatorul care decide „ridică eroare vs recuperează vs acceptă” e el însuși condus de lungimea error_log la runtime, nu hard-codat, motiv pentru care un document bine format rulat cu recover=True e etichetat corect „acceptă” (log gol), nu „recuperează”. Prima mea versiune de clasificator eticheta orice rezultat cu recover=True drept „recuperează” și marca greșit inputul curat; citirea efectivă a error_log a rezolvat problema.
Ce îți oferă asta în practică: validare strictă acolo unde un feed stricat trebuie să eșueze vizibil — folosești lxml.etree. HTML murdar din lumea reală, pe care doar vrei să-l procesezi — folosești lxml.html. Iar cazul de mijloc pe care majoritatea instrumentelor nu-l pot acoperi — „fii tolerant, dar spune-mi exact ce a fost stricat ca să pot loga” — folosești recover=True și citești logul de erori. selectolax are doar modul tolerant și nimic altceva, fără mod strict și fără error log.
iterparse: modul de streaming pe care selectolax nu îl are deloc
Asta e o linie de capabilitate, nu un buton de viteză. selectolax ingerează doar un șir complet — nu există interfață incrementală. iterparse din lxml (documentație) emite elementele pe măsură ce se închid, iar împreună cu tiparul clasic fast_iter (apelezi elem.clear() și ștergi frații anteriori pe parcurs) menține memoria plată, indiferent cât de mare e documentul.

Am măsurat direct caracteristica de memorie — RSS maxim prin ru_maxrss, fiecare subiect într-un proces proaspăt, pe 300.000 de elemente <record> însumând aproximativ 15 MB.
| Mod | Delta RSS maxim | Observații |
|---|---|---|
iterparse + clear (fast_iter) | ~1-2 MB | eliberat pe măsură ce rulează; plat indiferent de volum |
iterparse fără clear | ~386 MB | păstrează referințele; la fel de greu ca încărcarea completă |
etree.parse (încărcare completă, referință) | ~386 MB | încărcare grea, confirmă că metrica vede magnitudinea |
Modul limitat ține delta RSS maxim la aproximativ 1-2 MB față de ~386 MB în cazul încărcării complete — o diferență de ordinul 0,3-0,4% — iar primul eveniment de tip record apare înainte ca fișierul să fie citit complet, deci e într-adevăr incremental, nu streaming „de fațadă”. Linia instructivă este cea din mijloc. Rulezi aceeași buclă iterparse, dar sari peste clear(), iar memoria urcă iar la ~386 MB, fiindcă păstrezi referințe la tot. Câștigul stă în clear(), nu în iterparse în sine. Faptul că lectura de referință din încărcarea completă e mult peste modul limitat confirmă și că metrul RSS chiar vede diferența de magnitudine, nu doar citește „în orb”. (Acest test de memorie e unul pe care l-am rulat în acest pachet — e o măsurare de footprint, distinctă de cifrele de timp împrumutate.)
Varianta reală e simplă: un export XML de mai mulți gigabytes, care nu încape în RAM, nu are nicio rută prin selectolax. Acolo ai doar parserul de streaming din lxml sau un alt limbaj.
Namespace-uri: RSS, SVG și capcana namespace-ului implicit
Douăsprezece cazuri de namespace, acoperind RSS prin trei namespace-uri, SVG cu un namespace implicit plus xlink, și XML cu namespace implicit. Toate cele douăsprezece au trecut.
lxml extrage //dc:creator/text() dintr-un feed RSS exact ca [Alice, Bob], rezolvă //atom:link/@href și //content:encoded peste trei namespace-uri separate în același document, tratează //s:rect și //s:use/@xlink:href în al doilea namespace din SVG, împarte numele în notație Clark {uri}local cu QName și permite introspecție prin nsmap. Acesta e comportamentul întreținut și documentat, și e o dimensiune întreagă pe care selectolax nu o atinge, pentru că selectolax e doar HTML5 și nu procesează namespace-uri XML arbitrare.
Există însă o capcană documentată care merită ținută minte. XPath nu are noțiunea de namespace implicit. Dacă aplici //book pe un document care declară xmlns="urn:...", obții zero rezultate — prefixul gol nu este definit pentru XPath, așa cum explică documentația lxml. Trebuie să legi un prefix artificial (//c:book cu namespaces={"c": "urn:..."}, care a găsit toate cele trei elemente) sau să revii la //*[local-name()='book'] (tot trei rezultate). Nu e bug — e specificația XPath, implementată fidel. Doar că surprinde pe toată lumea exact o dată.
Pagini murdare reale: fidelitate pe 11 scrapes concrete
Testele sintetice sunt curate; web-ul nu este. Am reutilizat unsprezece pagini reale capturate din setul de fixture-uri al pachetului selectolax (valabile la 2026-07-10, read-only) și am trecut lxml.html prin ele, având lxml ca subiect.
| Fixture | Mărime | Linkuri | Erori recuperate de libxml2 | XML strict |
|---|---|---|---|---|
| news_bbc.html | 398 KB | 255 | 4 | ok |
| docs_mdn_array.html | 243 KB | 508 | 0 | ridicat eroare |
| wiki_scraping.html | 227 KB | 460 | 0 | ridicat eroare |
| gov_whitehouse.html | 289 KB | 154 | 0 | ridicat eroare |
| oldstyle_craigslist.html | 561 KB | 351 | 0 | ridicat eroare |
| forum_reddit.html | 129 KB | 318 | 0 | ridicat eroare |
| docs_python.html | 80 KB | 341 | 2 | ridicat eroare |
| ecommerce_books.html | 51 KB | 94 | 0 | ridicat eroare |
| news_hackernews.html | 35 KB | 229 | 0 | ridicat eroare |
| ecommerce_webscraper_allinone.html | 16 KB | 35 | 0 | ridicat eroare |
| spa_quotes_js.html | 6 KB | 5 | 0 | ridicat eroare |
Toate cele unsprezece au fost parcurse cu lxml.html, iar numărul de linkuri, heading-uri și imagini a coincis pe toate cele unsprezece cu numerele lxml reutilizate din pachetul selectolax — verificare încrucișată true. Acea concordanță îmi spune că reutilizarea e cu adevărat comparabilă, nu două măsurători diferite care poartă aceeași etichetă.
Constatarea secundară: parserul XML strict a ridicat eroare pe zece dintre cele unsprezece pagini. Pagina web reală e, în majoritatea cazurilor, departe de a fi XML bine format, exact de aceea există modul de recuperare HTML din libxml2. Singura excepție a fost BBC News, redată de Next.js și suficient de bine formată încât să supraviețuiască parsării XML stricte. Nu tot ce e etichetat „HTML” are nevoie de calea de recuperare.
O notă de numărare care te poate încurca ușor. Pe docs_python.html, //a[@href] (existență de atribut) a numărat 343, în timp ce pachetul selectolax folosea if n.get("href") (valoare truthy) și a numărat 341. Cele două în plus sunt linkuri cu href="" gol. Este o diferență de convenție de numărare — atribut existent versus atribut nenul — nu o diferență de comportament lxml, iar numerele se aliniază imediat ce uniformizezi predicatul. Merită știut când faci scraping: dacă href-urile goale contează sau nu ține de filtrul tău, nu de parser.
Limita de adâncime care pare bug, dar nu este
Pachetul pentru selectolax înregistrase că lxml pierde conținutul cel mai adânc în markup cu 1.000 și 5.000 de niveluri de <div>, formulat ca „lxml pierde în tăcere conținutul cel mai adânc”. Am vrut mecanismul, așa că am rulat parserul implicit și apoi huge_tree=True.

| Adâncime cerută | Parserul implicit ajunge la | huge_tree=True ajunge la |
|---|---|---|
| 300 | 253 (taie restul) | 299 (recuperat) |
| 1000 | 253 (taie restul) | 999 (recuperat) |
| 5000 | 253 (taie restul) | 2045 (tot mai taie) |
Parserul implicit taie documentul pe la aproximativ 253 de niveluri și ignoră în tăcere tot ce e mai adânc. Asta nu e un bug — e apărarea libxml2 împotriva DoS, o limită de înlănțuire de circa 256 de niveluri care împiedică un document ostil să îți facă stack overflow, iar asta e documentat și în thread-ul Launchpad despre lxml privind XML_PARSE_HUGE. Dacă setezi huge_tree=True, adâncimile de 300 și 1.000 revin complet. Totuși, la 5.000 de niveluri ajunge doar la 2.045 chiar și cu huge_tree activ — există o a doua limită, mai dură, de recursie în libxml2, peste cea configurabilă, iar huge_tree nu o elimină.
Așadar, acțiunea concretă e simplă: când parsezi markup foarte adânc dintr-o sursă în care ai încredere, folosește lxml.html.HTMLParser(huge_tree=True). Ce aduce acest pachet peste observația reutilizată e mecanismul (un plafon de siguranță, nu corupție de date), soluția (huge_tree) și faptul că există și un al doilea prag la care soluția nu ajunge.
DOM citire/scriere, serializare, encoding
lxml este un arbore complet read/write, nu un extractor doar pentru citire, iar suprafața de editare a fost verificată caz cu caz. Toate cele opt operații DOM au trecut: SubElement, insert, remove, replace, strip_tags (șterge tag-urile, păstrează textul), strip_elements (șterge tag-urile și textul lor), drop_tree (exclusiv lxml.html) și modelul dual text/tail care îi încurcă pe începători — în <p>head<b>bold</b>tail</p>, p.text este "head", b.text este "bold", iar b.tail este "tail".
Serializarea a trecut cinci din cinci: tostring în mod XML și HTML (HTML lasă corect elementele void neînchise auto), pretty_print, canonizarea C14N (method="c14n", încă o exclusivitate lxml) și un round-trip curat.
Encoding-ul este zona în care lxml se separă discret. Dacă îi dai bytes care nu sunt UTF-8 — "<p>café éè</p>".encode("latin-1") prin lxml.html.fromstring — recuperează café éè intact, fără caractere U+FFFD de înlocuire și fără bytes pierduți. Asta reproduce direct rolul său de „referință curată” din pachetul selectolax, unde același input era corupt în liniște de celelalte două motoare (Lexbor producea caractere de înlocuire, iar Modest tăia bytes-ii de tot). Detecția de charset bazată pe libxml2 e pur și simplu mai stabilă aici.
Partea opusă e strictețea în felul în care declari encoding-ul. encoding="latin-1" într-o declarație XML ridică XMLSyntaxError: Unsupported encoding: latin-1, în timp ce forma canonică IANA encoding="ISO-8859-1" se parsează corect și returnează café. libxml2 acceptă doar nume canonice de encoding, nu aliasuri — detaliu documentat încă din launchpad #613302. Enervant dacă nu știi, banal după ce afli.
Ultimul punct: ciclul de viață al nodurilor. Am rulat trei scenarii de handle-uri vechi în subprocessuri izolate (un crash sever ar fi apărut ca un exit code nenul): păstrarea unui nod după ce arborele lui a fost colectat de garbage collector, citirea unui handle după drop_tree() și folosirea unui nod după remove(). Niciun segfault în niciun caz — lxml păstrează referința nodului către arborele lui activă pentru a preveni use-after-free. Același rezultat curat l-a obținut și selectolax la acest test.
Viteză și memorie (împrumutate, și prezentate onest)
Tot ce este în această secțiune este reutilizat din pachetul selectolax, valabil la 2026-07-13. Acest pachet nu a produs niciun număr de timp propriu, și prefer să spun asta de două ori decât să creadă cineva că am rerulat ceva.
| Dimensiune | Valoare lxml | Interpretare |
|---|---|---|
| Pure parse p50 (10 MB) | 77.9 ms | cu ~33-34% mai rapid decât selectolax-Lexbor |
| Full parse + extract p50 (1 MB / 10 MB) | 14.18 ms / 172.9 ms | aproximativ egal cu Lexbor la dimensiuni mici |
| Throughput CSS pe 100k noduri | 3,002,646 noduri/s | cea mai rapidă categorie dintre cele trei motoare C |
| Delta RSS la 10 MB | 128.9 MB | cel mai economic dintre cei șase parseri, ~1.7x mai eficient decât BeautifulSoup |
| Import cold start | 14.1 ms | ~2.3x mai rapid decât importurile tip parsel |
Cifrele pentru parse pur și throughput sunt solide, iar lxml este cel mai economic din punct de vedere al memoriei dintre cei șase parseri măsurați. Totuși, partea de threading are nevoie de o precizare. Datele reutilizate arată un speedup wall-clock de doar 1.21x la 4 thread-uri, marcat neconcludent — dar aceasta este calea cu parser partajat implicit. FAQ-ul lxml spune clar că GIL este eliberat în timpul parsării doar când fiecare thread folosește propriul parser (sau o copie a celui implicit); un parser partajat serializează accesul. Am verificat structural API-ul necesar pentru a face asta corect (XMLParser.copy() există, există get/set_default_parser, iar XPathEvaluator are un lock intern), dar nu am măsurat accelerația per-thread-parser — ar fi o măsurătoare nouă de timp, iar acest pachet nu produce astfel de numere. Deci citește „1.21x” ca „pe calea naivă cu parser partajat”, nu ca plafonul de threading al lui lxml.
Și încă o notă pe marginea tuturor acestor cifre: sunt numere de o singură platformă, macOS arm64. Afirmația că parse-ul pur din lxml îl depășește pe Lexbor contrazice consensul obișnuit conform căruia parserul bazat pe Lexbor este cel mai rapid, așa că merită cu adevărat o verificare pe Linux x86_64 înainte ca cineva să o considere definitivă.
Licențiere: victoria plictisitoare, dar importantă
lxml este distribuit sub BSD-3-Clause, iar bibliotecile C incluse — libxml2 și libxslt — sunt ambele sub MIT. E un lanț complet permisiv, fără copyleft nicăieri, iar asta contează imediat ce redistribui produsul. Ca termen de comparație, wheel-ul selectolax include Modest sub LGPL-2.1 și Lexbor sub Apache-2.0, deci lxml are o poveste mai curată pentru livrarea într-un produs closed-source.
Mai există și un avantaj practic la instalare: lxml publică wheel-uri precompilate care leagă static libxml2 și libxslt, deci pip install lxml de regulă nu are nevoie de libxml2 de sistem și nici de compilator pe mașina ta — o experiență diferită față de build-ul din surse.
Unde se potrivește lxml — și unde preia un strat de extracție AI
E momentul să fixăm clar granița, fiindcă e foarte ușor să amesteci nivelurile. lxml este o bibliotecă de parsare. Îți dă un arbore și un motor excelent de interogare, iar tot ce înconjoară acel arbore rămâne treaba ta: preluarea paginii, randarea JavaScript-ului, trecerea de anti-bot, scrierea și întreținerea XPath-ului și structurarea rezultatului. E un nivel diferit de un serviciu găzduit de extracție, iar cele două nu sunt atât rivali, cât vecini.
Pentru un developer care preferă să nu-și asume singur tot stack-ul de fetch-render-select-maintain, stratul superior e locul în care intră ceva precum Thunderbit — iar pentru publicul acesta contează API-ul, serverul MCP și CLI-ul, nu extensia de browser. Thunderbit Open API expune POST /distill pentru a transforma o pagină în Markdown curat și POST /extract pentru a scoate date structurate pe baza unui JSON Schema, cu un comutator renderMode și joburi batch pentru volum. Același motor e disponibil și ca server MCP (thunderbit_suggest_fields, thunderbit_distill, thunderbit_extract) pentru agenți și asistenți de cod, precum și ca CLI pe care îl poți rula direct din terminal prin npx @thunderbit/thunderbit-cli. Gestionează randarea JS, anti-bot și CAPTCHAs din start și returnează JSON aliniat la schemă — adică un strat deasupra parsării, nu un înlocuitor pentru ea.
Încearcă Thunderbit pentru extragerea datelor web
Ideea e simplă. Alege lxml când controlezi tu pipeline-ul și vrei control chirurgical prin XPath asupra unui arbore pe care îl înțelegi. Alege o API de extracție AI când nu vrei deloc să întreții selectori și randare. Multe sisteme reale le folosesc pe ambele — lxml pentru feedurile structurate pe care le controlează, un serviciu de extracție pentru paginile dezordonate și lungul cozii pe care nu le controlează.
Ce nu a testat acest review
Acesta este un review provizoriu, nu un scor final, deci iată ce nu acoperă.
Toate cifrele de timp și memorie sunt reutilizate, de pe o singură platformă (macOS arm64, Python 3.14) și moștenesc avertismentele acelui pachet — rezultatul „lxml este mai rapid la parse pur” contrazice consensul și cere o reverificare pe Linux x86_64. Accelerația threading per-thread-parser nu e testată (ar necesita timing nou). Am măsurat memoria iterparse pe 300k de recorduri, dar nu pe XML real la scară de GB, nici iterparse pe HTML versus XML, nici un soak de mai multe ore. XSLT 1.0 din lxml, validarea RelaxNG / XMLSchema / DTD și extensiile EXSLT nu au fost deloc testate aici — o suprafață de capabilități mare, dar în afara nucleului de parsare și selecție. Am observat și al doilea plafon de adâncime la 2.045, dar nu am determinat constanta exactă de recursie a libxml2. Am testat doar versiunea stabilă 6.1.1, nu alpha 7.0.0. Windows, build-urile din sursă și build-ul free-threaded 3.14t nu au fost testate. Iar în interiorul XPath am acoperit funcțiile built-in, dar nu variabilele XPath, funcțiile Python custom sau reutilizarea obiectelor precompilate etree.XPath.
Verdictul
lxml nu e lucrul nou și rapid, iar acesta e exact motivul pentru care îl recomand. Este un binding libxml2 vechi de două decenii, cu un motor complet XPath 1.0 pe care nu-l egalează nicio alternativă Python mainstream, cu trei trepte previzibile de strictete la parsare și un error log în mijloc, cu un parser real de streaming pentru documente care nu încap în memorie, cu gestionare corectă a namespace-urilor multiple și a encodingului, și cu o licență complet permisivă. Câteva muchii ascuțite — plafonul de adâncime de ~253 niveluri și valoarea de threading cu parser partajat — sunt documentate, configurabile și, acum, explicate.
Dacă tu controlezi pipeline-ul de scraping și te bazezi pe XPath, lxml rămâne parserul la care ajungi primul. Dacă preferi să nu întreții selectori și randare, pentru asta există un strat de extracție AI precum API-ul, MCP-ul și CLI-ul Thunderbit — o împărțire clară a responsabilităților, nu o competiție. În orice caz, tratează aceste numere ca provizorii și re-verifică timingul pe platforma ta înainte să îl citezi într-un design doc.
Încearcă Thunderbit pentru extragerea datelor web Get Started Free
Întrebări frecvente
Este lxml un web scraper?
Nu. lxml este un parser și serializer — un binding Python pentru libxml2/libxslt care transformă markup-ul într-un arbore editabil și interogabil. Nu preia pagini, nu randează JavaScript și nu gestionează apărarea anti-bot; tu furnizezi stratul de request-uri (prin requests, httpx, un browser headless sau un serviciu de scraping) și pasezi bytes-ii către lxml.
Când ar trebui să folosesc lxml în loc de BeautifulSoup sau selectolax? Alege lxml când ai nevoie de XPath. BeautifulSoup poate folosi lxml ca parser backend, dar nu expune XPath nativ, iar selectolax e doar CSS și mai rapid în nișa lui îngustă. Dacă logica de selecție are nevoie de filtrare după text, navigare către părinte sau ancestor, extragere de atribute sau text nodes, ori predicate pe bază de numărare, motorul XPath din lxml e singura opțiune Python mainstream care le exprimă direct.
De ce lxml elimină în tăcere conținutul foarte adânc imbricat?
Parserul său implicit limitează adâncimea la aproximativ 253 de niveluri — o apărare libxml2 împotriva atacurilor DoS, nu un bug. Setează huge_tree=True (de exemplu lxml.html.HTMLParser(huge_tree=True)) și recuperează complet adâncimi de 300 și 1.000. Ține cont însă de un al doilea prag, mai dur, în jur de 2.045 de niveluri, pe care huge_tree nu îl elimină.
Eliberează lxml GIL-ul pentru parsare multi-threaded? Doar în condițiile potrivite. FAQ-ul lxml spune că GIL este eliberat în timpul parsării atunci când fiecare thread folosește propriul parser sau o copie a parserului implicit; un parser partajat, în schimb, serializează accesul. Accelerarea reutilizată de 1.21x la 4 thread-uri reflectă calea naivă cu parser partajat, nu plafonul per-thread-parser, care nu a fost măsurat aici.
Mai este lxml întreținut în 2026? Da. Versiunea stabilă 6.1.1 a fost publicată pe 2026-05-18, repo-ul a avut ultimul push pe 2026-07-02, iar o versiune alpha 7.0.0 este în lucru. Cu aproximativ 3.000 de stele GitHub și cu libxml2 activ întreținut dedesubt, rămâne o bibliotecă actuală și bine susținută, nu una de arhivă.


