BeautifulSoup в 2026: самый дружелюбный HTML-парсер оказался и самым медленным (в 12–17 раз)

Последнее обновление: July 17, 2026
BeautifulSoup в 2026: самый дружелюбный HTML-парсер оказался и самым медленным (в 12–17 раз)
AI-сводка
Этот обзор рассматривает BeautifulSoup как самый доступный HTML-парсер для Python и точно измеряет цену этой дружелюбности. В статье сравниваются bs4 и парсеры на C-основе по скорости, устойчивости к битому HTML, покрытию CSS-селекторов, удержанию объектов в памяти и восстановлению кодировок. Показано, что BeautifulSoup заметно медленнее — часто в 12–17 раз, — но также объясняется, почему разработчики всё равно его выбирают: читаемый API, терпимый разбор, мощная поддержка селекторов через soupsieve и удобство для разовых задач на грязных данных. Это практическое руководство о том, когда штраф за скорость приемлем, а когда лучше выбрать более быстрый парсер.

BeautifulSoup — это библиотека, к которой почти все тянутся в первый раз, когда начинают парсить веб-страницы на Python. И да, среди серьёзных HTML parser’ов она действительно самая медленная. Оба утверждения верны, и ни одно из них не является упрёком. Самое интересное в том, что «самый медленный» — это не ощущение, а вполне измеряемая и покупаемая цифра.

Я прогнал bs4 (то есть beautifulsoup4, версия 4.15.0, вышедшая в июне 2026 года, под лицензией MIT) через набор свежих тестов на возможности и повторно использованные данные по времени из того же benchmark. Картина получилась стабильной: за примерно десятикратную потерю скорости ты получаешь самый дружелюбный API и лучшую в отрасли терпимость к битому HTML. Насколько это выгодный обмен — зависит только от твоей нагрузки, поэтому в этом обзоре я держу обе стороны вопроса в поле зрения.

Что такое BeautifulSoup на самом деле — и чем он не является

Большинство туториалов пропускают самую важную часть: BeautifulSoup не парсит HTML. Это обёртка. Под капотом он передаёт документ одному из трёх настоящих парсеров — встроенному в Python html.parser, lxml или html5lib — а затем заворачивает получившееся дерево в один очень удобный API для навигации и поиска. Задача bs4 — не разбирать HTML, а сделать результат удобным для работы.

Сам автор называет его «библиотекой для screen scraping», и идея всегда была одной и той же: укажи на HTML, настолько кривой, что браузер бы поморщился, — и библиотека всё равно вытащит нужные данные. Эта репутация заслужена, хотя и с одной оговоркой, к которой мы ещё вернёмся.

Сначала зафиксируем несколько фактов:

ПолеЗначение
Пакетbeautifulsoup4 (импортируется как bs4)
Тестируемая версия4.15.0 (загружена 2026-06-07)
Требование к Python>=3.7.0
ЛицензияMIT
Официальный сайтcrummy.com/software/BeautifulSoup
Исходники и баг-трекерLaunchpadне GitHub
ПоддержкаАктивная (4.15.0 в июне 2026, шесть релизов за последний год)

Фраза «не GitHub» важнее, чем кажется. bs4 — это библиотека с 20-летней историей, которая живёт на crummy.com и Launchpad, так что привычная проверка «сколько звёзд на GitHub» здесь не работает. Оценивать её нужно по частоте релизов — и по этому признаку у неё всё в порядке: проект жив и развивается.

Небольшая тонкость по лицензии для тех, кому приходится согласовывать зависимости с compliance-командами: сама обёртка MIT, но то, что именно подтягивается в дерево зависимостей при использовании bs4, зависит от выбранного backend’а. html.parser входит в стандартную библиотеку Python (лицензия PSF, без дополнительных зависимостей). lxml имеет BSD-лицензию, но опирается на libxml2/libxslt — внешнюю C-зависимость, которую либо собирают сами, либо получают как готовый wheel. html5lib — чистый Python и MIT. Если тебе нужен самый «чистый» набор зависимостей, встроенный html.parser даёт его. И, как ни странно, именно он таит главный подвох. Об этом — дальше.

Цена скорости, в цифрах

