lxml, na warsztacie: silnik XPath, który wciąż przewyższa większość parserów Pythona

Ostatnia aktualizacja: August 12, 2026
lxml, na warsztacie: silnik XPath, który wciąż przewyższa większość parserów Pythona
Podsumowanie AI
Ten przegląd lxml przedstawia bibliotekę jako długoletnie powiązanie Pythona z libxml2 i libxslt, którego największą przewagą nad nowszymi parserami jest wciąż prawdziwy silnik XPath. Artykuł testuje pokrycie XPath, tryby rygoru parsera, zachowanie pamięci w trybie strumieniowym, wyrazistość XPath względem CSS oraz limity głębokości libxml2. Pokazuje lxml jako szybkie, oszczędne pamięciowo i wyjątkowo wszechstronne narzędzie do XML i HTML, gdy potrzebujesz osi, predykatów, funkcji, streamingu lub solidnych trybów recovery. Przegląd wyjaśnia też domyślne ograniczenia głębokości ustawione z myślą o bezpieczeństwie oraz to, kiedy huge_tree zmienia ten próg.

Co kilka miesięcy pojawia się szybszy parser HTML, rankingi krążą po sieci, a ktoś ogłasza, że starzy wyjadacze są już przestarzali. A potem próbujesz zaznaczyć każdy akapit, który zawiera konkretne słowo, albo pobrać element nadrzędny dopasowanego węzła i przypominasz sobie, dlaczego lxml wciąż masz otwarte w innej karcie.

lxml to 20-letnie powiązanie do libxml2. Nie jest efektowne. Nie jest nowe. A do jednego konkretnego zadania — wszystkiego, co wymaga prawdziwego XPath — w głównym nurcie Pythona nikt realnie z nim nie konkuruje. To praktyczny przegląd tego, co potrafi, gdzie po cichu wygrywa i w jakich dwóch miejscach jego domyślne ustawienia mogą ugryźć, jeśli nie wiesz, że tam są.

lxml w jednym akapicie: czym tak naprawdę jest

lxml to biblioteka Pythona będąca powiązaniem do bibliotek C libxml2 i libxslt. To parser i serializer, nie scraper i nie przeglądarka — zamienia markup w drzewo, które można przeszukiwać i edytować, a potem z powrotem w bajty. Oferuje API zgodne z ElementTree, pełny silnik XPath 1.0, XSLT 1.0 i walidację schematów, utrzymywane przez Stefana Behnel’a pod hasłem „the most feature-rich and easy-to-use library for processing XML and HTML in the Python language”.

Tak wyglądał jego stan według zrzutu z GitHuba i PyPI pobranego 2026-07-14:

PoleWartość
Repolxml/lxml
Gwiazdki3,043
Forki620
Otwarte zgłoszenia16
LicencjaBSD-3-Clause
Utworzono2011-02-11
Ostatni push2026-07-02
Stabilna wersja PyPI6.1.1 (2026-05-18)
Dołączony silniklibxml2 2.14.6 + libxslt 1.1.43

Jedna rzecz na początek, żeby nikt nie oskarżył mnie o hype: w tym przeglądzie nie ma żadnych sekretów. lxml jest wystarczająco stary, by każde zachowanie opisano gdzieś w dokumentacji lxml, changelogu libxml2 albo w wątku na launchpadzie. Nie znalazłem żadnego ukrytego, nieudokumentowanego triku i nie zamierzam go wymyślać. Wartość poniższego tekstu polega na tym, że porządkuje fakty, nadaje im liczby i układa je wokół lxml jako tematu — a nie na tym, że przynosi sensację.

Środowisko testowe (i dlaczego liczby czasowe są zapożyczone)

Do tego przeglądu wchodzą dwie kategorie danych i pochodzą z dwóch różnych miejsc, więc od razu zaznaczam, co jest czym.

