BeautifulSoup4 (bs4) - это библиотека Python для извлечения данных из файлов HTML и XML. Для естественной навигации, поиска и изменения дерева HTML, модуль BeautifulSoup4, по умолчанию использует встроенный в Python парсер html.parser
. BS4 так же поддерживает ряд сторонних парсеров Python, таких как lxml
, html5lib
и xml
(для разбора XML-документов).
# создаем виртуальное окружение, если нет $ python3 -m venv .venv --prompt VirtualEnv # активируем виртуальное окружение $ source .venv/bin/activate # ставим модуль beautifulsoup4 (VirtualEnv):~$ python3 -m pip install -U beautifulsoup4
BeautifulSoup4 представляет один интерфейс для разных парсеров, но парсеры неодинаковы. Разные парсеры, анализируя один и того же документ создадут различные деревья HTML. Самые большие различия будут между парсерами HTML и XML. Так же парсеры различаются скоростью разбора HTML документа.
Если дать BeautifulSoup4 идеально оформленный документ HTML, то различий построенного HTML-дерева не будет. Один парсер будет быстрее другого, но все они будут давать структуру, которая выглядит точно так же, как оригинальный документ HTML. Но если документ оформлен с ошибками, то различные парсеры дадут разные результаты.
Различия в построении HTML-дерева разными парсерами, разберем на короткой HTML-разметке: <a></p>
.
lxml
.Характеристики:
lxml
. >>> from bs4 import BeautifulSoup >>> BeautifulSoup("<a></p>", "lxml") # <html><body><a></a></body></html>
Обратите внимание, что тег <a>
заключен в теги <body>
и <html>
, а висячий тег </p>
просто игнорируется.
html5lib
.Характеристики:
html5lib
. >>> from bs4 import BeautifulSoup >>> BeautifulSoup("<a></p>", "html5lib") # <html><head></head><body><a><p></p></a></body></html>
Обратите внимание, что парсер html5lib
НЕ игнорирует висячий тег </p>
, и к тому же добавляет открывающий тег <p>
. Также html5lib
добавляет пустой тег <head>
(lxml
этого не сделал).
html.parser
.Характеристики:
lxml
.html5lib
.>>> from bs4 import BeautifulSoup >>> BeautifulSoup("<a></p>", 'html.parser') # <a></a>
Как и lxml
, встроенный в Python парсер игнорирует закрывающий тег </p>
. В отличие от html5lib
, этот парсер не делает попытки создать правильно оформленный HTML-документ, добавив теги <html>
или <body>
.
Вывод: Парсер html5lib
использует способы, которые являются частью стандарта HTML5, поэтому он может претендовать на то, что его подход самый "правильный".
Чтобы разобрать HTML-документ, необходимо передать его в конструктор класса BeautifulSoup()
. Можно передать строку или открытый дескриптор файла:
from bs4 import BeautifulSoup # передаем объект открытого файла with open("index.html") as fp: soup = BeautifulSoup(fp, 'html.parser') # передаем строку soup = BeautifulSoup("<html>a web page</html>", 'html.parser')
Первым делом документ конвертируется в Unicode, а HTML-мнемоники конвертируются в символы Unicode:
>>> from bs4 import BeautifulSoup >>> html = "<html><head></head><body>Sacré bleu!</body></html>" >>> parse = BeautifulSoup(html, 'html.parser') >>> print(parse) # <html><head></head><body>Sacré bleu!</body></html>
Дальнейшие примеры будут разбираться на следующей HTML-разметке.
html_doc = """<html><head><title>The Dormouse's story</title></head> <body> <p class="title"><b>The Dormouse's story</b></p> <p class="story">Once upon a time there were three little sisters; and their names were <a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>, <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>; and they lived at the bottom of a well.</p> <p class="story">...</p>"""
Передача этого HTML-документа в конструктор класса BeautifulSoup()
создает объект, который представляет документ в виде вложенной структуры:
>>> from bs4 import BeautifulSoup >>> soup = BeautifulSoup(html_doc, 'html.parser') >>> print(soup.prettify()) # <html> # <head> # <title> # The Dormouse's story # </title> # </head> # <body> # <p class="title"> # <b> # The Dormouse's story # </b> # </p> # <p class="story"> # Once upon a time there were three little sisters; and their names were # <a class="sister" href="http://example.com/elsie" id="link1"> # Elsie # </a> # , # <a class="sister" href="http://example.com/lacie" id="link2"> # Lacie # </a> # and # <a class="sister" href="http://example.com/tillie" id="link3"> # Tillie # </a> # ; and they lived at the bottom of a well. # </p> # <p class="story"> # ... # </p> # </body> # </html>
# извлечение тега `title` >>> soup.title # <title>The Dormouse's story</title> # извлечение имя тега >>> soup.title.name # 'title' # извлечение текста тега >>> soup.title.string # 'The Dormouse's story' # извлечение первого тега `<p>` >>> soup.p # <p class="title"><b>The Dormouse's story</b></p> # извлечение второго тега `<p>` и # представление его содержимого списком >>> soup.find_all('p')[1].contents # ['Once upon a time there were three little sisters; and their names were\n', # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>, # ',\n', # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, # ' and\n', # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>, # ';\nand they lived at the bottom of a well.'] # выдаст то же самое, только в виде генератора >>> soup.find_all('p')[1].strings # <generator object Tag._all_strings at 0x7ffa2eb43ac0>
Перемещаться по одному уровню можно при помощи атрибутов .previous_sibling
и .next_sibling
. Например, в представленном выше HTML, теги <a>
обернуты в тег <p>
- следовательно они находятся на одном уровне.
>>> first_a = soup.a >>> first_a # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a> >>> first_a.previous_sibling # 'Once upon a time there were three little sisters; and their names were\n' >>> next = first_a.next_sibling >>> next # ',\n' >>> next.next_sibling # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
Так же можно перебрать одноуровневые элементы данного тега с помощью .next_siblings
или .previous_siblings
.
for sibling in soup.a.next_siblings: print(repr(sibling)) # ',\n' # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> # ' and\n' # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a> # '; and they lived at the bottom of a well.' for sibling in soup.find(id="link3").previous_siblings: print(repr(sibling)) # ' and\n' # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a> # ',\n' # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a> # 'Once upon a time there were three little sisters; and their names were\n'
Атрибут .next_element
строки или HTML-тега указывает на то, что было разобрано непосредственно после него. Это могло бы быть тем же, что и .next_sibling
, но обычно результат резко отличается.
Возьмем последний тег <a>
, его .next_sibling
является строкой: конец предложения, которое было прервано началом тега <a>
:
last_a = soup.find("a", id="link3") last_a # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a> last_a.next_sibling # ';\nand they lived at the bottom of a well.'
Однако .next_element
этого тега <a>
- это то, что было разобрано сразу после тега <a>
- это слово Tillie, а не остальная часть предложения.
last_a_tag.next_element # 'Tillie'
Это потому, что в оригинальной разметке слово Tillie появилось перед точкой с запятой. Парсер обнаружил тег <a>
, затем слово Tillie, затем закрывающий тег </a>
, затем точку с запятой и оставшуюся часть предложения. Точка с запятой находится на том же уровне, что и тег <a>
, но слово Tillie
встретилось первым.
Атрибут .previous_element
является полной противоположностью .next_element
. Он указывает на элемент, который был обнаружен при разборе непосредственно перед текущим:
last_a_tag.previous_element # ' and\n' last_a_tag.previous_element.next_element # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
При помощи атрибутов .next_elements
и .previous_elements
можно получить список элементов, в том порядке, в каком он был разобран парсером.
for element in last_a_tag.next_elements: print(repr(element)) # 'Tillie' # ';\nand they lived at the bottom of a well.' # '\n' # <p class="story">...</p> # '...' # '\n'
Одна из распространенных задач, это извлечение URL-адресов, найденных на странице в HTML-тегах <a>
:
>>> for a in soup.find_all('a'): ... print(a.get('href')) # http://example.com/elsie # http://example.com/lacie # http://example.com/tillie
Другая распространенная задача - извлечь весь текст со HTML-страницы:
# Весь текст HTML-страницы с разделителями `\n` >>> soup.get_text('\n', strip='True') # "The Dormouse's story\nThe Dormouse's story\n # Once upon a time there were three little sisters; and their names were\n # Elsie\n,\nLacie\nand\nTillie\n;\nand they lived at the bottom of a well.\n..." # а можно создать список строк, а потом форматировать как надо >>> [text for text in soup.stripped_strings] # ["The Dormouse's story", # "The Dormouse's story", # 'Once upon a time there were three little sisters; and their names were', # 'Elsie', # ',', # 'Lacie', # 'and', # 'Tillie', # ';\nand they lived at the bottom of a well.', # '...']
Найти первый совпавший HTML-тег можно методом BeautifulSoup.find()
, а всех совпавших элементов - BeautifulSoup.find_all()
.
# ищет все теги `<title>` >>> soup.find_all("title") # [<title>The Dormouse's story</title>] # ищет все теги `<a>` и все теги `<b>` >>> soup.find_all(["a", "b"]) # [<b>The Dormouse's story</b>, # <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>, # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>] # ищет все теги `<p>` с CSS классом "title" >>> soup.find_all("p", "title") # [<p class="title"><b>The Dormouse's story</b></p>] # ищет все теги с CSS классом, в именах которых встречается "itl" soup.find_all(class_=re.compile("itl")) # [<p class="title"><b>The Dormouse's story</b></p>] # ищет все теги с id="link2" >>> soup.find_all(id="link2") # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>] # ищет все теги `<a>`, содержащие указанные атрибуты >>> soup.find_all('a', attrs={'class': 'sister', 'id': 'link1'}) # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>] # ищет текст внутри всех тегов, который содержит 'sisters' >>> import re >>> soup.find(string=re.compile("sisters")) # 'Once upon a time there were three little sisters; and their names were\n' # ищет все теги, имена которых начинаются на букву 'b' for tag in soup.find_all(re.compile("^b")): print(tag.name) # body # b # ищет все теги в документе, но не текстовые строки for tag in soup.find_all(True): print(tag.name) # html # head # title # body # p # b # p # a # a # a # p
>>> soup.select("title") # [<title>The Dormouse's story</title>] >>> soup.select("p:nth-of-type(3)") # [<p class="story">...</p>]
Поиск тега под другими тегами:
>>> soup.select("body a") # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>, # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>] >>> soup.select("html head title") # [<title>The Dormouse's story</title>]
Поиск тега непосредственно под другими тегами:
>>> soup.select("head > title") # [<title>The Dormouse's story</title>] >>> soup.select("p > a:nth-of-type(2)") # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>] >>> soup.select("p > #link1") # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>]
Поиск одноуровневых элементов:
# поиск всех `.sister` в которых нет `#link1` >>> soup.select("#link1 ~ .sister") # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>] # поиск всех `.sister` в которых есть `#link1` >>> soup.select("#link1 + .sister") # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>] # поиск всех `<a>` у которых есть сосед `<p>`
Поиск тега по классу CSS:
>>> soup.select(".sister") # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>, # <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, # <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
Поиск тега по ID:
>>> soup.select("#link1") # [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>] >>> soup.select("a#link2") # [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]
Извлечение НЕПОСРЕДСТВЕННЫХ дочерних элементов тега. Если посмотреть на HTML-разметку в коде ниже, то, непосредственными дочерними элементами первого <ul>
будут являться три тега <li>
и тег <ul>
со всеми вложенными тегами.
Обратите внимание, что все переводы строк \n
и пробелы между тегами, так же будут считаться дочерними элементами. Так что имеет смысл заранее привести исходный HTML к "нормальному виду", например так: re.sub(r'>\s+<', '><', html.replace('\n', ''))
html = """ <div> <ul> <li>текст 1</li> <li>текст 2</li> <ul> <li>текст 2-1</li> <li>текст 2-2</li> </ul> <li>текст 3</li> </ul> </div> """ >>> from bs4 import BeautifulSoup >>> root = BeautifulSoup(html, 'html.parser') # найдем в дереве первый тег `<ul>` >>> first_ul = root.ul # извлекаем список непосредственных дочерних элементов # переводы строк `\n` и пробелы между тегами так же # распознаются как дочерние элементы >>> first_ul.contents # ['\n', <li>текст 1</li>, '\n', <li>текст 2</li>, '\n', <ul> # <li>текст 2-1</li> # <li>текст 2-2</li> # </ul>, '\n', <li>текст 3</li>, '\n'] # убираем переводы строк `\n` как из списка, так и из тегов # лучше конечно сразу убрать переводы строк из исходного HTML >>> [str(i).replace('\n', '') for i in first_ul.contents if str(i) != '\n'] # ['<li>текст 1</li>', # '<li>текст 2</li>', # '<ul><li>текст 2-1</li><li>текст 2-2</li></ul>', # '<li>текст 3</li>'] # то же самое, что и `first_ul.contents` # только в виде итератора >>> first_ul.children # <list_iterator object at 0x7ffa2eb52460>
Извлечение ВСЕХ дочерних элементов. Эта операция похожа на рекурсивный обход HTML-дерева в глубину от выбранного тега.
>>> import re # сразу уберем переводы строк из исходного HTML >>> html = re.sub(r'>\s+<', '><', html.replace('\n', '')) >>> root = BeautifulSoup(html, 'html.parser') # найдем в дереве первый тег `<ul>` >>> first_ul = root.ul # извлекаем список ВСЕХ дочерних элементов >>> list(first_ul.descendants) # [<li>текст 1</li>, # 'текст 1', # <li>текст 2</li>, # 'текст 2', # <ul><li>текст 2-1</li><li>текст 2-2</li></ul>, # <li>текст 2-1</li>, # 'текст 2-1', # <li>текст 2-2</li>, # 'текст 2-2', # <li>текст 3</li>, # 'текст 3']
Обратите внимание, что простой текст, который находится внутри тега, так же считается дочерним элементом этого тега.
Если внутри тега есть более одного дочернего элемента (как в примерен выше) и необходимо извлечь только текст, то можно использовать атрибут .strings
или генератор .stripped_strings
.
Генератор .stripped_strings
дополнительно удаляет все переводы строк \n
и пробелы между тегами в исходном HTML-документе.
>>> list(first_ul.strings) # ['текст 1', 'текст 2', 'текст 2-1', 'текст 2-2', 'текст 3'] >>> first_ul.stripped_strings # <generator object Tag.stripped_strings at 0x7ffa2eb43ac0> >>> list(first_ul.stripped_strings) # ['текст 1', 'текст 2', 'текст 2-1', 'текст 2-2', 'текст 3']
Что бы получить доступ к родительскому элементу, необходимо использовать атрибут .parent
.
html = """ <div> <ul> <li>текст 1</li> <li>текст 2</li> <ul> <li>текст 2-1</li> <li>текст 2-2</li> </ul> <li>текст 3</li> </ul> </div> """ >>> from bs4 import BeautifulSoup >>> import re # сразу уберем переводы строк и пробелы # между тегами из исходного HTML >>> html = re.sub(r'>\s+<', '><', html.replace('\n', '')) >>> root = BeautifulSoup(html, 'html.parser') # найдем теги `<li>` вложенные во второй `<ul>`, # используя CSS селекторы >>> child_ul = root.select('ul > ul > li') >>> child_ul # [<li>текст 2-1</li>, <li>текст 2-2</li>] # получаем доступ к родителю >>> child_li[0].parent # <ul><li>текст 2-1</li><li>текст 2-2</li></ul> # доступ к родителю родителя >>> child_li[0].parent.parent.contents [<li>текст 1</li>, <li>текст 2</li>, <ul><li>текст 2-1</li><li>текст 2-2</li></ul>, <li>текст 3</li>]
Taк же можно перебрать всех родителей элемента с помощью атрибута .parents
.
>>> child_li[0] # <li>текст 2-1</li> >>> [parent.name for parent in child_li[0].parents] # ['ul', 'ul', 'div', '[document]']
>>> soup = BeautifulSoup('<p><b class="boldest">Extremely bold</b></p>', 'html.parser') >>> tag = soup.b # присваиваем новое имя тегу >>> tag.name = "blockquote" >>> tag # <blockquote class="boldest">Extremely bold</blockquote> >>> soup # <p><blockquote class="boldest">Extremely bold</blockquote></p>
Изменение HTML-тега <p>
на тег <div>
:
>>> soup = BeautifulSoup('<p><b>Extremely bold</b></p>', 'html.parser') >>> soup.p.name = 'div' >>> soup # <div><b>Extremely bold</b></div>
Добавление нового тега в дерево HTML:
>>> soup = BeautifulSoup("<p><b></b></p>", 'html.parser') >>> original_tag = soup.b # создание нового тега `<a>` >>> new_tag = soup.new_tag("a", href="http://example.com") # строка нового тега `<a>` >>> new_tag.string = "Link text" # добавление тега `<a>` внутрь `<b>` >>> original_tag.append(new_tag) >>> original_tag # <b><a href="http://example.com">Link text.</a></b> >>> soup # <p><b><a href="http://example.com">Link text</a></b></p>
Добавление новых тегов до/после определенного тега или внутрь тега.
>>> soup = BeautifulSoup("<p><b>leave</b></p>", 'html.parser') >>> tag = soup.new_tag("i", id='new') >>> tag.string = "Don't" # добавление нового тега <i> до тега <b> >>> soup.b.insert_before(tag) >>> soup.b # <p><i>Don't</i><b>leave</b></p> # добавление нового тега <i> после тега <b> >>> soup.b.insert_after(tag) >>> soup # <p><b>leave</b><i>Don't</i></p> # добавление нового тега <i> внутрь тега <b> >>> soup.b.string.insert_before(tag) >>> soup.b # <p><b><i>Don't</i>leave</b></p>
Удаляем тег или строку из дерева HTML:
>>> html = '<a href="http://example.com/">I linked to <i>example.com</i></a>' >>> soup = BeautifulSoup(html, 'html.parser') >>> a_tag = soup.a # удаляем HTML-тег `<i>` с сохранением # в переменной `i_tag` >>> i_tag = soup.i.extract() # смотрим что получилось >>> a_tag # <a href="http://example.com/">I linked to</a> >>> i_tag # <i>example.com</i>
Заменяем тег и/или строку в дереве HTML:
>>> html = '<a href="http://example.com/">I linked to <i>example</i></a>' >>> soup = BeautifulSoup(html, 'html.parser') >>> a_tag = soup.a # создаем новый HTML тег >>> new_tag = soup.new_tag("b") >>> new_tag.string = "sample" # производим замену тега `<i>` внутри тега `<a>` >>> a_tag.i.replace_with(new_tag) >>> a_tag # <a href="http://example.com/">I linked to <b>sample</b></a>
У тега может быть любое количество атрибутов. Тег <b id = "boldest">
имеет атрибут id
, значение которого равно boldest
. Доступ к атрибутам тега можно получить, обращаясь с тегом как со словарем:
>>> soup = BeautifulSoup('<p><b id="boldest">bolder</b></p>', 'html.parser') >>> tag = soup.b >>> tag['id'] # 'boldest' # доступ к словарю с атрибутами >>> tag.attrs # {'id': 'boldest'}
Можно добавлять и изменять атрибуты тега.
# изменяем `id` >>> tag['id'] = 'bold' # добавляем несколько значений в `class` >>> tag['class'] = ['new', 'bold'] # или >>> tag['class'] = 'new bold' >>> tag # <b class="new bold" id="bold">bolder</b>
А так же производить их удаление.
>>> del tag['id'] >>> del tag['class'] >>> tag # <b>bolder</b> >>> tag.get('id') # None