Сразу назовём главное число, чтобы не прятать его за эвфемизмами. На реалистичной задаче «распарсить и извлечь данные» — разобрать строку, вытащить все <h3 class="title"> и все <a href> — BeautifulSoup оказывается самым медленным парсером в сравнении, причём с большим отрывом.

BeautifulSoup speed tax: 232 ms versus 15 ms C parsers

Эти замеры взяты из того же benchmark selectolax (то же железо, та же методика из 3 прогонов, актуально на 2026-07-13); сам обзор не запускал отдельные тайминги, чтобы не создавать лишнюю нагрузку на CPU и не дублировать работу. Медианная задержка p50, в миллисекундах:

Размер страницыbs4 (html.parser)bs4 (lxml)selectolax-Lexborlxmlbs4-hp медленнееbs4-lxml медленнее
1 KB0.3230.2810.0270.03612.0x10.5x
10 KB2.0491.7050.1600.16612.8x10.7x
100 KB20.59916.5401.4641.42314.1x11.3x
1 MB232.558181.85514.90114.17715.6x12.2x
10 MB2788.7462261.557159.935172.93317.4x14.1x

Итак, bs4(html.parser) работает примерно в 12–17 раз медленнее, чем C-парсер вроде selectolax-Lexbor, а переход на backend lxml возвращает только 10.5–14x — всё равно полный порядок величины отставания. Причина структурная, а не баг: независимо от того, какой backend делает разбор, bs4 создаёт полноценный Python-объект (Tag или NavigableString) для каждого узла. Этот слой materialization объектов — налог, который C-парсеры просто не платят.

Обрати внимание: множитель растёт по мере увеличения страниц — 12.0x на 1 KB и 17.4x на 10 MB. Значит, речь не о фиксированном накладном расходе при старте, который можно «размазать» по объёму. Это налог на каждый узел, который линейно растёт вместе с количеством созданных узлов.

Теперь посмотрим на это иначе, потому что «в 10 раз медленнее» звучит страшнее, чем есть на практике. На странице размером 1 MB это 232 мс против 15 мс. Если твоя задача — «собрать несколько сотен или несколько тысяч страниц по несколько сотен килобайт каждая», разница в абсолютных значениях почти незаметна: ты её не почувствуешь, и оптимизация ничего не даст. Если же ты строишь конвейер на миллион страниц, тот же коэффициент уже решает, закончится ли задача вообще. Одна и та же цифра, но совершенно разный вывод. Оценивай по реальному объёму, а не по benchmark.

Нет, смена backend’а это не чинит

Есть стойкий миф, будто можно передать bs4 backend lxml и получить скорость lxml. Это не так, и важно понять почему. На batch CSS-запросе по 100 000 узлам (выбрать каждый <a> и прочитать его href, дерево уже построено) разрыв по пропускной способности очень заметен:

ПарсерQuery p50Узлов/сек
lxml33.30 ms3,002,646
selectolax-Modest34.19 ms2,924,550
selectolax-Lexbor39.46 ms2,534,027
bs4 (lxml)250.56 ms399,111

bs4(lxml) обрабатывает примерно 399 000 узлов в секунду — то есть примерно в 6.3–7.5 раза медленнее, чем три C-движка, хотя его собственный backend — это как раз lxml. Backend ускоряет построение дерева. Но запросы и обход всё равно проходят через soupsieve и объекты bs4 Tag, и каждый совпавший узел всё равно упаковывается в Python. Поэтому модель «дай bs4 lxml, и он станет как lxml» неверна: backend ускоряет одну фазу, а самая медленная фаза находится не в ней.

Затраты на память и холодный старт дополняют картину. На документе 10 MB bs4 использует примерно в 1.5–1.75 раза больше resident memory, чем selectolax или lxml (218–226 MB против 129–145 MB) — по той же причине: один Python-объект на узел. А импорт bs4 занимает около 33.4 мс против 14.1 мс у lxml.html, то есть он в 2.36 раза медленнее при запуске. Для долгоживущего процесса это почти погрешность, но для CLI-утилиты или serverless-функции с постоянными cold start’ами это уже реальный, хоть и небольшой, издержки.

Почему дополнительные потоки не спасут

Если твой инстинкт при медленной CPU-bound задаче — «завалим её потоками», bs4 этот инстинкт накажет. На странице 1 MB, распарсенной 48 раз, сравнение одного потока и четырёх потоков выглядит так:

