lxml в обзоре: XPath-движок, который по-прежнему сильнее любого Python-парсера

Последнее обновление: July 17, 2026
lxml в обзоре: XPath-движок, который по-прежнему сильнее любого Python-парсера
AI-сводка
Этот обзор lxml представляет библиотеку как давно существующую Python-обёртку над libxml2 и libxslt, чьё главное преимущество по-прежнему редко встречается у новых парсеров: настоящий XPath-движок. В статье проверяются покрытие XPath, режимы строгости парсинга, поведение потоковой обработки памяти, выразительность CSS по сравнению с XPath и ограничения глубины libxml2. Показано, что lxml быстрый, экономный по памяти и очень мощный для XML и HTML-задач, где нужны оси, предикаты, функции, streaming или надёжное восстановление данных. Также объясняются безопасные ограничения глубины дерева и то, когда huge_tree меняет этот предел.

Каждые несколько месяцев всплывает очередной более шустрый HTML-парсер, бенчмарки разлетаются по лентам, и кто-то уже спешит списать старую гвардию. А потом тебе нужно выбрать все абзацы, которые содержат нужное слово, или взять родителя найденного узла — и сразу вспоминается, почему lxml всё ещё открыт во вкладке рядом.

lxml — это Python-обёртка над libxml2, которой уже 20 лет. Он не выглядит эффектно. Он не новый. И в одной конкретной задаче — во всём, что требует настоящего XPath — в мейнстримном Python ему, по сути, нет равных. Это практический обзор того, что он умеет, где тихо выигрывает и в каких местах его настройки могут неприятно удивить, если не знать о них заранее.

lxml в одном абзаце: что это на самом деле

lxml — это Python-обёртка для C-библиотек libxml2 и libxslt. Это парсер и сериализатор, а не скрапер и не браузер: он превращает разметку в дерево, с которым можно работать, а потом снова собирает это дерево в байты. Он даёт API, совместимый с ElementTree, полноценный движок XPath 1.0, XSLT 1.0 и валидацию схем. Проект поддерживает Stefan Behnel под девизом "самая функциональная и удобная библиотека для обработки XML и HTML на языке Python".

Вот как он выглядит по состоянию на снимок GitHub и PyPI, сделанный 2026-07-14:

ПолеЗначение
Репозиторийlxml/lxml
Звёзды3,043
Форки620
Открытые issues16
ЛицензияBSD-3-Clause
Создан2011-02-11
Последний push2026-07-02
Стабильная версия на PyPI6.1.1 (2026-05-18)
Встроенный движокlibxml2 2.14.6 + libxslt 1.1.43

Сразу оговорюсь, чтобы не было разговоров про хайп: в этом обзоре нет никаких секретов. lxml достаточно стар, и почти каждое его поведение уже где-то описано — в документации lxml, в changelog libxml2 или в одном из launchpad-обсуждений. Я не нашёл ни одного скрытого, неописанного трюка и не собираюсь ничего выдумывать. Ценность ниже — в том, что всё систематизировано, измерено и разложено именно вокруг lxml как объекта обзора, а не в том, что это какая-то сенсация.

Как проводилось тестирование (и почему цифры по времени заимствованы)

В этот обзор входят две категории данных, и получены они из двух разных источников, так что сразу поясню, что откуда.

