Сбор и обработка данных на Python: от парсинга до анализа

Этот курс научит вас эффективно собирать данные из различных источников, включая веб-сайты и API, используя современные инструменты Python. Вы освоите методы очистки, структурирования и предварительной обработки информации с помощью библиотек Pandas и NumPy для дальнейшего анализа.

Основы сбора данных: работа с HTTP-запросами, REST API и форматами JSON

Основы сбора данных: работа с HTTP-запросами, REST API и форматами JSON

Добро пожаловать на курс «Сбор и обработка данных на Python». Мы начинаем наше путешествие в мир Data Engineering и Data Science с самого фундаментального навыка — умения получать данные из интернета.

Представьте, что интернет — это огромный океан информации. Чтобы использовать эту воду для питья (анализа) или полива полей (обучения моделей), нам нужно построить трубопровод. В этой статье мы разберем, из чего состоят эти «трубы»: протокол HTTP, архитектурный стиль REST API и формат данных JSON.

Как общаются компьютеры: Клиент-серверная архитектура

Прежде чем писать код, важно понять физику процесса. Весь современный веб построен на клиент-серверной архитектуре.

Когда вы открываете браузер и вводите адрес сайта, происходит следующее:

  1. Клиент (ваш браузер или скрипт на Python) формирует сообщение — запрос (Request).
  2. Это сообщение летит через сеть к Серверу (мощному компьютеру, где хранятся данные).
  3. Сервер обрабатывает запрос и отправляет обратно ответ (Response).

Язык, на котором общаются клиент и сервер, называется HTTP (HyperText Transfer Protocol).

Анатомия HTTP-запроса

HTTP-запрос — это не просто «дай мне страницу». Это структурированное сообщение, которое состоит из нескольких ключевых частей:

  • URL (Uniform Resource Locator): Адрес, куда мы стучимся. Например, https://api.github.com/users.
  • Метод (Method): Глагол, указывающий, что мы хотим сделать.
  • Заголовки (Headers): Служебная информация (какой у нас браузер, какой формат данных мы понимаем).
  • Тело (Body): Данные, которые мы передаем на сервер (используется не всегда).

Основные методы HTTP

В сборе данных мы чаще всего используем два метода:

  1. GET — «Дай мне». Используется для получения данных. Когда вы открываете любую страницу в браузере, он делает GET-запрос. Тело запроса обычно пустое.
  2. POST — «Возьми это». Используется для отправки данных на сервер (например, ввод логина и пароля или загрузка файла).

Существуют и другие методы (PUT, DELETE, PATCH), но на начальном этапе они встречаются реже.

Коды состояния (Status Codes)

Как понять, что ответил сервер? Для этого существуют трехзначные числа — коды состояния. Вы наверняка видели их раньше:

  • 200 OK: Всё отлично, запрос выполнен успешно.
  • 404 Not Found: Ресурс не найден (ошибка в адресе).
  • 403 Forbidden: Доступ запрещен (нужна авторизация).
  • 500 Internal Server Error: Проблема на стороне сервера (вы не виноваты).

Практика: Библиотека requests в Python

Python — идеальный язык для работы с сетью благодаря библиотеке requests. Она не входит в стандартную библиотеку, но является стандартом де-факто в индустрии.

Установим её:

pip install requests

Теперь попробуем сделать наш первый запрос к сайту GitHub:

import requests

# Отправляем GET-запрос
response = requests.get('https://api.github.com')

# Проверяем статус-код
print(f"Статус-код: {response.status_code}")

# Если статус 200, значит всё хорошо
if response.status_code == 200:
    print("Запрос прошел успешно!")
else:
    print("Что-то пошло не так.")

Объект response содержит всё, что вернул сервер: заголовки, статус и, самое главное, содержимое.

Что такое API и REST?

Если мы будем скачивать обычные HTML-страницы (как их видит человек), нам придется долго вычищать оттуда лишний мусор: теги верстки, стили, скрипты. Это называется парсинг HTML, и мы займемся этим в следующих статьях.

Но программисты придумали способ, как программам общаться друг с другом напрямую, без визуальной шелухи. Это называется API (Application Programming Interface).

API — это интерфейс, который позволяет одной программе использовать функции другой программы.

Представьте ресторан:

  • Вы — это Клиент (программа).
  • Кухня — это Сервер (база данных и логика).
  • Меню — это документация API.
  • Официант — это API.

Вы не идете на кухню сами. Вы говорите официанту (API) строго по меню: «Хочу стейк». Официант уходит и возвращает вам готовое блюдо.

REST (Representational State Transfer)

REST — это набор правил (архитектурный стиль) того, как должен выглядеть «правильный» API.

Основные принципы REST для нас как для сборщиков данных:

  1. Клиент-сервер: Разделение ответственности.
  2. Stateless (Без состояния): Сервер не запоминает нас между запросами. Каждый запрос должен содержать всю необходимую информацию (например, ключ доступа).
  3. Единообразие интерфейса: Мы обращаемся к ресурсам по понятным адресам. Например, /users вернет список пользователей, а /users/1 — конкретного пользователя с ID 1.

Формат JSON: Язык данных

Когда API возвращает нам данные, они чаще всего приходят в формате JSON (JavaScript Object Notation). Несмотря на название, он прекрасно работает с Python.