Парсер1 поток4 потокаУскорение
selectolax-Lexbor0.563 s0.159 s3.54x
lxml0.459 s0.378 s1.21x
bs4 (lxml)6.945 s26.842 s0.26x

Прочитай нижнюю строку ещё раз. Четыре потока сделали bs4 примерно в 3.9 раза медленнее, а не быстрее. Эмпирический сигнал тут такой: вероятно, удерживается GIL. Построение дерева в bs4 написано на чистом Python, поэтому всё сериализуется под Global Interpreter Lock, а дополнительные потоки лишь добавляют накладные расходы планировщика к задаче, которая всё равно не может выполняться параллельно. selectolax получает ускорение примерно в 3.5 раза, потому что его C-ядро освобождает блокировку; у bs4 такого пространства для манёвра нет.

Для эпохи free-threading практический вывод такой: если тебе нужно распараллелить BeautifulSoup, бери multiprocessing (ProcessPoolExecutor), а не потоки. selectolax и lxml умеют масштабироваться на потоках; bs4 — нет. Небольшая оговорка по строгости: это единичное наблюдение для одного числа потоков (4) и одного размера страницы (1 MB), а механизм «держит GIL» — гипотеза, выведенная из поведения по времени, а не результат инструментального подтверждения конкретного участка кода. Направление понятно, но точный механизм остаётся предварительным.

Самый опасный дефолт — это backend по умолчанию. Сначала прочитай это

Если ты вынесешь из этого обзора только одну мысль, пусть это будет она. Обычный BeautifulSoup(html) без второго аргумента использует html.parser, а html.parser не реализует правила HTML5 для необязательных закрывающих тегов. Звучит академично, пока это не начинает тихо портить твои данные.

BeautifulSoup backend tolerance matrix: html.parser 12/15, lxml and html5lib 15/15

Я прогнал 15 намеренно испорченных HTML-образцов через все три backend’а, причём для каждого заранее была зарегистрирована независимая от backend’а структурная проверка, чтобы никто не выбирал победителя постфактум. Результаты такие:

BackendСоответствует ожиданию / 15
lxml15
html5lib15
html.parser12

Все три сбоя имеют одну причину. Возьмём незакрытую таблицу: <table><tr><td>a<td>b<tr><td>c<td>d</table>. В html.parser извлечённый текст ячеек получается ['abcd','bcd','cd','d'] — каждый <td> «съедает» всё, что идёт после него, потому что парсер вкладывает ячейки друг в друга вместо того, чтобы закрывать их. lxml и html5lib правильно возвращают ['a','b','c','d']. Незаполненные элементы списка ведут себя так же: <li>a<li>b<li>c даёт вложенные ['abc','bc','c'] в html.parser и аккуратные ['a','b','c'] в двух других. Дубликаты атрибутов тоже отличаются: <div id="first" id="second"> сохраняет "second" в html.parser, но "first" в lxml/html5lib, а HTML5-спецификация требует оставлять первый.

Почему это опасно, а не просто раздражает? Потому что это происходит без ошибки. Скрейпер, который просто делает BeautifulSoup(html) и натыкается на незакрытую таблицу или список — а это, увы, очень часто встречается на старых сайтах, в HTML, написанном вручную, и в шаблонах, где забыли закрывающий тег, — смешает текст соседних ячеек в одно поле, отдаст тебе грязные данные и ни разу не пожалуется. Исправление — одна строка: BeautifulSoup(html, "lxml") или BeautifulSoup(html, "html5lib").

Если быть справедливым к html.parser, то в остальных 12 из 15 испорченных примеров все три backend’а дали одинаковый результат — криво вложенные теги вроде <b><i></b></i>, отсутствие каркаса html/body, некавыченные атрибуты, лишние закрывающие теги, незакрытые комментарии, вложенные формы, смешанный регистр и т. д. Терпимость bs4 действительно высокая. Расхождения почти полностью сосредоточены в семействе необязательных закрывающих тегов. И всё это не открытие: в документации bs4 в разделе «Differences between parsers» уже прямо сказано, что html.parser «less lenient» — менее снисходителен. Ценность этой матрицы в том, что она показывает конкретные, воспроизводимые случаи, где это «менее снисходителен» превращается в неверный результат.

