Парсинг сайтов на Python: изучаем BeautifulSoup

BeautifulSoup используется для эффективного поиска элементов на html странице.

Установка:

pip install beautifulsoup4, lxml

Для запросов установите библиотеку requests, если она у вас не установлена:

pip install requests

Тренироваться будем на "тренажёре".

Получаем HTML-документ по HTTP и строим DOM-дерево с помощью BeautifulSoup

from bs4 import BeautifulSoup
import requests

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
soup = BeautifulSoup(response.text, 'lxml')
print(soup)

Первым параметром мы передаем html-код страницы, а вторым парсер. Помимо lxml (это лучший вариант) парсер может быть:

  • html.parser - не прощает ошибок в html, зато не требует установки

  • html5lib - более медленный, чем lxml, требует установки

  • xml - для распарса xml страниц

Пример того, как можно считать код из html файла на пк.

from bs4 import BeautifulSoup
import requests

with open('test.html', 'r', encoding='utf-8') as file:
    soup = BeautifulSoup(file, 'lxml')

print(soup.prettify()) # prettify делает красивый вывод html

Также на всякий случай напомню вам, что страницы состоят из html тэгов. У тэгов могут быть атрибуты, а у атрибутов значения.

Кроме того, одни тэги вложены в другие.

<div class="me">
  <h2 class="vacy" id="gdsfg">12</h2>
  <span>Lorem <a href="#">ipsum</a> dolor sit amet <b>consectetur</b> adipisicing elit. A, earum itaque hic enim dicta quae. Doloremque officia quibusdam ut, a ratione totam repellendus ipsam, at dolorum iusto consequatur dolores fuga!</span>
</div>

Например тут в тэг <div> вложены <h2> и <span>, а внутри <span> находятся уже <a>, <b>. При чем <h2> и <span> находятся на первом уровне вложенности (являются детьми), а <a> и <b> на 2-м уровне (внуки). Получается некая иерархия. Это хорошо видно при просмотре через devtools.

Поиск элементов

Поиск 1 элемента

После того, как мы с ��ами получили объект BeautifulSoup, мы можем оттуда извлекать конкретные тэги. Самый простой вариант - это просто указать название тэга. В этом случае вернётся первый найденный результат. Если такого тэга на странице нет, то нам вернётся None.

Поиск по тэгу. Найдём заголовок <h1> на странице.

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.content
soup = BeautifulSoup(tree, 'lxml')
header = soup.h1  # получаем объект Tag
print(header)

Отмечу, что в результате выполнения этого действия нам возвращается объект Tag. Раз это объект, у него есть свойства и методы, и мы можем с ним поработать. Например, можно получить текст элемента или значение конкретного атрибута. Но об этом позже.

Давайте теперь попробуем запросить несуществующий тэг и убедимся, что нам вернётся None.

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
header = soup.h6  # получаем None, т.к. тэга <h6> на указанной странице нет
if header is None: print('Элемент не найден')

Указанный выше способ прост, но не эффективен. Рассмотрим другие методы поиска HTML тэгов.

Поиск по тэгу и его атрибутам. С помощью метода find() можно задавать не просто название тэга, но и его атрибуты (вместе со значением). Данный метод возвращает первый найденный результат, а если ничего не найдено, то вернёт None.

Отмечу, что ищется именно точное совпадение.

Давайте найдем заголовок <h2> с атрибутом id="additionalinfo".

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
header_addinitional_info = soup.find('h2', id='additionalinfo')
print(header_addinitional_info)

Вы можете искать сразу по нескольким значениям атрибута. Передаем их в виде списка. В примере ниже мы ищем <h2> у которого id может быть или add или info или additionalinfo.

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
header_addinitional_info = soup.find('h2', id=['add', 'info', 'additionalinfo'])
print(header_addinitional_info)

Также мы можем уточнить наш поиск. Давайте найдём тэг <span>, у которого id="special-text" и style="color: blue;" (если какое-то из условий не совпадёт, вернётся None).

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
unique = soup.find('span', style='color: blue;', id='special-text')
print(unique)

