lxml — самая быстрая библиотека разбора HTML и XML в экосистеме Python. Она построена на C-библиотеках libxml2 и libxslt, поэтому обгоняет чистые Python-парсеры в разы, а заодно даёт полноценный XPath — мощнее CSS-селекторов. Если вы упёрлись в производительность BeautifulSoup на больших объёмах или вам нужны сложные выборки, lxml — правильный выбор.
Эта статья углубляет тему из обзорного материала «Парсинг на Python», где lxml упомянут в ряду других парсеров. Здесь — детальный разбор именно его.
Оглавление
- Установка и два модуля lxml
- Разбор HTML
- XPath: основа мощи lxml
- CSS-селекторы через cssselect
- Извлечение данных: текст, атрибуты, ссылки
- Кириллица в lxml
- Большие документы: iterparse
- lxml vs BeautifulSoup
- Плюсы и минусы
1. Установка и два модуля lxml
pip install lxml
В lxml два ключевых модуля:
lxml.html— для разбора HTML (прощает «грязную» разметку, как браузер).lxml.etree— для строгого XML (см. отдельную статью «Парсинг XML на Python»).
Для веб-парсинга в 95% случаев нужен lxml.html.
2. Разбор HTML
import requests
from lxml import html
resp = requests.get("https://example.com", timeout=10)
tree = html.fromstring(resp.content) # передаём байты — lxml сам поймёт кодировку
fromstring возвращает корневой элемент дерева. Дальше к нему применяют XPath или CSS-селекторы. Можно парсить и из файла:
tree = html.parse("page.html").getroot()
Важно: передавайте resp.content (байты), а не resp.text (строку) — тогда lxml корректно прочитает объявление кодировки из самого HTML. Это снимает большинство проблем с кириллицей.
3. XPath: основа мощи lxml
XPath — язык адресации узлов в дереве документа, и здесь lxml раскрывается полностью.
# текст всех заголовков h2 с классом title
titles = tree.xpath('//h2[@class="title"]/text()')
# атрибут href всех ссылок
links = tree.xpath('//a/@href')
# текст внутри конкретного блока
price = tree.xpath('//div[@class="price"]/text()')[0]
Полезные конструкции XPath
# по частичному совпадению класса (когда class="title big featured")
tree.xpath('//h2[contains(@class, "title")]/text()')
# по тексту элемента
tree.xpath('//a[text()="Подробнее"]/@href')
# n-й элемент (нумерация с 1!)
tree.xpath('(//div[@class="item"])[3]')
# относительный путь от найденного узла
for card in tree.xpath('//div[@class="card"]'):
name = card.xpath('.//h3/text()') # точка = «от текущего узла»
link = card.xpath('.//a/@href')
Ведущая точка (.//) в относительном XPath критична: без неё поиск пойдёт от корня документа, а не от текущей карточки. Это самая частая ошибка новичков.
Оси XPath
XPath умеет ходить по дереву в любом направлении — то, чего нет в CSS:
# родитель элемента
tree.xpath('//span[@class="price"]/parent::div')
# следующий элемент того же уровня
tree.xpath('//h2/following-sibling::p[1]/text()')
# предок с определённым классом
tree.xpath('//a[@id="buy"]/ancestor::div[@class="product"]')
4. CSS-селекторы через cssselect
Если XPath кажется громоздким, lxml поддерживает CSS-селекторы (нужен пакет cssselect):
pip install cssselect
# метод .cssselect() возвращает список элементов
cards = tree.cssselect("div.product-card")
title = tree.cssselect("h1.title")[0].text_content()
links = [a.get("href") for a in tree.cssselect("a.product-link")]
CSS читается привычнее для тех, кто пришёл из фронтенда. Под капотом cssselect транслирует CSS в XPath, так что скорость сохраняется. Для сложных же выборок (по тексту, по предкам) всё равно понадобится XPath.
5. Извлечение данных: текст, атрибуты, ссылки
el = tree.cssselect(".product")[0]
el.text_content() # весь текст внутри, включая вложенные теги
el.get("href") # значение атрибута
el.attrib # словарь всех атрибутов
el.tag # имя тега
el.text # только прямой текст узла (без вложенных)
Разница между .text и .text_content() важна:
# <p>Привет, <b>мир</b>!</p>
p.text # "Привет, " (только до вложенного тега)
p.text_content() # "Привет, мир!" (весь текст рекурсивно)
Достроить относительные ссылки до абсолютных:
tree.make_links_absolute("https://example.com")
links = tree.xpath('//a/@href') # теперь все ссылки абсолютные
make_links_absolute экономит ручную склейку URL — удобно при обходе сайта.
6. Кириллица в lxml
lxml отлично работает с кириллицей, если ему дать байты, а не уже декодированную строку:
# ПРАВИЛЬНО: байты — lxml прочитает кодировку из <meta charset>
tree = html.fromstring(resp.content)
# РИСКОВАННО: строка, уже декодированная requests (возможно, неверно)
tree = html.fromstring(resp.text)
Если кодировку нужно задать жёстко (например, сервер врёт в заголовках):
from lxml import html
parser = html.HTMLParser(encoding="windows-1251")
tree = html.fromstring(resp.content, parser=parser)
Полная теория проблемы кодировок — в хабе, раздел «Кириллица».
7. Большие документы: iterparse
Когда документ огромен (XML-фид на сотни мегабайт), грузить его целиком в память расточительно. iterparse читает потоково, обрабатывая элементы по мере появления и освобождая память:
from lxml import etree
for event, element in etree.iterparse("huge.xml", tag="item"):
title = element.findtext("title")
process(title)
element.clear() # освобождаем память
while element.getprevious() is not None:
del element.getparent()[0] # удаляем обработанные узлы
Этот приём (чтение + clear()) позволяет разбирать файлы, которые не помещаются в RAM целиком. Подробнее о потоковом разборе XML — в «Парсинг XML на Python».
8. lxml vs BeautifulSoup
| Критерий | lxml | BeautifulSoup |
|---|---|---|
| Скорость | очень высокая (C) | ниже (если без движка lxml) |
| API | строже, требует знания XPath | дружелюбный, читается как текст |
| XPath | полноценный | нет (только CSS-поиск) |
| «Грязный» HTML | хорошо | очень хорошо, прощает многое |
| Память | экономнее, есть iterparse | выше |
| Порог входа | выше | ниже |
На практике их часто совмещают: BeautifulSoup может использовать lxml как движок (BeautifulSoup(html, "lxml")) — вы получаете удобный API bs4 и скорость lxml. Если же нужны XPath или потоковый разбор гигабайтных файлов — берите lxml напрямую. Сравнение разных парсеров — в хабе, а извлечение таблиц — в отдельной статье.
9. Плюсы и минусы lxml
Плюсы:
- Самый быстрый парсер в экосистеме Python (ядро на C).
- Полноценный XPath с осями (parent, sibling, ancestor) — недостижимо для CSS.
- Поддержка CSS через cssselect — лучшее из двух миров.
iterparseдля потоковой обработки гигантских документов.- Экономный расход памяти.
Минусы:
- Более крутая кривая обучения, особенно XPath.
- API строже и менее «прощающий», чем у BeautifulSoup.
- Сообщения об ошибках бывают непонятными.
- Установка иногда требует системных библиотек (на некоторых ОС).
Вывод: lxml — выбор для производительности и сложных выборок. Для разовых задач и максимальной читаемости проще BeautifulSoup; для гигантских объёмов с CSS-выборками стоит присмотреться и к selectolax. Именно из-за скорости lxml чаще всего ставят в пару к асинхронному парсингу: когда сотни страниц скачиваются одновременно, медленный парсер становится узким местом, а C-движок lxml его снимает.