От чего ты не отказываешься: API и CSS — сильнейшая сторона

Итак, bs4 медленный, однопоточный и имеет ловушку в backend’е по умолчанию. Но люди всё равно выбирают его, потому что «дружелюбная» половина обмена абсолютно реальна — и в тестах она подтверждается.

BeautifulSoup soupsieve CSS coverage wins with 41/41 plus 20/20

Я прогнал 29 проверок API, охватывающих поиск, CSS, навигацию по дереву, извлечение текста и изменение DOM. Все 29 прошли, причём результат каждой проверки считался сравнением фактического значения с ожидаемым, а не на глаз. Две из этих возможностей особенно важны как эргономика, которую C-парсеры просто не предлагают:

  • Функциональные предикаты в find / find_all. Можно написать soup.find(lambda t: t.name == "a" and "btn" in t.get("class", [])) и выразить сложное условие в одну строку Python — без двухшаговой схемы «сначала выбери всё, потом фильтруй».
  • Именованная двунаправленная навигация по дереву. .parent, .next_sibling, .find_parent, .stripped_strings, .descendants — обходы читаются почти как обычный текст и работают в обе стороны. В selectolax для некоторых сценариев нужны дополнительные шаги, а некоторые и вовсе отсутствуют.

Вот и та самая часть, которая экономит время разработчика. Это не маркетинг, это 29 зелёных галочек.

Есть ещё два подводных камня, которые стоит отметить, потому что честный обзор обязан показывать обе стороны. Во-первых, булевы атрибуты: <input disabled> возвращает в bs4 пустую строку "" для disabled (в selectolax — None). В обоих случаях это falsy, поэтому if node.get("disabled") молча пропустит реально присутствующий булев атрибут в обеих библиотеках — безопасная проверка здесь "disabled" in tag.attrs. Во-вторых, get_text(strip=True) склеивает текст узлов без разделителя после strip, так что "...with " + "link1" превращается в "withlink1". Когда нужны границы слов, передавай separator=" ". Ни один из этих подвохов не уникален для bs4; это общие ловушки для нескольких библиотек.

И вот что многих удивляет: выбор bs4 не означает проигрыш в CSS-покрытии. Его CSS-engine, soupsieve, — самый полный в этом сравнении. На базовой матрице из 41 кейса (взята из benchmark selectolax) soupsieve набрал 41/41 — единственный идеальный результат в тесте, опередив selectolax-Lexbor с 39/41 и cssselect (lxml/parsel) с 37/41. Затем я прогнал ещё 20 расширенных кейсов, которые заявлены в документации soupsieve, и получил 20/20, включая селекторы, которые Lexbor вообще отвергает: :lang(en), только для soupsieve :-soup-contains('featured'), :is(), :where() и :has(> a). Реальные пробелы — это XPath (soupsieve работает только с CSS) и псевдоэлементы ::text / ::attr() из parsel, которые являются расширениями Scrapy. Если ты живёшь в XPath, миграция будет болезненной.

Вывод для этого раздела простой: выбирая BeautifulSoup, ты жертвуешь скоростью. Но не эргономикой API и уж точно не CSS-покрытием.

Две производственные проблемы, на которые стоит заложить бюджет

Помимо backend’а по умолчанию, есть ещё два поведения, которые особенно неприятно проявляются в долгоживущих процессах и при работе с не-UTF-8 данными.

Циклы ссылок: вызывай decompose() в длинных циклах

Каждый Tag в bs4 хранит ссылку и на родителя, и на детей, из-за чего возникает цикл ссылок. Подсчёт ссылок в CPython не может сам удалить такой цикл — это работа поколенческого garbage collector’а. Чтобы понять, насколько это важно, я 300 раз создавал и удалял дерево с отключённым GC, а затем считал оставшиеся в памяти объекты Tag:

BeautifulSoup reference cycles retain 120,900 objects with GC off and 0 with GC on

СценарийTags, оставшиеся после del
GC off120,900 (300 циклов, ничего не освобождено)
GC on26,598 (поколенческий GC сработал в середине цикла)
После принудительного gc.collect()0 (всё освобождено)
Контроль без циклов (список строк, GC off)delta 0