Или передаем их в виде словаря (код ниже аналогичен предыдущему).

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
unique = soup.find('span', attrs={'style':'color: blue;', 'id':'special-text'})
print(unique)

Мы можем указать несколько тэгов в виде списка. Давайте скажем, что нам подходит или тэг <div> или тэг <span> с id="special-text".

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
unique = soup.find(['div', 'span'], id='special-text')
print(unique)

Можно и вовсе не указывать название тэга. В примере ниже будет найден элемент с id="special-text" (вне зависимости у какого тэга этот атрибут).

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
unique = soup.find(id='special-text')
print(unique)

Также у нас есть возможность искать по тексту. Для этого используем атрибут text. Поиск идет по точному совпадению.

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
unique = soup.find('span', text='уникальным идентификатором')
print(unique)

Отдельно нужно поговорить про атрибут class. Дело в том, что слово class является зарезервированным в python, поэтому если нам нужно по нему искать, то мы прописываем его с нижним подчеркиванием.

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
footer = soup.find('div', class_='content-section')  # когда ищем по class добавляем _
print(footer)

Кр��ме того иногда class (на странице) может содержать несколько значений через пробел. Например в примере ниже у тэга <div> class имеет сразу 3 значения: content-section, content и cont. 

<div class="content-section content cont">

Мы можем искать по любому из этих значений, не обязательно указывать все 3.

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
footer_div = soup.find('div', class_='cont')
print(footer_div)

Поиск с использованием регулярок. Ещё одним продвинутым методом поиска является использование регулярных выражений. В примере ниже мы ищем <span>, у которого id начинается с spec.

import requests
from bs4 import BeautifulSoup
import re

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
span = soup.find('span', id=re.compile('spec*'))
print(span)

Поиск по css селекторам. Также хорошим способом поиска будет использование css селекторов. В этом случае вместо метода find() нам понадобится метод select_one(). По css селекторам также есть отдельный материал.

Найдём тэг <h2> с id="additionalinfo".

import requests
from bs4 import BeautifulSoup
import re

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
span = soup.select_one('h2[id="additionalinfo"]')
print(span)

Поиск с использованием lambda.

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')

example_1 = soup.find(lambda tag: tag.has_attr('class') and 'header' in tag['id']) # любой тэг у которого есть class и есть "header" в id.
print(example_1)

example_2 = soup.find(lambda tag: tag.name=="td" and 'гипертекст' in tag.text) # поиск по части текста.
print(example_2)

Поиск всех совпадений

В примерах выше мы с вами всегда получали одно совпадение, которое встречается самым первым на странице. Но у нас также есть возможность получить сразу все найденные совпадения в виде списка.

Для этого мы воспользуемся методами find_all() для поиска по атрибутам и select() для поиска по css селекторам. Все остальные "фишки" поиска аналогичны тем, что мы использовали выше.

Найдём все тэги <h2>.

import requests
from bs4 import BeautifulSoup
import re

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
headers = soup.find_all('h2')

# выведем все найденные совпадения
print(headers)  # [<h2>О чем эта страница</h2>, <h2>Списки примеров</h2>, <h2>Пример таблицы</h2>, <h2 id="additionalinfo">Дополнительная информация</h2>]

# выведем только первое совпадение
print(headers[0])

Отмечу, что если даже будет найден только 1 элемент, он всё равно будет в списке, просто список будет из одного элемента. Если же совпадений не будет вовсе, то нам вернётся уже не None, а пустой список. Давайте в этом убедимся, попробовав отыскать несуществующий на странице тэг <h6>.

import requests
from bs4 import BeautifulSoup
import re

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
headers = soup.find_all('h6')

print(headers) # []

# проверить можно так
if headers:
    print('нашли')
else:
    print('ничего не найдено')

Мы можем указать, сколько именно совпадений нам нужно. Для этого прописываем параметр limit. 

Давайте скажем, что мы хотим получить только первые два заголовка <h2>.

