Парсинг на Python с помощью lxml: скорость и XPath

Почему lxml — самый быстрый HTML/XML-парсер для Python: XPath-запросы, сравнение с BeautifulSoup и приёмы работы с большими документами.

КP
Команда Parsing.agency
Сбор данных под задачи бизнеса
Опубликовано: 21 января 2025

lxml — самая быстрая библиотека разбора HTML и XML в экосистеме Python. Она построена на C-библиотеках libxml2 и libxslt, поэтому обгоняет чистые Python-парсеры в разы, а заодно даёт полноценный XPath — мощнее CSS-селекторов. Если вы упёрлись в производительность BeautifulSoup на больших объёмах или вам нужны сложные выборки, lxml — правильный выбор.

Эта статья углубляет тему из обзорного материала «Парсинг на Python», где lxml упомянут в ряду других парсеров. Здесь — детальный разбор именно его.

Оглавление

  1. Установка и два модуля lxml
  2. Разбор HTML
  3. XPath: основа мощи lxml
  4. CSS-селекторы через cssselect
  5. Извлечение данных: текст, атрибуты, ссылки
  6. Кириллица в lxml
  7. Большие документы: iterparse
  8. lxml vs BeautifulSoup
  9. Плюсы и минусы

1. Установка и два модуля lxml

bash
pip install lxml

В lxml два ключевых модуля:

  • lxml.html — для разбора HTML (прощает «грязную» разметку, как браузер).
  • lxml.etree — для строгого XML (см. отдельную статью «Парсинг XML на Python»).

Для веб-парсинга в 95% случаев нужен lxml.html.


2. Разбор HTML

python
import requests
from lxml import html

resp = requests.get("https://example.com", timeout=10)
tree = html.fromstring(resp.content)   # передаём байты — lxml сам поймёт кодировку

fromstring возвращает корневой элемент дерева. Дальше к нему применяют XPath или CSS-селекторы. Можно парсить и из файла:

python
tree = html.parse("page.html").getroot()

Важно: передавайте resp.content (байты), а не resp.text (строку) — тогда lxml корректно прочитает объявление кодировки из самого HTML. Это снимает большинство проблем с кириллицей.


3. XPath: основа мощи lxml

XPath — язык адресации узлов в дереве документа, и здесь lxml раскрывается полностью.

python
# текст всех заголовков h2 с классом title
titles = tree.xpath('//h2[@class="title"]/text()')

# атрибут href всех ссылок
links = tree.xpath('//a/@href')

# текст внутри конкретного блока
price = tree.xpath('//div[@class="price"]/text()')[0]

Полезные конструкции XPath

python
# по частичному совпадению класса (когда 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:

python
# родитель элемента
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):

bash
pip install cssselect
python
# метод .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. Извлечение данных: текст, атрибуты, ссылки

python
el = tree.cssselect(".product")[0]

el.text_content()          # весь текст внутри, включая вложенные теги
el.get("href")             # значение атрибута
el.attrib                  # словарь всех атрибутов
el.tag                     # имя тега
el.text                    # только прямой текст узла (без вложенных)

Разница между .text и .text_content() важна:

python
# <p>Привет, <b>мир</b>!</p>
p.text              # "Привет, "         (только до вложенного тега)
p.text_content()    # "Привет, мир!"     (весь текст рекурсивно)

Достроить относительные ссылки до абсолютных:

python
tree.make_links_absolute("https://example.com")
links = tree.xpath('//a/@href')   # теперь все ссылки абсолютные

make_links_absolute экономит ручную склейку URL — удобно при обходе сайта.


6. Кириллица в lxml

lxml отлично работает с кириллицей, если ему дать байты, а не уже декодированную строку:

python
# ПРАВИЛЬНО: байты — lxml прочитает кодировку из <meta charset>
tree = html.fromstring(resp.content)

# РИСКОВАННО: строка, уже декодированная requests (возможно, неверно)
tree = html.fromstring(resp.text)

Если кодировку нужно задать жёстко (например, сервер врёт в заголовках):

python
from lxml import html

parser = html.HTMLParser(encoding="windows-1251")
tree = html.fromstring(resp.content, parser=parser)

Полная теория проблемы кодировок — в хабе, раздел «Кириллица».


7. Большие документы: iterparse

Когда документ огромен (XML-фид на сотни мегабайт), грузить его целиком в память расточительно. iterparse читает потоково, обрабатывая элементы по мере появления и освобождая память:

python
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 его снимает.