При выключенном GC del soup не освободил ничего — все 120 900 объектов остались в памяти, потому что цикл ссылок ломает обычный подсчёт ссылок. Один gc.collect() очистил их все. Контрольная группа без циклов (обычный список строк, заведомо без цикла) показала нулевую delta, что доказывает: накопление произошло именно из-за циклов bs4, а не из-за шума измерения. В документации bs4 прямо сказано, что объекты «плотно переплетены ... именно так, как любит запутывать GC», так что это задокументированное поведение; тест добавляет сюда численное количество удержанных объектов и доказательство, что collect() обнуляет их.

Практическое правило простое: если в пайплайне ты парсишь много больших страниц в плотном цикле, и твой код — или какая-то высокопроизводительная настройка — отключает GC либо не запускает его достаточно часто, деревья bs4 будут висеть в памяти, а RAM начнёт расти. После каждой страницы вызывай soup.decompose() — именно для разрыва цикла и раннего освобождения памяти это и предусмотрено. У C-деревьев selectolax и lxml этой проблемы нет вовсе.

Кодировка: UnicodeDammit — тихое преимущество bs4

В bs4 есть компонент, которого нет у быстрых парсеров: UnicodeDammit. Он определяет кодировку документа и автоматически переводит её в Unicode. Я проверил его на матрице из 8 случаев «заявленная кодировка против реальной»:

BeautifulSoup UnicodeDammit recovers 5 of 8 encoding cases

СлучайИстинная кодировкаКакую кодировку угадал UnicodeDammitВосстановлено?
utf8_no_declutf-8utf-8Да
utf16_bomutf-16utf-16leДа
gbk_chinesegbkgb18030Да (надмножество)
shiftjisshift_jiscp932Да (надмножество)
latin1_declared_utf8latin-1 (объявлено как utf-8)iso-8859-1Да (проигнорировал ложь)
latin1_no_decllatin-1cp720Нет
cp1252_no_declcp1252cp862Нет
utf8_declared_latin1utf-8 (объявлено как latin-1)iso-8859-1Нет (поверил ложному объявлению)

Из восьми случаев удалось восстановить пять. UTF-8, UTF-16 с BOM, GBK, Shift-JIS и даже неправильно подписанный latin-1 были распознаны корректно, а варианты с надмножеством кодировок (GBK→gb18030, Shift-JIS→cp932) тоже декодируются без проблем. Два сценария сбоя тоже полезно знать: короткие фрагменты latin-1/cp1252 распознаются как DOS code page, потому что статистический детектор ненадёжен на коротких входах, а символы рамок DOS пересекаются с code points Latin-1; и если <meta charset> просто неверен, UnicodeDammit доверяет объявлению. В документации bs4 прямо указано и то и другое — образец может быть «настолько коротким, что Unicode, Dammit не может за него зацепиться», а больше данных означает более точное предположение.

По сравнению с selectolax, который молча портит байты не-UTF-8 и ждёт, что ты сам их декодируешь, это реальное преимущество: bs4 хотя бы пытается угадать кодировку и часто справляется. Но гарантий нет. Если кодировка известна, не полагайся на угадывание — укажи её явно: BeautifulSoup(bytes, from_encoding="...").

А бывают ли реальные расхождения между backend’ами на настоящих страницах?

Матрица с битым HTML показывает, что backend’ы расходятся на специально поломанном вводе. Следующий очевидный вопрос: важно ли это в реальном мире? Поэтому я прогнал все три backend’а на 11 реальных страницах, загруженных с сайтов BBC, Wikipedia, Craigslist, MDN, old.reddit, документации Python, Hacker News, Books to Scrape, webscraper.io, whitehouse.gov и на странице с цитатами, рендеримой через JS, сравнивая количество ссылок, заголовков и изображений.

На всех 11 страницах все три backend’а совпали. Ноль расхождений. Это означает, что различия из раздела про ловушку проявляются только на намеренно повреждённом HTML; если современный production-site структурирован достаточно хорошо — даже если он «грязноватый», — выбор backend’а не меняет то, что ты извлекаешь. Практический вывод: для массовых, нормально написанных сайтов html.parser вполне достаточно и не требует дополнительных зависимостей. Только когда ты скрапишь явно нестандартный, написанный вручную или очень старый HTML, выбор backend’а начинает влиять на результат, и тогда стоит переключиться на lxml или html5lib.