JSON выглядит почти как словарь (dictionary) в Python. Это текстовый формат, который легко читают и люди, и машины.

Пример JSON:

{
  "name": "Иван",
  "age": 25,
  "is_student": true,
  "skills": ["Python", "SQL", "Git"],
  "address": {
    "city": "Москва",
    "zip": "101000"
  }
}

Обратите внимание на отличия от Python:

  • true пишется с маленькой буквы (в Python True).
  • null вместо None.
  • Кавычки всегда двойные ".

Работа с JSON в Python

Библиотека requests умеет автоматически превращать ответ сервера из формата JSON в словарь Python. Для этого используется метод .json().

Рассмотрим пример работы с реальным тестовым API — JSONPlaceholder. Это сервис, который имитирует базу данных постов блога.

import requests

# URL для получения списка постов
url = 'https://jsonplaceholder.typicode.com/posts/1'

response = requests.get(url)

if response.status_code == 200:
    # Преобразуем ответ в словарь Python
    data = response.json()

    print("Тип данных:", type(data))
    print("Заголовок поста:", data['title'])
    print("Текст поста:", data['body'])
else:
    print("Ошибка при получении данных")

Результат выполнения:

Тип данных: <class 'dict'>
Заголовок поста: sunt aut facere repellat provident occaecati excepturi optio reprehenderit
Текст поста: quia et suscipit...

Мы получили данные, и они уже готовы к анализу! Нам не пришлось искать текст внутри тегов <div> или <span>.

Заголовки и притворство браузером

Иногда серверы защищаются от скриптов. Если вы просто сделаете requests.get(), сервер увидит, что к нему стучится Python-скрипт (в заголовке User-Agent будет написано python-requests). Некоторые сайты блокируют такие запросы.

Чтобы обойти это, мы можем подменить заголовки, притворившись обычным браузером:

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}

response = requests.get('https://httpbin.org/user-agent', headers=headers)
print(response.text)

Теперь сервер думает, что мы — Google Chrome на Windows 10.

Заключение

Сегодня мы заложили фундамент для всего курса. Мы узнали:

  1. Как работает HTTP (запросы и ответы).
  2. Что такое REST API и почему это лучший друг аналитика данных.
  3. Как использовать библиотеку requests для отправки запросов.
  4. Как обрабатывать данные в формате JSON.

В следующих статьях мы углубимся в более сложные темы: научимся парсить обычные HTML-страницы, когда API недоступен, и работать с автоматизацией браузера через Selenium.

Парсинг HTML-страниц: извлечение структурированной информации с помощью BeautifulSoup и lxml

Парсинг HTML-страниц: извлечение структурированной информации с помощью BeautifulSoup и lxml

В предыдущей статье мы обсуждали идеальный мир сбора данных: мир, где серверы любезно предоставляют нам информацию в чистом формате JSON через API. Мы сравнили это с заказом еды в ресторане, где официант (API) приносит именно то, что вы просили.

Однако реальность Data Engineer часто напоминает не ресторан, а дикий лес. Данные есть, но они «растут» на деревьях, спрятаны в кустах или зарыты в землю. Они предназначены для глаз человека, а не для алгоритмов машины. Эти данные упакованы в HTML — язык разметки веб-страниц.

Сегодня мы научимся превращать хаос HTML-кода в стройные таблицы данных. Этот процесс называется парсингом (или скрапингом).

Понимание структуры веб-страницы: DOM

Прежде чем доставать инструменты, нужно понять материал, с которым мы работаем. Веб-страница — это не просто текст. Это иерархическая структура, дерево объектов.

Когда браузер получает HTML-код, он строит из него DOM (Document Object Model). Представьте это как генеалогическое древо:

  • Корневой элемент (<html>) — это прародитель.
  • У него есть дети: «голова» (<head>) и «тело» (<body>).
  • Внутри «тела» живут заголовки (<h1>), параграфы (<p>), списки (<ul>) и контейнеры (<div>).

Чтобы извлечь данные, нам нужно научиться навигации по этому дереву: «Найди мне все ссылки внутри третьего параграфа, который лежит в блоке с новостями».

Инструментарий: BeautifulSoup и lxml

Для работы с HTML в Python существуют два основных инструмента, которые часто используются в тандеме:

  1. BeautifulSoup (bs4) — библиотека, которая делает процесс навигации по дереву DOM простым и интуитивным. Она «всеядна» и может переварить даже «ломаный» HTML с ошибками верстки.
  2. lxml — это очень быстрый парсер, который умеет работать как с HTML, так и с XML. Часто его используют как «движок» внутри BeautifulSoup для ускорения работы.

Установим необходимые библиотеки:

pip install beautifulsoup4 lxml requests

Обратите внимание: нам всё еще нужен requests (из прошлой лекции), чтобы скачать страницу, прежде чем начать её разбирать.

Работа с BeautifulSoup

Давайте представим, что мы скачали HTML-код простой страницы книжного магазина. Начнем с создания объекта «супа».

from bs4 import BeautifulSoup

