{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Python для сбора и анализа данных\n", "\n", "*Алла Тамбовцева, НИУ ВШЭ*\n", "\n", "*Оригинальная постановка задачи и наработки по выгрузке данных: Арам Габрелян, 1 курс*\n", "\n", "### Пример обработки динамической страницы" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Импортируем необходимые модули и библиотеки:\n", " \n", "* `sleep`: для выставления задержки по времени;\n", "* `bs4`: для обработки кода HTML с помощью `BeautifulSoup`." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import time\n", "from bs4 import BeautifulSoup" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь импортируем необходимые модули `selenium`. Они все те же, модуль `webdriver` для соединения Python с браузером через драйвер и модуль для поиска `By`:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "from selenium import webdriver as wd\n", "from selenium.webdriver.common.by import By" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Открываем новое окно браузера:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/opt/anaconda3/lib/python3.7/site-packages/ipykernel_launcher.py:1: DeprecationWarning: executable_path has been deprecated, please pass in a Service object\n", " \"\"\"Entry point for launching an IPython kernel.\n" ] } ], "source": [ "br = wd.Chrome(executable_path = \"/Users/allat/Documents/chromedriver\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Отправим запрос к странице с профилем Норвегии за 2000 год на сайте [OEC](https://oec.world/), где, в числе прочего, находятся данные по экспорту стран. Технически, после `country` в ссылку ниже можно подставить аббревиатру для любой страны, а после `yearSelector1` – любой год (вспоминаем про форматирование строк и f-строки):" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "https://oec.world/en/profile/country/nor?depthSelector1=HS4Depth&yearSelector1=2000\n" ] } ], "source": [ "url = \"https://oec.world/en/profile/country/nor?depthSelector1=HS4Depth&yearSelector1=2000\"\n", "print(url)" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "# подождем 5 секунд, сайт тяжелый\n", "\n", "br.get(url)\n", "time.sleep(5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Предположим, нам нужна информация об объеме экспорта различных товаров из мозаичного графика, который появляется только тогда, когда мы доскролливаем до раздела *Historical data* где-то в середине страницы. \n", "\n", "Если открыть страницу в браузере (не с Selenium, чтобы не мешать) и выбрать *Просмотреть код*, откроются инструменты разработчика. При работе в режиме разработчика можно находить элементы на странице, а соответствующие им фрагменты будут выделяться в исходном коде. Так вот: до тех пор пока мы не дойдем до нужного момента, в исходном коде просто не будет блока с данными об экспорте. Страница обновляется динамически, ее наполнение изменяется в зависимости от действий пользователя. То же самое будет, если мы откроем полностью исходный код страницы в отдельной вкладке. Если попробовать найти код с текстом *Historical data*, ничего не получится.\n", "\n", "Поэтому воспользуемся возможностями Selenium и организуем скроллинг. К объекту `br`, в котором у нас сохранено соединение с браузером, можно применить метод `.execute_script()` для исполнения кода на языке JavaScript, отвечающего за интерактив на веб-страницах. А код будет такой:\n", "\n", " window.scrollTo(0, document.body.scrollHeight);\n", " \n", "Функция `window.scrollTo()` реализует прокрутку страницы, а в качестве аргументов этой функции мы указываем начальную и конечную точку. Начальная точка 0, а конечная – конец страницы, который вычисляется автоматически, извлекается из информации о документе. " ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "br.execute_script(\"window.scrollTo(0, document.body.scrollHeight);\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Итак, страница в браузере пролисталась до конца. Для примера вернемся назад:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "br.execute_script(\"window.scrollTo(0, 0);\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "А теперь проскроллим на 2500 пикселей, чтобы дойти до нужного места с *Historical Data* (значение подобрано экспериментально, на глаз):" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "br.execute_script(\"window.scrollTo(0, 2500);\")\n", "time.sleep(2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Готово! Нужный фрагмент теперь точно есть на странице. Найдем его, используя XPATH (вот тут, конечно, сложновато, это последовательность тэгов, которые нужно пройти, чтобы оказаться в нужной части):" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "part = br.find_element(By.XPATH, \n", " './/*[@id=\"cp-section-625\"]/div/div[2]/div[1]/div[2]/div')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Исходный код всей страницы нам не нужен, заберем только фрагмент, который хранится в `part` выше:" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "html = part.get_attribute('innerHTML')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Преобразуем текст в объект BeautifulSoup:" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "soup = BeautifulSoup(html)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Если внимательно изучить исходный код, можно заметить, что нужная информация об экспорте хранится в тэгах `` с определенным классом. Найдем все подходящие фрагменты кода HTML:" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "# ищем блоки с текстом через методы BeautifulSoup\n", "\n", "blocks = soup.find_all(\"g\", {\"class\" : \"d3plus-textBox\"})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Извлечем текст из каждого блока и посмотрим на результаты:" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['Total: $64.5B', 'Crude Petroleum', '44.9%', 'PetroleumGas', '9.39%', 'RefinedPetroleum', '8.13%', 'RawAluminium', '3.13%', 'Non-filletFresh Fish', '1.9%', 'Non-filletFrozenFish', '1.32%', 'Passenger andCargo Ships', '1.08%', 'FishFillets', '1.02%', 'Ferroalloys', '0.99%', 'Raw Nickel', '0.92%', 'Fish: dried,salted, smoked...', '0.87%', 'Newsprint', '0.75%', '', '0.65%', 'Gas...', '0.56%', 'Motor...', '0.51%', '', '0.47%', '', '0.45%', 'Mixed...', '0.44%', 'Computers', '0.38%', '', '0.33%', '', '', 'Telephones', '', '', '', '', '', '', '', 'Semi-...', '', 'Kraft...', '', 'Liquid...', 'Planes...', '', '', '', '', '', 'Raw...', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']\n" ] } ], "source": [ "texts = [block.text for block in blocks]\n", "print(texts)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Не очень красиво, но если нам нужна информация по общему объему экспорта и процентам, которые приходятся на экспорт самых «топовых» товаров, для этого у нас есть все данные. А со списками работать мы точно умеем." ] } ], "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": 5 }