Небольшое замечание из того прогона, потому что это реальный крайний случай. На странице MDN есть элемент <template>, и все backend’ы bs4 вернули 508 ссылок — то есть bs4 «разворачивает» содержимое <template> в основное дерево. Это ставит bs4 на одну сторону с lxml и противопоставляет его selectolax-Lexbor, который строго следует HTML5-спецификации (<template> — это инертный DocumentFragment) и возвращает 497, молча игнорируя 11 ссылок внутри template. Так что bs4 действительно захватывает данные внутри <template> — это удобно, но одновременно может привести к появлению «фантомного» контента, который браузер никогда бы не показал. Оба поведения не ошибочны; это разные интерпретации спецификации, и важно знать, какую именно ты получаешь.

Где BeautifulSoup уместен — и где нет

Чтобы не сводить всё к одной оценке 0–100, которая скрыла бы именно те компромиссы, которые важны, вот таблица по отдельным измерениям с оговоркой по каждой строке:

ПараметрЧто показали тестыОговорка для читателя
Установка / первый запускЧистая обёртка, без браузера и сложной настройки; html.parser без зависимостей; все wheels готовыДля backend’а lxml нужна C-зависимость
Скорость относительно C-парсеровв 12–17x медленнее (html.parser) / в 10.5–14x (backend lxml), на всех размерахОдин стенд; повторно использованы данные selectolax
Скорость CSS-запросовпримерно в 6–7.5x медленнее на 100k узлов; backend lxml не спасаетПовторное использование; платишь налог за Python Tag
Памятьв 1.5–1.75x больше, чем у selectolax/lxml; самый тяжёлыйПовторное использование; измерение по RSS
Холодный старт импортав 2.36x медленнее (33.4 против 14.1 мс)Повторное использование; небольшая статья затрат
Масштабирование по потокамbs4-lxml примерно в 3.9x медленнее на 4 потоках (держит GIL)Одно наблюдение; используй multiprocessing
Эргономика API29/29 проверок; функциональные предикаты в find + двунаправленная навигацияЛовушки с пустой строкой у булевых атрибутов и strip/границами слов
Покрытие CSSsoupsieve лучший: 41/41 базовых + 20/20 расширенных; поддерживает :langНет XPath, нет ::text
Три backend’а и терпимостьlxml/html5lib 15/15; html.parser 12/15Расхождения только на битом HTML
Согласованность на реальных страницах3 backend’а совпали 11/11; все «разворачивают» <template> (508)Для хорошо структурированных сайтов backend не важен
Сборка циклов ссылокДерево — это цикл; 300 циклов оставили 120,900 объектов, collect всё очистилВ длинных циклах нужен decompose()
КодировкаUnicodeDammit восстанавливает 5/8; короткие образцы угадывает плохо, ложные декларации принимаетОдно наблюдение
ПоддержкаАктивная (4.15.0, июнь 2026); MITСайт на crummy/Launchpad, не на GitHub

Так для кого же BeautifulSoup? Для тех, кому важнее читаемый API и терпимость к кривому HTML, чем максимальная производительность, и кто работает на умеренных объёмах: прототипы, разовые скрейпы, внутренние инструменты, команды, где время разработчика дороже времени выполнения. А кому стоит смотреть в другую сторону? Конвейерам на миллион страниц, где налог на скорость превращается в реальные деньги, задачам, которым нужна параллельность на уровне потоков, и всем, кто жёстко сидит на XPath.

Ещё одно замечание о том, как bs4 вписывается в реальный стек парсинга и где в эту картину попадает наш инструмент. BeautifulSoup предполагает, что HTML у тебя уже есть. Он не загружает страницы, не рендерит JavaScript и не решает проблемы anti-bot защиты или CAPTCHA — это отдельная, и по-настоящему сложная задача в современном вебе. Именно на другом уровне работает AI scraping API: стек разработчика Thunderbit — REST API, MCP server и CLI — берёт на себя загрузку, рендеринг JS и антибот-задачи, а затем возвращает либо чистый Markdown (POST /distill), либо структурированный JSON, совпадающий со схемой (POST /extract), без необходимости писать селекторы. Это не конкуренты, а дополняющие друг друга инструменты. bs4 парсит HTML, который у тебя уже есть; API, MCP и CLI Thunderbit помогают получить HTML, до которого непросто добраться изначально. Если твой узкий участок — парсинг, bs4 отвечает на задачу отлично. Если узкое место — получение данных, это уже другой слой.

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

