Python для сбора и обработки данных: Интенсив для разработчиков

Курс предназначен для программистов, владеющих C++ и SQL, желающих быстро освоить Python для задач Data Engineering. Программа фокусируется на синтаксических особенностях языка, библиотеках для парсинга веб-ресурсов и инструментах первичного анализа данных.

Быстрый старт: Синтаксис Python, структуры данных и отличия от C++ и PHP

Быстрый старт: Синтаксис Python, структуры данных и отличия от C++ и PHP

Добро пожаловать в курс «Python для сбора и обработки данных». Поскольку вы уже владеете C++, PHP и SQL, мы пропустим объяснения того, что такое переменная или цикл. Ваша задача — перенести существующие ментальные модели на рельсы Python и понять, почему этот язык стал стандартом де-факто в мире Data Science.

В этой статье мы разберем ключевые отличия синтаксиса, погрузимся в структуры данных, которые вы будете использовать 90% времени при обработке информации, и научимся писать «pythonic» код.

Философия и базовый синтаксис: Культурный шок

Первое, что бросается в глаза после C++ и PHP — это чистота кода. В Python нет фигурных скобок {} для обозначения блоков кода и точек с запятой ; для завершения инструкций (хотя технически ; допустима, ее использование считается дурным тоном).

Отступы как закон

В C++ и PHP отступы служат для читаемости. В Python отступы — это синтаксис. Блок кода определяется его смещением вправо (обычно 4 пробела).

Сравним условие:

PHP / C++:

if (x > 0) {
    y = x * 2;
    print(y);
}

Python:

if x > 0:
    y = x * 2
    print(y)

Динамическая типизация и ссылки

Python — язык с сильной динамической типизацией.

  • Динамическая: Тип переменной определяется в момент присваивания (как в PHP).
  • Сильная: Вы не можете просто сложить строку и число (в отличие от PHP, который попытается привести типы, или JS). Python выбросит ошибку TypeError.

Важное отличие от C++: переменные в Python — это не коробки с данными, а ссылки (ярлыки) на объекты в памяти.

Пример:

a = [1, 2, 3]
b = a  # b ссылается на ТОТ ЖЕ объект, что и a
b.append(4)

# В a тоже появится 4, так как это один объект
print(a) # Вывод: [1, 2, 3, 4]

В C++ аналогом было бы использование указателей или ссылок (std::vector<int>& b = a;). В Python это поведение по умолчанию для всех изменяемых типов.

Структуры данных для обработки информации

В Data Science правильный выбор структуры данных определяет эффективность алгоритма. Рассмотрим основные встроенные типы.

Списки (Lists)

Это аналог std::vector в C++ или индексированного массива в PHP. Списки изменяемы, упорядочены и могут хранить элементы разных типов (хотя на практике обычно хранят однотипные данные).

Киллер-фича Python при работе с данными — срезы (slicing).

Синтаксис среза: list[start:stop:step].

data = [10, 20, 30, 40, 50, 60]

print(data[0])    # 10 (первый элемент)
print(data[-1])   # 60 (последний элемент, очень удобно!)
print(data[1:4])  # [20, 30, 40] (с 1-го по 4-й не включительно)
print(data[::-1]) # [60, 50, ..., 10] (разворот списка)

Срезы создают копию данных, а не ссылку на оригинальный список (для простых списков).

Словари (Dictionaries)

Аналог ассоциативных массивов в PHP ($arr['key']) или std::unordered_map в C++. Это структура «ключ-значение». В задачах сбора данных (парсинг JSON, работа с API) это самая используемая структура.

user = {
    "id": 42,
    "name": "Alice",
    "roles": ["admin", "editor"]
}

print(user["name"])  # Alice
user["email"] = "alice@example.com" # Добавление новой пары

Поиск по ключу в словаре происходит очень быстро.

Tsearch=O(1)T_{search} = O(1)

Где TsearchT_{search} — время поиска элемента, а O(1)O(1) — константная временная сложность (в среднем случае), не зависящая от количества элементов в словаре.

Кортежи (Tuples)

Это неизменяемые списки. Записываются в круглых скобках (1, 2, 3).

Зачем они нужны, если есть списки?

  1. Защита данных: Вы гарантируете, что данные не будут изменены случайно.
  2. Производительность: Они занимают меньше памяти и работают быстрее списков.
  3. Ключи словаря: Кортеж может быть ключом словаря (так как он хешируем), а список — нет.

Множества (Sets)

Аналог std::unordered_set. Неупорядоченная коллекция уникальных элементов. Идеально подходит для удаления дубликатов из собранных данных.

