Beautiful Soup (буквально — «прекрасный суп») — это библиотека Python для извлечения данных из HTML и XML. То есть, библиотека для веб-скрапинга.
Давайте разберем задание для технического собеседования.
# Путем веб-краулинга исследуйте веб-страницу и выведите слово, # которое встречается на ней чаще всего, а также # количество вхождений этого слова. # Страница для краулинга: # https://en.wikipedia.org/wiki/Apple_Inc. # Учитывайте только слова из раздела «History».
Итак, дана страница Apple в Википедии. Нам нужно найти слово, которое чаще всего встречается в разделе «History», а также посчитать, сколько раз оно там встречается. Приступим.
1. Первоначальная подготовка
Для начала нам нужно импортировать Beautiful Soup. Установим библиотеку через командную строку:
pip3 install bs4
Если у вас возникли проблемы с установкой, обратитесь к документации.
Далее нужно запросить нашу библиотеку вверху кода. Вот все, что нам понадобится:
from bs4 import BeautifulSoup, Tag import requests from collections import defaultdict
Теперь мы готовы определить нашу функцию.
def find_most_common():
2. Извлечение страницы
Давайте извлечем нашу страницу и распарсим ее при помощи Beautiful Soup. Для этого мы воспользуемся библиотекой requests:
page = requests.get("https://en.wikipedia.org/wiki/Apple_Inc.")
Теперь прогоним полученный документ через Beautiful Soup.
soup = BeautifulSoup(page.text, "html.parser")
Как нам извлечь только раздел «History»? Нужно изучить HTML-код страницы. Там много всякой тарабарщины, но если подчистить, он выглядит так:
<h2> <span class="mw-headline" id="History">History</span> </h2> <div ...>...</div> <div ...>...</div> <h3> <span ...></span> <span class="mw-headline" id="1976–1984:_Founding_and_incorporation">1976–1984: Founding and incorporation</span> </h3> . . . <p>Apple Computer Company was founded on April 1, 1976, by <a href="/wiki/Steve_Jobs" title="Steve Jobs">Steve Jobs</a>...
На сайте Википедии, судя по всему, все содержимое страницы помещено в один блок div
. Это значит, что раздел «History» не лежит в собственном div
. Это всего лишь заголовок и какое-то содержимое внутри родительского div, в котором содержатся все разделы.
Лучшее, что мы можем сделать, чтобы извлечь только исторический раздел, это просто захватить заголовок и все, что идет после него. Мы захватываем тег <span>
с ID «History», а затем идем к его родителю — <h2>
. Чтобы захватить все идущее за заголовком, мы можем использовать нотацию Beautiful Soup: next_siblings
. Все вместе:
history = soup.find(id="History").parent.next_siblings
3. Подсчет слов
Давайте инициализируем пару переменных. Нам нужна переменная для искомого слова и для количества его вхождений. Еще нам нужен словарь для подсчета всех слов. Что касается словаря, мы используем дефолтный словарь — defaultdict. Он позволяет задать целые числа в качестве типа по умолчанию. Благодаря этому при запросе несуществующего ключа будет создана пара из этого ключа и дефолтного значения — нуля. (Пример использования defaultdict можно посмотреть в статье «Определяем, все ли символы в строке уникальны. Разбор задачи», — прим. ред. Pythonist.ru).
max_count = 0 max_word = "" dd = defaultdict(int)
Теперь мы готовы краулить. Давайте переберем в цикле history
, проверяя каждый элемент — elem
. Бывает, что Beautiful Soup возвращает вместо элемента то, что называется Navigable String
. Мы отфильтруем все, что не является элементом, при помощи метода isinstance()
из нашей библиотеки.
for elem in history: if isinstance(elem, Tag):
Давайте подумаем, что будет происходить дальше. Для каждого элемента в history
нам нужно просмотреть текст и посчитать вхождения каждого слова. Но не забудьте, что нам нужно остановиться, когда раздел истории закончится. Следующий раздел находится в том же блоке div, но начинается с тега <h2>
. В конце мы можем вывести слово, встречающееся чаще всего, и количество его вхождений. Возвращать будем max_count
.
for elem in history: if isinstance(elem, Tag): if elem.name == "h2": print(max_word, "is the most common, appearing", max_count, "times.") return max_count
А что, если мы еще не дошли до конца раздела? Нам нужно извлечь текст из элемента, вызвав метод get_text() Beautiful Soup, а затем разбить его на слова (по пробелам) при помощи метода split().
words = elem.get_text().split()
Что дальше? Переберем в цикле каждое слово, обновляя его значение в словаре. Поскольку мы используем defaultdict
, нам не нужно проверять, есть ли уже такое слово в словаре: мы можем просто добавлять единицу к значению этого слова. Но нужно не забывать обновлять max_word и max_count, если находим слово, встречающееся чаще, чем предыдущее.
for word in words: dd[word] += 1 if dd[word] > max_count: max_count = dd[word] max_word = word
Вот и все! Код должен работать… если только Википедия не сменит макет своего сайта. Давайте-ка добавим итоговую проверку на случай, если это все же произойдет. Все вместе выглядит следующим образом:
from bs4 import BeautifulSoup, Tag import requests from collections import defaultdict def find_most_common(): page = requests.get("https://en.wikipedia.org/wiki/Apple_Inc.") soup = BeautifulSoup(page.text, "html.parser") history = soup.find(id="History").parent.next_siblings max_count = 0 max_word = "" dd = defaultdict(int) for elem in history: if isinstance(elem, Tag): if elem.name == "h2": print(max_word, "is the most common, appearing", max_count, "times.") return max_count words = elem.get_text().split() for word in words: dd[word] += 1 if dd[word] > max_count: max_count = dd[word] max_word = word return "Error"
Проверяем
Эта функция выводит результат на экран, так что мы можем просто запустить ее при помощи find_most_common()
. Мы получим следующее:
the is the most common, appearing 328 times.
Это оно! Конечно, эта функция работает только для конкретной страницы (и на момент написания статьи). Основная проблема веб-краулинга в том, что если собственник сайта хоть немного изменит свой контент, функция может сломаться. Также мы не учитывали здесь регистры и пунктуацию: вы можете реализовать это самостоятельно.