В этом параграфе мы познакомимся с основными инструментами для извлечения данных из интернета — и в конце соберём свой небольшой датасет о самых популярных фильмах Кинопоиска. В этом нам поможет пакет BeautifulSoup, который извлекает нужные элементы информации с веб-страниц. Приступим.
Подготовка
Мы уже скопировали веб-страницу и данные для неё и положили в отдельный репозиторий — это нужно, поскольку веб-страницы меняются со временем, а значит код ниже мог бы перестать работать. Вам нужно скачать HTML-файл из репозитория и сохранить в папке учебного проекта (т.е в той, в которой запущен Python). Откройте его в браузере и изучите содержимое.
Как видите, структура довольна простая:
- одна страница с фильмами;
- на ней — список из 50 фильмов;
- в карточке каждого фильма — его название, обложка, оригинальное название, рейтинг, количество отзывов и некоторая дополнительная информация.
Каждый из элементов на странице размечен с помощью языка HTML. Сейчас коротко расскажем, как он устроен — это пригодится нам дальше.
Совсем немного про HTML
С помощью HTML мы подсказываем браузеру (Google Chrome, Safari, Opera), из каких семантических единиц (заголовок, абзац, ссылка и так далее) состоит контент на странице.
Совсем простой пример выглядит так:
<h1>Это заголовок</h1>
<p>Это текст.</p>
🔍 HTML — это язык разметки веб-страниц.
Ключевое слово — язык разметки. Это не язык программирования, не путайте. В разницу между этими понятиями мы углубляться не будем, просто запомните это.
Вот что происходит, когда вы вводите в браузер адрес веб-сайта:
- Ваш компьютер отправляет запрос на сервер, на котором хранится HTML-документ.
- Сервер отвечает на запрос и присылает HTML-документ для запрошенной страницы.
- Браузер читает этот код и вычисляет, как отобразить элементы (заголовки, картинки, ссылки) и где они должны располагаться на странице.
HTML состоит из тегов — в нашем примере это <h1></h1>
и <p></p>
. Именно благодаря тегам браузер понимает, из каких элементов состоит страница: для него это что-то вроде инструкции по сборке элементов.
У тега могут быть атрибуты. Они нужны для передачи дополнительной информации браузеру. Выглядят атрибуты так: это пара ключ="значение"
. В примере ниже тэг <a></a>
— это элемент «гиперссылка». У него есть атрибут href, значение которого — адрес сайта, куда будет отправлен пользователь при клике.
<a href="https://www.w3schools.com/html/html_attributes.asp">Ссылка на статью об HTML-аттрибутах</a>
Тегов существует несколько десятков, и у каждого из них своя роль. Например:
h1
— создаёт заголовки;a
— создаёт ссылки;p
— создаёт блоки текста (параграфы);span
— создаёт строки текста;div
— создаёт контейнер для группировки других тегов.
Подробнее о том, какие ещё бывают теги и какие у них атрибуты можно почитать по ссылке.
Теперь посмотрим на то, как выглядит веб-страница «под капотом». Для этого нужно нажать клавишу F12 на странице — откроются инструменты разработчика. В них необходимо перейти на вкладку Elements (или Элементы).
В выделенной строке с элементом мы можем увидеть тег <body>
. Он обозначает начало основной части страницы. У многих тегов есть атрибут под названием class — он помогает группировать похожие элементы.
Код выстроен по принципу лестницы. Элементы хранят в себе другие элементы. Зная их расположение относительно друг друга мы можем проследить путь до каждого элемента на странице. Мы можем спуститься вниз по структуре кода страницы, чтобы достигнуть нужного нам элемента.
Чтобы быстро найти местоположение элемента — наведите на искомый объект и через правую кнопку мыши выберите Просмотреть код
. В английской версии это будет называться Inspect
. Нажатие сразу перенесет вас к нужному элементу с HTML кодом.
При наведении на фрагмент кода элемента страницы выделяется область, соответствующая ему. К примеру, так выглядит выделение элемента кода, в котором хранится название фильма «Зелёная миля».
Попробуйте самостоятельно выделить несколько элементов на этой странице. Вам важно уловить идею того, как мы можем перемещаться между элементами.
Мы разобрали абсолютный минимум, который нужен вам для чтения этого параграфа. Но если вам хочется узнать больше об HTML и веб-разработке, то советуем пройти бесплатный вводный курс в «Яндекс.Практикуме».
Сбор данных
Теперь мы готовы приступить к сбору данных. Алгоритм действий очень простой:
- загружаем страницу;
- визуально определяем нужные нам узлы элементов в коде страницы;
- извлекаем информацию из нужного элемента;
- записываем в датафрейм.
Для выполнения запроса по загрузке кода с интернет-страницы нам понадобится пакет requests
, который поможет осуществить запрос по ссылке. А для сбора нужных фрагментов данных нам нужен пакет BeautifulSoup
, с помощью которого мы можем осуществлять навигацию по коду страницы и выискивать нужные нам элементы.
>>> import requests
>>> from bs4 import BeautifulSoup as BS
Мы будем извлекать информацию из HTML-файла, который мы скачали в начале этого параграфа.
Для этого нам нужно указать путь до него. Если вы положите его в папку с Python, то в пути нужно будет указать только название файла. Если у вас тут что-то не получается — посмотрите на параграф 1.4.
Ссылку мы вынесем в отдельную переменную. Далее, с помощью метода get()
из пакета requests
сделаем запрос, получим страницу и сохраним ее в переменной page
. Фактически, мы открыли страницу, но не в браузере, а в Python.
>>> path = "URL ссылка на нужную страницу"
>>> page = requests.get(path)
Содержимое страницы переносим в BeautifulSoup
. Создаем объект BS
, в который переносим содержимое страницы (page.content
) и уточняем по странице какого типа мы хотим осуществлять навигацию. Они бывают разные (по ссылке — таблица с достоинствами и недостатками каждого), но для нашей страницы подойдет html.parser
.
>>> soup = BS(page.content, "html.parser")
В задачах мы столкнемся с ситуацией, где мы будем работать с уже выгруженной страницей. И команда get
нам уже не поможет. Чтобы работать с выгруженными html-страницами, нам нужно загрузить эту страницу в отдельный объект с помощью pandas
и уже потом выгрузить в BeautifulSoup
, чтобы путешествовать по разметке.
import pandas as pd
>>> page = pd.read_html('файл_с_html_разметкой.html')
>>> soup = BS(page.content, "html.parser")
Попробуем вывести название страницы. Для этого мы можем обратиться к объекту soup
и получим название страницы с помощью title
. Сохраним его в переменную и выведем результат.
>>> title = soup.title
>>> print(title)
<title data-tid="57f72b5">500 лучших фильмов — Кинопоиск</title>
В выдаче видно название страницы. Но оно сопровождается разными тэгами и атрибутами. Чтобы вывести название в чистом виде — нужно использовать text
.
>>> print(title.text)
500 лучших фильмов — Кинопоиск
Идеально. Мы сумели достать данные из страницы автоматически. Попробуем сделать больше. Давайте поищем информацию о названиях фильмов. Сделаем это с помощью метода find()
. Он ищет в структуре страницы первый элемент, подходящий под запрос. BeautifulSoup читает страницу последовательно сверху-вниз.
Внутри .find()
укажем название тэга span
. Вторым аргументом указываем class_
, обозначающий принадлежность конкретного span’a к конкретному классу. Класс обязательно указываем с нижним подчеркиванием.
Вы можете спросить, откуда мы их взяли. Вот, из кода страницы, который мы уже показывали раньше:
Структура страницы непростая и в обозначениях каждого элемента присутствует сложный набор символов. На практике, более простые страницы называют классы элементов понятно и логично, в соответствии с принадлежностью к тому или иному элементу.
Чтобы не переписывать все вручную, можно копировать элементы прямо из кода разметки страницы. Двойной клик по названию внутри class
позволяет выделить нужное нам название класса.
>>> title_film = soup.find("span", class_ = "styles_activeMovieTittle__kJdJj")
>>> print(title_film)
<span class="styles_mainTitle__IFQyZ styles_activeMovieTittle__kJdJj" data-tid="4502216a">Зеленая миля</span>
Пойдём дальше и усложним задачу. Достанем названия всех фильмов со страницы. Такие задачи выполняются с помощью команды findAll
или find_all
. Команда находит все элементы с указанными характеристиками. Она выписывает их по порядку, сверху вниз. Создадим объект, в который запишем все элементы. Можно просто взять предыдущую команду и поменять find
на find_all
.
>>> title_film_all = soup.find_all("span", class_ = "styles_activeMovieTittle__kJdJj")
Все названия записаны в объект title_film_all
. Нужно проверить, сколько элементов записалось в объект и удостовериться, что не записалось лишнего. Это можно сделать с помощью команды len
.
>>> len(title_film_all)
50
Все соответствует действительности. На странице действительно 50 фильмов.
Можно вывести все элементы объекта с помощью написания простого цикла (на всякий случай напомним, что о циклах мы говорили в параграфе 3.3). Это позволит лишний раз убедиться, что все сделано правильно. В цикле мы говорим что для каждого элемента i
в объекте title_film_all
нужно вывести текст i
. В данном случае i
— это каждый элемент объекта. Но можно назвать его как угодно, это лишь наименование, нужное для обозначения составной части объекта в цикле.
>>> for i in title_film_all:
>>> print(i.text)
Зеленая миля
Список Шиндлера
Побег из Шоушенка
Форрест Гамп
Тайна Коко
...
Зеленая книга
Остров сокровищ
Если бы мы хотели получить больше информации о каждом фильме, то было бы полезно открыть ссылку, которая ведет на страницу с полной информацией. Все активные ссылки хранятся в атрибутах элемента. Атрибут — это свойство элемента. Если проводить аналогию с данными, то элемент — это наблюдение, а атрибут — переменная. Это не вполне точное сравнение, но надеемся, что вам там будет понятнее.
Если элемент содержит ссылку, то она будет храниться в атрибуте href
в элементе с тегом а
. Можем достать все элементы с таким тэгом.
>>> film_links = soup.find_all("a")
Для извлечения атрибута используем метод get()
. С помощью неё мы извлечем содержимое href
.
>>> for i in film_links:
>>> item_url = i.get("href")
>>> print(item_url)
https://hd.kinopoisk.ru
/lists/categories/movies/1/
/lists/categories/movies/3/
/afisha/new/
/media/
/
...
/film/435/
/film/435/
/film/329/
/film/329/
...
https://music.yandex.ru
https://afisha.yandex.ru
https://yandex.ru/all
С результатом явно что-то не так. В ней ссылки на фильмы дублированы, а в начале и конце есть куча других лишних ссылок, которые не подходят нам. Так получилось, потому что мы взяли абсолютно все атрибуты с тэгом а
. Нам нужно было сделать более точный запрос. Для этого мы сначала найдем название фильма (мы делали это выше), а потом обратимся к элементу а
и достанем нужный атрибут. Получилось, что мы скомбинировали два метода find()
.
>>> link = soup.find(class_="styles_main__Y8zDm styles_mainWithNotCollapsedBeforeSlot__x4cWo").find('a', class_="base-movie-main-info_link__YwtP1").get('href')
>>> print(link)
/film/435/
Мы можем доставать атрибуты с помощью get()
, а можем сохранить нужную часть в переменную и обратиться так же, как мы обращаемся к элементам листа.
>>> link1 = soup.find(class_="styles_main__Y8zDm styles_mainWithNotCollapsedBeforeSlot__x4cWo").find('a', class_="base-movie-main-info_link__YwtP1")
>>> film = link1.get("href")
>>> film_1 = link1["href"]
>>> print(film)
>>> print(film_1)
/film/435/
/film/435/
Но это только часть ссылки, нам нужно добавить к ней её первую половину. Объединим ссылку на Кинопоиск и то, что мы получили из кода страницы.
>>> "[https://www.kinopoisk.ru](https://www.kinopoisk.ru/)" + link
[https://www.kinopoisk.ru/film/435/](https://www.kinopoisk.ru/film/435/)
Если скопировать эту ссылку в браузере, то мы увидим страницу нужного нам фильма.
Напишем цикл для извлечения всех ссылок. Нужно извлечь элемент с классом названия и затем получить из него нужный нам атрибут. Обратите внимание, что после основной ссылки мы добавляем элемент объекта urls
, из которого извлекаем атрибут со ссылкой.
>>> urls = soup.find_all(class_="styles_main__Y8zDm styles_mainWithNotCollapsedBeforeSlot__x4cWo")
>>> for url in urls:
>>> link = "https://www.kinopoisk.ru"+url.find('a', class_="base-movie-main-info_link__YwtP1").get('href')
>>> print(link)
https://www.kinopoisk.ru/film/435/
https://www.kinopoisk.ru/film/329/
...
https://www.kinopoisk.ru/film/77335/
https://www.kinopoisk.ru/film/470553/
У нас получилось! Теперь создадим цикл, в котором сразу вытащим все доступные на странице сведения о фильме. К уже извлеченным добавим ещё оригинальное название, оценку пользователей и количество отзывов. Будем действовать по следующему алгоритму:
- создаем пустой лист, куда будет записываться информация о фильмах;
- пишем цикл, с помощью которого будем извлекать информацию и записывать её в пустой объект с листом;
- Проверяем длину и наполнение листа.
Чтобы извлекать все элементы, найдем тег, внутри которых лежит вся нужная информация.
В выделенную область входят все элементы, которые нам потребуется. Это то что нужно.
В конце цикла командой append
вписываем каждый элемент в пустой объект data
. Она запишет их в том порядке, в котором указано внутри функции. Обратите внимание, что при каждом доставании элемента внутри цикла, нам уже не нужно обращаться к объекту soup
, мы обращаемся к уже выделенной группе элементов.
>>> films = soup.find_all('div', class_="styles_content__nT2IG")
>>> data = []
>>> for film in films:
>>> title = film.find("span", class_ = "styles_activeMovieTittle__kJdJj").text
>>> link = "https://www.kinopoisk.ru"+film.find('a', class_="base-movie-main-info_link__YwtP1").get('href')
>>> original_name = film.find('div', class_='desktop-list-main-info_secondaryTitleSlot__mc0mI').find('span').text
>>> rating = film.find('div', class_='styles_kinopoiskValueBlock__qhRaI').find('span', class_='styles_kinopoiskValuePositive__vOb2E styles_kinopoiskValue__9qXjg styles_top250Type__mPloU').text
>>> rating_count = film.find('span', class_='styles_kinopoiskCount__2_VPQ').text
>>> data.append([title, original_name, link, rating, rating_count])
Прекрасно! Мы получили нужные нам элементы с одной страницы. Но рейтинг называется “500 лучших фильмов”, а у нас только 50. Сделать это довольно просто. Нам нужно просто загружать страницы с помощью requests
и обрабатывать каждую из них по отдельности.
Теперь вы можете собирать практически любые публичные данные из интернета. Пример, который мы привели — довольно простой, но логика сбора данных не меняется: ищем в общей структуре элементов нужные, а потом последовательно их обходим.
В следующем разделе мы поговорим о том, как ещё можно хранить данные и что делать с пропущенными значениями.