# Имитация HTML-кода, который мы могли бы получить через requests.get().text
html_doc = """
<html>
<head><title>Книжный клуб</title></head>
<body>
    <h1>Популярные книги</h1>
    <div class="book-list">
        <div class="book" id="b1">
            <p class="title">Мастер и Маргарита</p>
            <p class="author">Михаил Булгаков</p>
            <p class="price">500 руб.</p>
        </div>
        <div class="book" id="b2">
            <p class="title">1984</p>
            <p class="author">Джордж Оруэлл</p>
            <p class="price">450 руб.</p>
        </div>
    </div>
    <a href="https://example.com/more" class="link">Больше книг</a>
</body>
</html>
"""

# Создаем объект супа, указывая парсер lxml
soup = BeautifulSoup(html_doc, 'lxml')

Поиск элементов: методы .find() и .find_all()

Это два самых важных метода в библиотеке.

  1. .find(tag, attributes) — находит первый попавшийся элемент, удовлетворяющий условиям.
  2. .find_all(tag, attributes) — находит все элементы и возвращает их в виде списка.

Примеры:

# Найти заголовок h1
h1 = soup.find('h1')
print(h1.text)  # Вывод: Популярные книги

# Найти все цены
prices = soup.find_all('p', class_='price')
for price in prices:
    print(price.text)
# Вывод:
# 500 руб.
# 450 руб.

Обратите внимание на аргумент class_. В Python слово class зарезервировано, поэтому в BeautifulSoup используется нижнее подчеркивание.

Извлечение данных: Текст и Атрибуты

Найти элемент — это полдела. Нам нужно достать из него информацию.

  • .text или .get_text() — возвращает текст, который находится внутри тегов (без самих тегов).
  • ['attribute_name'] или .get('attribute_name') — позволяет достать значение атрибута (например, ссылку href или картинку src).
# Извлечение ссылки
link_tag = soup.find('a')

# Получаем текст ссылки
link_text = link_tag.text

# Получаем сам URL (значение атрибута href)
link_url = link_tag['href']

print(f"Текст: {link_text}, Ссылка: {link_url}")
# Вывод: Текст: Больше книг, Ссылка: https://example.com/more

Продвинутый поиск: CSS-селекторы

Если вы знакомы с веб-разработкой, вам будет удобнее использовать CSS-селекторы. Это мощный способ указывать путь к элементу.

В BeautifulSoup для этого используется метод .select() (возвращает список) и .select_one() (возвращает один элемент).

Основные правила CSS-селекторов:

  • tag — поиск по тегу (например, div).
  • .class — поиск по классу (например, .price).
  • #id — поиск по ID (например, #b1).
  • tag1 tag2 — поиск вложенных элементов (например, div.book p.title — найти параграф с классом title внутри div с классом book).
# Найти название книги, у которой id="b2"
book_orwell = soup.select_one('#b2 .title')
print(book_orwell.text)  # Вывод: 1984

Мощь XPath и библиотека lxml

Иногда структура страницы настолько сложная, что обычных методов поиска недостаточно. Здесь на сцену выходит XPath (XML Path Language). Это язык запросов к элементам XML/HTML документа.

BeautifulSoup не поддерживает XPath напрямую (хотя есть сторонние расширения), поэтому для сложных задач часто используют библиотеку lxml напрямую.

Синтаксис XPath напоминает путь к файлу на компьютере:

  • / — прямой потомок (как / в пути к файлу).
  • // — любой потомок (искать везде внутри).
  • @ — обращение к атрибуту.
  • text() — обращение к тексту.

Пример использования lxml с XPath:

from lxml import html

# Преобразуем строку HTML в дерево объектов lxml
tree = html.fromstring(html_doc)

# XPath запрос: найти текст внутри p с классом author
# //p - найти все параграфы где угодно
# [@class='author'] - у которых атрибут class равен 'author'
# /text() - взять их текст
authors = tree.xpath("//p[@class='author']/text()")

print(authors) # Вывод: ['Михаил Булгаков', 'Джордж Оруэлл']

XPath позволяет делать очень точные выборки. Например, «найти вторую ссылку в таблице» или «найти кнопку, которая содержит текст 'Купить'».

Полный цикл: от запроса до данных

Давайте объединим знания из прошлой статьи (requests) и текущей (BeautifulSoup) в единый скрипт. Напишем парсер, который собирает цитаты с известного тренировочного сайта quotes.toscrape.com.

import requests
from bs4 import BeautifulSoup

# 1. Делаем запрос
url = 'http://quotes.toscrape.com/'
response = requests.get(url)

# Проверяем, что сайт ответил успешно
if response.status_code == 200:
    # 2. Создаем суп
    soup = BeautifulSoup(response.text, 'lxml')

    # 3. Анализируем структуру сайта
    # Каждая цитата лежит в <div class="quote">
    quote_elements = soup.find_all('div', class_='quote')

    print(f"Найдено цитат: {len(quote_elements)}\n")

    for element in quote_elements:
        # Извлекаем текст цитаты (он в <span class="text">)
        text = element.find('span', class_='text').text

        # Извлекаем автора (он в <small class="author">)
        author = element.find('small', class_='author').text

        print(f"Цитата: {text}")
        print(f"Автор: {author}")
        print("-" * 20)
else:
    print("Не удалось получить страницу")

Этот скрипт выполняет полный цикл работы Data Engineer на этапе сбора данных (Ingestion).

