BeautifulSoup в Python, это библиотека, к которой обращается большинство разработчиков, когда нужно извлечь структурированные данные из «грязного» HTML-документа. Она превращает сырую страницу в навигируемое дерево объектов Python, а затем предоставляет небольшой читаемый API для поиска нужных элементов и чтения их текста или атрибутов. Вам не нужно изучать язык запросов или писать парсер; вы описываете, что хотите, с помощью имени тега, атрибута или CSS-селектора, и BeautifulSoup возвращает результат.
Это руководство, практическое знакомство с этим API. Мы установим BeautifulSoup с быстрым парсером, создадим суп из примера разметки, а затем разберём find и find_all, методы CSS-селекторов select и select_one, навигацию по дереву через родителей и соседей, а также чтение текста и атрибутов. В завершение рассмотрим реалистичный рабочий пример, который извлекает список записей и следует пагинации. Одна вещь, о которой важно помнить: BeautifulSoup только парсит. Он никогда не загружает URL и не запускает JavaScript, поэтому передаваемый ему HTML уже должен содержать нужные данные.
Что умеет BeautifulSoup и что нет
BeautifulSoup, это библиотека парсинга. Вы передаёте ей строку HTML или XML, и она строит дерево, которое можно искать и по которому можно перемещаться. Это вся её задача. Она не открывает сетевые соединения, не выполняет скрипты и не знает, что браузер отрендерил бы из разметки. Всё, что вы извлекаете, должно присутствовать в переданной разметке.
Эта граница важна, потому что две части скрапинга, это отдельные задачи. Получение страницы, одна проблема; её парсинг, другая. Для статических страниц можно совместить BeautifulSoup с библиотекой requests для получения HTML. Для страниц, строящих контент на стороне клиента с помощью JavaScript, «сырой» запрос возвращает почти пустую оболочку, и BeautifulSoup там ничего не найдёт. Мы вернёмся к этому случаю позже. Пока воспринимайте BeautifulSoup как парсерную часть пайплайна и ничего больше.
Установка BeautifulSoup и парсера
Сам BeautifulSoup поставляется в пакете beautifulsoup4. Ему также нужен парсер для фактического чтения HTML. Стандартная библиотека включает html.parser, не имеющий дополнительных зависимостей и подходящий для большинства задач. Для скорости и устойчивости к некорректной разметке установите также lxml и используйте его как парсер.
python -m venv bs_env source bs_env/bin/activate pip install beautifulsoup4 lxml requests
В Windows для активации окружения используйте bs_env\Scripts\activate вместо команды с source. Установка requests необязательна; мы используем её только для загрузки статических страниц в рабочем примере. После установки всего необходимого вы импортируете класс из bs4, а не из пакета с именем библиотеки.
Создание супа
Для создания супа нужны два аргумента: разметка и имя парсера. Чтобы следовать примерам без обращения к реальному сайту, начните с встроенной HTML-строки, чтобы входные данные были предсказуемы.
from bs4 import BeautifulSoup html = """ <html> <body> <h1 id="title">Books</h1> <ul class="catalog"> <li class="book"><a href="/b/1">Dune</a><span class="price">12.99</span></li> <li class="book"><a href="/b/2">Neuromancer</a><span class="price">9.50</span></li> </ul> </body> </html> """ soup = BeautifulSoup(html, "lxml") print(soup.title) # None here; no <title> in the markup print(soup.h1.get_text()) # Books
Замените "lxml" на "html.parser", если lxml не установлен; остальной API идентичен. Доступ к тегу по имени, например soup.h1, возвращает первый подходящий элемент как быстрый способ. Это удобно для быстрых проверок, но ограничено, поэтому настоящий поиск происходит через методы, описанные ниже.
Выбранный парсер влияет на то, как исправляется некорректный HTML. html.parser встроен и не требует зависимостей. lxml быстрее и терпимее к некорректным страницам, а это большинство реальных страниц. html5lib парсит точно как браузер, но медленнее. Когда два парсера расходятся на сложной странице, это обычно и есть причина, поэтому явно указывайте парсер, не позволяя BeautifulSoup угадывать.
find и find_all
Два основных рабочих метода, find и find_all. find возвращает первый подходящий элемент или None, если совпадений нет. find_all возвращает список всех совпадений, пустой при отсутствии таковых. Оба принимают имя тега и необязательные фильтры.
first_book = soup.find("li") print(first_book.a.get_text()) # Dune all_books = soup.find_all("li") print(len(all_books)) # 2 for book in all_books: print(book.a.get_text())
Фильтры сужают поиск. Можно искать по CSS-классу, id, произвольному атрибуту или словарю атрибутов. Поскольку class, зарезервированное слово в Python, BeautifulSoup использует именованный аргумент class_ с завершающим подчёркиванием.
# By class prices = soup.find_all("span", class_="price") # By id heading = soup.find(id="title") # By any attribute, via the attrs dict links = soup.find_all("a", attrs={"href": True}) # Limit how many you get back one_link = soup.find_all("a", limit=1)
Можно также передать список имён тегов для совпадения с любым из них или скомпилированное регулярное выражение для сопоставления имён тегов или значений атрибутов по паттерну. В большинстве задач скрапинга фильтры по классу и атрибуту охватывают всё необходимое, а методы CSS-селекторов ниже нередко читаются чище для вложенных условий.
select и select_one с CSS-селекторами
Если вы уже мыслите в терминах CSS-селекторов, select и select_one позволяют напрямую применять эти знания. select возвращает список всех совпадений; select_one, первое совпадение или None. Они принимают тот же синтаксис селекторов, что вы писали бы в таблице стилей или передавали бы в document.querySelectorAll.
# Descendant: every <a> inside a .book li titles = soup.select("li.book a") # First price under the catalog list first_price = soup.select_one("ul.catalog .price") # Attribute selector internal = soup.select("a[href^='/b/']") # By id heading = soup.select_one("#title")
Селекторы удобны, когда цель определяется своим положением в дереве, например «ссылка внутри второго элемента списка». Длинная цепочка вызовов find читается хуже, чем эквивалентный однострочный селектор. Предпочтение find_all или select, во многом дело вкуса; оба взаимозаменяемы для большинства задач, и один скрипт нередко смешивает оба. Подробное сравнение стилей селекторов смотрите в статье веб-скрапинг с XPath и CSS-селекторами.
Навигация по дереву
Найдя элемент, можно перемещаться по дереву относительно него, а не искать с самого верха. Каждый тег открывает доступ к родителю, дочерним элементам и соседям, это именно то, что нужно, когда нужные данные находятся рядом с уже найденным элементом.
price = soup.select_one(".price") # Up: the <li> that contains this price row = price.parent # Down: direct children, ignoring whitespace text nodes children = [c for c in row.children if c.name] # Sideways: the <a> just before the price in the same <li> title_link = price.find_previous_sibling("a") print(title_link.get_text()) # Dune
Несколько замечаний, позволяющих избежать путаницы. .children и .contents включают текстовые узлы, например пробелы между тегами, поэтому фильтрация по c.name оставляет только реальные элементы. .find_next_sibling и .find_previous_sibling пропускают эти текстовые узлы и принимают имя тега для совпадения. Используйте .find_parent для подъёма к конкретному предку, а не только к непосредственному родителю. Относительная навигация, самый надёжный способ работать со страницами, где нужное значение находится рядом со стабильной меткой.
Получение текста и атрибутов
Извлечение сводится к двум вещам: текст внутри элемента и значения его атрибутов. Для текста get_text возвращает всё строковое содержимое элемента и его потомков, объединённое вместе. Передайте strip=True для обрезки окружающих пробелов, что почти всегда нужно.
link = soup.select_one("li.book a") # Text content print(link.get_text(strip=True)) # Dune # Attribute by key; raises KeyError if absent print(link["href"]) # /b/1 # Safe attribute read with a default print(link.get("title", ""))
Чтение атрибута через квадратные скобки, например link["href"], вызывает KeyError при отсутствии атрибута, поэтому предпочитайте link.get("href"), когда атрибут может не существовать. Различие между текстом и атрибутами сбивает с толку новичков: видимая подпись ссылки берётся через get_text, а URL назначения, из атрибута href, и эти две вещи никак не связаны.
Когда селектор ничего не находит, find и select_one возвращают None, и вызов .get_text() на None вызовет AttributeError. Реальные страницы непоследовательны: не в каждой строке есть цена, не в каждой карточке рейтинг. Проверяйте существование элемента перед обращением к нему или используйте небольшой вспомогательный метод, возвращающий None при неудаче поиска, чтобы одно отсутствующее поле не рушило весь цикл.
Рабочий пример: извлечение списка записей
Теперь соберём всё воедино на статической странице, созданной для практики скрапинга. Сайт quotes.toscrape.com отдаёт чистый серверный HTML, поэтому requests может его загрузить, а BeautifulSoup, спарсить напрямую. Каждая цитата находится в блоке div.quote с текстом, автором и списком тегов, хороший аналог повторяющейся записи в реальных задачах скрапинга.
import requests from bs4 import BeautifulSoup def parse_quotes(html): soup = BeautifulSoup(html, "lxml") records = [] for block in soup.select("div.quote"): text_el = block.select_one("span.text") author_el = block.select_one("small.author") tags = [t.get_text(strip=True) for t in block.select("a.tag")] records.append({ "quote": text_el.get_text(strip=True) if text_el else None, "author": author_el.get_text(strip=True) if author_el else None, "tags": tags, }) return records url = "https://quotes.toscrape.com/" resp = requests.get(url, timeout=15) if resp.status_code == 200: for row in parse_quotes(resp.text): print(row)
Паттерн здесь тот, который вы будете использовать повсюду: выбирайте повторяющийся контейнер через select, затем выполняйте второй, ограниченный запрос внутри каждого контейнера для извлечения отдельных полей. Привязка поиска по отдельным полям к block, а не ко всему документу, гарантирует, что автор второй строки не попадёт в первую. Проверка каждого элемента перед вызовом get_text означает, что цитата без автора вернёт None вместо сбоя цикла.
Следование пагинации
Одна страница, это демонстрация; полный набор данных обычно занимает много страниц. Тренировочный сайт связывает следующую страницу через элемент li.next > a, поэтому цикл прост: парсим текущую страницу, ищем ссылку на следующую, разрешаем её относительно базового URL и останавливаемся, когда ссылки нет.
import time from urllib.parse import urljoin base = "https://quotes.toscrape.com/" next_url = base all_rows = [] while next_url: resp = requests.get(next_url, timeout=15) if resp.status_code != 200: break soup = BeautifulSoup(resp.text, "lxml") all_rows.extend(parse_quotes(resp.text)) next_link = soup.select_one("li.next a") next_url = urljoin(base, next_link["href"]) if next_link else None time.sleep(1) print(f"Collected {len(all_rows)} quotes")
Два детали делают это надёжным. urljoin превращает относительный href вроде /page/2/ в полный URL без строковых манипуляций, поэтому работает даже при изменении структуры пути. time.sleep(1) распределяет запросы, чтобы вы не нагружали сервер, это одновременно корректно и простейший способ оставаться в рамках лимита. Подробнее о получении и структурировании данных от начала до конца, в статье как парсить сайт с помощью Python.
Когда BeautifulSoup недостаточен: JavaScript-страницы
Всё вышесказанное предполагает, что данные находятся в полученном HTML. Многие современные сайты работают иначе. Они отдают минимальную HTML-оболочку и строят настоящий контент в браузере с помощью JavaScript, подтягивая данные из фоновых API-вызовов после загрузки страницы. Загрузите такую страницу через requests, и тело, переданное в BeautifulSoup, будет иметь пустые контейнеры там, где должны быть записи. BeautifulSoup работает корректно; данных просто не было в строке.
Есть два выхода. Можно запустить настоящий браузер с помощью Selenium или Playwright, дождаться рендеринга контента и передать отрендеренный page_source в BeautifulSoup. Это работает, но означает запуск и обслуживание флота браузеров, а на защищённых сайтах придётся ещё управлять прокси и блокировками. Второй способ, переложить шаг получения и рендеринга на сервис, возвращающий готовый HTML, а затем парсить этот HTML тем же кодом BeautifulSoup. В любом случае слой парсинга не меняется; изменяется только способ получения HTML. Подробнее об этом разделении, в статье как парсить JavaScript-страницы с помощью Python.
BeautifulSoup только парсит; он не может рендерить JavaScript-страницу и обходить серьёзные блокировки. Crawling API берёт на себя шаг получения и рендеринга: отправьте ему URL с JS-токеном, он запустит страницу в реальном браузере за ротируемыми резидентными IP и вернёт готовый HTML. Затем вы парсите этот HTML точно тем же кодом BeautifulSoup из этого руководства. Попробуйте на бесплатном уровне.
Вот структура такой связки. Получение идёт через Crawling API с JavaScript-токеном, а возвращаемое тело сразу поступает в ваш существующий парсер.
from crawlbase import CrawlingAPI from bs4 import BeautifulSoup api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"}) response = api.get("https://example.com/spa-page", {"ajax_wait": "true", "page_wait": 4000}) if response["status_code"] == 200: html = response["body"].decode("utf-8") soup = BeautifulSoup(html, "lxml") # same find/select calls as before print(soup.select_one("h1").get_text(strip=True))
Если вы предпочитаете маршрутизировать собственный клиент через ротируемые IP, а не вызывать управляемый эндпоинт, Smart AI Proxy предоставляет резидентную ротацию как drop-in прокси, а Crawling API возвращает структурированные поля для поддерживаемых сайтов без BeautifulSoup вообще.
Ключевые выводы
- BeautifulSoup только парсит. Он строит дерево для поиска из уже имеющегося HTML; никогда не загружает URL и не выполняет JavaScript.
-
Установите
beautifulsoup4вместе с парсером. Используйтеhtml.parserдля нулевых зависимостей илиlxmlдля скорости и устойчивости к некорректной разметке; явно указывайте парсер. -
Изучите четыре метода.
findиfind_allищут по тегу и фильтрам;selectиselect_one, по CSS-селектору. Для большинства задач они взаимозаменяемы. -
Читайте текст и атрибуты раздельно.
get_text(strip=True)даёт видимый контент;element["href"]илиelement.get("href"), значение атрибута. -
Ограничивайте область, защищайтесь и пагинируйте. Выбирайте повторяющийся контейнер, запрашивайте каждое поле внутри него, проверяйте на
Noneи следуйте ссылкам на следующую страницу черезurljoinс небольшой задержкой. - Для JavaScript-страниц исправьте получение. Используйте Crawling API или headless-браузер для получения отрендеренного HTML, затем парсите его тем же кодом BeautifulSoup.
Часто задаваемые вопросы
Как установить BeautifulSoup в Python?
Установите его командой pip install beautifulsoup4. Имя импорта отличается от имени пакета: в коде вы пишете from bs4 import BeautifulSoup. BeautifulSoup также нужен парсер для работы. Встроенный html.parser не требует ничего дополнительного, но установка lxml через pip install lxml даёт более быстрый и терпимый парсер, это оправдано для реальных страниц.
В чём разница между find и find_all?
find возвращает единственный первый элемент, соответствующий вашим критериям, или None при отсутствии совпадений. find_all возвращает список всех подходящих элементов, пустой при отсутствии совпадений. Используйте find, когда ожидаете ровно один элемент (например, основной заголовок страницы), и find_all, когда собираете много (например, все строки в списке). Эквиваленты для CSS-селекторов, select_one и select.
Как получить текст внутри элемента и как получить атрибут?
Используйте element.get_text(strip=True) для видимого текстового содержимого, включая текст из вложенных тегов, с обрезанными пробелами. Используйте element["href"] для чтения значения атрибута или element.get("href") для безопасного чтения с умолчанием при отсутствии атрибута. Подпись ссылки и её URL назначения, это разные вещи: подпись, текст, URL, атрибут href.
Почему BeautifulSoup возвращает пустой результат на некоторых страницах?
Почти всегда потому, что данных нет в спарсенном HTML. Многие сайты рендерят контент в браузере с помощью JavaScript, поэтому «сырой» запрос возвращает пустую оболочку, и BeautifulSoup корректно ничего не находит. BeautifulSoup не выполняет JavaScript. Для работы с такими страницами сначала получите отрендеренный HTML, через headless-браузер вроде Selenium или Playwright, либо через Crawling API, а затем парсите этот отрендеренный HTML тем же кодом.
Может ли BeautifulSoup самостоятельно обрабатывать пагинацию?
Сам по себе нет, поскольку BeautifulSoup не загружает страницы. Пагинацию обрабатывают циклом: парсите текущую страницу, используйте BeautifulSoup для поиска ссылки на следующую страницу, загружайте этот URL через HTTP-клиент и повторяйте, пока следующей ссылки нет. Разрешайте относительные ссылки через urllib.parse.urljoin и добавляйте небольшую задержку между запросами, чтобы не перегружать сервер.
Что лучше использовать в качестве парсера: lxml или html.parser?
Используйте lxml, когда это возможно: он быстрее и лучше обрабатывает некорректный HTML, что характерно для большинства реальных страниц. Используйте встроенный html.parser, когда нужны нулевые дополнительные зависимости и страницы корректно сформированы. Для разметки, которую необходимо парсить точно так же, как браузер, html5lib наиболее точен за счёт скорости. Всегда явно указывайте имя парсера, чтобы поведение оставалось стабильным на разных машинах.
Обходите любой сайт в масштабе, без борьбы с инфраструктурой.
Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.
