Beautiful Soup: Как использовать find_all и исключать классы?

Краткое описание Beautiful Soup и его назначение

Beautiful Soup — это Python-библиотека для парсинга HTML и XML документов. Она создает дерево разбора из исходного кода страницы, что позволяет легко извлекать данные, навигировать, искать и изменять это дерево. Основное назначение — скрапинг веб-страниц: сбор информации, мониторинг изменений, анализ контента.

Обзор метода find_all: поиск элементов по различным критериям

Метод find_all является одним из самых мощных инструментов Beautiful Soup. Он возвращает список всех тегов, соответствующих заданным критериям. Критерии могут быть разнообразными: имя тега, значения атрибутов (таких как class, id, href), текстовое содержимое и даже пользовательские функции.

Базовый синтаксис find_all: имя тега, атрибуты, текст

Основной синтаксис метода find_all выглядит так:

from bs4 import BeautifulSoup
from typing import List, Optional

# Пример HTML
html_doc: str = """
<html><head><title>Пример</title></head>
<body>
<div class="item active">Элемент 1</div>
<div class="item special">Элемент 2</div>
<div class="item">Элемент 3</div>
<p class="item">Параграф</p>
<div class="other">Другой элемент</div>
</body>
</html>
"""

soup: BeautifulSoup = BeautifulSoup(html_doc, 'html.parser')

# Поиск по имени тега
tags_div: List[BeautifulSoup] = soup.find_all('div')

# Поиск по атрибуту class
tags_item: List[BeautifulSoup] = soup.find_all(class_='item')

# Поиск по тексту (здесь не используется find_all, но для полноты картины)
# text_element = soup.find(string="Элемент 1") 

Метод find_all принимает имя тега как первый аргумент, а атрибуты передаются как именованные аргументы (например, class_, так как class — зарезервированное слово в Python).

Исключение классов при использовании find_all

Необходимость исключения классов: зачем это нужно?

При парсинге веб-страниц часто возникает задача извлечь элементы с определенными характеристиками, кроме тех, что помечены специфическими классами. Например, при сборе списка товаров нужно исключить рекламные блоки или неактивные предложения, которые могут иметь схожую разметку, но дополнительные классы ('ad', 'inactive', 'promo'). Исключение классов позволяет получить более чистый и релевантный набор данных.

Использование ‘not’ для исключения классов (примеры)

Beautiful Soup 4.7.0 и выше поддерживает использование css_selector с псевдоклассом :not(). Для find_all напрямую нет простого оператора not, но можно использовать CSS-селекторы с методом select (рассмотрено ниже) или функции.

Однако, если нужно найти теги, у которых нет определенного класса, но есть другие, можно использовать функцию:

from bs4 import BeautifulSoup
from bs4.element import Tag # Для type hinting
from typing import List, Optional, Callable

html_doc: str = """
<div class="product featured">Продукт A</div>
<div class="product sale">Продукт B</div>
<div class="product">Продукт C</div>
<div class="ad featured">Реклама</div>
"""
soup: BeautifulSoup = BeautifulSoup(html_doc, 'html.parser')

# Функция для проверки отсутствия класса 'featured'
def not_featured(tag: Tag) -> bool:
    """Проверяет, что у тега есть класс 'product', но нет класса 'featured'."""
    # Проверяем, что класс вообще есть
    classes = tag.get('class', [])
    return 'product' in classes and 'featured' not in classes

# Применение функции в find_all
products_not_featured: List[Tag] = soup.find_all(not_featured)

# print(products_not_featured)
# Вывод: [<div class="product sale">Продукт B</div>, <div class="product">Продукт C</div>]

Применение lambda-функций для более сложных условий исключения

Lambda-функции предоставляют компактный способ определения фильтрующих функций прямо внутри вызова find_all. Это удобно для простых и одноразовых условий.

from bs4 import BeautifulSoup
from bs4.element import Tag
from typing import List

html_doc: str = """
<a href="/page1" class="link active">Ссылка 1</a>
<a href="/page2" class="link internal">Ссылка 2</a>
<a href="#anchor" class="link disabled">Ссылка 3</a>
<a href="/page4" class="link">Ссылка 4</a>
<span class="link">Не ссылка</span>
"""
soup: BeautifulSoup = BeautifulSoup(html_doc, 'html.parser')

# Найти все теги 'a' с классом 'link', но без класса 'disabled'
active_links: List[Tag] = soup.find_all(
    lambda tag: tag.name == 'a' and 
                'link' in tag.get('class', []) and 
                'disabled' not in tag.get('class', [])
)

# print(active_links)
# Вывод: [
#  <a class="link active" href="/page1">Ссылка 1</a>, 
#  <a class="link internal" href="/page2">Ссылка 2</a>, 
#  <a class="link" href="/page4">Ссылка 4</a>
# ]