raw_emails = ["a@test.com", "b@test.com", "a@test.com"]
unique_emails = set(raw_emails)
# Результат: {"a@test.com", "b@test.com"}

Управляющие конструкции и итераторы

Цикл for

Забудьте про for (int i = 0; i < n; i++). В Python цикл for — это всегда итерация по коллекции (foreach).

users = ["Alice", "Bob", "Charlie"]

for user in users:
    print(f"Processing {user}")

Если вам нужен индекс, используйте функцию enumerate():

for index, user in enumerate(users):
    print(f"User #{index}: {user}")

List Comprehensions (Списковые включения)

Это мощный инструмент для преобразования данных в одну строку. Это то, что отличает Python-разработчика от того, кто просто пишет на Python как на C++.

Задача: Получить список квадратов четных чисел.

Стиль C++:

numbers = [1, 2, 3, 4, 5]
squares = []
for x in numbers:
    if x % 2 == 0:
        squares.append(x * x)

Pythonic Style:

numbers = [1, 2, 3, 4, 5]
squares = [x * x for x in numbers if x % 2 == 0]

Это читается как естественный язык: «Список из x∗xx * x для каждого xx в numbers, если xx четный».

Функции

Функции объявляются ключевым словом def. Важное отличие от C++: функция может возвращать несколько значений (технически это возвращается кортеж).

def parse_url(url):
    # Логика парсинга...
    domain = "example.com"
    path = "/index"
    return domain, path

d, p = parse_url("http://example.com/index")

Аргументы

Python поддерживает именованные аргументы и значения по умолчанию, что делает вызовы функций очень читаемыми.

def connect_db(host, port=5432, timeout=10):
    pass

# Можно вызывать так, пропуская port:
connect_db("localhost", timeout=30)

Работа с файлами (Context Managers)

В C++ вы должны помнить о закрытии файла или использовать RAII. В Python для этого есть оператор with.

# Файл автоматически закроется после выхода из блока
with open("data.txt", "r", encoding="utf-8") as f:
    content = f.read()

Это гарантирует освобождение ресурсов даже в случае возникновения исключения внутри блока.

Заключение

Переход с C++/PHP на Python для работы с данными требует смены парадигмы:

  1. Меньше шаблонного кода (boilerplate).
  2. Больше работы со встроенными структурами данных (списки, словари).
  3. Использование срезов и списковых включений вместо явных циклов с индексами.

В следующей статье мы рассмотрим библиотеки NumPy и Pandas, которые превращают Python в мощнейший инструмент аналитики, сравнимый с MATLAB и R.

Документация Python по структурам данных

Продвинутый Python: ООП, модули, виртуальные окружения и работа с файловой системой

Продвинутый Python: ООП, модули, виртуальные окружения и работа с файловой системой

В предыдущей статье мы разобрали базовый синтаксис Python, списки и словари. Теперь пришло время углубиться в архитектурные особенности языка. Как разработчик с опытом в C++ и PHP, вы привыкли к определенным паттернам проектирования. В Python они тоже есть, но часто реализуются иначе.

В этой статье мы рассмотрим объектно-ориентированное программирование (ООП) в стиле Python, разберемся, как организовать код с помощью модулей, почему виртуальные окружения критически важны для Data Science, и научимся современно работать с файловой системой.

Объектно-ориентированное программирование: Pythonic Way

В Python всё является объектом: числа, функции, модули и даже сами классы. Это отличает его от C++, где есть примитивные типы, и от PHP, где функции могут существовать отдельно от объектов.

Классы и self

Объявление класса выглядит просто, но есть нюанс, который часто сбивает с толку новичков: явная передача ссылки на экземпляр (self).

В C++ вы используете this неявно (или явно this->). В Python первый аргумент любого метода экземпляра — это всегда ссылка на сам объект. По конвенции его называют self.

class DataProcessor:
    # Конструктор называется __init__, а не именем класса
    def __init__(self, source_name):
        self.source = source_name  # Создание атрибута экземпляра
        self._cache = []           # "Protected" атрибут (конвенция)

    def process(self):
        print(f"Processing data from {self.source}")

# Создание экземпляра (без new)
processor = DataProcessor("SQL Database")
processor.process()

В Python нет строгих модификаторов доступа private или protected на уровне языка, как в C++ (private:, public:). Всё основано на соглашениях: имя, начинающееся с _, считается внутренним.

Магические методы (Dunder methods)

Сила Python — в так называемых «магических» методах, которые начинаются и заканчиваются двойным подчеркиванием (double underscore). Они позволяют перегружать операторы и определять поведение объектов в стандартных ситуациях.

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