Этика и ограничения

Парсинг — мощный инструмент, но с большой силой приходит большая ответственность.

  1. Не DDOS-те сайты. Если вы отправляете тысячи запросов в секунду, вы можете «уронить» чужой сервер. Используйте задержки (time.sleep()) между запросами.
  2. Читайте robots.txt. У каждого сайта есть файл site.com/robots.txt, где прописаны правила для роботов. Если там написано Disallow: /private/, туда ходить нельзя.
  3. User-Agent. Как мы обсуждали в прошлой статье, всегда представляйтесь браузером, но не используйте парсинг для вредоносных целей.

Заключение

Мы научились извлекать данные из HTML-страниц, используя BeautifulSoup для простых задач и познакомились с мощью XPath через lxml. Теперь вы можете собирать данные с новостных порталов, интернет-магазинов и блогов.

Однако, современный веб сложен. Многие сайты (например, YouTube или Aviasales) не отдают данные сразу в HTML. Они подгружают их динамически с помощью JavaScript уже после открытия страницы. Обычный requests + BeautifulSoup там увидит лишь пустой экран.

В следующей части курса мы разберем, как бороться с динамическим контентом, используя инструменты автоматизации браузера, такие как Selenium.

Продвинутый скрейпинг: автоматизация браузера и работа с динамическим контентом через Selenium

Продвинутый скрейпинг: автоматизация браузера и работа с динамическим контентом через Selenium

В предыдущих статьях мы освоили два важных инструмента: библиотеку requests для получения данных и связку BeautifulSoup + lxml для их извлечения. Мы научились отправлять запросы официанту (API) и разбирать готовые блюда (HTML).

Но что делать, если вы приходите в ресторан, а вместо еды вам приносят набор сырых ингредиентов и плитку, и говорят: «Готовьте сами»? Именно так работают современные динамические веб-сайты.

Если вы попробуете скачать страницу YouTube или бесконечную ленту социальной сети с помощью обычного requests.get(), вы, скорее всего, получите почти пустой HTML-файл. Данных там нет — они подгружаются позже с помощью JavaScript.

Сегодня мы научимся решать эту проблему с помощью Selenium — инструмента, который позволяет управлять браузером программно, притворяясь обычным пользователем.

Проблема «Пустой тарелки»: Статика vs Динамика

Чтобы понять, зачем нам нужен такой тяжелый инструмент, как Selenium, нужно разобраться в двух подходах к созданию сайтов.

Server-Side Rendering (SSR)

Это классический подход. Когда вы запрашиваете страницу, сервер собирает её целиком (вставляет текст в HTML-теги) и отправляет вам готовый документ. Именно с такими сайтами мы работали ранее. requests получает тот же HTML, что и вы видите в браузере.

Client-Side Rendering (CSR)

Это современный подход (React, Vue, Angular). Сервер отправляет вам пустой каркас страницы и скрипт JavaScript. Браузер запускает этот скрипт, который стучится за данными на сервер и «рисует» контент прямо у вас на глазах.

Для Python-скрипта на requests такой сайт выглядит пустым, потому что requests не умеет выполнять JavaScript. Он просто скачивает текст и останавливается.

Что такое Selenium?

Selenium — это инструмент для автоматизации действий веб-браузера. Изначально он был создан для тестировщиков, чтобы проверять, как работают кнопки и формы на сайте. Но Data Engineers быстро поняли: если инструмент может нажимать кнопки и видеть результат, значит, он может и собирать данные.

Selenium запускает настоящий браузер (Chrome, Firefox), загружает страницу, ждет выполнения всех скриптов, и только потом позволяет нам забрать данные.

Установка

Нам понадобится сама библиотека и драйвер для браузера. Чтобы не скачивать драйверы вручную, мы используем удобный менеджер.

pip install selenium webdriver-manager

Первый запуск: Hello, Robot!

Давайте напишем скрипт, который просто откроет браузер и перейдет на сайт. Мы будем использовать Google Chrome.

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
import time

# Устанавливаем и запускаем драйвер
service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service)

# Переходим на сайт
driver.get("https://www.python.org")

# Получаем заголовок страницы
print(f"Заголовок страницы: {driver.title}")

# Ждем 5 секунд, чтобы увидеть результат глазами
time.sleep(5)

# Закрываем браузер
driver.quit()

Когда вы запустите этот код, вы увидите магию: откроется отдельное окно Chrome, в котором будет написано «Браузером управляет автоматизированное ПО», сайт загрузится, а затем окно закроется.

Поиск элементов: Локаторы

В Selenium, как и в BeautifulSoup, нам нужно искать элементы на странице. Для этого используется класс By.

Основные стратегии поиска:

  • By.ID — поиск по атрибуту id (самый быстрый и надежный).
  • By.CSS_SELECTOR — поиск по CSS-селекторам (как в soup.select()).
  • By.XPATH — поиск по XPath (как в lxml).
  • By.CLASS_NAME — поиск по классу.

Синтаксис выглядит так:

from selenium.webdriver.common.by import By

# Найти один элемент (аналог find)
element = driver.find_element(By.CSS_SELECTOR, "div.content")

# Найти много элементов (аналог find_all)
elements = driver.find_elements(By.TAG_NAME, "a")