Итог

BeautifulSoup даёт самый дружелюбный API, лучшую терпимость к битому HTML и самый полный CSS engine в этом сравнении — и всё это оплачивается примерно десятикратным штрафом по скорости и самым тяжёлым потреблением памяти. Это и есть весь компромисс, если сказать прямо. Главная реальная ловушка — backend html.parser по умолчанию: он тихо портит незакрытые таблицы и списки, поэтому передавай "lxml" или "html5lib", если вход может быть неаккуратным. Потоки его не ускорят — ускорит multiprocessing. А в длинных циклах после каждой страницы вызывай decompose(), чтобы циклы ссылок не накапливались.

Два ограничения напоследок. Все измерения сделаны на одной платформе (macOS arm64, Python 3.14, готовые wheels), а множители по времени взяты повторно из benchmark selectolax (тот же стенд, актуально на 2026-07-13), а не прогонялись заново — значит, они унаследовали ограничения одного стенда, и на Linux x86_64 или в сборке из исходников цифры могут немного сдвинуться. И ни один из результатов не является новым открытием: bs4 — это библиотека с 20-летней историей, поэтому всё, что здесь тестировалось, либо задокументировано, либо уже было публично известно. Ценность этой работы не в сенсации, а в том, что она ставит реальные числа на компромиссы, которые в документации обычно описаны только качественно.

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

BeautifulSoup медленный? Да, и это измеримо. На задаче parse-plus-extract он работает примерно в 12–17 раз медленнее, чем C-парсер вроде selectolax-Lexbor с backend’ом html.parser по умолчанию, и в 10.5–14 раз медленнее с backend’ом lxml, потому что создаёт Python-объект для каждого узла. Насколько это важно, зависит от масштаба: на странице 1 MB это 232 мс против 15 мс — незаметно для нескольких тысяч страниц, но критично для pipeline на миллион страниц.

Какой парсер BeautifulSoup выбрать — html.parser, lxml или html5lib? Для хорошо сформированных, массовых сайтов встроенный html.parser подходит и не добавляет зависимостей. Но он не реализует HTML5 optional-end-tags, поэтому на незакрытых таблицах или списках он незаметно смешивает соседний текст. Если вход может быть битым, написанным вручную или старым, явно передавай "lxml" или "html5lib" — оба дали чистые 15/15 на матрице битого HTML, где html.parser получил 12/15.

Можно ли распараллелить BeautifulSoup потоками? Нет. Построение дерева в bs4 написано на чистом Python и держит GIL, поэтому добавление потоков делает его медленнее, а не быстрее — в тесте четыре потока обработали страницу 1 MB примерно в 3.9 раза медленнее, чем один поток. Для параллелизации bs4 используй multiprocessing (ProcessPoolExecutor). А вот библиотеки с C-ядром, такие как selectolax и lxml, уже умеют выигрывать от параллелизма на потоках.

Хорошо ли BeautifulSoup работает с битым HTML? В целом да — на наборе испорченных примеров (криво вложенные теги, отсутствие каркаса, некавыченные атрибуты и многое другое) все три backend’а в большинстве случаев восстановились корректно. Слабое место — это backend html.parser и optional-end-tags: незакрытые <td> / <li> вкладываются друг в друга вместо закрытия, из-за чего текст извлекается неправильно. Переключись на lxml или html5lib, и этот класс проблемы исчезнет.

BeautifulSoup против lxml — что лучше? Это разные инструменты. lxml гораздо быстрее и при построении дерева, и при запросах, а также поддерживает XPath. BeautifulSoup, среди прочего, оборачивает lxml в намного более удобный API и даже даёт более широкое покрытие CSS через soupsieve. Только не жди, что backend lxml сделает bs4 таким же быстрым, как lxml: backend ускоряет только разбор, а запросы и обход всё равно платят за Python-объект на каждый узел, поэтому на больших выборках bs4 остаётся примерно в 6–7.5 раза медленнее.

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

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

Попробуй Thunderbit

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

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