import math

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    # Перегрузка оператора сложения (+)
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    # Строковое представление для разработчика (аналог toString)
    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

    # Вычисление длины вектора
    def length(self):
        return math.sqrt(self.x**2 + self.y**2)

v1 = Vector(2, 4)
v2 = Vector(1, -1)

# Благодаря __add__ мы можем использовать +
v3 = v1 + v2
print(v3) # Вывод: Vector(3, 3)

В методе length мы используем формулу длины вектора (евклидова норма):

∣v⃗∣=x2+y2|\vec{v}| = \sqrt{x^2 + y^2}

Где ∣v⃗∣|\vec{v}| — длина (модуль) вектора, xx — координата по оси абсцисс, а yy — координата по оси ординат.

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

Модули и пакеты

Любой файл с расширением .py — это модуль. Папка с файлом __init__.py (даже пустым) — это пакет.

Импорт и name

В PHP вы используете include или require. В C++ — #include. В Python система импорта работает как выполнение кода модуля в отдельном пространстве имен.

# my_script.py

def main():
    print("Запуск основной логики")

if __name__ == "__main__":
    main()

Конструкция if __name__ == "__main__": — это стандартная идиома. Она означает: «Выполняй этот блок, только если файл запущен напрямую, а не импортирован как модуль». Это позволяет писать код, который может быть и библиотекой, и исполняемым скриптом одновременно.

Виртуальные окружения: Dependency Hell

В мире Data Science это тема №1. У вас будет проект А, требующий pandas версии 1.0, и проект Б, требующий pandas версии 2.0. Если ставить всё в глобальный Python, возникнет конфликт версий.

Виртуальное окружение (venv) — это изолированная директория, содержащая свою копию интерпретатора Python и библиотек.

Как это работает в консоли:

# Создание окружения в папке .venv
python -m venv .venv

# Активация (Windows)
.venv\Scripts\activate

# Активация (Linux/macOS)
source .venv/bin/activate

После активации команда pip install будет ставить пакеты только в эту папку. Список зависимостей принято фиксировать в файле requirements.txt.

Работа с файловой системой: Pathlib

Забудьте про конкатенацию строк для путей, как вы делали это в старом PHP или C (strcat). В Python долгое время использовали модуль os.path, но современный стандарт — это модуль pathlib.

pathlib реализует объектно-ориентированный подход к путям.

Почему Pathlib лучше?

Допустим, нам нужно сформировать путь к файлу данных data/raw/users.csv.

Старый стиль (os.path):

import os

base_dir = "data"
full_path = os.path.join(base_dir, "raw", "users.csv")

Современный стиль (pathlib):

from pathlib import Path

base_dir = Path("data")
# Оператор / перегружен для путей!
full_path = base_dir / "raw" / "users.csv"

print(full_path.exists()) # Проверка существования
print(full_path.suffix)   # Получение расширения (.csv)
print(full_path.parent)   # Родительская папка

Это делает код кроссплатформенным (Windows использует \, Linux /), читаемым и безопасным.

Чтение и запись файлов

Объекты Path имеют удобные методы для быстрого чтения и записи текста, что избавляет от необходимости писать конструкцию with open(...) для простых операций.

report_file = Path("report.txt")

# Запись текста
report_file.write_text("Analysis complete.", encoding="utf-8")

# Чтение текста
content = report_file.read_text(encoding="utf-8")

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

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

project/
│
├── .venv/              # Виртуальное окружение (не коммитим в git)
├── data/
│   └── raw/            # Сырые данные
├── src/
│   ├── __init__.py
│   ├── scraper.py      # Класс для сбора данных
│   └── parser.py       # Функции обработки
├── main.py             # Точка входа
└── requirements.txt    # Зависимости

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

Заключение

Мы рассмотрели инструменты, которые превращают скрипты в полноценные инженерные решения:

  1. ООП помогает структурировать логику парсеров и обработчиков.
  2. Магические методы делают объекты выразительными.
  3. Виртуальные окружения спасают от конфликтов библиотек.
  4. Pathlib упрощает навигацию по файловой системе.

В следующей части курса мы перейдем к «тяжелой артиллерии» Data Science: библиотеке NumPy, которая позволяет работать с многомерными массивами данных с производительностью C++.

Документация pathlib

Сбор данных: Работа с HTTP-запросами, REST API и парсинг HTML с BeautifulSoup

Сбор данных: Работа с HTTP-запросами, REST API и парсинг HTML с BeautifulSoup

В предыдущих модулях мы освоили синтаксис Python, объектно-ориентированное программирование и работу с файловой системой. Теперь мы переходим к одной из самых востребованных областей применения Python — сбору данных (Data Scraping/Mining).