Взаимодействие с элементами

Самое интересное в Selenium — это возможность взаимодействовать со страницей. Мы можем кликать, писать текст и скроллить.

Представим, что нам нужно найти что-то на сайте Python.org:

from selenium.webdriver.common.keys import Keys

# Находим поле поиска по ID (предварительно посмотрев код страницы в браузере)
search_box = driver.find_element(By.ID, "id-search-field")

# Очищаем поле (на всякий случай)
search_box.clear()

# Вводим текст
search_box.send_keys("data science")

# Нажимаем Enter
search_box.send_keys(Keys.RETURN)

После выполнения этого кода браузер реально выполнит поиск и загрузит страницу с результатами.

Искусство ожидания: Waits

Это самая важная часть работы с Selenium.

Динамические сайты не загружаются мгновенно. Если вы скажете драйверу «Нажми кнопку» сразу после driver.get(), скрипт упадет с ошибкой NoSuchElementException, потому что кнопка еще не успела отрисоваться.

Новички часто используют time.sleep(5), просто останавливая скрипт. Это плохая практика:

  1. Если сайт загрузился за 1 секунду, вы теряете 4 секунды впустую.
  2. Если сайт загружается 6 секунд, скрипт упадет.

Правильный подход — Явные ожидания (Explicit Waits). Мы говорим драйверу: «Жди до 10 секунд, пока кнопка не станет кликабельной».

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# Создаем объект ожидания (максимум 10 секунд)
wait = WebDriverWait(driver, 10)

try:
    # Ждем, пока элемент с id="result-list" появится в DOM
    results = wait.until(
        EC.presence_of_element_located((By.ID, "content"))
    )
    print("Результаты загружены!")
except:
    print("Не дождались загрузки :(")

Основные условия ожидания (expected_conditions):

  • presence_of_element_located — элемент появился в коде.
  • visibility_of_element_located — элемент виден глазу (не скрыт).
  • element_to_be_clickable — элемент готов к клику.

Headless Mode: Работаем без интерфейса

Когда вы отлаживаете скрипт, удобно видеть открывающийся браузер. Но когда скрипт работает на сервере, графический интерфейс не нужен и потребляет лишние ресурсы. Для этого существует Headless режим (безголовый).

options = webdriver.ChromeOptions()
options.add_argument("--headless")  # Запуск без UI
options.add_argument("--disable-gpu")

driver = webdriver.Chrome(service=service, options=options)

Теперь браузер работает в фоне, невидимо для вас.

Пример: Скрейпинг динамического контента

Давайте соберем всё вместе. Представим сайт с цитатами, где новые цитаты появляются только после прокрутки или нажатия кнопки (JS-генерация).

Алгоритм действий:

  1. Запускаем драйвер.
  2. Открываем страницу.
  3. Ждем загрузки контента.
  4. Забираем HTML страницы.
  5. Передаем HTML в BeautifulSoup (да, их можно комбинировать!).

Почему мы передаем HTML в BeautifulSoup, а не парсим через Selenium? Потому что BeautifulSoup работает с текстом и делает это в сотни раз быстрее, чем Selenium, который обращается к объектам браузера.

# ... импорты ...

driver.get("http://quotes.toscrape.com/js/") # Версия сайта на JS

# Ждем загрузки первой цитаты
wait = WebDriverWait(driver, 10)
wait.until(EC.presence_of_element_located((By.CLASS_NAME, "quote")))

# Получаем полный HTML код страницы в текущем состоянии
page_source = driver.page_source

# Дальше работаем привычным BeautifulSoup
from bs4 import BeautifulSoup
soup = BeautifulSoup(page_source, "lxml")

quotes = soup.find_all("div", class_="quote")
for quote in quotes:
    print(quote.find("span", class_="text").text)

driver.quit()

Плюсы и минусы Selenium

Плюсы:

  • Универсальность: Может собрать данные с любого сайта, который открывается в браузере.
  • Имитация человека: Сложнее заблокировать, так как поведение похоже на реального пользователя.
  • Визуализация: Легко отлаживать, так как видно процесс.

Минусы:

  • Скорость: Это очень медленно. Запуск браузера и рендеринг страницы занимают секунды, тогда как requests справляется за миллисекунды.
  • Ресурсоемкость: Chrome потребляет много оперативной памяти.

Заключение

Selenium — это «тяжелая артиллерия» в мире сбора данных. Его стоит использовать только тогда, когда более легкие методы (requests, API) не работают из-за сложного JavaScript или защиты сайта.

Мы научились:

  1. Запускать автоматизированный браузер.
  2. Искать элементы через By.
  3. Взаимодействовать с формами и кнопками.
  4. Правильно ждать загрузки элементов через WebDriverWait.

В следующей статье мы поговорим о том, как хранить собранные данные: от простых CSV файлов до баз данных SQL и NoSQL.

Обработка и очистка данных: манипуляции с DataFrame и работа с пропусками в Pandas

Обработка и очистка данных: манипуляции с DataFrame и работа с пропусками в Pandas

Поздравляю! В предыдущих модулях мы научились добывать «цифровую руду»: скачивать HTML-страницы, обходить защиты сайтов и извлекать сырую информацию с помощью BeautifulSoup и Selenium. Но, как и настоящая руда, эти данные редко бывают чистыми. Они содержат «пустую породу» (пропуски), «примеси» (лишние символы валют, пробелы) и имеют неправильную форму (строки вместо чисел).

