हर कुछ महीनों में कोई न कोई तेज़ HTML parser सामने आ जाता है, benchmark घूमते रहते हैं, और कोई न कोई पुराने विकल्पों को बेकार घोषित कर देता है। फिर आपको किसी ऐसे शब्द वाले हर paragraph को चुनना होता है जो contains करता हो, या matched node का parent निकालना होता है — और याद आता है कि lxml अभी भी आपके दूसरे tab में क्यों खुला है।
lxml, libxml2 का 20 साल पुराना binding है। यह न तो रोमांचक है, न नया। और एक खास काम के लिए — यानी जहाँ असली XPath चाहिए — mainstream Python में इसका कोई सीधा मुकाबला नहीं है। यह एक hands-on समीक्षा है: यह क्या करता है, कहाँ चुपचाप जीतता है, और किन दो-चार default सेटिंग्स की वजह से अगर आपको पता न हो तो दिक्कत हो सकती है।
lxml एक नज़र में: यह असल में है क्या
lxml, C libraries libxml2 और libxslt के लिए Python binding है। यह parser और serializer है, scraper या browser नहीं — यह markup को एक tree में बदल देता है जिसे आप query और edit कर सकते हैं, और फिर उस tree को bytes में वापस बदल देता है। इसमें ElementTree-compatible API, पूरा XPath 1.0 engine, XSLT 1.0, और schema validation मिलती है। Stefan Behnel इसे "the most feature-rich and easy-to-use library for processing XML and HTML in the Python language" के tagline के साथ maintain करते हैं।
नीचे GitHub और PyPI snapshot के आधार पर इसकी स्थिति है, जो 2026-07-14 को लिया गया था:
| फ़ील्ड | मान |
|---|---|
| Repo | lxml/lxml |
| Stars | 3,043 |
| Forks | 620 |
| Open issues | 16 |
| License | BSD-3-Clause |
| Created | 2011-02-11 |
| Last push | 2026-07-02 |
| PyPI stable | 6.1.1 (2026-05-18) |
| Bundled engine | libxml2 2.14.6 + libxslt 1.1.43 |
एक बात पहले साफ कर दूँ, ताकि बाद में hype का आरोप न लगे: इस समीक्षा में कोई secret नहीं है। lxml इतना पुराना है कि यहाँ दिखने वाला हर behavior कहीं न कहीं lxml docs, libxml2 changelog, या किसी launchpad thread में पहले से दर्ज है। मैंने कोई undocumented trick नहीं खोजा, और न ही गढ़ने वाला हूँ। इस पूरे लेख की कीमत यह है कि इसे systematic, quantified, और lxml-centered तरीके से organize किया गया है — न कि इसलिए कि इसमें कोई नई खबर है।
Test Setup (और timing numbers उधार क्यों लिए गए)
इस review में दो तरह का data है, और वे दो अलग जगहों से आए हैं — इसलिए शुरुआत में ही साफ कर रहा हूँ कि कौन-सा किससे आया है।
Capability tests — यानी XPath behavior, दो parser APIs, namespaces, encoding, node lifecycle — मैंने एक machine पर fresh run किए: macOS arm64, Python 3.14.2, lxml 6.1.1, libxml2 2.14.6। artifacts/raw/*.json files में जो भी number है, वह script-run से निकला है, हाथ से लिखा नहीं गया। Capability tests deterministic booleans और enums होते हैं, इसलिए एक run काफी stable रहता है — machine load से यह नहीं बदलता कि //a/@href attribute string लौटाएगा या नहीं।
Timing और memory-footprint numbers इस pack से नहीं हैं। इन्हें पहले वाले selectolax benchmark pack से ज्यों-का-त्यों लिया गया है — वही machine, वही virtual environment, वही lxml और libxml2 build, benchmarks as-of 2026-07-13 — और मैंने उन्हें यहाँ दोबारा run नहीं किया। यह जानबूझकर किया गया। Timing benchmarks को capability scripts के batch के साथ run करने से CPU contention पैदा हो सकता है, जिससे reused figures खराब हो जाते; और वह duplicate work भी होता, क्योंकि उस pack में lxml पहले ही पूरी तरह measured control library था। एक ही bench को reuse करने से apples-to-apples तुलना बनी रहती है, दूसरी हल्की-सी अलग measurement नहीं बनती। इसलिए नीचे जहाँ millisecond figure दिखे, उसे "same test rig, as-of 2026-07-13" समझें, न कि "मैंने आज दोबारा timing निकाली है।"
Findings के साथ confidence tag दिए गए हैं: deterministic capability tests के लिए single-observation, reused timing distributions के लिए triple-run, और जहाँ मैं कोई ऐसा mechanism सुझा रहा हूँ जिसे isolate नहीं किया गया, वहाँ hypothesis।
XPath: वह एक चीज़ जो selectolax और BeautifulSoup के पास नहीं है
यही headline है, इसलिए यहीं से शुरू करते हैं।

मैंने lxml के xpath() को 37-item pre-registered matrix में चलाया — हर case का expected result test शुरू होने से पहले source में लिखा गया था, ताकि grading में अनजाने में दया का असर न पड़े। दस axes, नौ predicate styles, दस built-in functions, तीन scalar return types, और पाँच deliberate trap cases जिनमें XPath 2.0-only syntax था, जिसे lxml का 1.0 engine reject करना चाहिए।
| Category | Coverage | Result |
|---|---|---|
| Axes | child / descendant / parent / ancestor / following-sibling / preceding-sibling / self / attribute / following / preceding | 10/10 pass |
| Predicates | [1] / last() / position()<n / attribute equality / attribute existence / and / or / nested [.//a] / not() | 9/9 pass |
| Functions | text() / contains() / starts-with() / count() / string-length() / normalize-space() / concat() / substring() / name() / string() | 10/10 pass |
| Return types | boolean / number scalars | 3/3 pass |
| Trap cases | matches() / sequences / if-then-else / except / syntax error | 5/5 correctly rejected |
स्कोर 37/37 है, और trap कॉलम सबसे ज़रूरी है। matches(), sequence expressions, if/then/else, और except सब XPath 2.0 syntax हैं, और libxml2 का 1.0 engine उन्हें आधा-सा support नहीं करता — यह XPathEvalError उठाकर साफ मना कर देता है, गलत node set चुपचाप नहीं लौटाता। यानी यह perfect score है उसे तोड़ने की कोशिश के बाद, न कि softballs से जमा किया गया score। यहाँ हर व्यवहार बिल्कुल वैसा है जैसा lxml XPath docs बताते हैं, और यही इसका point है।
एक बात मैं मानूँगा: harness ने एक जगह गलती पकड़ी, और वही 37/37 वाला version है जिस पर भरोसा किया जा सकता है। //div[.//a[@href]] के लिए मेरा पहला expected set दो hits बता रहा था; run में एक ही मिला। मैंने लगभग तीस सेकंड तक मान लिया कि lxml गलत है, फिर fixture देखा और पता चला कि दूसरा element <footer> था, <div> नहीं — गलती मेरी थी, engine की नहीं। मैंने expected set ठीक किया और comment में गलती भी रहने दी। blame का सही क्रम यही है: पहले अपने test पर शक करें, 20 साल पुरानी C library पर बाद में।
XPath बनाम CSS: CSS में आप क्या लिख ही नहीं सकते
"XPath ज़्यादा powerful है" — इस abstract दावे के लिए concrete number चाहिए, इसलिए मैंने gap को quantify किया। lxml आपको .xpath() और .cssselect() दोनों देता है (दूसरा CSS को internally XPath में translate करता है)। मैंने दस selection targets लिए और देखा कि CSS में किन्हें सचमुच express किया जा सकता है।

| Target | XPath | CSS (cssselect) |
|---|---|---|
Filter by text content (contains(text(),"bargain")) | Yes | No text predicate |
Select parent from child (//b/parent::p) | Yes | No parent selector |
Return an attribute value (//a/@href) | Yes | Elements only |
Return a text node (//p/text()) | Yes | No text nodes |
Ancestor axis (//td/ancestor::div) | Yes | No upward navigation |
Filter parent by child count (//ul[count(li)=4]) | Yes | No count predicate |
Filter by text length (string-length(text())>5) | Yes | No length predicate |
nth-child / last-child / adjacent sibling | Yes | Yes (3 baseline) |
दस में से सात targets के लिए CSS का कोई equivalent ही नहीं है। text-content filtering, parent और ancestor तक ऊपर जाना, attribute value या bare text node को result की तरह लेना, count-based predicates — CSS इनमें से कुछ भी नहीं कह सकता। सिर्फ तीन (nth-child, last-child, adjacent sibling) दोनों में काम करते हैं। यही quantified जवाब है उस सवाल का: "lxml लेने से मुझे असल में क्या मिलता है?" selectolax सिर्फ CSS तक सीमित है और उसके पास xpath() method नहीं है, इसलिए उन सात query types के लिए वहाँ या तो multi-step Python loops लिखने पड़ेंगे, या काम नहीं होगा। अगर आपकी scraping logic इनमें से किसी पर निर्भर है, तो आपका फैसला पहले ही तय हो चुका है।
(और हाँ, harness ने यहाँ मुझे दूसरी बार पकड़ा: मैंने string-length(text())>5 के लिए empty set अनुमानित किया था, लेकिन दो छह-character strings match हुए। expectation ठीक किया, tool नहीं।)
Strictness के तीन मोड: etree, recover, और lxml.html
XPath lxml चुनने की वजह है। Strictness control के तीन मोड इसकी वजह हैं कि आप इसे इस्तेमाल करते रहें।

ज़्यादातर parser खराब input के लिए सिर्फ एक behavior देते हैं। lxml तीन देता है — और वे इतने predictable हैं कि मैंने छह तरह के malformed markup को हर mode से गुज़ारा और पहले से तय किया कि हर रास्ते पर क्या होना चाहिए।
| Malformed input | lxml.etree (strict) | etree + recover=True | lxml.html (lenient) |
|---|---|---|---|
Unclosed tag <root><a>x</root> | raises | recovers | accepts |
Mis-nested <b><i></b></i> | raises | recovers | accepts |
Undefined entity | raises | recovers | accepts |
Bare & (Tom & Jerry) | raises | recovers | accepts |
Multiple roots <a>1</a><b>2</b> | raises | recovers | accepts |
| Well-formed XML | accepts | accepts (0 errors) | accepts |
Boolean attribute <input disabled> | raises | recovers | accepts |
सात में से सात cases ने pre-registered expectation से मेल खाया। lxml.etree सभी छह malformed classes पर XMLSyntaxError उठाता है। उसी parser में recover=True जोड़िए और यह errors को निगलकर usable tree बना देता है — और underrated बात यह है कि parser.error_log फिर भी हर swallowed error को सूचीबद्ध करता है। lxml.html बिना शिकायत सब कुछ स्वीकार कर लेता है।
यह तय करने वाला classifier कि "raised vs recovered vs accepted" क्या होगा, runtime error_log length पर चलता है, hard-coded नहीं है — इसलिए recover=True के तहत well-formed document को सही ढंग से "accepts" (empty log) कहा जाता है, न कि "recovers"। classifier का मेरा पहला version recover=True वाले हर result को "recovers" बता देता था और clean input को गलत label कर देता था; असली error_log पढ़ने से यह ठीक हुआ।
व्यावहारिक रूप से यह आपको क्या देता है: जहाँ broken feed को loudly fail करना चाहिए, वहाँ lxml.etree लें। गंदे real-world HTML को बस किसी तरह पढ़ लेना है, तो lxml.html लें। और वह बीच का case जो ज़्यादातर tools नहीं कर पाते — "ढील रखो, लेकिन बताओ क्या टूटा था ताकि मैं log कर सकूँ" — उसमें recover=True इस्तेमाल करें और error log पढ़ें। selectolax के पास lenient gear है, बस; strict mode नहीं, error log नहीं।
iterparse: वह streaming gear जो selectolax के पास बिल्कुल नहीं है
यह speed knob नहीं, capability line है। selectolax सिर्फ एक पूरा string ingest करता है — incremental interface नहीं है। lxml का iterparse elements को उनके close होने पर yield करता है, और classic fast_iter pattern (जाते-जाते elem.clear() कॉल करें और preceding siblings delete करें) के साथ यह document कितना भी बड़ा हो, memory को लगभग flat रखता है।

मैंने memory characteristic सीधे मापी — ru_maxrss के जरिए peak RSS, हर subject के लिए अलग fresh process, 300,000 <record> elements पर, जो कुल मिलाकर लगभग 15 MB थे।
| Mode | Peak RSS delta | Notes |
|---|---|---|
iterparse + clear (fast_iter) | ~1-2 MB | released as it goes; flat regardless of count |
iterparse without clear | ~386 MB | keeps references; as heavy as full load |
etree.parse (full load, anchor) | ~386 MB | known-heavy; proves the meter reads magnitude |
bounded mode peak RSS delta को full load के ~386 MB की तुलना में लगभग 1-2 MB तक सीमित रखता है — यानी लगभग 0.3-0.4% का अंतर — और पहला record event file पूरी तरह read होने से पहले ही आ जाता है, इसलिए यह सचमुच incremental है, नकली streaming नहीं। सबसे instructive line बीच वाली है। वही iterparse loop चलाइए लेकिन clear() छोड़ दीजिए, और memory फिर ~386 MB तक चढ़ जाती है, क्योंकि आप हर चीज़ के references पकड़े हुए हैं। जीत clear() में है, iterparse अकेले में नहीं। full-load anchor का bounded mode से काफी ऊपर रहना यह भी साबित करता है कि RSS meter सचमुच magnitude gap देख पा रहा है, अँधेरे में नहीं पढ़ रहा। (यह memory test मैंने इसी pack में किया — यह footprint measurement है, borrowed timing numbers से अलग।)
असल दुनिया में इसका मतलब: multi-gigabyte XML export जो RAM में फिट नहीं होता, उसके लिए selectolax का कोई रास्ता ही नहीं है। वहाँ या तो lxml का streaming parser है, या फिर दूसरी भाषा।
Namespaces: RSS, SVG, और default-namespace trap
बारह namespace cases, जिनमें RSS के तीन namespaces, default namespace + xlink वाला SVG, और default-namespace XML शामिल थे। बारहों पास हुए।
lxml किसी RSS feed से //dc:creator/text() को ठीक ["Alice", "Bob"] की तरह निकालता है, एक ही document में तीन अलग namespaces के across //atom:link/@href और //content:encoded को resolve करता है, SVG के दूसरे namespace में //s:rect और //s:use/@xlink:href संभालता है, Clark-notation {uri}local names को QName से split करता है, और nsmap के जरिए introspect करता है। यह documented और maintained behavior है, और यह selectolax के लिए एक पूरी dimensional gap है, क्योंकि selectolax HTML5-only है और arbitrary XML namespaces को process नहीं करता।
एक documented trap याद रखने लायक है। XPath में default namespace की कोई अवधारणा नहीं होती। xmlns="urn:..." वाले document पर //book चलाइए और zero hits मिलेंगे — XPath में empty prefix undefined है, जैसा कि lxml docs में साफ लिखा है। आपको या तो एक artificial prefix bind करना पड़ता है (//c:book के साथ namespaces={"c": "urn:..."}), जिसने तीनों मैच किए, या फिर //*[local-name()='book'] पर जाना पड़ता है, जिसने भी तीन मैच किए। यह bug नहीं है — XPath spec यही कहती है, और lxml उसे ठीक वैसे ही लागू करता है। बस हर किसी को यह एक बार चौंकाती है।
असली गंदे pages: 11 वास्तविक scrapes पर fidelity
Synthetic tests साफ-सुथरे होते हैं; web नहीं। मैंने selectolax pack के fixture set से ग्यारह असली captured pages reuse किए (as-of 2026-07-10, read-only) और lxml के subject के रूप में lxml.html को उन पर चलाया।
| Fixture | Size | Links | libxml2 recovered errors | Strict XML |
|---|---|---|---|---|
| news_bbc.html | 398 KB | 255 | 4 | ok |
| docs_mdn_array.html | 243 KB | 508 | 0 | raised |
| wiki_scraping.html | 227 KB | 460 | 0 | raised |
| gov_whitehouse.html | 289 KB | 154 | 0 | raised |
| oldstyle_craigslist.html | 561 KB | 351 | 0 | raised |
| forum_reddit.html | 129 KB | 318 | 0 | raised |
| docs_python.html | 80 KB | 341 | 2 | raised |
| ecommerce_books.html | 51 KB | 94 | 0 | raised |
| news_hackernews.html | 35 KB | 229 | 0 | raised |
| ecommerce_webscraper_allinone.html | 16 KB | 35 | 0 | raised |
| spa_quotes_js.html | 6 KB | 5 | 0 | raised |
सभी ग्यारह lxml.html से parse हुए, और link, heading, और image counts सभी ग्यारह पर selectolax pack में मौजूद lxml counts से मेल खाए — cross-check true। यही agreement बताता है कि reuse सचमुच apples-to-apples है, न कि same label के नीचे दो अलग measurements।
एक side finding यह थी: strict XML parser ने ग्यारह में से दस pages पर raise किया। असली web pages overwhelmingly well-formed XML नहीं होतीं, और इसी वजह से libxml2 का HTML recovery mode मौजूद है — ताकि वह उन्हें झेल सके। एकमात्र अपवाद BBC News था, जो Next.js द्वारा rendered था और strict XML parsing से बच निकलने लायक well-formed था। हर चीज़ जिसे "HTML" कहा जाता है, उसे recovery path की ज़रूरत नहीं होती।
एक counting note जिस पर फँसना आसान है। docs_python.html पर //a[@href] (attribute-existence) ने 343 count किया, जबकि selectolax pack के if n.get("href") (truthy value) ने 341 count किया। ये दो extra links empty href="" थे। यह counting-convention का फर्क है — attribute exists बनाम attribute non-empty — न कि lxml behavior का, और predicate align होने पर counts मिल जाते हैं। scraping में यह जानना उपयोगी है: empty href count होंगे या नहीं, यह parser का नहीं, आपके filter का फैसला है।
वह depth limit जो bug जैसी लगती है (लेकिन नहीं है)
selectolax pack में यह दर्ज था कि lxml 1,000 और 5,000 स्तर वाले nested <div> markup में सबसे गहरा content छोड़ देता है, और इसे "lxml silently loses the deepest content" की तरह पेश किया गया था। मैं mechanism जानना चाहता था, इसलिए मैंने default parser को huge_tree=True के साथ run किया।

| Requested depth | Default parser reaches | huge_tree=True reaches |
|---|---|---|
| 300 | 253 (drops rest) | 299 (recovered) |
| 1000 | 253 (drops rest) | 999 (recovered) |
| 5000 | 253 (drops rest) | 2045 (still drops) |
Default parser लगभग 253 levels पर truncate कर देता है और उससे गहरा सब कुछ चुपचाप छोड़ देता है। यह bug नहीं है — यह libxml2 की DoS defense है, लगभग 256-level nesting cap, जो hostile document को stack उड़ाने से रोकता है, और यह lxml launchpad thread में XML_PARSE_HUGE पर documented है। huge_tree=True सेट करने पर 300 और 1,000 depth पूरी तरह वापस मिल जाती है। लेकिन 5,000 depth पर भी huge_tree के साथ केवल 2,045 तक ही पहुँचा जा सका — यानी libxml2 में एक दूसरी, कड़ी recursion ceiling है जो configurable cap से ऊपर रहती है, और huge_tree उसे नहीं हटाता।
इसलिए practical action item साफ है: जब आप trusted source से deep markup parse कर रहे हों, lxml.html.HTMLParser(huge_tree=True) इस्तेमाल करें। इस pack का नया योगदान mechanism है (safety cap, data corruption नहीं), fix है (huge_tree), और यह तथ्य भी कि एक दूसरी ceiling है जिसे fix पार नहीं करता।
Read/Write DOM, Serialization, Encoding
lxml एक full read/write tree है, read-only extractor नहीं — और मैंने editing surface को case by case verify किया। सभी आठ DOM operations पास हुए: SubElement, insert, remove, replace, strip_tags (tag हटाकर text रहने देना), strip_elements (tag और उसका text दोनों हटाना), drop_tree (एक lxml.html exclusive), और text/tail dual-slot model जो newcomers को confuse करता है — <p>head<b>bold</b>tail</p> में p.text "head" होता है, b.text "bold" होता है, और b.tail "tail" होता है।
Serialization पाँच में पाँच पास हुई: XML और HTML modes में tostring (HTML void elements को सही तरह self-close नहीं करता), pretty_print, C14N canonicalization (method="c14n", एक और lxml exclusive), और clean round-trip।
Encoding वह जगह है जहाँ lxml चुपचाप खुद को अलग करता है। इसे non-UTF-8 bytes दीजिए — "<p>café éè</p>".encode("latin-1") को lxml.html.fromstring में डालिए — और यह café éè को सही-सलामत recover कर लेता है, न U+FFFD replacement characters, न dropped bytes। यह सीधे selectolax pack में इसकी भूमिका को दोहराता है, जहाँ वही input दूसरे दो engines में silently corrupt हो गया था (Lexbor ने replacement characters दिए, Modest ने bytes ही गिरा दिए)। libxml2-backed charset detection यहाँ बस अधिक stable है।
दूसरी तरफ, यह encoding को declare करने के तरीके में strict है। XML declaration में encoding="latin-1" देने पर XMLSyntaxError: Unsupported encoding: latin-1 मिलता है, जबकि IANA-canonical encoding="ISO-8859-1" ठीक parse होकर café लौटाता है। libxml2 aliases नहीं, सिर्फ canonical encoding names स्वीकार करता है — यह detail launchpad #613302 में document है। अगर यह पता न हो तो irritate करने वाली बात है, पता हो तो trivial।
आख़िर में, node lifecycle। मैंने तीन stale-handle scenarios isolated subprocesses में run किए (hard crash non-zero exit के रूप में दिखता): tree garbage-collected होने के बाद node पकड़ कर रखना, drop_tree() के बाद handle पढ़ना, और remove() के बाद node इस्तेमाल करना। इनमें से किसी में भी segfault नहीं हुआ — lxml node की tree-reference को alive रखता है ताकि use-after-free न हो। यही clean bill of health selectolax को भी इस test में मिला।
Speed और Memory (उधार ली गई, और ईमानदारी से बताई गई)
इस section की हर चीज़ selectolax pack से reuse की गई है, as-of 2026-07-13। इस pack ने अपना कोई timing number नहीं बनाया, और मैं यह बात दो बार कह देना बेहतर समझता हूँ बजाय इसके कि आपको लगे मैंने कुछ re-time किया है।
| Dimension | lxml value | Reading |
|---|---|---|
| Pure parse p50 (10 MB) | 77.9 ms | ~33-34% faster than selectolax-Lexbor |
| Full parse + extract p50 (1 MB / 10 MB) | 14.18 ms / 172.9 ms | roughly even with Lexbor at small sizes |
| 100k-node CSS throughput | 3,002,646 nodes/s | fastest tier of the three C engines |
| 10 MB RSS delta | 128.9 MB | leanest of six parsers, ~1.7x leaner than BeautifulSoup |
| Import cold start | 14.1 ms | ~2.3x faster than parsel-style imports |
pure-parse और throughput के number मज़बूत हैं, और lxml छह measured parsers में सबसे कम memory खाता है। लेकिन threading picture के साथ एक caveat ज़रूरी है। reused data दिखाती है कि 4-thread wall-clock speedup सिर्फ 1.21x था, और उसे inconclusive चिह्नित किया गया — पर वह shared-default-parser path था। lxml FAQ साफ कहती है कि parsing के दौरान GIL तभी release होता है जब हर thread अपना parser इस्तेमाल करे (या default का copied version); shared parser access को serialize कर देता है। मैंने सही तरीके से काम करने के API surface को structurally verify किया (XMLParser.copy() मौजूद है, get/set_default_parser मौजूद हैं, XPathEvaluator के पास internal lock है), लेकिन per-thread-parser speedup measure नहीं किया — वह नई timing measurement होती, और इस pack का काम वह नहीं है। इसलिए 1.21x को "naive shared path के नीचे" पढ़ें, न कि lxml की threading ceiling के रूप में।
और इन सब पर एक asterisk भी है: ये single-platform numbers हैं, macOS arm64 पर। यह दावा कि lxml का pure parse Lexbor से तेज़ है, आम consensus के उलट जाता है, जहाँ Lexbor-backed parser को सबसे तेज़ माना जाता है — इसलिए कोई भी इसे final मानने से पहले Linux x86_64 recheck चाहता है।
Licensing: वह boring लेकिन बढ़िया जीत
lxml BSD-3-Clause के तहत ship होता है, और जो C libraries यह bundle करता है — libxml2 और libxslt — वे दोनों MIT हैं। यह पूरी तरह permissive chain है, कहीं भी copyleft नहीं, और redistribution के समय यही मायने रखता है। तुलना में selectolax wheel LGPL-2.1 Modest और Apache-2.0 Lexbor bundle करता है, इसलिए closed product में shipping के लिए lxml की कहानी साफ़-सुथरी है।
एक practical install advantage भी है: lxml prebuilt wheels publish करता है जो libxml2 और libxslt को statically link करते हैं, इसलिए pip install lxml के लिए आमतौर पर आपके सिस्टम पर libxml2 या compiler की ज़रूरत नहीं पड़ती — source से build करने के मुकाबले यह अलग अनुभव है।
lxml कहाँ fit होता है — और कहाँ AI extraction layer काम संभाल लेती है
Boundary साफ कर लेते हैं, क्योंकि यहाँ category error होना आसान है। lxml एक parsing library है। यह आपको tree और शानदार query engine देता है, और उस tree के आसपास की हर चीज़ अभी भी आपकी ज़िम्मेदारी है: page fetch करना, JavaScript render करना, anti-bot defenses पार करना, XPath लिखना और संभालना, और result structure बनाना। यह hosted extraction service से अलग layer है, और दोनों rival कम, पड़ोसी ज़्यादा हैं।
जो developer fetch-render-select-maintain stack खुद नहीं चलाना चाहता, उसके लिए ऊपर वाली layer में Thunderbit जैसा कुछ बैठता है — और इस audience के लिए browser extension नहीं, बल्कि API, MCP server, और CLI ज़्यादा relevant हैं। Thunderbit Open API POST /distill देता है ताकि page को clean Markdown में बदला जा सके, और POST /extract देता है ताकि JSON Schema के आधार पर structured data निकाला जा सके, साथ में renderMode switch और volume के लिए batch jobs भी हैं। वही engine MCP server (thunderbit_suggest_fields, thunderbit_distill, thunderbit_extract) के रूप में agents और coding assistants के लिए उपलब्ध है, और CLI के रूप में भी जिसे आप सीधे terminal से npx @thunderbit/thunderbit-cli के जरिए चला सकते हैं। यह JS rendering, anti-bot, और CAPTCHAs out of the box संभालता है और schema-matched JSON लौटाता है — यानी parsing के ऊपर वाली layer, उसका replacement नहीं।
वेब डेटा एक्सट्रैक्शन के लिए Thunderbit आज़माएँ
सीधी बात यह है: जब आप pipeline खुद नियंत्रित करते हैं और tree पर surgical XPath control चाहिए, तब lxml लें। जब selectors और rendering संभालना ही नहीं चाहते, तब AI extraction API लें। कई real systems दोनों का इस्तेमाल करते हैं — controlled feeds के लिए lxml, और messy long-tail pages के लिए extraction service।
इस समीक्षा ने क्या टेस्ट नहीं किया
यह provisional review है, final scorecard नहीं — इसलिए यह भी साफ़ है कि इसमें क्या शामिल नहीं है।
सारे timing और memory figures reused हैं, single-platform हैं (macOS arm64, Python 3.14), और उस pack की caveats inherit करते हैं — "lxml pure parsing में तेज़ है" वाला result counter-consensus है और Linux x86_64 recheck मांगता है। per-thread-parser threading speedup untested है (नई timing चाहिए होती)। मैंने 300k records पर iterparse memory मापी, लेकिन GB-scale real XML पर नहीं, iterparse on HTML बनाम XML पर नहीं, और multi-hour soak पर भी नहीं। lxml का XSLT 1.0, RelaxNG / XMLSchema / DTD validation, और EXSLT extensions यहाँ पूरी तरह untested हैं — capability surface बड़ी है, लेकिन parsing-and-selection core से बाहर। मैंने 2,045 पर दूसरी depth ceiling तो देखी, लेकिन libxml2 की exact recursion constant pin down नहीं की। सिर्फ stable 6.1.1 टेस्ट हुआ, 7.0.0 alpha नहीं। Windows, source builds, और free-threaded 3.14t build — सब untested हैं। और XPath के भीतर भी, मैंने built-in functions कवर किए, लेकिन XPath variables, custom Python extension functions, या precompiled etree.XPath object reuse नहीं देखा।
अंतिम फैसला
lxml नया तेज़ tool नहीं है, और यही इसकी सिफारिश का कारण है। यह दो दशक पुराना libxml2 binding है, जिसके पास पूरा XPath 1.0 engine है — ऐसा जो mainstream Python में कोई और नहीं दे पाता — parsing strictness के तीन भरोसेमंद मोड हैं जिनके बीच error log बीच में मौजूद है, memory में न आने वाले documents के लिए असली streaming parser है, multi-namespace और encoding handling सही है, और license पूरी तरह permissive है। कुछ sharp edges — लगभग 253-level depth cap और shared-parser threading number — documented हैं, configurable हैं, और अब समझाए भी जा चुके हैं।
अगर आप अपनी scraping pipeline खुद चलाते हैं और XPath पर निर्भर हैं, तो lxml आज भी वही parser है जिसे आप चुनते हैं। अगर selectors और rendering खुद maintain नहीं करना चाहते, तो Thunderbit API, MCP, और CLI जैसी AI extraction layer उसी काम के लिए है — काम बाँटने का साफ तरीका, competition नहीं। दोनों का साथ में इस्तेमाल भी आम है। चाहे जो भी चुनें, इन numbers को provisional मानें और अपने platform पर timing दोबारा check करें, उससे पहले कि आप उन्हें किसी design doc में quote करें।
वेब डेटा एक्सट्रैक्शन के लिए Thunderbit आज़माएँ Get Started Free
FAQs
क्या lxml एक web scraper है?
नहीं। lxml एक parser और serializer है — libxml2/libxslt का Python binding, जो markup को editable और queryable tree में बदलता है। यह pages fetch नहीं करता, JavaScript render नहीं करता, और anti-bot defenses handle नहीं करता; request layer आपको देना होता है (requests, httpx, headless browser, या scraping service के जरिए), और फिर bytes lxml को देने होते हैं।
BeautifulSoup या selectolax की जगह lxml कब इस्तेमाल करना चाहिए? जब आपको XPath चाहिए, तब lxml चुनें। BeautifulSoup backend parser के रूप में lxml का उपयोग कर सकता है, लेकिन native XPath नहीं देता, और selectolax CSS-only है तथा अपने narrow niche में तेज़ है। अगर आपकी selection logic में text-content filtering, parent या ancestor navigation, attribute/text-node extraction, या count predicates चाहिए, तो lxml का XPath engine ही mainstream Python विकल्प है जो इन्हें सीधे express करता है।
lxml गहरी nesting वाला content चुपचाप क्यों हटा देता है?
इसका default parser nesting को लगभग 253 levels पर cap कर देता है — यह libxml2 की DoS defense है, bug नहीं। huge_tree=True सेट करें (उदाहरण के लिए lxml.html.HTMLParser(huge_tree=True)) और यह 300 और 1,000 depths पूरी तरह वापस ला देता है। ध्यान रहे, लगभग 2,045 levels पर एक दूसरी, कड़ी recursion ceiling भी है जिसे huge_tree नहीं हटाता।
क्या lxml multithreaded parsing में GIL छोड़ता है? सिर्फ सही परिस्थितियों में। lxml FAQ के अनुसार parsing के दौरान GIL तब release होता है जब हर thread अपना parser या copied default parser इस्तेमाल करता है; shared parser access को serial कर देता है। यहाँ reusable 4-thread speedup 1.21x था, और वह naive shared-parser path को दिखाता है, per-thread-parser ceiling को नहीं — जिसे यहाँ measure नहीं किया गया।
क्या lxml 2026 में भी maintained है? हाँ। stable release 6.1.1, 2026-05-18 को आया, repository का last push 2026-07-02 को हुआ, और 7.0.0 alpha प्रगति पर है। लगभग 3,000 GitHub stars और नीचे सक्रिय libxml2 maintenance के साथ, यह legacy नहीं बल्कि एक current, well-supported library है।