Проверки возможностей — поведение XPath, два API парсинга, namespaces, кодировка, жизненный цикл узлов — я запускал заново на одной машине: macOS arm64, Python 3.14.2, lxml 6.1.1, libxml2 2.14.6. Все значения в файлах artifacts/raw/*.json вычислены скриптом во время запуска, а не внесены вручную. Проверки возможностей — это детерминированные булевы значения и перечисления, поэтому один прогон стабилен: нагрузка на машину не меняет, вернёт ли //a/@href строку-атрибут.

Числа по времени и памяти — не из этого набора. Они взяты дословно из более раннего набора бенчмарков selectolax — та же машина, та же виртуальная среда, тот же билд lxml и libxml2, замеры по состоянию на 2026-07-13 — и здесь я их не запускал заново. Это сделано намеренно. Если гонять тайминги параллельно с пачкой capability-скриптов, CPU-конкуренция исказит повторно используемые цифры, а дублировать работу нет смысла: в том наборе lxml уже был полноценно измеренной контрольной библиотекой. Повторное использование сохраняет apples-to-apples сравнение вместо того, чтобы вводить второй, слегка отличающийся замер. Поэтому когда ниже увидишь число в миллисекундах, читай его как «тот же стенд, состояние на 2026-07-13», а не как «я замерил это сегодня заново».

Для выводов используется тег уверенности: single-observation для детерминированных capability-тестов, triple-run для повторно использованных распределений времени, hypothesis там, где я предлагаю механизм, который не изолировал.

XPath: единственное, чего нет ни у selectolax, ни у BeautifulSoup

Это главный тезис, так что начну с него.

lxml XPath coverage moat with axes predicates and functions

Я прогнал xpath() в lxml через матрицу из 37 заранее зарегистрированных случаев — ожидаемый результат для каждого был записан в код до запуска теста, так что я не мог случайно "натянуть" оценку. Десять осей, девять типов предикатов, десять встроенных функций, три скалярных типа результата и пять заведомых ловушек с синтаксисом XPath 2.0, который движок XPath 1.0 в lxml должен отвергать.

КатегорияПокрытиеРезультат
Осиchild / descendant / parent / ancestor / following-sibling / preceding-sibling / self / attribute / following / preceding10/10 pass
Предикаты[1] / last() / position()<n / равенство атрибутов / наличие атрибута / and / or / вложенный [.//a] / not()9/9 pass
Функцииtext() / contains() / starts-with() / count() / string-length() / normalize-space() / concat() / substring() / name() / string()10/10 pass
Типы результатаboolean / number scalars3/3 pass
Ловушкиmatches() / sequences / if-then-else / except / syntax error5/5 correctly rejected

Итого 37/37, и важнее всего здесь колонка с ловушками. matches(), выражения последовательностей, if/then/else и except — это синтаксис XPath 2.0, и движок libxml2 версии 1.0 не пытается поддерживать его частично: он выбрасывает XPathEvalError и отказывается выполнять запрос, а не молча возвращает неверный набор узлов. Так что это идеальный результат после попытки сломать систему, а не «идеально» потому, что тесты были слишком простыми. Всё поведение здесь ровно такое, как описано в документации XPath в lxml, а в этом и была идея.

Признаю одну ошибку в harness, потому что это как раз тот вариант «37/37», которому можно доверять. Моё первое ожидаемое множество для //div[.//a[@href]] предсказывало два совпадения; фактический запуск вернул одно. Около тридцати секунд я думал, что ошибается lxml, затем проверил фикстуру и обнаружил, что второй элемент был <footer>, а не <div> — ошибся я, а не движок. Я исправил ожидаемое множество и оставил пометку в комментарии к исходнику. Это правильный порядок обвинений: сначала проверь свой тест, а потом уже 20-летнюю C-библиотеку.

XPath против CSS: что в CSS буквально невозможно выразить

Общее утверждение «XPath мощнее» заслуживает конкретных цифр, поэтому я измерил разрыв. В lxml есть и .xpath(), и .cssselect() (второе внутри переводит CSS в XPath). Я взял десять целей и проверил, какие из них CSS действительно может выразить.

XPath expresses seven of ten tasks CSS cannot express

ЦельXPathCSS (cssselect)
Фильтрация по тексту (contains(text(),"bargain"))ДаНет текстового предиката
Выбрать родителя от дочернего узла (//b/parent::p)ДаНет селектора родителя
Вернуть значение атрибута (//a/@href)ДаТолько элементы
Вернуть текстовый узел (//p/text())ДаТекстовые узлы не поддерживаются
Ось ancestor (//td/ancestor::div)ДаНет навигации вверх
Фильтрация родителя по числу детей (//ul[count(li)=4])ДаНет предиката по количеству
Фильтрация по длине текста (string-length(text())>5)ДаНет предиката по длине
nth-child / last-child / adjacent siblingДаДа (3 базовых случая)

Семь из десяти целей CSS вообще не умеет выразить. Фильтрация по тексту, движение вверх к родителям и предкам, получение значения атрибута или "голого" текстового узла в результате, предикаты на основе количества — CSS этого не умеет. Только три случая (nth-child, last-child, соседний sibling) работают в обоих вариантах. Вот и ответ на вопрос «что я реально получаю, когда выбираю lxml». selectolax работает только с CSS и вообще не имеет метода xpath(), поэтому те же семь типов запросов там либо превращаются в многошаговые Python-циклы, либо становятся невозможными. Если твоя логика скрапинга на них завязана — выбор уже сделан.

(И да, harness снова поймал меня на ошибке: я ожидал пустой результат для string-length(text())>5, но совпали две шестисимвольные строки. Исправил ожидание, а не инструмент.)

Три режима строгости: etree, recover и lxml.html

XPath — причина выбрать lxml. Три режима строгости — причина не расставаться с ним.

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

Большинство парсеров дают одно поведение для битого входа. lxml даёт три, и они достаточно предсказуемы, чтобы я прогнал через каждый шесть классов некорректной разметки и заранее зафиксировал, как должен вести себя каждый путь.

Некорректный входlxml.etree (строгий)etree + recover=Truelxml.html (мягкий)
Незакрытый тег <root><a>x</root>raisesrecoversaccepts
Неверная вложенность <b><i></b></i>raisesrecoversaccepts
Неопределённая сущность &nbsp;raisesrecoversaccepts
Голый & (Tom & Jerry)raisesrecoversaccepts
Несколько корней <a>1</a><b>2</b>raisesrecoversaccepts
Корректный XMLacceptsaccepts (0 errors)accepts
Булев атрибут <input disabled>raisesrecoversaccepts

Все семь из семи совпали с заранее заданным ожиданием. lxml.etree выбрасывает XMLSyntaxError на все шесть классов ошибок. Если добавить к тому же парсеру recover=True, он проглатывает ошибки и восстанавливает пригодное дерево — и это недооценённая часть — при этом parser.error_log перечисляет каждую ошибку, которую он проигнорировал. lxml.html принимает всё без возражений.

Классификатор, решающий «выброшено / восстановлено / принято», сам опирается на длину runtime-error_log, а не на жёстко прошитое правило, поэтому корректный документ, пропущенный через recover=True, правильно определяется как "accepts" (пустой лог), а не "recovers". В первой версии классификатора любой результат с recover=True помечался как "recovers", и чистый ввод классифицировался неверно; чтение фактического error_log это исправило.

Что это даёт на практике: нужна жёсткая валидация там, где сломанный поток должен падать с ошибкой — бери lxml.etree. Нужен разбор грязного HTML из реального мира — бери lxml.html. А промежуточный случай, который мало кто умеет: «будь терпимым, но скажи мне точно, что было сломано, чтобы я мог залогировать это» — используй recover=True и читай error log. У selectolax есть только мягкий режим, без строгого режима и без журнала ошибок.

iterparse: потоковый режим, которого у selectolax вообще нет

Это не ускоритель, а характеристика возможностей. selectolax принимает только целую строку — инкрементального интерфейса у него нет. У lxml есть iterparse: он выдаёт элементы по мере закрытия тегов, а в связке с классическим приёмом fast_iter (вызывать elem.clear() и удалять предыдущих siblings по ходу) удерживает память на ровном уровне, сколько бы ни весил документ.

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

Я измерил это напрямую — пик RSS через ru_maxrss, каждый вариант в отдельном свежем процессе, на 300,000 элементах <record> общей массой около 15 MB.

РежимПик RSS deltaПримечания
iterparse + clear (fast_iter)~1-2 MBпамять освобождается по мере чтения; почти не зависит от объёма
iterparse без clear~386 MBудерживает ссылки; по тяжести почти как полная загрузка
etree.parse (полная загрузка, опорный)~386 MBзаведомо тяжёлый; показывает, что измеритель видит масштаб

Ограниченный режим удерживает пик RSS примерно в пределах 1–2 MB против ~386 MB при полной загрузке — разница по масштабу около 0,3–0,4% — и событие первого record срабатывает ещё до того, как файл дочитывается до конца, так что это действительно инкрементальная обработка, а не фальшивый streaming. Самая поучительная строка — средняя. Запусти тот же цикл iterparse, но убери clear(), и память снова вырастет до ~386 MB, потому что ты держишь ссылки на всё подряд. Выигрыш живёт в clear(), а не в iterparse как таковом. Сильный разрыв между полной загрузкой и ограниченным режимом также подтверждает, что RSS-метр действительно видит разницу в масштабе, а не смотрит в пустоту. (Этот тест памяти я запускал уже в рамках данного набора — это замер footprint, а не повторно использованные тайминги.)

Практический вывод: если у тебя есть гигабайтный XML-экспорт, который не помещается в RAM, у selectolax нет вообще никакого пути. Либо потоковый парсер lxml, либо другой язык.

Namespaces: RSS, SVG и ловушка с namespace по умолчанию

Двенадцать сценариев с namespaces: RSS сразу в трёх пространствах имён, SVG с namespace по умолчанию плюс xlink, а также XML с namespace по умолчанию. Все двенадцать прошли.

lxml извлекает //dc:creator/text() из RSS-ленты ровно как ["Alice", "Bob"], находит //atom:link/@href и //content:encoded через три разных namespace в одном документе, обрабатывает //s:rect и //s:use/@xlink:href во втором namespace SVG и разбирает имена в Clark-notation {uri}local через QName, а также показывает map пространств имён через nsmap. Это задокументированное и поддерживаемое поведение, и это целое измерение, которого selectolax вообще не касается, потому что selectolax — только HTML5 и не обрабатывает произвольные XML namespaces.

Есть одна задокументированная ловушка, которую стоит запомнить. У XPath нет понятия namespace по умолчанию. Если направить //book на документ, где объявлен xmlns="urn:...", ты получишь ноль совпадений — пустой префикс для XPath не определён, как и сказано в документации lxml. Нужно либо привязать искусственный префикс (//c:book с namespaces={"c": "urn:..."}, что нашло все три), либо использовать //*[local-name()='book'] (тоже три). Это не баг — это спецификация XPath, реализованная как положено. Просто каждый человек удивляется этому ровно один раз.

Реальные грязные страницы: точность на 11 фактических скрапах

Синтетические тесты чистые; веб — нет. Я повторно использовал одиннадцать реально сохранённых страниц из набора selectolax (состояние на 2026-07-10, только чтение) и прогнал их через lxml.html, сделав lxml главным объектом теста.

ФикстураРазмерСсылкиОшибки, восстановленные libxml2Строгий 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

Все одиннадцать страниц разобрались через lxml.html, а количество ссылок, заголовков и изображений совпало с повторно использованными counts из набора selectolax во всех одиннадцати случаях — cross-check true. Именно это говорит мне, что повторное использование честно и сопоставимо, а не что под одинаковой меткой скрываются две разные метрики.

Побочный вывод: строгий XML-парсер дал ошибку на десяти из одиннадцати страниц. Реальные веб-страницы в подавляющем большинстве не являются корректным XML, и именно для этого существует HTML-recovery mode в libxml2. Единственным исключением оказалась BBC News, отрендеренная на Next.js и достаточно корректная, чтобы пройти строгий XML-парсинг. Не всё, что называется "HTML", требует режима восстановления.

Одна тонкость со счётом, на которую легко попасться. В docs_python.html //a[@href] (наличие атрибута) дал 343, а в наборе selectolax if n.get("href") (truthy value) дал 341. Две лишние ссылки — это пустые href="". Это разница в конвенции подсчёта — атрибут существует против атрибута не пустой — а не различие в поведении lxml, и числа сходятся, когда ты приводишь предикат к одной логике. Полезно помнить при скрапинге: считать ли пустые href — это выбор фильтра, а не парсера.

Ограничение глубины, которое выглядит как баг, но им не является

В наборе selectolax уже было отмечено, что lxml теряет самый глубокий контент в разметке с вложенностью на 1,000 и 5,000 уровней <div>, и это было описано как "lxml silently loses the deepest content". Мне захотелось понять механизм, поэтому я прогнал парсер по умолчанию и затем huge_tree=True.

lxml default depth guard around 253 levels and huge_tree to 2045

Запрошенная глубинаДоходит парсер по умолчаниюДоходит при huge_tree=True
300253 (дальше обрезает)299 (восстановлено)
1000253 (дальше обрезает)999 (восстановлено)
5000253 (дальше обрезает)2045 (всё ещё обрезает)

Парсер по умолчанию обрезает дерево примерно на 253 уровнях и молча отбрасывает всё, что глубже. Это не баг — это защита libxml2 от DoS, примерно 256-уровневый лимит вложенности, который не даёт враждебному документу переполнить стек, и он задокументирован в launchpad-треде lxml про XML_PARSE_HUGE. Если поставить huge_tree=True, глубины 300 и 1,000 восстанавливаются полностью. Но на глубине 5,000 даже с huge_tree парсер доходит только до 2,045 — существует второй, более жёсткий потолок рекурсии libxml2 выше настраиваемого, и huge_tree его не снимает.

Практический вывод простой: когда разбираешь очень глубокую разметку из доверенного источника, используй lxml.html.HTMLParser(huge_tree=True). Что добавляет этот набор помимо повторно использованного наблюдения — это механизм (защитный лимит, а не порча данных), способ исправления (huge_tree) и тот факт, что есть ещё один потолок, до которого этот способ не дотягивается.

DOM для чтения и записи, сериализация, кодировки

lxml — это полноценное дерево для чтения и записи, а не только инструмент извлечения данных, и я поэлементно проверил поверхность редактирования. Все восемь DOM-операций прошли: SubElement, insert, remove, replace, strip_tags (убирает теги, сохраняя их текст), strip_elements (убирает теги вместе с текстом), drop_tree (эксклюзив lxml.html) и модель с двумя текстовыми слотами text/tail, которая путает новичков — в <p>head<b>bold</b>tail</p> у p.text значение "head", у b.text"bold", а у b.tail"tail".

Сериализация прошла пять из пяти: tostring в XML- и HTML-режиме (в HTML void elements корректно не само-закрываются), pretty_print, каноникализация C14N (method="c14n", ещё один эксклюзив lxml) и чистый round-trip.

Кодировки — это то место, где lxml тихо отстраивается от остальных. Передай ему байты не в UTF-8 — например, "<p>café éè</p>".encode("latin-1") через lxml.html.fromstring — и он восстановит café éè без потерь, без символов замены U+FFFD, без отрезанных байтов. Это напрямую повторяет его роль как "чистого эталона" в наборе selectolax, где тот же вход у двух других движков искажался: Lexbor давал replacement characters, Modest просто отбрасывал байты. Charset detection на базе libxml2 здесь просто стабильнее.

Обратная сторона — строгость к тому, как ты объявляешь кодировку. encoding="latin-1" в XML-декларации вызывает XMLSyntaxError: Unsupported encoding: latin-1, тогда как каноническое имя IANA encoding="ISO-8859-1" разбирается без проблем и возвращает café. libxml2 принимает только канонические имена кодировок, а не алиасы — это зафиксировано ещё в launchpad #613302. Неприятно, если не знать, и тривиально, если знать.

И наконец, жизненный цикл узлов. Я прогнал три сценария со stale-handle в изолированных subprocess'ах (жёсткий crash проявился бы как ненулевой код выхода): удержание узла после сборки его дерева GC, чтение handle после drop_tree() и использование узла после remove(). Ни одного segfault ни в одном случае — lxml удерживает ссылку узла на его дерево, чтобы избежать use-after-free. У selectolax в этом тесте тоже всё чисто.

Скорость и память (заимствованные, но честно обозначенные)

Всё в этом разделе повторно использовано из набора selectolax, состояние на 2026-07-13. Этот набор не произвёл ни одной собственной цифры по времени, и я лучше скажу это дважды, чем ты подумаешь, что я что-то перемерял заново.

ПараметрЗначение lxmlИнтерпретация
Pure parse p50 (10 MB)77.9 msпримерно на 33–34% быстрее selectolax-Lexbor
Полный parse + extract p50 (1 MB / 10 MB)14.18 ms / 172.9 msпримерно на уровне Lexbor на малых размерах
Пропускная способность CSS на 100k узлах3,002,646 nodes/sверхний скоростной эшелон среди трёх C-движков
Дельта RSS на 10 MB128.9 MBсамый экономный из шести парсеров, примерно в 1.7 раза экономнее BeautifulSoup
Холодный старт импорта14.1 msпримерно в 2.3 раза быстрее parsel-подобных импортов

Чистый парсинг и throughput у lxml сильные, и это самый экономный по памяти из шести измеренных парсеров. Но с многопоточностью есть важная оговорка. Повторно использованные данные показывают ускорение wall-clock лишь в 1.21 раза на 4 потоках, и это отмечено как inconclusive — однако там использовался общий default parser. FAQ lxml прямо говорит, что GIL освобождается во время парсинга только тогда, когда у каждого потока свой parser (или копия default parser); общий parser сериализует доступ. Я структурно проверил поверхность API для правильного использования (XMLParser.copy() существует, get/set_default_parser существуют, XPathEvaluator имеет внутренний lock), но не измерял ускорение при parser-per-thread — это был бы новый timing-замер, а этот набор таких замеров не производит. Поэтому 1.21x нужно читать как «при наивном общем parser-path», а не как предел для потоков в lxml.

И ещё одна звёздочка на всём этом: это цифры для одной платформы, macOS arm64. Утверждение, что pure parse у lxml опережает Lexbor, идёт вразрез с обычным консенсусом, где именно Lexbor-движок считается самым быстрым, так что этот результат действительно стоит перепроверить на Linux x86_64, прежде чем считать его окончательным.

Лицензия: скучная, но важная победа

lxml распространяется под BSD-3-Clause, а встроенные C-библиотеки — libxml2 и libxslt — под MIT. Это полностью permissive-цепочка без copyleft на каком-либо этапе, и это важно, как только ты начинаешь распространять продукт. Для сравнения, wheel selectolax включает LGPL-2.1 Modest и Apache-2.0 Lexbor, поэтому lxml — более простой выбор, если ты поставляешь закрытое приложение.

Есть и практическое преимущество при установке: lxml публикует готовые wheels со статически слинкованными libxml2 и libxslt, так что pip install lxml обычно не требует системного libxml2 и компилятора на твоей машине — совсем другой опыт, чем сборка из исходников.

Где lxml уместен — и где уже нужен AI-слой извлечения данных

Важно чётко обозначить границу, потому что здесь легко промахнуться с категорией. lxml — это библиотека парсинга. Она даёт тебе дерево и отличный движок запросов, а всё, что происходит вокруг этого дерева, остаётся твоей задачей: получить страницу, отрендерить JavaScript, пройти антибот-защиту, написать и поддерживать XPath, структурировать результат. Это уже другой слой по сравнению с hosted extraction service, и эти два подхода скорее соседи, чем конкуренты.

Если разработчику не хочется самому владеть стеком fetch-render-select-maintain, то на верхнем уровне как раз и работает что-то вроде Thunderbit — и для этой аудитории важны не расширение браузера, а API, MCP server и CLI. Thunderbit Open API даёт POST /distill, чтобы превратить страницу в чистый Markdown, и POST /extract, чтобы извлечь структурированные данные по JSON Schema, с переключателем renderMode и batch-задачами для больших объёмов. Тот же движок доступен как MCP server (thunderbit_suggest_fields, thunderbit_distill, thunderbit_extract) для агентов и coding assistants, а также как CLI, который можно запускать прямо из терминала через npx @thunderbit/thunderbit-cli. Он из коробки обрабатывает JS-rendering, антибот-защиту и CAPTCHA и возвращает JSON, совпадающий со схемой — то есть это слой над парсингом, а не его замена.

Попробовать Thunderbit для извлечения веб-данных

Логика выбора простая. Бери lxml, когда ты контролируешь pipeline и хочешь точечный XPath по дереву, которое понимаешь. Бери AI API для извлечения, когда не хочешь вообще поддерживать селекторы и рендеринг. Во многих реальных системах используются оба подхода: lxml — для структурированных фидов, которыми ты управляешь, а сервис извлечения — для грязного длинного хвоста страниц, которые тебе не принадлежат.

Что этот обзор не тестировал

Это предварительный обзор, а не финальная таблица результатов, так что вот что в него не входит.

Все цифры по времени и памяти повторно использованы, относятся к одной платформе (macOS arm64, Python 3.14) и наследуют оговорки того набора — результат «lxml быстрее на чистом парсинге» идёт против общего консенсуса и требует перепроверки на Linux x86_64. Ускорение многопоточного парсинга при parser-per-thread не тестировалось (для этого нужны новые timing-замеры). Я измерял память iterparse на 300k record'ов, но не на реальном XML гигабайтного масштаба, не сравнивал iterparse для HTML и XML и не делал многочасовой soak-тест. XSLT 1.0 в lxml, его валидация RelaxNG / XMLSchema / DTD и расширения EXSLT здесь вообще не проверялись — это большой набор возможностей, но он выходит за пределы ядра «парсинг и выборка». Второй потолок глубины на уровне 2,045 я заметил, но точную рекурсивную константу libxml2 не определял. Тестировалась только стабильная версия 6.1.1, а не alpha 7.0.0. Windows, сборка из исходников и free-threaded сборка 3.14t тоже не тестировались. И внутри самого XPath я проверял встроенные функции, но не переменные XPath, не собственные Python extension functions и не повторное использование заранее скомпилированного объекта etree.XPath.

Вердикт

lxml — не новинка с громкой скоростью, и именно в этом его сила. Это 20-летняя обёртка над libxml2 с полноценным XPath 1.0, которой в мейнстримном Python нет равных; с тремя предсказуемыми режимами строгости парсинга и журналом ошибок посередине; с настоящим потоковым парсером для документов, которые не помещаются в память; с корректной поддержкой нескольких namespaces и кодировок; и с полностью permissive-лицензией. Пара острых углов — лимит глубины примерно на 253 уровня и число для shared-parser в многопоточности — задокументированы, настраиваются и теперь понятны.

Если ты контролируешь свой scraping pipeline и опираешься на XPath, lxml по-прежнему тот парсер, к которому стоит тянуться. Если ты не хочешь поддерживать селекторы и рендеринг, для этого и существует AI-слой извлечения вроде Thunderbit API, MCP и CLI — это не конкуренция, а чёткое разделение обязанностей. В любом случае считай эти цифры предварительными и перепроверяй timing на своей платформе, прежде чем цитировать их в design doc.

Попробовать Thunderbit для извлечения веб-данных Get Started Free

Часто задаваемые вопросы

lxml — это веб-скрапер?
Нет. lxml — это парсер и сериализатор, Python-обёртка над libxml2/libxslt, которая превращает разметку в редактируемое и запросопригодное дерево. Он не загружает страницы, не рендерит JavaScript и не занимается антибот-защитой; слой запроса ты предоставляешь сам (requests, httpx, headless browser или scraping service), а lxml уже получает байты.

Когда лучше использовать lxml вместо BeautifulSoup или selectolax?
Брать lxml стоит тогда, когда тебе нужен XPath. BeautifulSoup может использовать lxml как backend-парсер, но не даёт нативного XPath, а selectolax работает только с CSS и силён в своей узкой нише по скорости. Если твоей логике нужны фильтрация по тексту, переход к родителям или предкам, извлечение атрибутов/текстовых узлов или предикаты по количеству, то из мейнстримных Python-инструментов только XPath в lxml выражает это напрямую.

Почему lxml молча обрезает слишком глубоко вложенный контент?
У его парсера по умолчанию есть ограничение вложенности примерно в 253 уровня — это защита libxml2 от DoS, а не баг. Если включить huge_tree=True (например, lxml.html.HTMLParser(huge_tree=True)), глубины 300 и 1,000 восстанавливаются полностью. Но есть и второй, более жёсткий потолок около 2,045 уровней, который huge_tree не снимает.

Освобождает ли lxml GIL при многопоточном парсинге?
Только при правильных условиях. В FAQ lxml сказано, что GIL освобождается во время парсинга, когда у каждого потока свой parser или копия default parser; если parser общий, доступ сериализуется. Повторно использованное ускорение 1.21x на 4 потоках отражает наивный shared-parser путь, а не предел при parser-per-thread — его здесь не измеряли.

Поддерживается ли lxml в 2026 году?
Да. Стабильный релиз 6.1.1 вышел 2026-05-18, последний push в репозиторий был 2026-07-02, и в разработке уже есть alpha 7.0.0. При примерно 3,000 звёздах на GitHub и активно поддерживаемой libxml2 под капотом это по-прежнему актуальная и хорошо поддерживаемая библиотека, а не устаревшее наследие.

Ke
Ke
Технический директор в Thunderbit | Senior Data Scientist и эксперт по ML Имея почти десятилетний опыт в машинном обучении и data science, Кэ Шэнь — выпускник Колумбийского университета и бывший Senior Data Scientist в Walmart Labs. Обладая глубокой, признанной коллегами экспертизой в Python, R, Java и статистике, он делится проверенными на практике наблюдениями о том, как переводить сложные AI-алгоритмы из теории в production-grade архитектуру.
Topics
Инструменты для веб-скрапингаAI Web Scraper

Попробуй Thunderbit

Собирай лиды и другие данные всего в 2 клика. На базе AI.

Получить Thunderbit Это бесплатно
Извлекай данные с помощью AI
Легко передавай данные в Google Sheets, Airtable или Notion
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week