import requests
from bs4 import BeautifulSoup
import re

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
headers = soup.find_all('h2', limit=2)

# выведем первые два совпадения
print(headers)  # [<h2>О чем эта страница</h2>, <h2>Списки примеров</h2>]

Пример поиска по css селектору.

import requests
from bs4 import BeautifulSoup
import re

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
headers = soup.select('h2')

print(headers)

Оси

Введение

Не всегда возможно найти конкретный тэг. способами выше. Что если у тэга вообще нет никаких атрибутов, неужели нам искать только по номеру?

На самом деле нет. Мы с вами можем зацепиться за какой-то другой тэг, а от него уже перейти к нужному. Рассмотрим как это сделать.

P.S. Названия осей я взял "неофициальные" (из xpath), не обессудьте.

Ось child

Ось child позволяет нам перейти к тэгу на 1 уровень ниже текущего. Проще говоря к ребенку. К примеру мы хотим отыскать вот этот абзац.

Как видим, у тэга <p> атрибутов нет, зато у его родителя (тэг <div>) есть class="footer". Значит мы сначала найдём этот тэг <div> с классом "footer", а затем уже найдем его ребенка (тэг <p>).

Для этого мы ещё раз вызовем метод find() с параметром recursive=False, чтобы поиск производился только по детям (не затрагивая других потомков - внуков, правнуков и т.д.).

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
footer_p = soup.find('div', class_='footer').find('p', recursive=False)

print(footer_p)  # <p>© 2024 Пример HTML-страницы. Все права защищены.</p>

Напомню, что в этом случае нам вернётся первое найденное совпадение.

Чтобы получить все совпадения в виде списка, используйте метод find_all().

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
footer_p = soup.find('div', class_='footer').find_all('p', recursive=False)

print(footer_p)  # [<p>© 2024 Пример HTML-страницы. <strong>Все</strong> права защищены.</p>, <p id="abc">Создано с помощью HTML и CSS</p>]

Ось parent

Если ось child позволяет нам опуститься на 1 уровень ниже, то ось parent наоборот подняться на уровень выше, к родителю. Для этого используется метод find_parent().

Давайте найдём <span> с id="special-text", а затем поднимемся к его родителю тэгу <p>.

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
text = soup.find('span', id="special-text").find_parent('p')
print(text)

find_parent() возвращает первое найденное совпадение. Для получения всех совпадений используйте метод find_parents().

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
text = soup.find('span', id="special-text").find_parents('p')
print(text)  # тут уже будет список

Ось descendant

В этом случае мы будем искать не только среди потомков 1 уровня (детей), но и среди потомков всех остальных уровней (внуки, правнуки и т.д.). 

Давайте на всякий случай повторим ещё раз.

В данном случае потомками 1-го уровня (детьми) для тэга <div> будут <h2>, <h3>, <ol>, <ul>. Тэг <li> же будет уже потомком 2-го уровня (внуком) для тэга <div>. В то же время для тэга <ol> тэг <li> будет потомком 1-го уровня (ребенком).

Окей, продолжим. Чтобы нам найти всех потомков мы вызываем метод find(), с параметром recursive=True (можно не прописывать, т.к. это идет значение по умолчанию).

Отыщем тэг <strong>.

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
li = soup.find('div', class_='content-section').find('strong')

# выведем первые два совпадения
print(li)

А теперь давайте попробуем найти <h3>.

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
header = soup.find('div', class_='content-section').find('h3')

print(header)  # None

Получаем None! Но почему? Помните, мы говорили, что метод find() берет первое найденное совпадение. Когда мы искали <div>, то он взял только первое совпадение, т.е. вот это.

А тут нет никакого <li>. Что же делать? Первый вариант - найти все совпадения методом find_all(), а далее перебрать их в цикле.

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
content_section = soup.find_all('div', class_='content-section')

for content in content_section:
    header = content.find('h3')
    if header:
        print(f'Нашли совпадение! {header}')
        break

Второй вариант - использовать css селекторы.

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
content_section = soup.select_one('div.content-section li:nth-child(1)')

