Každých pár měsíců se objeví rychlejší HTML parser, rozjedou se benchmarky a někdo prohlásí starou gardu za zastaralou. Pak ale potřebujete vybrat každý odstavec, který obsahuje určité slovo, nebo získat rodiče nalezeného uzlu, a hned si vzpomenete, proč máte lxml pořád otevřené v dalším panelu.
lxml je 20 let starý binding na libxml2. Není okázalý. Není nový. A pro jednu konkrétní práci — cokoli, co skutečně potřebuje XPath — v mainstreamovém Pythonu prakticky nemá konkurenci. Tohle je praktická recenze toho, co umí, kde tiše vítězí a na kterých dvou místech vás jeho výchozí nastavení může nepříjemně překvapit, pokud o nich nevíte.
lxml v jedné větě: Co to vlastně je
lxml je Python binding na C knihovny libxml2 a libxslt. Je to parser a serializátor, ne scraper ani prohlížeč — vezme značky, přemění je na strom, se kterým můžete pracovat, a pak ten strom zase vrátí do bajtů. Nabízí API kompatibilní s ElementTree, plnohodnotný engine XPath 1.0, XSLT 1.0 a validaci schémat. Projekt spravuje Stefan Behnel a prezentuje ho jako "nejbohatší na funkce a nejjednodušší knihovnu pro zpracování XML a HTML v jazyce Python".
Tady je jeho pozice podle snapshotu z GitHubu a PyPI pořízeného 2026-07-14:
| Pole | Hodnota |
|---|---|
| Repo | lxml/lxml |
| Star | 3 043 |
| Forky | 620 |
| Otevřené issue | 16 |
| Licence | BSD-3-Clause |
| Vznik | 2011-02-11 |
| Poslední push | 2026-07-02 |
| Stabilní verze na PyPI | 6.1.1 (2026-05-18) |
| Přibalený engine | libxml2 2.14.6 + libxslt 1.1.43 |
Než mě někdo obviní z přehánění, jedna věc na úvod: v téhle recenzi nejsou žádná tajemství. lxml je dost starý na to, aby bylo všechno níže někde zdokumentované — v dokumentaci lxml, changelogu libxml2 nebo v nějakém vlákně na Launchpadu. Nenašel jsem žádný neveřejný ani nezdokumentovaný trik a nehodlám si ho vymýšlet. Hodnota toho, co následuje, je v tom, že je to systematizované, kvantifikované a uspořádané kolem lxml jako hlavního tématu — ne v tom, že je to zrovna novinka.
Nastavení testů (a proč jsou časová čísla vypůjčená)
Do téhle recenze vstupují dvě kategorie dat a pocházejí ze dvou různých míst, takže rovnou řeknu, co je co.
Testy schopností — chování XPath, dvě parserová API, namespace, kódování, životní cyklus uzlů — jsem spustil znovu na jednom stroji: macOS arm64, Python 3.14.2, lxml 6.1.1, libxml2 2.14.6. Každé číslo v souborech artifacts/raw/*.json je vypočtené skriptem, ne ručně zapsané. Testy schopností jsou deterministické boolean hodnoty a enumy, takže jeden běh stačí — zatížení stroje nemění, jestli //a/@href vrátí atribut jako řetězec.
Časová a paměťová čísla nejsou z tohoto balíčku. Jsou převzatá doslova z dřívějšího benchmarkového balíčku pro selectolax — stejný stroj, stejné virtuální prostředí, stejný build lxml a libxml2, benchmarky k datu 2026-07-13 — a já jsem je tady znovu nespouštěl. Je to záměr. Spouštět časové benchmarky současně s dávkou capability skriptů by zavedlo CPU contention, které by znečistilo převzatá čísla, a navíc by šlo o duplicitní práci: lxml už bylo v tom balíčku plně změřené jako kontrolní knihovna. Použití stejných benchmarků udržuje srovnání „jablka s jablky“ místo zavedení druhého, jemně odlišného měření. Takže když níže uvidíte milisekundové číslo, čtěte ho jako „stejné testovací prostředí, k datu 2026-07-13“, ne jako „měřil jsem to dnes znovu“.
Zjištění mají štítek důvěry: single-observation pro deterministické capability testy, triple-run pro převzaté distribuční časy a hypothesis tam, kde navrhuji mechanismus, který jsem neizoloval.
XPath: Jediná věc, kterou selectolax a BeautifulSoup prostě nemají
Tohle je hlavní pointa, takže začnu tady.

Prošel jsem xpath() u lxml přes 37 položek v předem zaregistrované matici — očekávaný výsledek pro každý případ byl zapsaný do zdrojáku ještě před spuštěním testu, takže jsem nemohl nechtěně „hodnotit s nadhodnocením“. Deset axes, devět stylů predikátů, deset vestavěných funkcí, tři skalární návratové typy a pět záměrných pastí využívajících syntaxi XPath 2.0, kterou by engine 1.0 v lxml měl odmítnout.
| Kategorie | Pokrytí | Výsledek |
|---|---|---|
| Axes | child / descendant / parent / ancestor / following-sibling / preceding-sibling / self / attribute / following / preceding | 10/10 prošlo |
| Predikáty | [1] / last() / position()<n / rovnost atributu / existence atributu / and / or / vnořené [.//a] / not() | 9/9 prošlo |
| Funkce | text() / contains() / starts-with() / count() / string-length() / normalize-space() / concat() / substring() / name() / string() | 10/10 prošlo |
| Návratové typy | boolean / number scalars | 3/3 prošlo |
| Pasti | matches() / sekvence / if-then-else / except / syntax error | 5/5 správně odmítnuto |
Skóre je 37/37 a právě sloupec s pastmi je ten důležitý. matches(), sekvenční výrazy, if/then/else i except jsou syntaxe XPath 2.0 a engine 1.0 v libxml2 je nepolovičně nepodporuje — místo tichého vrácení špatné sady uzlů vyhodí XPathEvalError a odmítne dotaz. To znamená perfektní výsledek po pokusu o rozbití, ne perfektní skóre poskládané z jednoduchých případů. Každé chování tady přesně odpovídá tomu, co popisuje dokumentace XPath v lxml, a o to jde.
Přiznám jednu věc, kterou harness vyhodnotil špatně, protože je to jediná verze „37/37“, které můžete věřit. Moje první očekávání pro //div[.//a[@href]] předpokládalo dva zásahy; běh ale vrátil jeden. Asi třicet sekund jsem si myslel, že lxml dělá chybu, pak jsem zkontroloval fixture a zjistil, že druhý prvek byl <footer>, ne <div> — špatně jsem měl očekávání, ne engine. Opravil jsem očekávanou množinu a chybu nechal jen v komentáři ve zdrojáku. To je správné pořadí podezřelých: nejdřív obviňte vlastní test, až potom 20 let starou C knihovnu.
XPath vs CSS: Co v CSS doslova nejde vyjádřit
Obecné tvrzení „XPath je silnější“ si zaslouží konkrétní číslo, tak jsem ten rozdíl změřil. lxml nabízí jak .xpath(), tak .cssselect() (to druhé převádí CSS na XPath pod pokličkou). Vzal jsem deset cílů výběru a zkontroloval, které z nich CSS skutečně umí vyjádřit.

| Cíl | XPath | CSS (cssselect) |
|---|---|---|
Filtrování podle textu (contains(text(),"bargain")) | Ano | Žádný textový predikát |
Výběr rodiče z potomka (//b/parent::p) | Ano | Žádný selector rodiče |
Vrácení hodnoty atributu (//a/@href) | Ano | Jen elementy |
Vrácení textového uzlu (//p/text()) | Ano | Žádné textové uzly |
Axis ancestor (//td/ancestor::div) | Ano | Žádná navigace směrem nahoru |
Filtrování rodiče podle počtu dětí (//ul[count(li)=4]) | Ano | Žádný count predikát |
Filtrování podle délky textu (string-length(text())>5) | Ano | Žádný predikát délky |
nth-child / last-child / sousední sibling | Ano | Ano (3 základní) |
Sedm z deseti cílů nemá v CSS vůbec žádný ekvivalent. Filtrování podle obsahu textu, pohyb nahoru k rodičům a předkům, získání hodnoty atributu nebo čistého textového uzlu jako výsledku, predikáty založené na počtu — CSS to neumí. Fungují jen tři (nth-child, last-child, sousední sibling) a ty navíc v obou přístupech. To je kvantifikovaná odpověď na otázku „co vlastně získám, když sáhnu po lxml“. selectolax je čistě CSS-only a nemá vůbec žádnou metodu xpath(), takže těchto sedm typů dotazů se tam buď musí přepisovat do vícekrokových Python smyček, nebo vůbec neprovede. Pokud vaše scraping logika na některém z nich stojí, je rozhodnuto.
(A ano, harness mě nachytal i podruhé: u string-length(text())>5 jsem předpověděl prázdnou množinu, ale dva šestiznakové řetězce prošly. Opravil jsem očekávání, ne nástroj.)
Tři režimy přísnosti: etree vs recover vs lxml.html
XPath je důvod, proč lxml zvolit. Tři stupně přísnosti jsou důvod, proč u něj i zůstat.

Většina parserů vám dá jedno chování pro rozbitý vstup. lxml dává tři a jsou natolik předvídatelná, že jsem přes ně poslal šest tříd nevalidního značkování a předem zaregistroval, jak se má každá cesta chovat.
| Rozbitý vstup | lxml.etree (striktní) | etree + recover=True | lxml.html (shovívavý) |
|---|---|---|---|
Neuzavřený tag <root><a>x</root> | vyhodí chybu | opraví | přijme |
Špatné zanoření <b><i></b></i> | vyhodí chybu | opraví | přijme |
Nedefinovaná entita | vyhodí chybu | opraví | přijme |
Sám o sobě ampersand & (Tom & Jerry) | vyhodí chybu | opraví | přijme |
Více rootů <a>1</a><b>2</b> | vyhodí chybu | opraví | přijme |
| Správně utvořené XML | přijme | přijme (0 chyb) | přijme |
Booleovský atribut <input disabled> | vyhodí chybu | opraví | přijme |
Sedm ze sedmi odpovědí sedělo s předem zaregistrovaným očekáváním. lxml.etree vyhazuje XMLSyntaxError u všech šesti rozbitých tříd. Když k parseru přidáte recover=True, chyby pohltí a znovu poskládá použitelný strom — a to je podceňovaná část — parser.error_log pak vypíše každou chybu, kterou potlačil. lxml.html přijme všechno bez řečí.
Klasifikátor, který rozhoduje mezi „vyhodí chybu“, „opraví“ a „přijme“, je sám řízen délkou error_log za běhu, ne natvrdo, a právě proto je dobře utvořený dokument spuštěný s recover=True správně označen jako „přijme“ (prázdný log), ne jako „opraví“. Moje první verze toho klasifikátoru označovala každý výsledek recover=True jako „opraví“ a špatně tagovala čistý vstup; čtení skutečného error_log to napravilo.
Co to znamená v praxi: pokud potřebujete přísnou validaci a rozbitý feed má selhat nahlas, použijte lxml.etree. Pro špinavé HTML z reálného webu, které prostě potřebujete projít, použijte lxml.html. A pro prostřední případ, který většina nástrojů neumí — „buď shovívavý, ale přesně mi řekni, co bylo rozbité, ať to můžu zalogovat“ — použijte recover=True a čtěte error log. selectolax má jen shovívavý režim a nic víc, žádný striktní mód ani error log.
iterparse: Streaming režim, který selectolax vůbec nemá
Tady jde o schopnost, ne o rychlostní metriku. selectolax umí jen načíst celý řetězec — žádné průběžné rozhraní nemá. lxml iterparse vrací elementy ve chvíli, kdy se uzavřou, a v kombinaci s klasickým vzorem fast_iter (průběžně volat elem.clear() a mazat předchozí sourozence) drží paměť plochou bez ohledu na velikost dokumentu.

Paměťové chování jsem změřil přímo — peak RSS přes ru_maxrss, každý subjekt ve vlastním čerstvém procesu, nad 300 000 elementy <record> o celkové velikosti asi 15 MB.
| Režim | Špičkový nárůst RSS | Poznámky |
|---|---|---|
iterparse + clear (fast_iter) | ~1–2 MB | uvolňuje průběžně; ploché bez ohledu na počet |
iterparse bez clear | ~386 MB | drží reference; paměťově stejně těžké jako plné načtení |
etree.parse (plné načtení, anchor) | ~386 MB | záměrně těžké; potvrzuje, že měřák vidí řád velikosti |
Omezený režim drží špičkový RSS nárůst zhruba na 1–2 MB oproti ~386 MB při plném načtení — rozdíl o velikosti 0,3–0,4 % — a první event s rekordem přijde ještě dřív, než se soubor dočte celý, takže jde o skutečné streamování, ne jen předstírané. Instruktivní je prostřední řádek. Spusťte stejnou smyčku iterparse, ale vynechte clear(), a paměť vyroste zpátky na ~386 MB, protože držíte reference ke všemu. Výhoda je v clear(), ne v samotném iterparse. Anchor pro plné načtení výrazně výš než omezený režim navíc potvrzuje, že RSS měřák skutečně vidí rozdíl velikosti a nečte naslepo. (Tenhle paměťový test jsem v tomhle balíčku opravdu spustil — je to měření footprintu, odlišné od převzatých časových čísel.)
Reálný důsledek: vícgigabajtový XML export, který se nevejde do RAM, nemá v selectolax žádnou cestu. Je to buď streaming parser lxml, nebo jiný jazyk.
Namespace: RSS, SVG a past s defaultním namespace
Dvanáct případů s namespace, pokrývajících RSS ve třech namespaces, SVG s defaultním namespace a xlink a XML s defaultním namespace. Všech dvanáct prošlo.
lxml vytáhne //dc:creator/text() z RSS feedu přesně jako ["Alice", "Bob"], rozpozná //atom:link/@href a //content:encoded napříč třemi oddělenými namespace v jednom dokumentu, pracuje s //s:rect a //s:use/@xlink:href ve druhém namespace SVG, rozpadá Clarkovu notaci {uri}local pomocí QName a introspektuje přes nsmap. To je dokumentované a udržované chování a je to úplně jiná dimenze, do které selectolax vůbec nesahá, protože selectolax je jen HTML5 a nepracuje s libovolnými XML namespace.
Je tu ale jedna zdokumentovaná past, kterou je dobré si zapamatovat. XPath vůbec nezná koncept defaultního namespace. Když namíříte //book na dokument, který deklaruje xmlns="urn:...", dostanete nulu zásahů — prázdný prefix je pro XPath nedefinovaný, jak popisuje dokumentace lxml. Musíte buď svázat umělý prefix (//c:book s namespaces={"c": "urn:..."}, které našlo všechny tři), nebo sáhnout po //*[local-name()='book'] (což také našlo tři). Není to bug — je to XPath spec, věrně implementovaná. Jen překvapí každého právě jednou.
Skutečně špinavé stránky: přesnost na 11 reálných scrapech
Syntetické testy jsou čisté; web ne. Znovu jsem použil jedenáct reálně zachycených stránek z fixture sady selectolax balíčku (k datu 2026-07-10, read-only) a pustil přes ně lxml.html jako hlavní předmět testu.
| Fixture | Velikost | Odkazy | libxml2 recovered errors | Strict XML |
|---|---|---|---|---|
| news_bbc.html | 398 KB | 255 | 4 | ok |
| docs_mdn_array.html | 243 KB | 508 | 0 | vyhodí chybu |
| wiki_scraping.html | 227 KB | 460 | 0 | vyhodí chybu |
| gov_whitehouse.html | 289 KB | 154 | 0 | vyhodí chybu |
| oldstyle_craigslist.html | 561 KB | 351 | 0 | vyhodí chybu |
| forum_reddit.html | 129 KB | 318 | 0 | vyhodí chybu |
| docs_python.html | 80 KB | 341 | 2 | vyhodí chybu |
| ecommerce_books.html | 51 KB | 94 | 0 | vyhodí chybu |
| news_hackernews.html | 35 KB | 229 | 0 | vyhodí chybu |
| ecommerce_webscraper_allinone.html | 16 KB | 35 | 0 | vyhodí chybu |
| spa_quotes_js.html | 6 KB | 5 | 0 | vyhodí chybu |
Všech jedenáct se načetlo přes lxml.html a počty odkazů, nadpisů i obrázků odpovídaly převzatým počtům lxml z balíčku selectolax u všech jedenácti — cross-check true. Právě to potvrzuje, že převzetí je opravdu jablka s jablky, ne dvě různá měření se stejným popiskem.
Vedlejší zjištění: striktní XML parser vyhodil chybu u deseti z jedenácti stránek. Skutečné webové stránky jsou zdaleka ne většinou dobře utvořené XML, a přesně proto existuje recovery mód libxml2, který je dokáže strávit. Jedinou výjimkou byl BBC News, renderovaný přes Next.js a dost dobře utvořený na to, aby prošel striktním XML parsingem. Ne všechno, co je označené jako „HTML“, potřebuje recovery cestu.
Jedna drobnost ke sčítání, na kterou se dá snadno nachytat. Na docs_python.html počítalo //a[@href] (existence atributu) 343, zatímco balíček selectolax s if n.get("href") (pravdivá hodnota) spočítal 341. Ty dva navíc jsou prázdné odkazy href="". To je rozdíl v konvenci počítání — atribut existuje versus atribut není prázdný — ne rozdíl v chování lxml, a počty se srovnají, jakmile sjednotíte predikát. Je dobré to vědět při scrapování: zda se prázdné href počítají, je vaše volba filtru, ne parseru.
Limit hloubky, který vypadá jako bug (ale není)
Balíček selectolax zaznamenal, že lxml na 1 000 a 5 000 úrovních vnořených <div> ztrácí nejhlubší obsah, a popsal to jako „lxml tiše ztrácí nejhlubší obsah“. Chtěl jsem znát mechanismus, tak jsem pustil výchozí parser proti huge_tree=True.

| Požadovaná hloubka | Výchozí parser dosáhne | huge_tree=True dosáhne |
|---|---|---|
| 300 | 253 (zbytek zahodí) | 299 (obnoveno) |
| 1000 | 253 (zbytek zahodí) | 999 (obnoveno) |
| 5000 | 253 (zbytek zahodí) | 2045 (stále zahazuje) |
Výchozí parser se zastaví zhruba na 253 úrovních a cokoli hlubšího tiše odřízne. To není bug — je to DoS obrana libxml2, zhruba 256úrovňový limit vnoření, který brání škodlivému dokumentu v přetečení zásobníku, a je zdokumentovaný v launchpad threadu lxml k XML_PARSE_HUGE. Když nastavíte huge_tree=True, hloubky 300 a 1 000 se vrátí celé. Hloubka 5 000 ale i s huge_tree dosáhne jen 2 045 — nad konfigurovatelným limitem je ještě druhý, tvrdší recursní strop libxml2, a huge_tree ho neodstraňuje.
Praktický závěr je jasný: když parsujete hluboké značkování ze zdroje, kterému věříte, použijte lxml.html.HTMLParser(huge_tree=True). Co tahle recenze přidává nad převzaté zjištění, je mechanismus (bezpečnostní limit, ne poškození dat), oprava (huge_tree) a fakt, že existuje ještě druhý strop, na který oprava nestačí.
DOM pro čtení i zápis, serializace, kódování
lxml je plnohodnotný strom pro čtení i zápis, ne jen read-only extraktor, a editovací rozhraní jsem ověřil případ po případu. Všech osm DOM operací prošlo: SubElement, insert, remove, replace, strip_tags (odstraní tagy, ale zachová text), strip_elements (odstraní tagy i jejich text), drop_tree (exkluzivní pro lxml.html) a model dvojího slotu text/tail, který mate nováčky — v <p>head<b>bold</b>tail</p> je p.text "head", b.text je "bold" a b.tail je "tail".
Serializace prošla pět z pěti: tostring v XML i HTML režimu (HTML správně nenechá void elementy samo-uzavřené), pretty_print, C14N canonicalizace (method="c14n", další exkluzivní funkce lxml) a čistý round-trip.
Kódování je místo, kde se lxml tiše odlišuje. Když mu dáte bajty mimo UTF-8 — například "<p>café éè</p>".encode("latin-1") přes lxml.html.fromstring — obnoví café éè beze změny, bez náhradních znaků U+FFFD a bez ztracených bajtů. To přímo odpovídá jeho roli „čisté reference“ v balíčku selectolax, kde stejný vstup u dalších dvou enginů potichu korumpoval data (Lexbor generoval náhradní znaky, Modest bajty odhazoval). lxml podložené detekcí charsetů v libxml2 je tu prostě stabilnější.
Druhá strana je přísnost v tom, jak deklarujete kódování. encoding="latin-1" v XML deklaraci vyhodí XMLSyntaxError: Unsupported encoding: latin-1, zatímco IANA-kánonické encoding="ISO-8859-1" projde a vrátí café. libxml2 přijímá jen kanonické názvy kódování, ne jejich aliasy — detail zdokumentovaný už v launchpad #613302. Otravné, pokud o tom nevíte, triviální, jakmile to víte.
A nakonec životní cyklus uzlů. Otestoval jsem tři scénáře s „mrtvými“ handly v izolovaných subprocessích (tvrdý pád by se projevil jako nenulový exit): držení uzlu po garbage collection stromu, čtení handle po drop_tree() a použití uzlu po remove(). Ani v jednom případě žádný segfault — lxml drží referenci uzlu na strom při životě, aby zabránil use-after-free. Stejné čisté vysvědčení dostal v tomto testu i selectolax.
Rychlost a paměť (vypůjčené, a poctivě přiznané)
Všechno v téhle části je převzaté z balíčku selectolax, k datu 2026-07-13. Tato recenze sama nevygenerovala žádná časová čísla a raději to řeknu dvakrát, než abyste si mysleli, že jsem něco znovu měřil.
| Dimenze | Hodnota lxml | Výklad |
|---|---|---|
| Čistý parse p50 (10 MB) | 77.9 ms | zhruba o 33–34 % rychlejší než selectolax-Lexbor |
| Plný parse + extrakce p50 (1 MB / 10 MB) | 14.18 ms / 172.9 ms | u malých velikostí přibližně na úrovni Lexbor |
| Propustnost 100k uzlů CSS | 3 002 646 uzlů/s | nejrychlejší třída ze tří C enginů |
| RSS delta na 10 MB | 128.9 MB | nejúspornější ze šesti parserů, asi 1,7× úspornější než BeautifulSoup |
| Chladný start importu | 14.1 ms | asi 2,3× rychlejší než imports ve stylu parsel |
Čísla pro čistý parse a propustnost jsou silná a lxml je z měřených šesti parserů nejúspornější na paměť. Vláknový obraz ale potřebuje poznámku. Převzatá data ukazují 4vláknové zrychlení wall-clock jen 1,21×, označené jako neprůkazné — ale to je cesta se sdíleným defaultním parserem. lxml FAQ výslovně říká, že GIL se při parsování uvolní jen tehdy, když každý thread používá vlastní parser (nebo kopii defaultního); sdílený parser přístup serializuje. Strukturálně jsem ověřil API povrch pro správný postup (XMLParser.copy() existuje, get/set_default_parser existují, XPathEvaluator má interní lock), ale nezměřil jsem zrychlení s parserem na vlákno — to by bylo nové časové měření a tento balíček ho negeneruje. Takže „1,21ד čtěte jako „naivní sdílená cesta“, ne jako strop threading schopností lxml.
A jedna hvězdička ke všemu: jsou to čísla z jedné platformy, macOS arm64. Tvrzení, že lxml v čistém parsování poráží Lexbor, jde proti běžnému konsenzu, že Lexbor-backed parser bývá nejrychlejší, takže si opravdu zaslouží ověření na Linuxu x86_64, než to někdo začne považovat za uzavřenou věc.
Licence: Nudné, ale výherní
lxml je vydáno pod BSD-3-Clause a C knihovny, které přibaluje — libxml2 a libxslt — jsou obě MIT. Je to plně permisivní řetězec bez copyleftu, což je důležité v okamžiku, kdy něco redistribuujete. Pro srovnání, wheel selectolax přibaluje LGPL-2.1 Modest a Apache-2.0 Lexbor, takže lxml má čistší příběh pro nasazení v uzavřeném produktu.
Je tu i praktická výhoda při instalaci: lxml publikuje předpřipravené wheely, které staticky linkují libxml2 a libxslt, takže pip install lxml obvykle nevyžaduje systémové libxml2 ani kompilátor na vašem stroji — jiný zážitek než build ze zdrojáků.
Kam lxml patří — a kde přebírá roli AI vrstva pro extrakci
Je čas jasně vymezit hranici, protože je snadné si splést kategorie. lxml je parser. Dá vám strom a vynikající query engine a všechno okolo toho stromu je pořád vaše práce: stáhnout stránku, vyrenderovat JavaScript, projít anti-bot ochranou, napsat a udržovat XPath a strukturovat výsledek. To je jiná vrstva než hostovaná služba pro extrakci, a ty dvě spolu nejsou spíš konkurenti než sousedé.
Pro vývojáře, který nechce vlastnit celý stack fetch-render-select-maintain, je právě v té vyšší vrstvě něco jako Thunderbit — a pro tohle publikum je důležité hlavně API, MCP server a CLI, ne browser extension. Thunderbit Open API nabízí POST /distill, které převede stránku na čistý Markdown, a POST /extract, které vytáhne strukturovaná data podle JSON Schema, s přepínačem renderMode a batch úlohami pro vyšší objem. Stejný engine je dostupný i jako MCP server (thunderbit_suggest_fields, thunderbit_distill, thunderbit_extract) pro agenty a coding asistenty a také jako CLI, které můžete spustit přímo z terminálu přes npx @thunderbit/thunderbit-cli. Zvládá JS rendering, anti-bot i CAPTCHA hned po instalaci a vrací JSON odpovídající schématu — tedy vrstvu nad parsováním, ne náhradu parsování.
Vyzkoušet Thunderbit pro extrakci webových dat
Zarámování je jednoduchá volba. Sáhněte po lxml, když vlastníte pipeline a chcete chirurgickou kontrolu XPath nad stromem, kterému rozumíte. Sáhněte po AI extrakční API, když nechcete udržovat selektory a rendering vůbec. Spousta skutečných systémů používá obojí — lxml pro strukturované feedy, které mají pod kontrolou, a extrakční službu pro nepořádné dlouhé stránky, které pod kontrolou nemají.
Co tahle recenze netestovala
Tohle je průběžná recenze, ne finální vysvědčení, takže tady je to, co nepokrývá.
Všechna časová a paměťová čísla jsou převzatá, z jedné platformy (macOS arm64, Python 3.14) a nesou omezení toho balíčku — výsledek „lxml je rychlejší v čistém parsování“ jde proti očekávání a zaslouží si kontrolu na Linuxu x86_64. Zrychlení s parserem na vlákno nebylo testované (vyžadovalo by nová měření). Měřil jsem paměť iterparse na 300k rekordů, ale ne na skutečném XML o velikosti gigabajtů, ne iterparse na HTML versus XML a ne dlouhodobý soak test. XSLT 1.0, RelaxNG / XMLSchema / DTD validace a EXSLT rozšíření lxml tu nejsou testované vůbec — je to velká schopnost, ale mimo jádro parsování a výběru. Zaznamenal jsem druhý hloubkový strop na 2 045, ale neurčil přesnou konstantu rekurze libxml2. Testovala se jen stabilní verze 6.1.1, ne alfa 7.0.0. Windows, build ze zdrojů a free-threaded 3.14t build zůstaly netestované. A v rámci samotného XPath jsem pokryl vestavěné funkce, ale ne XPath proměnné, vlastní Python extension funkce ani opakované použití předkompilovaných objektů etree.XPath.
Verdikt
lxml není ta rychlá nová věc, a přesně proto je doporučení kladné. Je to dvacet let starý binding na libxml2 s plným enginem XPath 1.0, který nemá v mainstreamovém Pythonu rovnocennou alternativu, se třemi předvídatelnými režimy přísnosti parsování a error logem uprostřed, se skutečným streaming parserem pro dokumenty, které se nevejdou do paměti, se správnou podporou více namespace i kódování a s plně permisivní licencí. Pár ostrých hran — limit hloubky kolem 253 úrovní a číslo pro threading se sdíleným parserem — je zdokumentovaných, konfigurovatelných a teď i vysvětlených.
Když vlastníte scraping pipeline a spoléháte na XPath, lxml je pořád parser, po kterém sáhnete. Když nechcete udržovat selektory a rendering, od toho je AI vrstva pro extrakci jako Thunderbit API, MCP a CLI — čisté rozdělení práce, ne soupeření. V obou případech berte tato čísla jako průběžná a před citováním do návrhového dokumentu si je na vlastní platformě znovu ověřte.
Vyzkoušet Thunderbit pro extrakci webových dat Get Started Free
Často kladené otázky
Je lxml web scraper?
Ne. lxml je parser a serializátor — Python binding na libxml2/libxslt, který přemění značky na strom, se kterým lze editovat a dotazovat. Nestahuje stránky, nevyrenderuje JavaScript ani neřeší anti-bot ochranu; síťovou vrstvu dodáte vy (přes requests, httpx, headless browser nebo scraping službu) a bajty pak předáte do lxml.
Kdy mám použít lxml místo BeautifulSoup nebo selectolax? Sáhněte po lxml, když potřebujete XPath. BeautifulSoup sice může používat lxml jako backend parser, ale nenabízí nativní XPath, a selectolax je čistě CSS-only a ve své úzké nice rychlejší. Pokud vaše selekce potřebují filtrování podle textu, navigaci na rodiče nebo předky, extrakci atributů nebo textových uzlů nebo predikáty podle počtu, je XPath engine v lxml jediná mainstreamová možnost v Pythonu, která to umí přímo vyjádřit.
Proč lxml tiše zahazuje hluboce zanořený obsah?
Výchozí parser omezuje zanoření na zhruba 253 úrovní — je to obrana libxml2 proti škodlivým dokumentům, ne bug. Nastavte huge_tree=True (například lxml.html.HTMLParser(huge_tree=True)) a hloubky 300 i 1 000 obnoví celé. Pozor ale na druhý, tvrdší strop kolem 2 045 úrovní, který huge_tree neodstraní.
Uvolňuje lxml GIL při vícevláknovém parsování? Jen za správných podmínek. FAQ lxml říká, že GIL se při parsování uvolní, když každý thread používá vlastní parser nebo kopii defaultního parseru; sdílený parser místo toho přístup serializuje. Převzaté 4vláknové zrychlení 1,21× odpovídá naivní cestě se sdíleným parserem, ne stropu pro parser na vlákno, který tu nebyl měřen.
Je lxml v roce 2026 stále aktivně udržované? Ano. Stabilní verze 6.1.1 vyšla 2026-05-18, repozitář měl poslední push 2026-07-02 a ve vývoji je alfa 7.0.0. S přibližně 3 000 hvězdičkami na GitHubu a aktivně spravovaným libxml2 pod kapotou jde pořád o aktuální a dobře podporovanou knihovnu, ne o mrtvé dědictví.


