{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Web-scraping: сбор данных из баз данных и интернет-источников\n", "\n", "*Алла Тамбовцева, НИУ ВШЭ*\n", "\n", "## Управление браузером с помощью Selenium и BeautifulSoup" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Библиотека `selenium` – набор инструментов для интерактивной работы в браузере средствами Python. В широком смысле Selenium – это целый проект, который предлагает различные возможности для управления браузером с использованием популярных языков программирования (Python, Java, R и другие). \n", "\n", "Мы рассмотрим один из самых распространённых инструментов – *Selenium WebDriver*, модуль, который позволяет Python встраиваться в браузер и имитировать в нём работу пользователя: кликать на ссылки и кнопки, заполнять формы, выбирать опции в меню, скроллить страницы и прочее.\n", "\n", "Для начала установим библиотеку:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!pip install selenium" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь скачаем драйвер для браузера в виде архива ([файлы](https://chromedriver.chromium.org/downloads) для Chrome, [файлы](https://github.com/mozilla/geckodriver/releases/) для Firefox). Этот драйвер нужен для того, чтобы Python получил доступ к браузеру и мог открыть в нём новое окно, управляемое автоматически. \n", "\n", "После скачивания архив необходимо распаковать и запомнить, где лежит файл с драйвером (`chromedriver.exe` на Windows, `chromedriver` на Mac). Сам файл с драйвером открывать/запускать не нужно\n", "\n", "Обратите внимание: версия драйвера должна совпадать с версией браузера!\n", "\n", "Импортируем из `selenium` модуль `webdriver` с сокращённым названием:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "from selenium import webdriver as wd" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Если используете драйвер для Chrome, необходимо прописать путь к файлу с драйвером внутри функции `Chrome()`:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "# пример для Mac\n", "\n", "br = wd.Chrome('/Users/allat/Downloads/chromedriver')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# пример для Windows\n", "\n", "br = wb.Chrome(r'C:\\\\Users\\\\allat\\\\Downloads\\\\chromedriver.exe')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Если используете драйвер для Mozilla Firefox, можно ничего не прописывать, функция `Firefox()` сама поймет, где найти `geckodriver`:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Если на Mac файл с драйвером упорно не хочет подсоединяться к Python, попробуйте выполнить действия в инструкции на странице курса (это займет некоторое время). \n", "\n", "После запуска строки кода выше в новом окне браузера открывается пустая страница. На эту страницу мы можем отправить ссылку на сайт и открыть его. Зайдём на сайт «Библио-глобуса»:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "br.get(\"http://www.biblio-globus.ru/\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Чтобы выполнить поиск в каталоге, нам нужно ввести запрос в поле поиска. Найдём это поле! Обратимся к исходному коду страницы и заметим, что поле для ввода запроса имеет атрибут `id` равный `search_string` (искать по id – самый надёжный способ, так как id всегда уникальный). Попросим `selenium` запомнить это поле:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "# find_element_by_ – набор методов \n", "# для поиска по id, тэгам, классам и проч\n", "\n", "search = br.find_element_by_id(\"search_string\") " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "А теперь введём в это поле слово *Python*, используя метод `.send_keys()`:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "search.send_keys(\"Python\") " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Отлично! В окне браузера должны были отразиться изменения. Осталось найти кнопку для поиска и кликнуть на неё. Опять вернёмся к изучению исходного кода страницы:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "button = br.find_element_by_id(\"search_submit\") # снова id\n", "button.click() " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Python в браузере кликнул на кнопку, теперь там должен быть список книг по запросу *Python*. Для того, чтобы собрать информацию по книгам с первой страницы результатов, `selenium` не понадобится, достаточно задействовать знакомый `BeautifulSoup`. Однако для дальнейшей работы нам будет нужен исходный код страницы, которая открыта в браузере в данный момент. Извлечём его:" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "html = br.page_source" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь импортируем `BeautifulSoup` и обработаем исходный код HTML:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "from bs4 import BeautifulSoup\n", "soup = BeautifulSoup(html) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Выгрузим основную информацию о книгах: название, ссылку, авторов, расположение в магазине и цену. Обратите внимание – информация по каждой книге находится в разделах с атрибутом `class` равным `details_1`. Найдём все такие блоки информации:" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "books = soup.find_all(\"div\", {\"class\" : \"details_1\"})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь поработаем с одним из них. " ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "
\n", "
Криволапов С.Я.
\n", "Математика на Python. (Бакалавриат). Учебник.\n", "
\n", " В наличии\n", "
\n", "
Расположение в торговом зале:
Уровень 1, зал № 07, секция 08, шкаф 76, полка 05
\n", "
Цена: 1639,00 руб.
\n", "
" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "books[0]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Выполним поиск по тэгам:" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "book = books[0]\n", "\n", "# ищем по тэгу с классом и извлекаем текст\n", "\n", "author = book.find(\"div\", {\"class\" : \"author\"}).text \n", "name = book.find(\"a\", {\"class\" : \"name\"}).text\n", "place = book.find(\"div\", {\"class\" : \"placement\"}).text\n", "\n", "# ищем по тэгу с классом и извлекаем значение атрибута href\n", "\n", "href = book.find(\"a\", {\"class\" : \"name\"})[\"href\"] \n", "\n", "# тэгу с классом и извлекаем текст\n", "# убираем лишний текст из цены и приводим её к числовому типу\n", "\n", "price_str = book.find(\"div\", {\"class\" : \"title_data price\"}).text\n", "price = int(price_str.split()[1].split(\",\")[0])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Пояснения к последней строке кода:\n", " \n", "1. Строка `price_str` выглядит так: `'Цена: 1639,00 руб.'`.\n", "2. Разбиваем её по пробелу через `.split()`: `['Цена:', '1639,00', 'руб.']`.\n", "3. Забираем элемент с индексом 1: `1639,00`.\n", "4. Разбиваем его по запятой через `.split(\",\")`: `['1639', '00']`.\n", "5. Забираем элемент с индексом 0: `'1639'`.\n", "6. Превращаем строку `'1639'` в целое число `1639` через `int()`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Готово! Теперь напишем функцию для выгрузки всей этой информации и применим ко всем книгам в списке `books`:" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "def books_info(book):\n", " author = book.find(\"div\", {\"class\" : \"author\"}).text \n", " href = book.find(\"a\", {\"class\" : \"name\"})[\"href\"] \n", " name = book.find(\"a\", {\"class\" : \"name\"}).text\n", " place = book.find(\"div\", {\"class\" : \"placement\"}).text\n", " price_str = book.find(\"div\", {\"class\" : \"title_data price\"}).text\n", " price = int(price_str.split()[1].split(\",\")[0])\n", " return author, href, name, place, price" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "res = []\n", "\n", "for b in books:\n", " res.append(books_info(b)) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Объект `res` – это список, состоящий из кортежей. Его можно превратить в красивый датафрейм `pandas`:" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
01234
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/10829190Python. Полное руководствоРасположение в торговом зале: Уровень 1, зал №...909
4Н. Гифт, К. Берман, А. Деза, Г. Георгиу/search/catalog/details/10814639Python и DevOps: Ключ к автоматизации LinuxРасположение в торговом зале: Уровень 1, зал №...2529
5Д. М. Кольцов , Е. В. Дубовик/search/catalog/details/10776656Справочник PYTHON. Кратко, быстро, под рукойРасположение в торговом зале: Уровень 1, зал №...499
6М. Яворски , Т. Зиаде/search/catalog/details/10766703Python. Лучшие практики и инструментыРасположение в торговом зале: Уровень 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
\n", "
" ], "text/plain": [ " 0 1 \\\n", "0 Криволапов С.Я. /search/catalog/details/10835402 \n", "1 Криволапов С.Я. /search/catalog/details/10835400 \n", "2 М. Лутц /search/catalog/details/10597875 \n", "3 Кольцов Д.М. /search/catalog/details/10829190 \n", "4 Н. Гифт, К. Берман, А. Деза, Г. Георгиу /search/catalog/details/10814639 \n", "5 Д. М. Кольцов , Е. В. Дубовик /search/catalog/details/10776656 \n", "6 М. Яворски , Т. Зиаде /search/catalog/details/10766703 \n", "7 Б. Любанович /search/catalog/details/10736311 \n", "8 М.Лутц /search/catalog/details/10632642 \n", "9 Л.Грессер,В.Кенг /search/catalog/details/10831874 \n", "\n", " 2 \\\n", "0 Математика на Python. (Бакалавриат). Учебник. \n", "1 Статистические вычисления на платформе Jupyter... \n", "2 Изучаем Python, том 1 \n", "3 Python. Полное руководство \n", "4 Python и DevOps: Ключ к автоматизации Linux \n", "5 Справочник PYTHON. Кратко, быстро, под рукой \n", "6 Python. Лучшие практики и инструменты \n", "7 Простой Python. Современный стиль программиров... \n", "8 Изучаем Python, том 2, \n", "9 Глубокое обучение с подкреплением: теория и пр... \n", "\n", " 3 4 \n", "0 Расположение в торговом зале: Уровень 1, зал №... 1639 \n", "1 Расположение в торговом зале: Уровень 1, зал №... 1499 \n", "2 Расположение в торговом зале: Уровень 1, зал №... 2849 \n", "3 Расположение в торговом зале: Уровень 1, зал №... 909 \n", "4 Расположение в торговом зале: Уровень 1, зал №... 2529 \n", "5 Расположение в торговом зале: Уровень 1, зал №... 499 \n", "6 Расположение в торговом зале: Уровень 1, зал №... 2759 \n", "7 Расположение в торговом зале: Уровень 1, зал №... 2019 \n", "8 Расположение в торговом зале: Уровень 1, зал №... 2849 \n", "9 Расположение в торговом зале: Уровень 1, зал №... 2509 " ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import pandas as pd\n", "dat = pd.DataFrame(res) \n", "dat" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Добавим названия столбцов:" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [], "source": [ "dat.columns = [\"author\", \"link\", \"title\", \"place\", \"price\"]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "И сделаем все ссылки в столбце `link` полными – доклеим к ним ссылку на сам сайт. Чтобы избежать циклов, воспользуемся методом `.apply()`, который позволяет применить некоторую функцию ко всем ячейкам в столбце, а саму функцию опишем через `lambda`:" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [], "source": [ "dat[\"link\"] = dat[\"link\"].apply(lambda x: \"http://www.biblio-globus.ru\" + x)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Выведем описательные статистики для цен (да, книг мало, но для примера):" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "count 10.000000\n", "mean 2006.000000\n", "std 842.035233\n", "min 499.000000\n", "25% 1534.000000\n", "50% 2264.000000\n", "75% 2701.500000\n", "max 2849.000000\n", "Name: price, dtype: float64" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# n, среднее, ст отклонение, квантили...\n", "\n", "dat[\"price\"].describe() " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Отлично! Однако мы можем пойти дальше и написать более интересный код, который не просто сгружает информацию с первой страницы, а проходит по всем страницам с результатами поиска. Это можно сделать и без `selenium`, но раз мы обсуждаем его, давайте всё-таки его применим! Выполним поиск по странице, открытой в браузере, по тексту ссылки – мы знаем, что последняя страница поиска на сайте называется «Последняя»:" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [], "source": [ "last = br.find_element_by_link_text(\"Последняя\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Забираем из полученного элемента ссылку на эту страницу:" ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=14'" ] }, "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ "last_href = last.get_attribute(\"href\")\n", "last_href" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Разбиваем строку со ссылкой по `page=`, чтобы извлечь только номер, и делаем этот номер целочисленным:" ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "14" ] }, "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ "last_page = int(last_href.split(\"page=\")[1])\n", "last_page" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Идеально! Теперь мы сможем получить ссылки на все страницы из результатов поиска – достаточно подставить в цикле в «шаблонную» строку номера страниц от 1 до 14:" ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [], "source": [ "all_pages = []\n", "\n", "for n in range(1, last_page + 1): \n", " h = f\"http://www.biblio-globus.ru/search/catalog/products?query=Python&page={n}\"\n", " all_pages.append(h) " ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['http://www.biblio-globus.ru/search/catalog/products?query=Python&page=1',\n", " 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=2',\n", " 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=3',\n", " 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=4',\n", " 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=5',\n", " 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=6',\n", " 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=7',\n", " 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=8',\n", " 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=9',\n", " 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=10',\n", " 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=11',\n", " 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=12',\n", " 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=13',\n", " 'http://www.biblio-globus.ru/search/catalog/products?query=Python&page=14']" ] }, "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ "all_pages" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "На этом мы пока остановимся, при желании можно написать универсальный код для поиска на сайте по любому запросу! Алгоритм примерно следующий: пройти в цикле по всем ссылкам на результаты в списке `all_pages` и выгрузить с каждой страницы информацию по книгам на ней, снова задействовав цикл и функцию `books_info()`." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.4" } }, "nbformat": 4, "nbformat_minor": 2 }