Lambda-функции особенно полезны, когда условия фильтрации включают комбинацию имени тега, наличия/отсутствия классов и других атрибутов.

Практические примеры исключения классов

Пример 1: Исключение элементов с определенным классом из списка результатов

Представим, что мы парсим страницу с новостными статьями, и хотим получить все div с классом article, но исключить те, что помечены как sponsored.

from bs4 import BeautifulSoup
from bs4.element import Tag
from typing import List

html_articles: str = """
<div class="article standard">Статья 1...</div>
<div class="article featured">Статья 2...</div>
<div class="article sponsored">Спонсорская статья 1...</div>
<div class="article standard">Статья 3...</div>
<div class="sponsored">Другой спонсорский блок</div>
"""
soup: BeautifulSoup = BeautifulSoup(html_articles, 'html.parser')

# Найти все div с классом 'article', но без класса 'sponsored'
articles: List[Tag] = soup.find_all(
    lambda tag: tag.name == 'div' and 
                'article' in tag.get('class', []) and 
                'sponsored' not in tag.get('class', [])
)

# for article in articles:
#     print(article.text)
# Вывод:
# Статья 1...
# Статья 2...
# Статья 3...

Пример 2: Исключение нескольких классов одновременно

Задача: извлечь все ссылки (a) с классом menu-item, исключив при этом неактивные (inactive) и внешние (external) ссылки.

from bs4 import BeautifulSoup
from bs4.element import Tag
from typing import List, Set

html_menu: str = """
<a class="menu-item active">Главная</a>
<a class="menu-item inactive">Профиль</a>
<a class="menu-item external">Партнеры</a>
<a class="menu-item">Контакты</a>
<a class="menu-item active external">Блог</a>
"""
soup: BeautifulSoup = BeautifulSoup(html_menu, 'html.parser')

excluded_classes: Set[str] = {'inactive', 'external'}

# Функция для проверки
def is_valid_menu_item(tag: Tag) -> bool:
    """Проверяет, является ли тег 'a' валидным пунктом меню."""
    if tag.name != 'a':
        return False

    classes: List[str] = tag.get('class', [])
    if 'menu-item' not in classes:
        return False

    # Проверяем пересечение множеств классов тега и исключаемых классов
    if not excluded_classes.isdisjoint(classes):
        return False

    return True

# Поиск с использованием функции
valid_menu_items: List[Tag] = soup.find_all(is_valid_menu_item)

# print(valid_menu_items)
# Вывод: [
#  <a class="menu-item active">Главная</a>, 
#  <a class="menu-item">Контакты</a>
# ]

# Альтернатива с lambda
valid_menu_items_lambda: List[Tag] = soup.find_all(
    lambda tag: tag.name == 'a' and 
                'menu-item' in tag.get('class', []) and 
                excluded_classes.isdisjoint(tag.get('class', []))
)
# print(valid_menu_items_lambda) # Результат тот же
Реклама

Пример 3: Исключение классов на основе частичного совпадения имени

Иногда классы могут генерироваться динамически или иметь префиксы/суффиксы (например, item-highlighted-123, item-promo-abc). Можно использовать регулярные выражения или строковые методы для исключения по частичному совпадению.

import re
from bs4 import BeautifulSoup
from bs4.element import Tag
from typing import List, Pattern

html_dynamic: str = """
<div class="card product-id-1">Карточка 1</div>
<div class="card special-offer">Карточка 2</div>
<div class="card product-id-3 promo">Карточка 3</div>
<div class="card product-id-4">Карточка 4</div>
<div class="card promo-block">Карточка 5</div>
"""
soup: BeautifulSoup = BeautifulSoup(html_dynamic, 'html.parser')

# Исключить все div с классом 'card', если у них есть *любой* класс, начинающийся с 'promo'
promo_pattern: Pattern[str] = re.compile(r'^promo')

# Функция для проверки
def exclude_promo_cards(tag: Tag) -> bool:
    """Исключает карточки с промо-классами."""
    if not (tag.name == 'div' and 'card' in tag.get('class', [])):
        return False

    classes: List[str] = tag.get('class', [])
    # Проверяем, есть ли хоть один класс, соответствующий паттерну
    has_promo_class: bool = any(promo_pattern.match(cls) for cls in classes)

    return not has_promo_class

# Поиск
regular_cards: List[Tag] = soup.find_all(exclude_promo_cards)

# print(regular_cards)
# Вывод: [
#  <div class="card product-id-1">Карточка 1</div>, 
#  <div class="card special-offer">Карточка 2</div>, 
#  <div class="card product-id-4">Карточка 4</div>
# ]

Альтернативные подходы и продвинутые техники

Использование CSS-селекторов для исключения классов (метод select)

Метод select использует CSS-селекторы и часто является более лаконичным способом для сложных выборок, включая исключение классов с помощью псевдокласса :not().

from bs4 import BeautifulSoup
from bs4.element import Tag
from typing import List