Testy możliwości — zachowanie XPath, dwa API parserów, przestrzenie nazw, kodowanie, cykl życia węzłów — uruchomiłem od nowa na jednej maszynie: macOS arm64, Python 3.14.2, lxml 6.1.1, libxml2 2.14.6. Każda liczba w plikach artifacts/raw/*.json jest wynikiem skryptu, a nie wpisem ręcznym. Testy możliwości są deterministyczne, więc pojedyncze uruchomienie jest stabilne — obciążenie maszyny nie zmienia tego, czy //a/@href zwróci ciąg atrybutu.

Liczby czasu i zużycia pamięci nie pochodzą z tego pakietu. Zostały bez zmian zapożyczone z wcześniejszego zestawu benchmarków selectolax — ta sama maszyna, to samo środowisko wirtualne, ten sam build lxml i libxml2, benchmarki według stanu na 2026-07-13 — i nie uruchamiałem ich tutaj ponownie. To celowe. Ponowne uruchamianie benchmarków czasowych równolegle z zestawem skryptów sprawdzających możliwości wprowadziłoby rywalizację o CPU i zanieczyściło zapożyczone wyniki, a poza tym byłaby to podwójna robota: lxml był już w tamtym pakiecie w pełni zmierzoną biblioteką referencyjną. Użycie tych samych pomiarów sprawia, że porównanie pozostaje uczciwe, zamiast wprowadzać drugi, subtelnie inny zestaw danych. Dlatego gdy zobaczysz poniżej liczbę w milisekundach, czytaj ją jako „to samo środowisko testowe, stan na 2026-07-13”, a nie „dziś to mierzyłem od nowa”.

Wnioski mają etykiety pewności: single-observation dla deterministycznych testów możliwości, triple-run dla zapożyczonych rozkładów czasowych, hypothesis tam, gdzie proponuję mechanizm, którego nie izolowałem.

XPath: jedyna rzecz, której selectolax i BeautifulSoup po prostu nie mają

To jest główna teza, więc od niej zaczynam.

lxml XPath coverage moat with axes predicates and functions

Przepuściłem xpath() w lxml przez 37-elementową, wcześniej zarejestrowaną matrycę — oczekiwany wynik dla każdego przypadku był zapisany w źródle jeszcze przed uruchomieniem testu, więc nie mogłem niechcący oceniać „na wyrost”. Dziesięć osi, dziewięć stylów predykatów, dziesięć wbudowanych funkcji, trzy typy zwracanych skalarów i pięć celowych pułapek z użyciem składni dostępnej tylko w XPath 2.0, którą silnik 1.0 lxml powinien odrzucić.

KategoriaZakresWynik
Osiechild / descendant / parent / ancestor / following-sibling / preceding-sibling / self / attribute / following / preceding10/10 pass
Predykaty[1] / last() / position()<n / równość atrybutu / istnienie atrybutu / and / or / zagnieżdżone [.//a] / not()9/9 pass
Funkcjetext() / contains() / starts-with() / count() / string-length() / normalize-space() / concat() / substring() / name() / string()10/10 pass
Typy zwracanewartości logiczne / skalarne liczby3/3 pass
Pułapkimatches() / sekwencje / if-then-else / except / błąd składni5/5 poprawnie odrzucone

Wynik to 37/37, a kolumna pułapek jest najważniejsza. matches(), wyrażenia sekwencji, if/then/else i except to składnia XPath 2.0, a silnik 1.0 w libxml2 nie wspiera ich „połowicznie” — zamiast zwrócić zły zestaw węzłów, zgłasza XPathEvalError i odmawia wykonania. To więc wynik idealny po próbie złamania silnika, a nie wynik zbudowany na łatwych przypadkach. Każde z tych zachowań odpowiada dokładnie temu, co opisuje dokumentacja XPath lxml, i właśnie o to chodzi.

Przyznam też jedną rzecz: generator testów popełnił błąd, ale to ten rodzaj błędu, któremu można ufać, bo został poprawiony. Mój pierwszy oczekiwany zbiór dla //div[.//a[@href]] przewidywał dwa trafienia; wynik dał jedno. Przez około trzydzieści sekund uważałem, że lxml się myli, po czym sprawdziłem fixture i okazało się, że drugi element był <footer>, a nie <div> — to moje założenie było błędne, nie silnik. Poprawiłem oczekiwany zbiór i zostawiłem komentarz o pomyłce w źródle. To właściwa kolejność obwiniania: najpierw podejrzewaj własny test, a dopiero potem 20-letnią bibliotekę C.

XPath kontra CSS: czego dosłownie nie da się wyrazić w CSS

Ogólne stwierdzenie „XPath jest mocniejszy” zasługuje na konkretną liczbę, więc policzyłem różnicę. lxml daje zarówno .xpath(), jak i .cssselect() (to drugie tłumaczy CSS na XPath pod spodem). Wziąłem dziesięć celów selekcji i sprawdziłem, które z nich CSS potrafi rzeczywiście opisać.

XPath expresses seven of ten tasks CSS cannot express

CelXPathCSS (cssselect)
Filtrowanie po treści tekstu (contains(text(),"bargain"))TakBrak predykatu tekstowego
Wybór rodzica z dziecka (//b/parent::p)TakBrak selektora rodzica
Zwrócenie wartości atrybutu (//a/@href)TakTylko elementy
Zwrócenie węzła tekstowego (//p/text())TakBrak węzłów tekstowych
Oś ancestor (//td/ancestor::div)TakBrak nawigacji w górę
Filtrowanie rodzica po liczbie dzieci (//ul[count(li)=4])TakBrak predykatu zliczającego
Filtrowanie po długości tekstu (string-length(text())>5)TakBrak predykatu długości
nth-child / last-child / sąsiedni elementTakTak (3 podstawowe)

Siedem z dziesięciu celów nie ma żadnego odpowiednika w CSS. Filtrowanie po treści, przechodzenie w górę do rodziców i przodków, pobieranie wartości atrybutu lub surowego węzła tekstowego jako wyniku, predykaty oparte na liczbie elementów — CSS po prostu tego nie potrafi. Tylko trzy (nth-child, last-child, sąsiadujący element) działają w obu. To jest policzona odpowiedź na pytanie „co właściwie zyskuję, sięgając po lxml”. selectolax działa wyłącznie na CSS i w ogóle nie ma metody xpath(), więc te siedem typów zapytań albo staje się tam wieloetapową pętlą Pythona, albo w ogóle nie dochodzi do skutku. Jeśli Twoja logika scrapingu na nich bazuje, decyzja jest już podjęta.

(I tak, tester złapał mnie tu po raz drugi: przewidywałem pusty zbiór dla string-length(text())>5, a dopasowały się dwa sześcioznakowe ciągi. Poprawiłem oczekiwanie, nie narzędzie.)

Trzy tryby rygoru: etree vs recover vs lxml.html

XPath jest powodem, dla którego warto wybrać lxml. Trójstopniowa kontrola rygoru jest powodem, żeby zostać przy tej bibliotece.

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

Większość parserów daje jedno zachowanie dla uszkodzonych danych. lxml daje trzy i są wystarczająco przewidywalne, że przepuściłem przez każdy z nich sześć klas niepoprawnego markup’u, wcześniej rejestrując, jak ma się zachować każdy wariant.

Uszkodzony inputlxml.etree (strict)etree + recover=Truelxml.html (lenient)
Niezamknięty tag <root><a>x</root>rzuca wyjątekodzyskujeakceptuje
Źle zagnieżdżone <b><i></b></i>rzuca wyjątekodzyskujeakceptuje
Niezdefiniowana encja &nbsp;rzuca wyjątekodzyskujeakceptuje
Samo & (Tom & Jerry)rzuca wyjątekodzyskujeakceptuje
Wiele korzeni <a>1</a><b>2</b>rzuca wyjątekodzyskujeakceptuje
Poprawny XMLakceptujeakceptuje (0 błędów)akceptuje
Atrybut logiczny <input disabled>rzuca wyjątekodzyskujeakceptuje

Siedem na siedem zgadza się z wcześniej zarejestrowanym oczekiwaniem. lxml.etree zgłasza XMLSyntaxError dla wszystkich sześciu klas błędów. Dodaj recover=True do tego samego parsera, a biblioteka połyka błędy i rekonstruuje użyteczne drzewo — i to jest niedoceniana część — parser.error_log wylicza potem każdy z połkniętych błędów. lxml.html akceptuje wszystko bez protestu.

Klasyfikator, który decyduje o tym, czy wynik to „rzuca wyjątek”, „odzyskuje” czy „akceptuje”, opiera się na runtime’owej długości error_log, a nie na twardo wpisanej regule, dlatego poprawnie oznacza poprawny dokument uruchomiony z recover=True jako „akceptuje” (pusty log), a nie „odzyskuje”. Pierwsza wersja tego klasyfikatora traktowała każdy wynik z recover=True jako „odzyskuje” i błędnie oznaczała czysty input; odczyt rzeczywistego error_log to naprawił.

Co to daje w praktyce: gdy potrzebujesz twardej walidacji i uszkodzony feed ma po prostu się wywalić — użyj lxml.etree. Gdy masz brudny, realny HTML, który po prostu trzeba przepuścić — użyj lxml.html. A dla środka, którego większość narzędzi nie obsługuje: „bądź pobłażliwy, ale powiedz mi dokładnie, co było zepsute, żebym mógł to zalogować” — użyj recover=True i czytaj log błędów. selectolax ma tylko tryb pobłażliwy i nic więcej: bez trybu ścisłego i bez logu błędów.

iterparse: tryb strumieniowy, którego selectolax w ogóle nie ma

To jest cecha, nie gałka wydajności. selectolax wczytuje tylko cały string — nie ma interfejsu inkrementalnego. iterparse w lxml zwraca elementy w momencie ich zamykania, a w połączeniu z klasycznym wzorcem fast_iter (wołasz elem.clear() i usuwasz poprzednich sąsiadów w trakcie) utrzymuje pamięć na stałym poziomie niezależnie od rozmiaru dokumentu.

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

Zmierzylem to bezpośrednio — maksymalny RSS przez ru_maxrss, każdy wariant w osobnym świeżym procesie, na 300,000 elementach <record> o łącznym rozmiarze około 26.7 MB (26,744,801 bajtów).

TrybPrzyrost maks. RSSUwagi
iterparse + clear (fast_iter)~1-2 MBpamięć zwalniana w locie; płasko niezależnie od liczby rekordów
iterparse bez clear~386 MBtrzyma referencje; równie ciężki jak pełne wczytanie
etree.parse (pełne wczytanie, punkt odniesienia)~386 MBciężki, ale pokazuje skalę pomiaru

Tryb ograniczony utrzymuje przyrost maks. RSS na poziomie około 1-2 MB wobec ~386 MB dla pełnego wczytania — to różnica rzędu 0,3-0,4% — a pierwsze zdarzenie rekordu pojawia się jeszcze przed zakończeniem odczytu pliku, więc to naprawdę jest przetwarzanie strumieniowe, a nie udawany streaming. Najbardziej pouczająca jest linia środkowa. Uruchom ten sam loop iterparse, ale pomiń clear(), a pamięć znów dobija do ~386 MB, bo trzymasz referencje do wszystkiego. Zysk siedzi w clear(), nie w samym iterparse. To, że pełne wczytanie jest dużo cięższe niż tryb ograniczony, potwierdza też, że licznik RSS rzeczywiście widzi różnicę skali, a nie mierzy „na ślepo”. (Ten test pamięci wykonałem w tym pakiecie — to pomiar footprintu, odrębny od zapożyczonych liczb czasowych.)

W praktyce oznacza to tyle: wielogigabajtowy eksport XML, który nie mieści się w RAM, nie ma ścieżki w selectolax. Zostaje parser strumieniowy lxml albo inny język.

Przestrzenie nazw: RSS, SVG i pułapka domyślnej przestrzeni

Dwanaście przypadków przestrzeni nazw: RSS przez trzy namespace’y, SVG z domyślną przestrzenią i xlink oraz XML z domyślną przestrzenią. Wszystkie dwanaście przeszły.

lxml wyciąga z feedu RSS //dc:creator/text() dokładnie jako ["Alice", "Bob"], rozwiązuje //atom:link/@href i //content:encoded przez trzy oddzielne przestrzenie nazw w tym samym dokumencie, obsługuje //s:rect i //s:use/@xlink:href w drugiej przestrzeni SVG, rozdziela nazwy Clark notation {uri}local przez QName i pozwala introspekcję przez nsmap. To jest zachowanie utrzymywane i opisane w dokumentacji — i cały wymiar, którego selectolax nie dotyka, bo jest wyłącznie HTML5 i nie przetwarza dowolnych przestrzeni nazw XML.

Jest jednak jedna udokumentowana pułapka, którą warto zapamiętać. XPath nie ma pojęcia o domyślnej przestrzeni nazw. Jeśli wskażesz //book dokumentowi deklarującemu xmlns="urn:...", dostaniesz zero trafień — pusty prefiks jest w XPath niezdefiniowany, co dokumentacja lxml wyjaśnia wprost. Trzeba związać sztuczny prefiks (//c:book z namespaces={"c": "urn:..."}, co znalazło wszystkie trzy) albo użyć //*[local-name()='book'] (też trzy). To nie błąd — tak mówi specyfikacja XPath i lxml wiernie ją implementuje. Po prostu zaskakuje każdego dokładnie raz.

Prawdziwy brudny web: zgodność na 11 rzeczywistych scrapach

Testy syntetyczne są czyste; web nie jest. Ponownie wykorzystałem jedenaście prawdziwych, zebranych stron z zestawu fixture’ów pakietu selectolax (stan na 2026-07-10, tylko do odczytu) i przepuściłem je przez lxml.html, traktując lxml jako główny obiekt testu.

FixtureRozmiarLinkiodzyskane błędy libxml2Strict XML
news_bbc.html398 KB2554ok
docs_mdn_array.html243 KB5080raised
wiki_scraping.html227 KB4600raised
gov_whitehouse.html289 KB1540raised
oldstyle_craigslist.html561 KB3510raised
forum_reddit.html129 KB3180raised
docs_python.html80 KB3412raised
ecommerce_books.html51 KB940raised
news_hackernews.html35 KB2290raised
ecommerce_webscraper_allinone.html16 KB350raised
spa_quotes_js.html6 KB50raised

Wszystkie jedenaście dokumentów sparsowało się przez lxml.html, a liczby linków, nagłówków i obrazów zgadzały się z ponownie wykorzystanymi liczbami z pakietu selectolax we wszystkich jedenastu przypadkach — cross-check true. To właśnie mówi mi, że reuse jest faktycznie porównaniem „jabłka do jabłka”, a nie dwoma różnymi pomiarami z tą samą etykietą.

Poboczny wniosek: strict XML parser zgłosił błąd na dziesięciu z jedenastu stron. Prawdziwe strony WWW są w przeważającej mierze niepoprawnym XML-em, i dokładnie dlatego istnieje tryb odzyskiwania libxml2, żeby je przełknąć. Jedyny wyjątek to BBC News, renderowany przez Next.js i wystarczająco poprawny, by przejść przez strict XML. Nie wszystko, co nazywa się „HTML”, wymaga trybu recovery.

Jest jeszcze jedna rzecz związana z liczeniem, na którą łatwo się nadziać. W docs_python.html //a[@href] (istnienie atrybutu) dało 343, podczas gdy pakiet selectolax liczył przez if n.get("href") (wartość prawdziwa) i dał 341. Te dwa dodatkowe trafienia to puste linki href="". To różnica w konwencji liczenia — atrybut istnieje kontra atrybut jest niepusty — a nie różnica w zachowaniu lxml; liczby się zgadzają, gdy wyrównasz predykat. Warto o tym pamiętać przy scrapingu: to, czy puste hrefy liczą się do wyniku, zależy od filtra, nie od parsera.

Limit głębokości, który wygląda jak błąd, ale nim nie jest

W pakiecie selectolax odnotowano, że lxml gubi najgłębszą treść przy znacznikach <div> zagnieżdżonych na 1,000 i 5,000 poziomach, opisując to jako „lxml po cichu traci najgłębszą zawartość”. Chciałem poznać mechanizm, więc uruchomiłem parser domyślny oraz huge_tree=True.

lxml default depth guard around 253 levels and huge_tree to 2045

Żądana głębokośćDomyślny parser osiągahuge_tree=True osiąga
300253 (reszta odpada)299 (odzyskane)
1000253 (reszta odpada)999 (odzyskane)
5000253 (reszta odpada)2045 (wciąż odpada)

Domyślny parser ucina się przy około 253 poziomach i po cichu wyrzuca wszystko głębiej. To nie błąd — to zabezpieczenie libxml2 przed DoS, około 256-poziomowy limit zagnieżdżenia, który powstrzymuje złośliwy dokument przed wysadzeniem stosu, i jest to opisane w wątku launchpada lxml dotyczącym XML_PARSE_HUGE. Ustaw huge_tree=True, a głębokości 300 i 1,000 wracają w całości. Przy 5,000 poziomach huge_tree nadal dochodzi tylko do 2,045 — istnieje drugi, twardszy limit rekurencji w libxml2, powyżej tego konfigurowalnego, i huge_tree go nie znosi.

Wniosek jest więc bardzo konkretny: gdy parsujesz bardzo głęboki markup ze źródła, któremu ufasz, użyj lxml.html.HTMLParser(huge_tree=True). To, co ten pakiet dodaje do wcześniejszej obserwacji, to mechanizm (limit bezpieczeństwa, nie uszkodzenie danych), poprawka (huge_tree) i fakt, że istnieje drugi sufit, którego ta poprawka nie dosięga.

DOM do odczytu i zapisu, serializacja, kodowanie

lxml to pełne drzewo do odczytu i zapisu, nie tylko extractor do odczytu, a powierzchnię edycji sprawdziłem przypadek po przypadku. Wszystkie osiem operacji DOM przeszły: SubElement, insert, remove, replace, strip_tags (usuwa tagi, zostawia tekst), strip_elements (usuwa tagi i ich tekst), drop_tree (specyficzne dla lxml.html) oraz model dwóch slotów dla text i tail, który zaskakuje nowicjuszy — w <p>head<b>bold</b>tail</p> p.text to "head", b.text to "bold", a b.tail to "tail".

Serializacja przeszła pięć na pięć: tostring w trybie XML i HTML (HTML poprawnie nie samodomyka pustych elementów), pretty_print, canonicalizacja C14N (method="c14n", kolejna funkcja charakterystyczna dla lxml) oraz czysty round-trip.

Kodowanie to miejsce, w którym lxml dyskretnie odcina się od konkurencji. Podaj mu bajty w kodowaniu innym niż UTF-8 — "<p>café éè</p>".encode("latin-1") przez lxml.html.fromstring — a odzyskuje café éè bez zniekształceń, bez znaków zastępczych U+FFFD i bez gubienia bajtów. To bezpośrednio odtwarza jego rolę jako „czystego punktu odniesienia” w pakiecie selectolax, gdzie ten sam input u dwóch innych silników cicho się psuł (Lexbor produkował znaki zastępcze, Modest gubił bajty). Obsługa wykrywania zestawu znaków w oparciu o libxml2 jest tu po prostu stabilniejsza.

Druga strona medalu to rygor dotyczący tego, jak deklarujesz kodowanie. encoding="latin-1" w deklaracji XML zgłasza XMLSyntaxError: Unsupported encoding: latin-1, natomiast kanoniczne IANA encoding="ISO-8859-1" parsuje się poprawnie i zwraca café. libxml2 akceptuje tylko kanoniczne nazwy kodowań, nie aliasy — detal opisany już dawno w launchpad #613302. Denerwujące, jeśli się tego nie wie; banalne, kiedy już się wie.

Na koniec cykl życia węzła. Uruchomiłem trzy scenariusze z wiszącymi uchwytami w izolowanych procesach potomnych (twardy crash pokazałby się jako niezerowy exit): trzymanie węzła po zebraniu odśmiecenia jego drzewa, odczyt uchwytu po drop_tree() i użycie węzła po remove(). W żadnym przypadku nie było segfaulta — lxml utrzymuje referencję węzła do jego drzewa, by zapobiec use-after-free. Taki sam czysty wynik selectolax dostał w tym teście.

Szybkość i pamięć (zapożyczone, ale uczciwie opisane)

Wszystko w tej sekcji zostało zapożyczone z pakietu selectolax, stan na 2026-07-13. Ten pakiet nie wygenerował własnych liczb czasowych i wolę powiedzieć to dwa razy, niż żebyś pomyślał, że cokolwiek mierzyłem od nowa.

WymiarWartość lxmlOdczyt
Czysty parse p50 (10 MB)77.9 ms~33-34% szybciej niż selectolax-Lexbor
Pełny parse + extract p50 (1 MB / 10 MB)14.18 ms / 172.9 msmniej więcej remis z Lexbor przy małych rozmiarach
Przepustowość 100k węzłów CSS3,002,646 węzłów/snajszybsza półka spośród trzech silników C
Przyrost RSS dla 10 MB128.9 MBnajoszczędniejszy z sześciu parserów, ~1.7x lżej niż BeautifulSoup
Ciepły start importu14.1 ms~2.3x szybciej niż importy w stylu parsel

Wyniki czystego parsowania i przepustowości są mocne, a lxml jest najbardziej oszczędny pamięciowo spośród sześciu mierzonych parserów. Obraz wielowątkowości wymaga jednak zastrzeżenia. Zapożyczone dane pokazują przyspieszenie przy 4 wątkach na poziomie tylko 1.21x, oznaczone jako niejednoznaczne — ale to ścieżka z jednym współdzielonym parserem. FAQ lxml jasno mówi, że GIL jest zwalniany podczas parsowania tylko wtedy, gdy każdy wątek używa własnego parsera (albo skopiowanego domyślnego); współdzielony parser serializuje dostęp. Strukturalnie potwierdziłem dostępność API potrzebnego do zrobienia tego poprawnie (XMLParser.copy() istnieje, get/set_default_parser istnieją, XPathEvaluator ma wewnętrzny lock), ale nie mierzyłem wzrostu wydajności przy parserze per wątek — to byłby nowy pomiar czasowy, a ten pakiet takich danych nie tworzy. Dlatego 1.21x czytaj jako „w naiwnym, współdzielonym wariancie”, a nie jako limit wielowątkowości lxml.

I jedna gwiazdka do całego tego zestawu: to są liczby z jednej platformy, macOS arm64. Twierdzenie, że czysty parse lxml przebija Lexbor, stoi w lekkim sporze z ogólną zgodą, że parser oparty na Lexbor zwykle jest najszybszy, więc naprawdę wymaga ponownego sprawdzenia na Linux x86_64, zanim ktoś uzna to za ostatecznie rozstrzygnięte.

Licencja: nudne zwycięstwo

lxml jest wydawany na licencji BSD-3-Clause, a dołączone biblioteki C — libxml2 i libxslt — są obie na MIT. To w pełni liberalny łańcuch bez żadnego copyleft, co ma znaczenie w chwili redystrybucji. Dla porównania, wheel selectolax dołącza LGPL-2.1 Modest i Apache-2.0 Lexbor, więc lxml jest prostszą historią, jeśli chcesz go dostarczać w produkcie zamkniętym.

Jest też praktyczna korzyść instalacyjna: lxml publikuje gotowe wheel’e, które statycznie linkują libxml2 i libxslt, więc pip install lxml zwykle nie wymaga systemowego libxml2 ani kompilatora na Twojej maszynie — to inne doświadczenie niż budowanie ze źródeł.

Gdzie lxml pasuje — a gdzie przejmuje warstwę ekstrakcji AI

Czas jasno określić granicę, bo łatwo tu o błąd kategorii. lxml jest biblioteką do parsowania. Daje Ci drzewo i świetny silnik zapytań, a wszystko wokół tego drzewa nadal jest Twoim zadaniem: pobranie strony, renderowanie JavaScriptu, przejście przez zabezpieczenia anty-bot, napisanie i utrzymanie XPath oraz ustrukturyzowanie wyniku. To inna warstwa niż hostowana usługa ekstrakcji i te dwa światy nie są konkurentami tak bardzo, jak są sąsiadami.

Dla dewelopera, który woli nie utrzymywać całego stacku fetch-render-select-maintain, wyższa warstwa to miejsce na coś takiego jak Thunderbit — i dla tej grupy ważniejsze są API, serwer MCP i CLI, a nie rozszerzenie przeglądarki. Thunderbit Open API udostępnia POST /distill, które zamienia stronę w czysty Markdown, oraz POST /extract, które wyciąga dane strukturalne według JSON Schema, z przełącznikiem renderMode i zadaniami wsadowymi dla większej skali. Ten sam silnik jest dostępny jako serwer MCP (thunderbit_suggest_fields, thunderbit_distill, thunderbit_extract) dla agentów i asystentów programistycznych oraz jako CLI uruchamiane bezpośrednio z terminala przez npx @thunderbit/thunderbit-cli. Obsługuje renderowanie JS, anty-bota i CAPTCHA od ręki oraz zwraca JSON zgodny ze schematem — czyli jest warstwą nad parsowaniem, a nie jego zamiennikiem.

Wypróbuj Thunderbit do ekstrakcji danych z sieci

Logika wyboru jest prosta. Sięgnij po lxml, jeśli kontrolujesz pipeline i chcesz chirurgicznej precyzji XPath nad drzewem, które rozumiesz. Sięgnij po API ekstrakcji AI, jeśli w ogóle nie chcesz utrzymywać selektorów i renderowania. Wiele prawdziwych systemów używa obu narzędzi: lxml dla kontrolowanych, strukturalnych feedów, a usługi ekstrakcyjnej dla brudnego long taila stron, których nie kontrolują.

Czego ten przegląd nie testował

To przegląd wstępny, nie końcowa karta wyników, więc uczciwie: czego nie obejmuje.

Wszystkie liczby czasu i pamięci są zapożyczone, z jednej platformy (macOS arm64, Python 3.14) i dziedziczą zastrzeżenia tamtego pakietu — wynik „lxml jest szybszy w czystym parsowaniu” jest sprzeczny z konsensusem i wymaga ponownego sprawdzenia na Linux x86_64. Nie testowałem przyspieszenia per-thread-parser (wymagałoby nowych pomiarów czasowych). Zmierzyłem pamięć iterparse na 300k rekordach, ale nie na rzeczywistym XML liczącym gigabajty, nie iterparse na HTML kontra XML i nie wielogodzinne testy stabilności. XSLT 1.0 lxml, walidacja RelaxNG / XMLSchema / DTD i rozszerzenia EXSLT w ogóle nie były tu testowane — to duża powierzchnia możliwości, ale poza rdzeniem parsowania i selekcji. Zaobserwowałem drugi limit głębokości na poziomie 2,045, ale nie ustaliłem dokładnej stałej rekurencji libxml2. Testowana była tylko stabilna wersja 6.1.1, nie alpha 7.0.0. Windows, build ze źródeł i bezwątkowy build 3.14t nie były testowane. A w samym XPath sprawdziłem wbudowane funkcje, ale nie zmienne XPath, niestandardowe funkcje Pythona ani ponowne użycie prekompilowanego obiektu etree.XPath.

Werdykt

lxml nie jest szybkim nowym wynalazkiem, i właśnie dlatego warto go polecić. To dwudziestoletnie powiązanie do libxml2 z pełnym silnikiem XPath 1.0, którego nie dorównuje żadna mainstreamowa alternatywa w Pythonie, trzema przewidywalnymi trybami rygoru parsowania z logiem błędów pośrodku, prawdziwym parserem strumieniowym dla dokumentów, które nie mieszczą się w pamięci, poprawną obsługą wielu przestrzeni nazw i kodowań oraz w pełni liberalną licencją. Dwie ostre krawędzie — limit głębokości około 253 poziomów i wynik przy współdzielonym parserze w wielowątkowości — są udokumentowane, konfigurowalne i właśnie wyjaśnione.

Jeśli kontrolujesz własny pipeline scrapingu i opierasz się na XPath, lxml nadal jest parserem, po który sięgasz. Jeśli wolisz nie utrzymywać selektorów i renderowania, od tego jest warstwa ekstrakcji AI, taka jak Thunderbit API, MCP i CLI — czysty podział obowiązków, nie konkurencja. Tak czy inaczej, traktuj te liczby jako wstępne i sprawdź wydajność na własnej platformie, zanim przytoczysz je w dokumencie projektowym.

Wypróbuj Thunderbit do ekstrakcji danych z sieci Get Started Free

FAQ

Czy lxml to web scraper? Nie. lxml to parser i serializer — powiązanie Pythona z libxml2/libxslt, które zamienia markup w edytowalne, możliwe do przeszukiwania drzewo. Nie pobiera stron, nie renderuje JavaScriptu i nie obsługuje zabezpieczeń anty-bot; warstwę żądań dostarczasz samodzielnie (np. przez requests, httpx, headless browser albo usługę scrapingu), a następnie przekazujesz bajty do lxml.

Kiedy używać lxml zamiast BeautifulSoup lub selectolax? Sięgaj po lxml, gdy potrzebujesz XPath. BeautifulSoup może używać lxml jako backendu parsera, ale nie oferuje natywnego XPath, a selectolax działa tylko na CSS i jest szybszy w swoim wąskim zastosowaniu. Jeśli Twoja logika selekcji wymaga filtrowania po treści tekstu, przechodzenia do rodziców lub przodków, pobierania atrybutów lub węzłów tekstowych albo predykatów opartych na liczbie elementów, silnik XPath w lxml to jedyna mainstreamowa opcja w Pythonie, która wyraża to bezpośrednio.

Dlaczego lxml po cichu ucina głęboko zagnieżdżoną treść? Domyślny parser ogranicza zagnieżdżenie do około 253 poziomów — to zabezpieczenie libxml2 przed DoS, a nie błąd. Ustaw huge_tree=True (na przykład lxml.html.HTMLParser(huge_tree=True)), a odzyska w pełni głębokości 300 i 1,000. Pamiętaj jednak o drugim, twardszym limicie rekurencji około 2,045 poziomów, którego huge_tree nie usuwa.

Czy lxml zwalnia GIL przy wielowątkowym parsowaniu? Tylko w odpowiednich warunkach. FAQ lxml mówi, że GIL jest zwalniany podczas parsowania, gdy każdy wątek używa własnego parsera albo kopii domyślnego parsera; współdzielony parser serializuje dostęp. Zapożyczony wynik przy 4 wątkach na poziomie 1.21x odzwierciedla naiwną ścieżkę ze współdzielonym parserem, a nie sufit per-thread-parser, którego tutaj nie mierzono.

Czy lxml jest nadal utrzymywany w 2026? Tak. Stabilne wydanie 6.1.1 ukazało się 2026-05-18, ostatni push w repozytorium był 2026-07-02, a w toku jest alpha 7.0.0. Przy około 3,000 gwiazdek na GitHubie i aktywnie utrzymywanym libxml2 pod spodem pozostaje aktualną, dobrze wspieraną biblioteką, a nie reliktem.

Ke
Ke
CTO w Thunderbit | Starszy data scientist i ekspert ML Dzięki prawie dziesięciu latom doświadczenia w uczeniu maszynowym i data science, Ke Shen jest absolwentem Columbia University i byłym starszym data scientistą w Walmart Labs. Dysponując dogłębną, uznaną przez branżowych ekspertów wiedzą w zakresie Python, R, Java i statystyki, dzieli się sprawdzonymi w boju spostrzeżeniami na temat wdrażania złożonych algorytmów AI — od teorii po architekturę gotową do produkcji.
Topics
Web Scraping ToolsAI Web Scraper
Spis treści
Thunderbit · Agent AI do danych z sieci

Wyodrębnij dane z dowolnej strony w 1 klik

Zaufało nam ponad 250 000 użytkowników
dostępny darmowy plan
Od strony WWW do arkusza
Opisz, czego potrzebujesz — agent AI Thunderbit to zeskrapuje i wyeksportuje do Excel, Google Sheets, Airtable lub Notion. Start jest darmowy.
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week