Как разработчик с опытом в PHP и C++, вы наверняка сталкивались с библиотекой libcurl или функцией file_get_contents. В Python работа с сетью реализована на порядок элегантнее. Мы научимся взаимодействовать с веб-серверами, потреблять API и извлекать данные из HTML-страниц, используя инструменты, ставшие индустриальным стандартом.

Библиотека Requests: HTTP для людей

В стандартной библиотеке Python есть модуль urllib, но в реальной разработке он используется редко из-за громоздкого API. Стандартом де-факто является сторонняя библиотека Requests.

Установка

Поскольку мы уже настроили виртуальное окружение в прошлой статье, установим библиотеку туда:

pip install requests

Выполнение запроса

Сравним выполнение GET-запроса в PHP (cURL) и Python.

PHP (cURL):

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://api.github.com/events");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
curl_close($ch);

Python (Requests):

import requests

response = requests.get("https://api.github.com/events")
print(response.status_code)

Код на Python не только короче, но и возвращает объект Response, который содержит всё необходимое: код статуса, заголовки, кодировку и тело ответа.

Передача параметров

Вам больше не нужно вручную формировать строку запроса (?key=value&q=test). Библиотека сделает это за вас, корректно экранируя спецсимволы.

params = {
    "q": "python data science",
    "page": 1,
    "sort": "desc"
}

# Итоговый URL будет: https://httpbin.org/get?q=python+data+science&page=1&sort=desc
r = requests.get("https://httpbin.org/get", params=params)

Работа с REST API

Поскольку вы знаете SQL, вы привыкли работать со структурированными данными. В вебе роль универсального формата данных выполняет JSON. Python имеет встроенную поддержку JSON, а requests делает работу с ним тривиальной.

Получение данных (GET)

url = "https://jsonplaceholder.typicode.com/users/1"
response = requests.get(url)

if response.status_code == 200:
    # Метод .json() автоматически парсит ответ в словарь Python
    user = response.json()
    print(f"User: {user['name']}, Email: {user['email']}")
else:
    print("Error fetching data")

Отправка данных (POST)

При отправке данных на сервер (например, авторизация или загрузка формы) мы используем метод post. Аргумент json автоматически добавит заголовок Content-Type: application/json и сериализует словарь.

new_post = {
    "title": "Python for Data",
    "body": "Requests library is awesome",
    "userId": 1
}

r = requests.post("https://jsonplaceholder.typicode.com/posts", json=new_post)
print(r.status_code) # 201 Created

Парсинг HTML с BeautifulSoup

Не всегда данные доступны через удобный API. Часто приходится «парсить» (скрапить) обычные веб-страницы. Здесь на сцену выходит библиотека BeautifulSoup4 (bs4).

Она строит дерево объектов Python из HTML-кода, позволяя навигировать по нему так же легко, как по DOM в JavaScript.

Установка:

pip install beautifulsoup4

Структура страницы и поиск элементов

Допустим, мы получили HTML-код страницы новостей. Наша задача — собрать заголовки.

from bs4 import BeautifulSoup

html_doc = """
<html>
    <body>
        <div class="news-item">
            <h2 class="title">Python 3.12 Released</h2>
            <p class="date">2023-10-02</p>
        </div>
        <div class="news-item">
            <h2 class="title">AI Revolution</h2>
            <p class="date">2023-10-05</p>
        </div>
    </body>
</html>
"""

soup = BeautifulSoup(html_doc, "html.parser")

Методы поиска

В BeautifulSoup есть два основных метода, которые покрывают 90% задач:

  1. find(tag, attributes) — возвращает первый найденный элемент.
  2. find_all(tag, attributes) — возвращает список всех найденных элементов.
# Поиск по тегу и классу
titles = soup.find_all("h2", class_="title")

for title in titles:
    # .text извлекает содержимое тега без HTML-разметки
    print(title.text)

Обратите внимание: аргумент называется class_ (с подчеркиванием), так как class является зарезервированным словом в Python.

CSS-селекторы

Так как вы владеете CSS, вам будет удобнее использовать метод .select(). Он принимает стандартные CSS-селекторы, к которым вы привыкли в верстке или jQuery.

# То же самое, но через CSS-селектор
# Найти все теги h2, находящиеся внутри элементов с классом news-item
items = soup.select("div.news-item h2.title")

for item in items:
    print(item.get_text())

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

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

User-Agent