print(content_section)

Ось ancestor

Ось ancestor ищет по всем предкам (на 1 уровень выше, на 2 и т.д.). Если find_parent() находил только родителя, то find_previous() будет искать также по дедушкам, прадедушкам и т.д.

Сейчас мы с вами сначала зацепимся за <span> с id="special-text", а затем от него найдём <div>.

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
div = soup.find('span', id='special-text').find_previous('div')

print(div)

Для получения всех совпадений используйте метод find_all_previous().

Ось next-sibling

Ось next-sibling позволяет искать среди элементов, которые находятся на этом же уровне после текущего. Можно сказать по младшим братьям или по соседним элементам.

Смотрите, вот тут <h2>, <p>, <p> все находятся на одном уровне. Мы можем найти этот <h2> по id, а затем с помощью оси next-sibling отыскать <p>.

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
text = soup.find('h2', id='additionalinfo').find_next_sibling('p')

print(text)  # <p>Этот раздел содержит <strong>важный текст</strong> и демонстрирует использование различных атрибутов.</p>

Для поиска всех совпадений используйте find_next_siblings().

Ось previous-sibling

Эта ось аналогична предыдущей, но ищет уже по "старшим братьям". 

Сейчас мы найдём тэг <p> с id="abc", а затем от него найдем его старшего брата <p>.

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
text = soup.find('p', id='abc').find_previous_sibling('p')

print(text)  # <p>© 2024 Пример HTML-страницы. Все права защищены.</p>

Свойства

Помимо описанных выше методов у нас также есть свойства. Во всех случаях (кроме parent, next, previous) возвращается итератор.

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
span = soup.find(class_='content-section cont')
childs = span.children  # дети
parent = span.parent  # родитель
parents = span.parents  # отец|дед|прадед
desc = span.descendants  # дети|внуки|правнуки
next_sibling = span.next_sibling  # сосед
next_siblings = span.next_siblings  # соседи (братья) ниже текущего
prev_sibling = span.previous_sibling  # сосед (братья) выше текущего
prev_siblings = span.previous_siblings  # соседи (братья) выше текущего
next = span.next  # следующий
prev = span.previous  # предыдущий
all_next = span.next_elements  # все следующие
all_previous = span.previous_elements  # все предыдущие

for x in childs:
      if hasattr(x, 'get'):
        print(x)

При использовании свойств надо отметить один важный момент. Поиск идет по всем узлам, доступным в BeautifulSoup - Tag, NavigableString, Comment. Т.е. помимо самого тэга также берется текст, комментарии, переносы строк.

Давайте посмотрим это на практике. Возьмем такую разметку и найдем <h2>.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <h2 class="heading">
        <!--Комментарий-->
        <a href="https://google.com">Незнакомцы. Глава 2(2025)</a>
        <img src="/templates/blueshab/dleimages/no_icon.gif">
    </h2>
</body>
</html>
from bs4 import BeautifulSoup

html = '<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <h2 class="heading"> <a href="https://google.com">Незнакомцы. Глава 2(2025)</a> <img src="/templates/blueshab/dleimages/no_icon.gif"> </h2> </body> </html>'
soup = BeautifulSoup(html, 'lxml')
div = soup.find('h2')

Теперь воспользуемся свойством children и посмотрим, что он нам найдёт.

from bs4 import BeautifulSoup

html = '<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <h2 class="heading"> <!--Комментарий--> <a href="https://google.com">Незнакомцы. Глава 2(2025)</a> <img src="/templates/blueshab/dleimages/no_icon.gif"> </h2> </body> </html>'
soup = BeautifulSoup(html, 'lxml')
div = soup.find('h2')

childrens = div.children
print(childrens.__next__())  #
print(childrens.__next__())  # <!--Комментарий-->
print(childrens.__next__())  #
print(childrens.__next__())  # <a href="https://google.com">Незнакомцы. Глава 2(2025)</a>
print(childrens.__next__())  #
print(childrens.__next__())  # <img src="/templates/blueshab/dleimages/no_icon.gif"/>