Сегодня мы переходим от Data Engineering к Data Analysis. Мы познакомимся с библиотекой Pandas — швейцарским ножом для работы с табличными данными в Python. Наша цель — превратить хаотичный список словарей, полученный после парсинга, в стройный и чистый аналитический отчет.

Знакомство с Pandas: Excel на стероидах

Представьте Excel, но без графического интерфейса, зато с возможностью обрабатывать миллионы строк за секунды и автоматизировать любые действия. Это и есть Pandas.

Основной строительный блок Pandas — это DataFrame (датафрейм). Это таблица, где:

  • Rows (Строки) — это ваши наблюдения (например, конкретные книги или товары).
  • Columns (Колонки) — это признаки (название, цена, рейтинг).
  • Index (Индекс) — это уникальный адрес каждой строки (по умолчанию просто порядковый номер).

Установка и импорт

Pandas не входит в стандартную библиотеку Python, поэтому его нужно установить:

pip install pandas

В коде его принято импортировать под псевдонимом pd:

import pandas as pd

Создание DataFrame из результатов парсинга

Вспомним, что после парсинга (например, книжного магазина) мы обычно получаем список словарей. Давайте превратим его в DataFrame.

import pandas as pd
import numpy as np

# Имитация данных, которые мы собрали с сайта
scraped_data = [
    {"title": "Мастер и Маргарита", "price": "500 руб.", "pages": 480},
    {"title": "1984", "price": "450 руб.", "pages": 320},
    {"title": "Зеленая миля", "price": None, "pages": 400},  # Пропущена цена
    {"title": "Гарри Поттер", "price": "600 руб.", "pages": None}, # Пропущены страницы
    {"title": "Толстой. Собрание", "price": "1500 руб.", "pages": 1200}
]

# Создаем DataFrame
df = pd.DataFrame(scraped_data)

# Выводим первые 5 строк (или меньше, если данных мало)
print(df.head())

Результат будет выглядеть как аккуратная таблица. Метод .head() — ваш лучший друг для быстрого осмотра данных.

Первичный осмотр и диагностика

Прежде чем что-то менять, нужно понять «диагноз» наших данных. Для этого есть метод .info().

print(df.info())

Вывод покажет:

  1. Количество строк (RangeIndex).
  2. Имена колонок.
  3. Количество непустых (non-null) значений.
  4. Тип данных в каждой колонке (int64 — целые числа, object — строки или смешанные типы, float64 — дробные числа).

Частая проблема парсинга: числа сохраняются как строки (тип object). Например, цена «500 руб.» для компьютера — это просто текст, с которым нельзя производить математические операции.

Манипуляции с данными: выбор и фильтрация

Чтобы работать с данными, нужно уметь доставать конкретные куски таблицы.

Выбор колонок

Если нам нужен только один столбец, мы обращаемся к нему как к ключу словаря. Это вернет объект Series (одна колонка).

prices = df['price']

Выбор строк и ячеек: loc и iloc

Это два основных инструмента навигации:

  1. .loc — работает по меткам (названиям). Если индекс — это числа, то по числам. Если индекс — даты, то по датам.
  2. .iloc — работает строго по позиции (порядковому номеру), как обычные списки Python (от 0 до N).
# Получить первую строку (индекс 0) через iloc
first_book = df.iloc[0]

# Получить значение ячейки: строка 1, колонка 'title'
book_title = df.loc[1, 'title']

Фильтрация (Boolean Indexing)

Допустим, мы хотим найти все книги, где больше 400 страниц. Но пока мы не можем этого сделать, так как в колонке pages есть пропуски, а price — вообще строки. Сначала нужно почистить данные.

Очистка данных: работа с типами

Посмотрите на колонку price. Там написано «500 руб.». Нам нужно убрать « руб.» и превратить текст в число.

В Pandas для работы со строками есть специальный аксессор .str.

# 1. Заменяем ' руб.' на пустоту
df['price'] = df['price'].str.replace(' руб.', '')

# 2. Преобразуем в числа (float, так как могут быть копейки)
# errors='coerce' превратит в NaN всё, что не сможет стать числом
df['price'] = pd.to_numeric(df['price'], errors='coerce')

print(df.dtypes)

Теперь price имеет тип float64. Мы можем считать среднюю цену, сумму и строить графики.

Работа с пропусками (Missing Values)

В реальном мире данные теряются. Сервер не отдал цену, верстка поехала, или товара нет в наличии. В Pandas отсутствие данных обозначается как NaN (Not a Number) для чисел или None для объектов.

Обнаружение пропусков

Метод .isna() (или .isnull()) возвращает таблицу из True и False, где True — это пропуск.

# Посчитаем количество пропусков в каждой колонке
print(df.isna().sum())

Стратегия 1: Удаление (dropna)

Самый простой способ — выкинуть испорченные данные. Если у книги нет цены, возможно, она нам не нужна для анализа выручки.

# Удалить строки, где есть ХОТЯ БЫ ОДИН пропуск
clean_df = df.dropna()