Многие серверы блокируют запросы, у которых нет заголовка User-Agent (по умолчанию 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(url, headers=headers)

Rate Limiting (Ограничение частоты запросов)

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

Допустим, сайт разрешает не более NN запросов в минуту. Тогда минимальная задержка DD (в секундах) между запросами вычисляется по формуле:

D=60ND = \frac{60}{N}

Где DD — время задержки в секундах, 6060 — количество секунд в минуте, а NN — допустимое количество запросов в минуту.

Пример реализации с модулем time:

import time

urls = ["http://site.com/page1", "http://site.com/page2", "http://site.com/page3"]

for url in urls:
    requests.get(url)
    time.sleep(1) # Пауза в 1 секунду

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

Объединим всё изученное. Напишем скрипт, который собирает названия и цены книг с вымышленного магазина (структура типична для реальных задач).

import requests
from bs4 import BeautifulSoup
import time

def scrape_books():
    url = "http://books.toscrape.com/"
    headers = {"User-Agent": "MyScraper/1.0"}

    try:
        response = requests.get(url, headers=headers, timeout=10)
        response.raise_for_status() # Выбросит исключение, если статус не 200

        soup = BeautifulSoup(response.text, "html.parser")

        # Ищем все статьи с товарами
        books = soup.select("article.product_pod")

        data = []
        for book in books:
            title = book.h3.a["title"] # Доступ к атрибутам как к словарю
            price = book.select_one("p.price_color").text

            data.append({
                "title": title,
                "price": price
            })

        return data

    except requests.exceptions.RequestException as e:
        print(f"Ошибка сети: {e}")
        return []

books_data = scrape_books()
print(f"Найдено книг: {len(books_data)}")

Заключение

Мы разобрали фундамент сбора данных в Python:

  1. Requests позволяет легко выполнять HTTP-запросы, заменяя сложные конструкции cURL.
  2. REST API — самый надежный способ получения данных, где Python автоматически конвертирует JSON в словари.
  3. BeautifulSoup дает мощные инструменты для навигации по HTML-дереву, используя знакомые вам CSS-селекторы.

В следующей статье мы научимся сохранять собранные данные не просто в переменные, а в профессиональные форматы (CSV, Excel) и базы данных, используя библиотеку Pandas.

Документация Requests Документация BeautifulSoup

Обработка данных: Введение в библиотеку Pandas и интеграция с SQL базами данных

Обработка данных: Введение в библиотеку Pandas и интеграция с SQL базами данных

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

Как разработчик, знакомый с SQL, вы привыкли мыслить таблицами, выборками и агрегациями. В экосистеме Python аналогом таблиц базы данных, но работающим в оперативной памяти (in-memory), является библиотека Pandas.

В этой статье мы разберем архитектуру Pandas, научимся манипулировать данными быстрее, чем на чистом C++, и интегрируем наши скрипты с SQL-базами данных.

Что такое Pandas и зачем он нужен?

Pandas — это высокоуровневая библиотека, построенная поверх NumPy (библиотеки для матричных вычислений на C). Это означает, что операции в Pandas векторизованы и выполняются на низком уровне, минуя медленный интерпретатор Python в циклах.

Если в PHP вы работаете с массивами массивов ($data[][]), а в C++ с векторами структур (std::vector<Struct>), то в Pandas вы работаете с DataFrame.

Основные структуры данных

  1. Series: Одномерный массив с метками (индексом). Аналог одного столбца в таблице или std::map в C++.
  2. DataFrame: Двумерная табличная структура данных с изменяемым размером и разнородными типами столбцов. Аналог таблицы SQL.

Создание и осмотр данных

Установим библиотеку (обычно она ставится вместе с numpy):

pip install pandas sqlalchemy

Рассмотрим пример создания DataFrame из данных, которые мы могли бы собрать парсером из прошлой статьи:

import pandas as pd

# Данные, полученные, например, после парсинга
data = [
    {"id": 1, "title": "Python Crash Course", "price": 25.50, "views": 1000},
    {"id": 2, "title": "Fluent Python", "price": 45.00, "views": 1500},
    {"id": 3, "title": "Clean Code", "price": 30.00, "views": 800},
    {"id": 4, "title": "The Pragmatic Programmer", "price": 35.00, "views": 1200}
]

# Создание DataFrame
df = pd.DataFrame(data)

# Вывод первых строк (аналог LIMIT 5 в SQL)
print(df.head())

Анализ структуры

Вместо того чтобы писать циклы для проверки типов данных, используйте метод info():

print(df.info())

Он покажет типы данных (int64, float64, object — аналог varchar), использование памяти и наличие пропусков (null).

Для быстрой статистики (среднее, мин/макс, квартили) используйте describe():

print(df.describe())

Индексация и выборка данных

Здесь часто возникает путаница у разработчиков, переходящих с PHP. В PHP ключи массива могут быть и индексами, и строками одновременно. В Pandas есть четкое разделение.

Выбор столбцов (Projection)

Аналог SELECT title, price FROM table:

subset = df[["title", "price"]]

Выбор строк (Selection)

Существует два основных способа доступа к данным:

  1. .iloc (Integer Location) — доступ по числовой позиции (как в массивах C++).
  2. .loc (Label Location) — доступ по метке индекса или булевой маске.
# Первая строка (индекс 0)
row_0 = df.iloc[0]

# Строки, где цена больше 30 (Аналог WHERE price > 30)
expensive_books = df.loc[df["price"] > 30]

Векторизация против Циклов

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

Допустим, мы хотим применить скидку 10% ко всем книгам.

Плохо (стиль C++/PHP):

for index, row in df.iterrows():
    df.at[index, "price"] = row["price"] * 0.9

Хорошо (Pandas Style):

df["price"] = df["price"] * 0.9

Вторая операция выполняется на уровне C и использует инструкции процессора SIMD (Single Instruction, Multiple Data). Разница в скорости может достигать 1000 раз.

Математическое обоснование агрегации

При анализе данных часто требуется вычислить среднее значение. Формально среднее арифметическое xˉ\bar{x} вычисляется так:

xˉ=1n∑i=1nxi\bar{x} = \frac{1}{n} \sum_{i=1}^{n} x_i

Где xˉ\bar{x} — среднее значение выборки, nn — количество элементов в выборке, а xix_i — значение ii-го элемента.

В Pandas это делается одной командой:

average_price = df["price"].mean()

Группировка (Group By)

Pandas реализует парадигму «Split-Apply-Combine», аналогичную SQL GROUP BY.

Добавим категорию книгам:

df["category"] = ["Programming", "Programming", "Engineering", "Engineering"]

# SQL: SELECT category, AVG(price) FROM table GROUP BY category
grouped = df.groupby("category")["price"].mean()
print(grouped)

Интеграция с SQL базами данных

Поскольку вы уже знаете SQL, Pandas станет идеальным компаньоном. Вы можете выгружать результаты сложных запросов в DataFrame, обрабатывать их средствами Python (например, использовать библиотеки машинного обучения) и загружать обратно.

Для соединения используется библиотека SQLAlchemy.

Чтение из SQL

from sqlalchemy import create_engine

# Создание подключения (SQLite для примера)
engine = create_engine("sqlite:///library.db")

# Чтение результата SQL-запроса прямо в DataFrame
query = "SELECT * FROM books WHERE views > 1000"
df_sql = pd.read_sql(query, engine)

Запись в SQL

После обработки данных (очистка, добавление вычисляемых столбцов) мы можем сохранить их в новую таблицу.

# if_exists='replace' удалит таблицу, если она есть, и создаст новую
# index=False означает, что мы не хотим сохранять технический индекс Pandas как колонку
df.to_sql("processed_books", engine, if_exists="replace", index=False)

Это избавляет от необходимости писать сотни строк кода с INSERT INTO ... VALUES (...).

Обработка отсутствующих данных

В реальном мире данные «грязные». В SQL это NULL, в Python — None, а в Pandas/NumPy — NaN (Not a Number).

# Проверка на наличие пропусков
print(df.isnull().sum())

# Удаление строк с пропусками
clean_df = df.dropna()

# Заполнение пропусков значением (например, нулем)
filled_df = df.fillna(0)

Заключение

Pandas — это «швейцарский нож» для данных в Python. Он заменяет собой Excel и значительную часть SQL-запросов, позволяя проводить глубокий анализ и трансформацию данных в несколько строк кода.

Мы рассмотрели:

  1. Структуры Series и DataFrame.
  2. Фильтрацию и векторизованные операции.
  3. Бесшовную интеграцию с SQL через read_sql и to_sql.

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

Документация Pandas Документация SQLAlchemy

Автоматизация и ETL: Обработка ошибок, логирование и построение конвейеров данных

Автоматизация и ETL: Обработка ошибок, логирование и построение конвейеров данных

В предыдущих модулях мы прошли путь от базового синтаксиса Python до сбора данных с помощью requests и их анализа в pandas. Вы уже умеете писать скрипты, которые делают полезную работу. Но есть проблема: скрипт, запущенный на локальной машине один раз — это еще не инженерное решение.

В реальном мире сети падают, API меняют форматы ответов, диски переполняются, а базы данных уходят на обслуживание. Если ваш скрипт просто падает с ошибкой в консоль в 3 часа ночи, это плохой скрипт.

В этой статье мы превратим разрозненный код в надежный ETL-конвейер (Pipeline). Мы разберем профессиональную обработку ошибок, настроим логирование вместо print() и научимся автоматизировать запуск задач.

Что такое ETL?

ETL — это аббревиатура, описывающая классический процесс интеграции данных:

  1. Extract (Извлечение): Получение данных из источника (API, парсинг сайта, чтение логов, SQL-запрос).
  2. Transform (Преобразование): Очистка, фильтрация, агрегация, приведение типов (то, что мы делали в Pandas).
  3. Load (Загрузка): Сохранение результата в целевую систему (CSV, Data Warehouse, новая таблица SQL).

Ваша задача как разработчика — обеспечить, чтобы этот поток не прерывался, а если прерывался — вы знали почему.

Обработка исключений: Pythonic Way

Вы знакомы с try-catch в PHP и C++. В Python используется конструкция try-except. Однако в Python принято следовать принципу EAFP: It's Easier to Ask for Forgiveness than Permission (Проще попросить прощения, чем разрешения).

Вместо того чтобы проверять, существует ли файл или доступен ли ключ в словаре (как часто делают в C++), в Python мы просто пытаемся выполнить операцию и ловим ошибку.

Полный синтаксис

В Python блок обработки ошибок богаче, чем во многих других языках. У него есть ветки else и finally.

import requests
import time

def fetch_data(url):
    try:
        # Блок, где может возникнуть ошибка
        response = requests.get(url, timeout=5)
        response.raise_for_status() # Вызывает исключение для кодов 4xx/5xx

    except requests.exceptions.Timeout:
        # Обработка конкретной ошибки
        print("Превышено время ожидания")
        return None

    except requests.exceptions.RequestException as e:
        # Обработка всех остальных сетевых ошибок
        print(f"Ошибка сети: {e}")
        return None

    else:
        # Выполняется ТОЛЬКО если в try НЕ было ошибок
        # Идеальное место для обработки данных, чтобы не раздувать try
        print("Данные успешно получены")
        return response.json()

    finally:
        # Выполняется ВСЕГДА (даже если был return или ошибка)
        # Аналог деструкторов или очистки ресурсов
        print("Завершение операции запроса")

Важное правило: Никогда не пишите просто except:. Это перехватит абсолютно всё, включая KeyboardInterrupt (Ctrl+C), и вы не сможете остановить свой скрипт. Всегда указывайте тип исключения или хотя бы except Exception:.

Логирование: Забудьте про print()

Использование print() для отладки продакшн-кода — это дурной тон.

  1. Вы не можете легко отключить print.
  2. Вы не знаете, когда и где произошло событие.
  3. Вы не можете разделить сообщения по важности.

В Python есть мощный встроенный модуль logging.

Уровни логирования

  1. DEBUG: Детальная информация для отладки (значения переменных, шаги цикла).
  2. INFO: Подтверждение того, что всё идет по плану («Скрипт запущен», «Подключились к БД»).
  3. WARNING: Неожиданное событие, но скрипт работает (например, «Медленный ответ API»).
  4. ERROR: Серьезная проблема, часть функции не выполнилась («Не удалось сохранить файл»).
  5. CRITICAL: Фатальная ошибка, программа не может продолжать работу.

Настройка логгера

import logging

# Базовая настройка
logging.basicConfig(
    level=logging.INFO, # Минимальный уровень для записи
    filename="etl_process.log", # Запись в файл
    filemode="a", # a - append (дописывать), w - write (перезаписывать)
    format="%(asctime)s - %(levelname)s - %(message)s" # Формат сообщения
)

logger = logging.getLogger(__name__)

logger.info("Запуск ETL процесса")

try:
    x = 1 / 0
except ZeroDivisionError:
    # exc_info=True добавит полный Traceback в лог
    logger.error("Произошла ошибка вычисления", exc_info=True)

Теперь в файле etl_process.log будет: 2023-10-25 14:30:01,123 - INFO - Запуск ETL процесса 2023-10-25 14:30:01,125 - ERROR - Произошла ошибка вычисления...

Паттерн Retry (Повторные попытки)

Сеть ненадежна. Если API вернул 503 (Service Unavailable), это не значит, что скрипт сломан. Это значит, что нужно подождать и попробовать снова.

Для этого используется алгоритм Exponential Backoff (Экспоненциальная задержка). Мы увеличиваем время ожидания после каждой неудачи, чтобы не "задудосить" сервер.

Формула расчета времени ожидания:

Tw=B⋅2nT_w = B \cdot 2^n

Где TwT_w — время ожидания в секундах, BB — базовое время задержки (например, 1 секунда), а nn — номер текущей попытки (начиная с 0 или 1).

Реализуем это с помощью декоратора (еще одна мощная фича Python):

import time
import functools

def retry(max_retries=3, base_delay=1):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            retries = 0
            while retries < max_retries:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    wait_time = base_delay * (2 ** retries)
                    logging.warning(f"Ошибка: {e}. Повтор через {wait_time} сек.")
                    time.sleep(wait_time)
                    retries += 1
            raise Exception(f"Не удалось выполнить {func.__name__} после {max_retries} попыток")
        return wrapper
    return decorator

@retry(max_retries=4)
def unstable_network_call():
    # Имитация сбоя
    if time.time() % 2 > 0:
        raise ConnectionError("Сбой сети!")
    return "Успех!"

Конфигурация и безопасность

Никогда не храните пароли от базы данных или API-ключи прямо в коде (.py файлах). Если вы случайно закоммитите их в Git, они станут достоянием общественности.

Стандарт индустрии — использование переменных окружения и .env файлов.

  1. Создайте файл .env в корне проекта:
    DB_HOST=localhost
    DB_USER=admin
    DB_PASS=super_secret_password
    API_KEY=xyz123
    
  2. Добавьте .env в .gitignore.
  3. Используйте библиотеку python-dotenv для чтения:
import os
from dotenv import load_dotenv

load_dotenv() # Загружает переменные из .env в окружение

db_password = os.getenv("DB_PASS")
if not db_password:
    raise ValueError("DB_PASS не найден в переменных окружения!")

Автоматизация запуска

Написанный ETL-скрипт нужно запускать регулярно. Есть два основных пути.

1. Системные планировщики (Cron / Task Scheduler)

Это классический путь администрирования.

Linux (Cron): 0 9 * * * /path/to/venv/bin/python /path/to/project/main.py (Запуск каждый день в 9:00 утра).

2. Python-планировщики (Библиотека schedule)

Если вы хотите, чтобы расписание было частью кода (Infrastructure as Code) и скрипт работал как сервис (демон), используйте библиотеку schedule.

import schedule
import time

def job():
    logger.info("Запуск ежедневного сбора данных...")
    # Вызов вашей ETL логики

# Человекочитаемый синтаксис
schedule.every().day.at("10:30").do(job)
schedule.every(10).minutes.do(job)

while True:
    schedule.run_pending()
    time.sleep(1)

Этот подход удобен тем, что он кроссплатформенный и не требует настройки ОС.

Практический пример: Класс ETL-конвейера

Соберем всё вместе в структуру, которую не стыдно показать на код-ревью.

import logging
import pandas as pd
from sqlalchemy import create_engine
import os

# Настройка логгера
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("ETL_Worker")

class DataPipeline:
    def __init__(self, db_connection_str):
        self.engine = create_engine(db_connection_str)

    def extract(self):
        logger.info("Начинаем этап Extract")
        # Имитация загрузки данных
        data = [
            {"id": 1, "val": 100, "status": "ok"},
            {"id": 2, "val": -50, "status": "error"}
        ]
        return pd.DataFrame(data)

    def transform(self, df):
        logger.info("Начинаем этап Transform")
        if df.empty:
            logger.warning("Получен пустой DataFrame")
            return df

        # Очистка данных
        df_clean = df[df["val"] >= 0].copy()
        logger.info(f"Отфильтровано строк: {len(df) - len(df_clean)}")
        return df_clean

    def load(self, df):
        logger.info("Начинаем этап Load")
        try:
            df.to_sql("metrics", self.engine, if_exists="append", index=False)
            logger.info("Данные успешно сохранены в БД")
        except Exception as e:
            logger.error(f"Ошибка записи в БД: {e}")
            raise # Пробрасываем ошибку выше

    def run(self):
        try:
            raw_data = self.extract()
            clean_data = self.transform(raw_data)
            self.load(clean_data)
            logger.info("Пайплайн успешно завершен")
        except Exception as e:
            logger.critical(f"Критический сбой пайплайна: {e}")

if __name__ == "__main__":
    # В реальности строку подключения берем из os.getenv
    db_url = "sqlite:///production.db"
    pipeline = DataPipeline(db_url)
    pipeline.run()

Заключение

Мы перешли от написания скриптов «на коленке» к созданию надежных приложений.

  1. Логирование позволяет вам видеть, что происходит внутри "черного ящика".
  2. Обработка исключений делает код устойчивым к внешним факторам.
  3. Переменные окружения защищают ваши секреты.
  4. Структура классов позволяет легко расширять логику (например, добавить новый источник данных).

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

Документация модуля logging Документация библиотеки schedule