Обратите внимание, что первым элементом стал не <a>, а перенос строки. Это связано с тем, что между <h2> и <a> у нас есть пробел, а значит это текстовый узел, состоящий из одного пробела.

Потом уже идет комментарий, перенос строки, тэг <a>, потом снова текстовый узел (перенос строки), потом <img>.

Если бы в нашем html коде не было бы пробелов и переносов строк между тэгами, то результат был бы таким.

from bs4 import BeautifulSoup

html = '<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <h2 class="heading"><!--Комментарий--><a href="https://google.com">Незнакомцы. Глава 2(2025)</a><img src="/templates/blueshab/dleimages/no_icon.gif"></h2> </body> </html>'
soup = BeautifulSoup(html, 'lxml')
div = soup.find('h2')

childrens = div.children
print(childrens.__next__())  # <!--Комментарий-->
print(childrens.__next__())  # <a href="https://google.com">Незнакомцы. Глава 2(2025)</a>
print(childrens.__next__())  # <img src="/templates/blueshab/dleimages/no_icon.gif"/>

Получение значений

Итак, мы с вами научились находить элементы. Чаще всего нам возвращается объект Tag. Теперь научимся получать из элемента текст, значения атрибутов и сам элемент.

Для получения текста элемента можно использовать метод get_text().

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
header = soup.h1  # получаем объект tag
text_header = header.get_text()
print(text_header)  # An Interesting Title

Ещё одним способом получения текста элемента является атрибут .text. Результат не поменяется.

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
header = soup.h1
text_header = header.text
print(text_header)  # An Interesting Title

В чём же отличие? Если присмотреться, то мы увидим, что метод get_text() имеет некоторые необязательные параметры

Наиболее полезен тут strip=True, удаляющий лишние пробелы в начале и конце.

import requests
from bs4 import BeautifulSoup

html = '<div>   Hello World !   </div>'
soup = BeautifulSoup(html, 'html.parser')

# Без обрезки
print(soup.get_text())  # '   Hello World !  '

# С обрезкой пробелов
print(soup.get_text(strip=True))  # 'Hello World !'

Как видите, в первом случае тэг <b> был просто откинут, при этом все пробелы исчезли. Во втором же случае мы явно указали, что хотим вставить пробел вместо тэга.

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

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
span = soup.find(id='special-text')
span_id = span['style']
print(span_id)  # color: blue;

Также можно воспользоваться методом get().

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
span = soup.find(id='special-text')
span_id = span.get('style')
print(span_id)  # color: blue;

Разница в том, что если указанного атрибута у элемента нет, то [''] выбросит исключение KeyError, а get() вернёт None. Также у get() можно передать второй параметр, который присвоит значение по умолчанию, если искомого атрибута у тэга нет.

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
span = soup.find(id='special-text')

try:
    span_id = span['gg']
except KeyError as e:
    print(f'Ошибка: {e}')

span_id = span.get('gg', 'нет атрибута') # 2-м параметром передали значение по умолч
print(span_id)  # нет атрибута

Также существует свойство .attrs, возвращающее все атрибуты тэга.

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
span = soup.find(id='special-text')

attrs = span.attrs  # вернёт словарь

# можно перебрать в цикле
for attr in attrs:
    print(attr)

Свойство .name вернет имя тэга.

import requests
from bs4 import BeautifulSoup

response = requests.get('http://parsingme.ru/beautifulsoup/1.html')
tree = response.text
soup = BeautifulSoup(tree, 'lxml')
span = soup.find(id='special-text')

attrs = span.name  # span

Спасибо всем, кто прочитал статью. Надеюсь, она была полезной. 1 февраля стартует мой курс по парсингу сайтов с помощью Python. Доступ к нему откроется на 7 дней. В курсе практика парсинга, ДЗ, поддержка в чате. Все, кто хочет залететь, пишите в тг - https://t.me/volody00

Информация на этой странице взята из источника: https://habr.com/ru/articles/986284/