# Удалить строки, где пропущена именно цена
clean_df_price = df.dropna(subset=['price'])

Важно: Удаление данных — это потеря информации. Если у вас всего 100 строк и в 50 из них есть пропуски, dropna() уничтожит половину вашего датасета.

Стратегия 2: Заполнение (fillna)

Более мягкий подход — заполнить пустоты разумным значением. Это называется импутация.

Чем заполнять?

  • Константой: Например, 0 (если пропущено количество продаж).
  • Средним или медианой: Если пропущен возраст или цена.
  • Соседним значением: Актуально для временных рядов (курсы валют).

Рассмотрим математическую формулу для заполнения средним значением. Мы вычисляем среднее арифметическое μ\mu:

μ=1n∑i=1nxi\mu = \frac{1}{n} \sum_{i=1}^{n} x_i

Где μ\mu — среднее значение, nn — количество существующих (не пустых) значений, а xix_i — каждое конкретное значение в колонке.

Применим это на практике для колонки pages (количество страниц):

# Вычисляем среднее количество страниц (Pandas сам игнорирует NaN при расчете среднего)
mean_pages = df['pages'].mean()

# Заполняем пропуски
df['pages'] = df['pages'].fillna(mean_pages)

Теперь наш датасет полон, и мы можем применять фильтры.

# Найти книги толще 400 страниц
thick_books = df[df['pages'] > 400]
print(thick_books)

Добавление новых данных (Feature Engineering)

Часто нам нужно создать новые колонки на основе старых. Например, мы хотим узнать цену одной страницы книги.

# Операции векторизированы: они применяются сразу ко всему столбцу
df['price_per_page'] = df['price'] / df['pages']

# Округлим до 2 знаков
df['price_per_page'] = df['price_per_page'].round(2)

Это одна из главных фишек Pandas: вам не нужно писать циклы for для прохода по строкам. Вы пишете формулу столбец_А / столбец_Б, и Pandas делает это для тысяч строк мгновенно.

Сохранение результатов

После того как данные очищены, пропуски заполнены, а типы приведены в порядок, результат нужно сохранить. Pandas поддерживает десятки форматов.

# Сохранить в CSV (без индекса, чтобы не плодить лишнюю колонку с номерами)
df.to_csv('cleaned_books.csv', index=False)

# Сохранить в Excel (требует библиотеку openpyxl)
# df.to_excel('cleaned_books.xlsx', index=False)

Заключение

Сегодня мы прошли путь от «сырого» списка словарей до чистого, обогащенного датасета. Мы научились:

  1. Создавать DataFrame.
  2. Использовать loc и iloc для навигации.
  3. Очищать строки и менять типы данных.
  4. Находить и обрабатывать пропуски (NaN) методами dropna и fillna.

Теперь ваши данные готовы к самому интересному этапу — глубокому анализу и визуализации, о чем мы поговорим в следующих статьях курса.

Хранение и экспорт: взаимодействие с SQL-базами данных и сохранение результатов в файлы

Хранение и экспорт: взаимодействие с SQL-базами данных и сохранение результатов в файлы

Мы прошли долгий путь. Мы научились отправлять запросы к серверам, парсить HTML-код, автоматизировать браузеры и очищать данные с помощью Pandas. Теперь у нас есть идеальный, чистый DataFrame, полный ценной информации. Но есть проблема: этот DataFrame живет только в оперативной памяти вашего компьютера. Как только вы закроете скрипт или выключите ноутбук, все данные исчезнут.

Чтобы наш труд не пропал даром, данные нужно персистировать — то есть сохранить на жесткий диск или в удаленное хранилище. В этой статье мы разберем два основных пути сохранения данных: экспорт в файлы (CSV, Excel, JSON) и запись в базы данных SQL.

Экспорт в файлы: просто и доступно

Самый простой способ сохранить данные — записать их в файл. Pandas поддерживает множество форматов «из коробки», и методы для сохранения обычно начинаются с приставки .to_.

CSV (Comma-Separated Values)

Это «золотой стандарт» обмена табличными данными. CSV — это простой текстовый файл, где значения разделены запятыми (или точками с запятой). Его можно открыть в Блокноте, Excel, Google Sheets и любой другой программе для работы с данными.

import pandas as pd

# Представим наш очищенный датафрейм из прошлой лекции
data = {
    'title': ['Мастер и Маргарита', '1984', 'Гарри Поттер'],
    'price': [500.0, 450.0, 600.0],
    'pages': [480, 320, 400]
}
df = pd.DataFrame(data)

# Сохранение в CSV
# index=False — критически важный параметр!
df.to_csv('books.csv', index=False, encoding='utf-8')

Почему важен index=False? По умолчанию Pandas сохраняет индекс (номера строк 0, 1, 2...) как отдельную колонку. Если вы потом снова загрузите этот файл, у вас появится лишняя колонка Unnamed: 0. Параметр index=False предотвращает это, сохраняя только ваши данные.

Excel (XLSX)

Если конечный потребитель ваших данных — менеджер или бухгалтер, ему удобнее получить готовый Excel-файл, а не «сырой» CSV.

# Для работы с Excel может потребоваться библиотека openpyxl
# pip install openpyxl

df.to_excel('books_report.xlsx', sheet_name='Книги', index=False)