html_doc: str = """
<div class="item active">Элемент 1</div>
<div class="item special">Элемент 2</div>
<div class="item">Элемент 3</div>
<p class="item">Параграф</p>
<div class="other">Другой элемент</div>
"""
soup: BeautifulSoup = BeautifulSoup(html_doc, 'html.parser')

# Найти все div с классом 'item', но без класса 'special'
# Селектор: 'div.item:not(.special)'
items_not_special: List[Tag] = soup.select('div.item:not(.special)')

# print(items_not_special)
# Вывод: [
#  <div class="item active">Элемент 1</div>, 
#  <div class="item">Элемент 3</div>
# ]

# Исключение нескольких классов
# Селектор: 'a.menu-item:not(.inactive):not(.external)'
html_menu: str = """
<a class="menu-item active">Главная</a>
<a class="menu-item inactive">Профиль</a>
<a class="menu-item external">Партнеры</a>
<a class="menu-item">Контакты</a>
<a class="menu-item active external">Блог</a>
"""
soup_menu: BeautifulSoup = BeautifulSoup(html_menu, 'html.parser')
valid_items_select: List[Tag] = soup_menu.select('a.menu-item:not(.inactive):not(.external)')

# print(valid_items_select)
# Вывод: [
#  <a class="menu-item active">Главная</a>, 
#  <a class="menu-item">Контакты</a>
# ]

Метод select часто предпочтительнее для таких задач из-за своей выразительности и соответствия стандартам CSS.

Комбинирование find_all с другими методами Beautiful Soup для достижения нужного результата

Иногда проще сначала получить более широкий набор элементов с помощью find_all, а затем отфильтровать его стандартными средствами Python или дополнительными вызовами методов Beautiful Soup.

from bs4 import BeautifulSoup
from bs4.element import Tag
from typing import List

html_complex: str = """
<ul class="product-list">
  <li class="item available">Товар 1</li>
  <li class="item unavailable">Товар 2 (нет в наличии)</li>
  <li class="item available special-offer">Товар 3 (акция)</li>
  <li class="item ad">Рекламный блок</li>
</ul>
"""
soup: BeautifulSoup = BeautifulSoup(html_complex, 'html.parser')

# Шаг 1: Найти все элементы списка с классом 'item'
all_items: List[Tag] = soup.find_all('li', class_='item')

# Шаг 2: Отфильтровать те, у которых есть класс 'unavailable' или 'ad'
filtered_items: List[Tag] = [
    item for item in all_items 
    if not ('unavailable' in item.get('class', []) or 'ad' in item.get('class', []))
]

# print(filtered_items)
# Вывод: [
#  <li class="item available">Товар 1</li>, 
#  <li class="item available special-offer">Товар 3 (акция)</li>
# ]

Этот подход может быть полезен, если логика фильтрации сложна или требует нескольких этапов.

Оптимизация производительности при работе с большими объемами данных

При обработке очень больших HTML-документов производительность find_all с lambda-функциями или сложными пользовательскими функциями может снижаться, так как функция вызывается для каждого потенциально подходящего тега.

  • Используйте select: CSS-селекторы часто обрабатываются быстрее, особенно при использовании парсера lxml.
  • Ограничивайте область поиска: Вместо поиска по всему документу (soup.find_all(...)), ищите внутри уже найденного родительского элемента (parent_element.find_all(...)).
  • Простые критерии: По возможности используйте встроенные аргументы find_all (имя тега, class_, id), так как они оптимизированы.
  • Установка lxml: Убедитесь, что установлен парсер lxml (pip install lxml), так как он значительно быстрее стандартного html.parser.

Заключение

Краткое повторение изученного материала

Мы рассмотрели, как использовать метод find_all в Beautiful Soup для поиска элементов и, что важно, как исключать элементы с определенными классами. Основные подходы включают:

  1. Пользовательские функции: Передача функции (включая lambda) в find_all для проверки условий, включая отсутствие определенных классов.
  2. CSS-селекторы: Использование метода select с псевдоклассом :not() для элегантного и часто более производительного исключения.
  3. Пост-фильтрация: Получение более широкого списка элементов и последующая фильтрация средствами Python.

Выбор метода зависит от сложности задачи, требований к читаемости кода и производительности.

Советы по дальнейшему изучению Beautiful Soup

  • Документация: Официальная документация Beautiful Soup — лучший источник информации.
  • CSS-селекторы: Глубже изучите синтаксис CSS-селекторов, так как метод select очень мощен.
  • Регулярные выражения: Освойте их для поиска по атрибутам и тексту, соответствующим шаблонам.
  • Обработка ошибок: Научитесь обрабатывать ситуации, когда элементы не найдены (None от find) или структура страницы неожиданно меняется.
  • Парсеры: Поймите различия между парсерами (html.parser, lxml, html5lib) и их влияние на результат итоговое дерево и производительность.

Добавить комментарий