Web-scraping: сбор данных из баз данных и интернет-источников

Алла Тамбовцева, НИУ ВШЭ

Управление браузером с помощью Selenium и BeautifulSoup

Библиотека selenium – набор инструментов для интерактивной работы в браузере средствами Python. В широком смысле Selenium – это целый проект, который предлагает различные возможности для управления браузером с использованием популярных языков программирования (Python, Java, R и другие).

Мы рассмотрим один из самых распространённых инструментов – Selenium WebDriver, модуль, который позволяет Python встраиваться в браузер и имитировать в нём работу пользователя: кликать на ссылки и кнопки, заполнять формы, выбирать опции в меню, скроллить страницы и прочее.

Для начала установим библиотеку:

In [ ]:
!pip install selenium

Теперь скачаем драйвер для браузера в виде архива (файлы для Chrome, файлы для Firefox). Этот драйвер нужен для того, чтобы Python получил доступ к браузеру и мог открыть в нём новое окно, управляемое автоматически.

После скачивания архив необходимо распаковать и запомнить, где лежит файл с драйвером (chromedriver.exe на Windows, chromedriver на Mac). Сам файл с драйвером открывать/запускать не нужно

Обратите внимание: версия драйвера должна совпадать с версией браузера!

Импортируем из selenium модуль webdriver с сокращённым названием:

In [1]:
from selenium import webdriver as wd

Если используете драйвер для Chrome, необходимо прописать путь к файлу с драйвером внутри функции Chrome():

In [3]:
# пример для Mac

br = wd.Chrome('/Users/allat/Downloads/chromedriver')
In [ ]:
# пример для Windows

br = wb.Chrome(r'C:\\Users\\allat\\Downloads\\chromedriver.exe')

Если используете драйвер для Mozilla Firefox, можно ничего не прописывать, функция Firefox() сама поймет, где найти geckodriver:

Если на Mac файл с драйвером упорно не хочет подсоединяться к Python, попробуйте выполнить действия в инструкции на странице курса (это займет некоторое время).

После запуска строки кода выше в новом окне браузера открывается пустая страница. На эту страницу мы можем отправить ссылку на сайт и открыть его. Зайдём на сайт «Библио-глобуса»:

In [4]:
br.get("http://www.biblio-globus.ru/")

Чтобы выполнить поиск в каталоге, нам нужно ввести запрос в поле поиска. Найдём это поле! Обратимся к исходному коду страницы и заметим, что поле для ввода запроса имеет атрибут id равный search_string (искать по id – самый надёжный способ, так как id всегда уникальный). Попросим selenium запомнить это поле:

In [5]:
# find_element_by_ – набор методов 
# для поиска по id, тэгам, классам и проч

search = br.find_element_by_id("search_string") 

А теперь введём в это поле слово Python, используя метод .send_keys():

In [6]:
search.send_keys("Python") 

Отлично! В окне браузера должны были отразиться изменения. Осталось найти кнопку для поиска и кликнуть на неё. Опять вернёмся к изучению исходного кода страницы:

In [7]:
button = br.find_element_by_id("search_submit")  # снова id
button.click() 

Python в браузере кликнул на кнопку, теперь там должен быть список книг по запросу Python. Для того, чтобы собрать информацию по книгам с первой страницы результатов, selenium не понадобится, достаточно задействовать знакомый BeautifulSoup. Однако для дальнейшей работы нам будет нужен исходный код страницы, которая открыта в браузере в данный момент. Извлечём его:

In [8]:
html = br.page_source

Теперь импортируем BeautifulSoup и обработаем исходный код HTML:

In [10]:
from bs4 import BeautifulSoup
soup = BeautifulSoup(html) 

Выгрузим основную информацию о книгах: название, ссылку, авторов, расположение в магазине и цену. Обратите внимание – информация по каждой книге находится в разделах с атрибутом class равным details_1. Найдём все такие блоки информации:

In [11]:
books = soup.find_all("div", {"class" : "details_1"})

Теперь поработаем с одним из них.

In [13]:
books[0]
Out[13]:
<div class="details_1">
<div class="author">Криволапов С.Я.</div>
<a class="name" href="/search/catalog/details/10835402">Математика на Python. (Бакалавриат). Учебник.</a>
<div class="title_data green">
                        В наличии
                    </div>
<div class="placement"><b>Расположение в торговом зале:</b> <br/>Уровень 1, зал № 07, секция 08, шкаф 76, полка 05</div>
<div class="title_data price">Цена: <span>1639,00</span> руб.</div>
</div>

Выполним поиск по тэгам:

In [17]:
book = books[0]

# ищем по тэгу с классом и извлекаем текст

author = book.find("div", {"class" : "author"}).text 
name = book.find("a", {"class" : "name"}).text
place = book.find("div", {"class" : "placement"}).text

# ищем по тэгу с классом и извлекаем значение атрибута href

href = book.find("a", {"class" : "name"})["href"] 

# тэгу с классом и извлекаем текст
# убираем лишний текст из цены и приводим её к числовому типу

price_str = book.find("div", {"class" : "title_data price"}).text
price = int(price_str.split()[1].split(",")[0])

Пояснения к последней строке кода:

  1. Строка price_str выглядит так: 'Цена: 1639,00 руб.'.
  2. Разбиваем её по пробелу через .split(): ['Цена:', '1639,00', 'руб.'].
  3. Забираем элемент с индексом 1: 1639,00.
  4. Разбиваем его по запятой через .split(","): ['1639', '00'].
  5. Забираем элемент с индексом 0: '1639'.
  6. Превращаем строку '1639' в целое число 1639 через int().