JSON

Если вы планируете передавать данные веб-разработчикам или использовать их в веб-приложениях, формат JSON подойдет лучше всего. Он сохраняет структуру данных и типы.

# orient='records' создает список словарей, что является стандартом для веба
df.to_json('books.json', orient='records', force_ascii=False)

Ограничения файлов

Файлы отлично подходят для небольших проектов и разовых выгрузок. Но у них есть недостатки:

  1. Сложность обновления: Чтобы добавить одну новую книгу, нужно перезаписывать весь файл или придумывать сложные механизмы дозаписи.
  2. Отсутствие связей: Файлы не знают друг о друге. Вы не можете легко соединить данные о книгах из одного файла с данными о продажах из другого.
  3. Производительность: Если у вас миллион строк, Excel может просто не открыться, а поиск по CSV будет очень медленным.

Здесь на сцену выходят Базы Данных (БД).

Введение в SQL и реляционные базы данных

SQL (Structured Query Language) — это язык для общения с базами данных. База данных похожа на умное хранилище, которое умеет не только хранить таблицы, но и быстро искать в них информацию, соединять их между собой и следить за целостностью данных.

Мы будем использовать SQLite. Это самая простая SQL-база данных в мире. Ей не нужен сервер, логин или пароль. Вся база данных хранится в одном обычном файле на вашем компьютере, но работать с ней можно как с настоящей взрослой системой.

SQLAlchemy: Мост между Python и SQL

Чтобы Pandas мог общаться с базой данных, нам нужен посредник. В мире Python стандартом является библиотека SQLAlchemy. Она работает как переводчик: берет объекты Python и превращает их в SQL-команды.

Установим её:

pip install sqlalchemy

Практика: Запись DataFrame в базу данных

Процесс работы с БД всегда начинается с создания «движка» (engine). Это объект, который знает, где лежит база и как к ней подключиться.

from sqlalchemy import create_engine

# Создаем подключение к базе данных SQLite
# 'sqlite:///my_library.db' — это строка подключения.
# Она говорит: используй драйвер sqlite и создай файл my_library.db в текущей папке.
engine = create_engine('sqlite:///my_library.db')

Теперь, когда у нас есть engine, мы можем использовать мощный метод Pandas — .to_sql().

# Записываем наш датафрейм в таблицу с именем 'books'
df.to_sql(
    name='books',       # Имя таблицы в базе данных
    con=engine,         # Объект подключения
    if_exists='replace', # Что делать, если таблица уже есть
    index=False         # Не сохранять индекс Pandas как колонку
)

Параметр if_exists

Это самый важный аргумент метода .to_sql(). У него есть три режима:

  1. 'fail' (по умолчанию): Если таблица уже существует, Python выдаст ошибку и ничего не сделает. Это защита от случайной потери данных.
  2. 'replace': Старая таблица будет удалена, а на её месте создана новая. Опасно, но удобно при полной перезагрузке данных.
  3. 'append': Новые данные будут добавлены в конец существующей таблицы. Идеально для сбора данных, который запускается каждый день (например, сбор курсов валют).

Чтение данных из SQL

Представьте, что прошло время, и нам нужно проанализировать накопленные данные. Нам не нужно открывать файл базы данных вручную. Pandas умеет читать прямо из SQL, используя метод .read_sql().

# Читаем всю таблицу целиком
df_from_db = pd.read_sql('books', con=engine)

print(df_from_db.head())

Но сила SQL в том, что мы можем загружать не всё, а только то, что нам нужно. Мы можем написать SQL-запрос прямо внутри Python-кода.

# Загрузим только дорогие книги (цена > 480)
sql_query = "SELECT title, price FROM books WHERE price > 480"

expensive_books = pd.read_sql(sql_query, con=engine)
print(expensive_books)

Это колоссально экономит память. Если в базе лежит 10 миллионов строк, а вам нужна только тысяча, вы загрузите в оперативную память только эту тысячу.

Масштабируемость: от SQLite к PostgreSQL

Самое прекрасное в использовании SQLAlchemy — это универсальность. Если завтра ваш проект вырастет и вам понадобится мощная серверная база данных вроде PostgreSQL или MySQL, вам почти не придется менять код.

Вам нужно будет только изменить строку подключения при создании engine:

Было (SQLite):

engine = create_engine('sqlite:///my_library.db')

Стало (PostgreSQL):

# Пример (требует установки драйвера psycopg2)
engine = create_engine('postgresql://user:password@localhost:5432/mydatabase')

Весь остальной код — to_sql, read_sql, работа с DataFrame — останется прежним. Это делает ваши навыки универсальными.

Резюме курса

В этой серии статей мы прошли полный цикл Data Engineering:

  1. Сбор (Ingestion): Мы научились делать HTTP-запросы и работать с API.
  2. Извлечение (Extraction): Мы освоили парсинг HTML с BeautifulSoup и автоматизацию с Selenium.
  3. Обработка (Transformation): Мы использовали Pandas для очистки данных, работы с пропусками и типами.
  4. Загрузка (Load): Сегодня мы научились сохранять результаты в файлы и базы данных.

Теперь у вас есть полный набор инструментов для создания собственных роботов-сборщиков данных и аналитических систем. Мир данных открыт для вас!