Парсинг HTML в Python: lxml против selectolax под нагрузкой
Пайплайн парсил 200 тысяч страниц за ночь, а стал упираться в 9 часов. Разбираем, где BeautifulSoup и lxml теряют время и когда selectolax даёт трёхкратный выигрыш.

Сервис собирал 200 тысяч HTML-страниц за ночь и разбирал их одним воркером на BeautifulSoup с парсером html.parser. Пока страниц было 50 тысяч, всё укладывалось в окно. На 200 тысячах разбор занял больше 9 часов и налез на утренний отчёт. Профилирование показало: сеть отнимала около 30% времени, остальное съедал сам парсинг и извлечение полей. Перевод разбора на selectolax сократил CPU-время парсинга примерно в 3-4 раза на том же наборе страниц. Ниже — почему так и где выбор между библиотеками действительно имеет значение.
Речь идёт о трёх инструментах: BeautifulSoup 4.x (высокоуровневая обёртка, сама не парсит, а вызывает бэкенд), lxml (обёртка над C-библиотеками libxml2/libxslt) и selectolax (обёртка над C-парсером Lexbor или Modest). Инвалидация кеша скачанных страниц и обход антибота — отдельные темы, здесь только разбор HTML в объектную модель и извлечение данных.
Где именно уходит время
Парсинг HTML — это две разные операции, которые часто путают в одну строчку кода:
- Построение дерева — из строки байтов в объектную модель. Здесь решает С-код: чистый Python на реальном грязном HTML (незакрытые теги, битая вложенность) проигрывает на порядок.
- Извлечение узлов — поиск по дереву через CSS-селекторы или XPath. Здесь цена зависит от того, компилируется селектор в С или интерпретируется в Python на каждом вызове.
BeautifulSoup с html.parser проигрывает на обоих этапах: дерево строит парсер стандартной библиотеки на Python, а навигация идёт через питоновские объекты Tag. Если подключить к нему бэкенд lxml, дерево начинает строить С-код, но обёртка BeautifulSoup всё равно оборачивает каждый узел в Python-объект — и на извлечении вы платите эту цену.
Что делает selectolax быстрым
selectolax почти не создаёт Python-объектов при построении дерева — оно живёт в C, а Python получает лёгкие ссылки-хендлы. CSS-селектор тоже обрабатывается на стороне C. Расплата: API беднее, чем у lxml, нет полноценного XPath (только CSS-селекторы), и дерево нельзя произвольно модифицировать так же свободно, как в lxml. Для задачи «прочитать и вытащить поля» это не мешает, для задачи «перестроить документ» — мешает сильно.
Правило простое: если вы только читаете HTML и достаёте из него данные — берите selectolax. Если вы документ меняете, применяете XSLT или ходите по нему сложным XPath — остаётесь на lxml.
Три подхода на одной задаче
Задача: из карточки товара вытащить заголовок и цену. Смотрим не на обвязку, а на разницу в разборе. Версии: beautifulsoup4 4.12.x, lxml 5.x, selectolax 0.3.x.
# BeautifulSoup + html.parser — самый медленный вариант
from bs4 import BeautifulSoup
def parse_bs(html: str) -> dict:
soup = BeautifulSoup(html, "html.parser")
title = soup.select_one("h1.product-title")
price = soup.select_one("span.price")
return {
"title": title.get_text(strip=True) if title else None,
"price": price.get_text(strip=True) if price else None,
}
# lxml через CSS-селекторы (нужен пакет cssselect) либо XPath
from lxml import html as lxml_html
def parse_lxml(html: str) -> dict:
tree = lxml_html.fromstring(html)
# cssselect транслирует CSS в XPath один раз за вызов
title = tree.cssselect("h1.product-title")
price = tree.cssselect("span.price")
return {
"title": title[0].text_content().strip() if title else None,
"price": price[0].text_content().strip() if price else None,
}
# selectolax на движке Lexbor
from selectolax.lexbor import LexborHTMLParser
def parse_selectolax(html: str) -> dict:
tree = LexborHTMLParser(html)
title = tree.css_first("h1.product-title")
price = tree.css_first("span.price")
return {
"title": title.text(strip=True) if title else None,
"price": price.text(strip=True) if price else None,
}
Три реализации возвращают одинаковый результат. Разница — в том, сколько Python-объектов создаётся под капотом и где выполняется поиск. У BeautifulSoup оба узла оборачиваются в тяжёлые Tag-объекты; у selectolax поиск и текст остаются в C до последнего момента.
Как замерять честно
Готовых цифр «в вакууме» не приводлю: скорость зависит от размера страницы, глубины дерева, числа селекторов и версии libxml2 в вашей сборке. Меряйте на своём наборе так:
- Возьмите 500-1000 реальных страниц из вашего источника, а не синтетический HTML — грязная разметка меняет расклад.
- Прогрейте: один холостой проход, чтобы импорты и JIT-кеши библиотек не попали в замер.
- Замеряйте только разбор, без сети и без записи в БД. Оберните цикл в
time.perf_counter()или используйтеtimeitс фиксированным числом повторов. - Смотрите на CPU-время (
process_time), а не на wall-clock, если тестируете на нагруженной машине.
По опыту на карточках товаров среднего размера selectolax обгоняет BeautifulSoup+html.parser в разы, а чистый lxml — примерно вдвое. Но конкретный множитель проверяйте у себя: на очень мелких документах разница смазывается, на крупных с сотнями селекторов — растёт.
Когда что выбирать
| Критерий | BeautifulSoup | lxml | selectolax |
|---|---|---|---|
| Скорость разбора | низкая (с html.parser) | высокая | самая высокая |
| CSS-селекторы | да | через cssselect | да, нативно |
| XPath | нет | да, полный | нет |
| Модификация дерева | удобная | полная | ограниченная |
| Толерантность к битому HTML | высокая | высокая | высокая |
| Порог входа API | низкий | средний | низкий |
Практический критерий без «зависит от задачи»:
- Массовый парсинг только на чтение (десятки-сотни тысяч страниц, нужны поля) — selectolax.
- Нужен XPath или трансформация документа (XSLT, перестройка дерева, генерация HTML) — lxml.
- Разовый скрипт, читаемость важнее скорости — BeautifulSoup, но подключите бэкенд lxml:
BeautifulSoup(html, "lxml"), а неhtml.parser.
Что ломается на практике
Быстрый парсер не спасает от типичных ошибок, из-за которых пайплайн падает или тихо теряет данные.
- Кодировка. lxml иногда путается в кодировке, если во входе одновременно и HTTP-заголовок, и метатег противоречат друг другу. Передавайте байты, а не декодированную строку с уже сломанными символами, и дайте библиотеке разобраться самой.
- Отсутствующий узел.
css_firstиcssselectвернутNoneили пустой список, а не бросят исключение. Если не проверять, вы получитеAttributeError: 'NoneType' object has no attribute 'text'в середине ночного прогона. Всегда проверяйте наNone. - Утечка памяти в lxml. При разборе миллионов документов в одном процессе объекты дерева lxml могут накапливаться, если держать на них ссылки. Не складывайте распарсенные деревья в общий список — извлекайте поля и отпускайте дерево.
- Разный движок selectolax. В selectolax два бэкенда: Lexbor (
selectolax.lexbor) и Modest (selectolax.parser). Они ведут себя чуть по-разному на битой разметке. Выберите один и не смешивайте в проекте, иначе получите расхождения в результатах между машинами.
Параллельность
Парсинг упирается в CPU, и asyncio тут не помогает — он про ожидание сети. Для распараллеливания разбора нужен ProcessPoolExecutor: GIL держит один поток на CPU-задаче, а несколько процессов дают линейный рост до числа ядер. Скачивание оставляйте асинхронным, а тяжёлый разбор отдавайте в пул процессов.
Что сделать дальше
Замерьте разбор на своих реальных страницах по схеме выше — до переписывания кода, чтобы знать стартовую точку. Если разбор занимает заметную долю времени и вам не нужен XPath — переведите извлечение на selectolax с движком Lexbor и повторите замер. Затем посмотрите в проде на CPU воркера: если после смены парсера узким местом стала сеть или запись в БД, дальнейшая оптимизация парсера бессмысленна — оптимизируйте следующее звено. Ключевой показатель для наблюдения — CPU-время на одну страницу и число страниц в секунду на воркер.
Саммаризатор: 50 страниц или созвон — в 5 главных тезисов.
Спросить за 15 ₽Источники: selectolax — документация и репозиторий на GitHub, lxml — официальная документация
Частые вопросы
У меня уже написано на BeautifulSoup, переписывать всё?
selectolax поддерживает XPath?
А если у меня другая версия lxml или selectolax?
Почему asyncio не ускоряет парсинг?
selectolax справляется с битым HTML так же, как BeautifulSoup?
Что делать, если после смены парсера скорость почти не изменилась?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.