Готово! Теперь напишем функцию для выгрузки всей этой информации и применим ко всем книгам в списке books:

In [21]:
def books_info(book):
    author = book.find("div", {"class" : "author"}).text 
    href = book.find("a", {"class" : "name"})["href"] 
    name = book.find("a", {"class" : "name"}).text
    place = book.find("div", {"class" : "placement"}).text
    price_str = book.find("div", {"class" : "title_data price"}).text
    price = int(price_str.split()[1].split(",")[0])
    return author, href, name, place, price
In [22]:
res = []

for b in books:
    res.append(books_info(b)) 

Объект res – это список, состоящий из кортежей. Его можно превратить в красивый датафрейм pandas:

In [23]:
import pandas as pd
dat = pd.DataFrame(res) 
dat
Out[23]:
0 1 2 3 4
0 Криволапов С.Я. /search/catalog/details/10835402 Математика на Python. (Бакалавриат). Учебник. Расположение в торговом зале: Уровень 1, зал №... 1639
1 Криволапов С.Я. /search/catalog/details/10835400 Статистические вычисления на платформе Jupyter... Расположение в торговом зале: Уровень 1, зал №... 1499
2 М. Лутц /search/catalog/details/10597875 Изучаем Python, том 1 Расположение в торговом зале: Уровень 1, зал №... 2849
3 Кольцов Д.М. /search/catalog/details/10829190 Python. Полное руководство Расположение в торговом зале: Уровень 1, зал №... 909
4 Н. Гифт, К. Берман, А. Деза, Г. Георгиу /search/catalog/details/10814639 Python и DevOps: Ключ к автоматизации Linux Расположение в торговом зале: Уровень 1, зал №... 2529
5 Д. М. Кольцов , Е. В. Дубовик /search/catalog/details/10776656 Справочник PYTHON. Кратко, быстро, под рукой Расположение в торговом зале: Уровень 1, зал №... 499
6 М. Яворски , Т. Зиаде /search/catalog/details/10766703 Python. Лучшие практики и инструменты Расположение в торговом зале: Уровень 1, зал №... 2759
7 Б. Любанович /search/catalog/details/10736311 Простой Python. Современный стиль программиров... Расположение в торговом зале: Уровень 1, зал №... 2019
8 М.Лутц /search/catalog/details/10632642 Изучаем Python, том 2, Расположение в торговом зале: Уровень 1, зал №... 2849
9 Л.Грессер,В.Кенг /search/catalog/details/10831874 Глубокое обучение с подкреплением: теория и пр... Расположение в торговом зале: Уровень 1, зал №... 2509

Добавим названия столбцов:

In [24]:
dat.columns = ["author", "link", "title", "place", "price"]

И сделаем все ссылки в столбце link полными – доклеим к ним ссылку на сам сайт. Чтобы избежать циклов, воспользуемся методом .apply(), который позволяет применить некоторую функцию ко всем ячейкам в столбце, а саму функцию опишем через lambda:

In [25]:
dat["link"] = dat["link"].apply(lambda x: "http://www.biblio-globus.ru" + x)

Выведем описательные статистики для цен (да, книг мало, но для примера):

In [26]:
# n, среднее, ст отклонение, квантили...

dat["price"].describe() 
Out[26]:
count      10.000000
mean     2006.000000
std       842.035233
min       499.000000
25%      1534.000000
50%      2264.000000
75%      2701.500000
max      2849.000000
Name: price, dtype: float64

Отлично! Однако мы можем пойти дальше и написать более интересный код, который не просто сгружает информацию с первой страницы, а проходит по всем страницам с результатами поиска. Это можно сделать и без selenium, но раз мы обсуждаем его, давайте всё-таки его применим! Выполним поиск по странице, открытой в браузере, по тексту ссылки – мы знаем, что последняя страница поиска на сайте называется «Последняя»:

In [27]:
last = br.find_element_by_link_text("Последняя")

Забираем из полученного элемента ссылку на эту страницу:

In [29]:
last_href = last.get_attribute("href")
last_href
Out[29]:
'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=14'

Разбиваем строку со ссылкой по page=, чтобы извлечь только номер, и делаем этот номер целочисленным:

In [30]:
last_page = int(last_href.split("page=")[1])
last_page
Out[30]:
14

Идеально! Теперь мы сможем получить ссылки на все страницы из результатов поиска – достаточно подставить в цикле в «шаблонную» строку номера страниц от 1 до 14:

In [31]:
all_pages = []

for n in range(1, last_page + 1): 
    h = f"http://www.biblio-globus.ru/search/catalog/products?query=Python&page={n}"
    all_pages.append(h) 
In [32]:
all_pages
Out[32]:
['http://www.biblio-globus.ru/search/catalog/products?query=Python&page=1',
 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=2',
 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=3',
 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=4',
 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=5',
 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=6',
 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=7',
 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=8',
 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=9',
 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=10',
 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=11',
 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=12',
 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=13',
 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=14']

На этом мы пока остановимся, при желании можно написать универсальный код для поиска на сайте по любому запросу! Алгоритм примерно следующий: пройти в цикле по всем ссылкам на результаты в списке all_pages и выгрузить с каждой страницы информацию по книгам на ней, снова задействовав цикл и функцию books_info().