Перейти к содержимому

Практическое занятие 8: Модули и наследование

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

Объектно-ориентированное программирование (ООП)

Заголовок раздела «Объектно-ориентированное программирование (ООП)»
class Person:
def __init__(self, name, age):
"""Конструктор класса"""
self.name = name
self.age = age
def introduce(self):
"""Метод класса"""
return f"Меня зовут {self.name}, мне {self.age} лет"
def birthday(self):
"""Метод изменения состояния"""
self.age += 1
return f"Теперь мне {self.age} лет"
# Создание объекта
person = Person("Анна", 25)
# Использование методов
print(person.introduce())
print(person.birthday())
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
pass
class Dog(Animal):
def speak(self):
return f"{self.name} говорит: Гав!"
class Cat(Animal):
def speak(self):
return f"{self.name} говорит: Мяу!"
# Использование
dog = Dog("Шарик")
cat = Cat("Мурка")
print(dog.speak())
print(cat.speak())
def make_animal_speak(animal):
print(animal.speak())
animals = [Dog("Бобик"), Cat("Вася")]
for animal in animals:
make_animal_speak(animal)

Создайте следующие файлы:

scrapers/
├── __init__.py
├── base_scraper.py
├── habr_scraper.py
└── dzen_scraper.py
from .base_scraper import BaseScraper
from .habr_scraper import HabrScraper
from .dzen_scraper import DzenScraper
__all__ = ["BaseScraper", "HabrScraper", "DzenScraper"]
import requests
from bs4 import BeautifulSoup
import json
from abc import ABC, abstractmethod
from typing import List, Dict, Optional
import time
class BaseScraper(ABC):
"""
Базовый класс для веб-скраперов.
Содержит общую логику для всех скраперов.
"""
def __init__(self, base_url: str):
"""
Инициализация скрапера
Args:
base_url: Базовый URL сайта
"""
self.base_url = base_url
self.session = requests.Session()
self.headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
}
def get_page(self, url: str, params: Optional[Dict] = None) -> Optional[requests.Response]:
"""
Получение страницы
Args:
url: URL страницы
params: Параметры запроса
Returns:
Response объект или None при ошибке
"""
try:
response = self.session.get(url, params=params, headers=self.headers, timeout=10)
response.raise_for_status()
return response
except requests.RequestException as e:
print(f"Ошибка при запросе {url}: {e}")
return None
def parse_html(self, html: str) -> BeautifulSoup:
"""
Парсинг HTML
Args:
html: HTML строка
Returns:
BeautifulSoup объект
"""
return BeautifulSoup(html, "html.parser")
@abstractmethod
def extract_articles(self, soup: BeautifulSoup) -> List[Dict]:
"""
Извлечение статей из HTML
(абстрактный метод, должен быть реализован в наследниках)
Args:
soup: BeautifulSoup объект
Returns:
Список словарей со статьями
"""
pass
def scrape_page(self, url: str) -> Optional[List[Dict]]:
"""
Скрапинг одной страницы
Args:
url: URL страницы
Returns:
Список словарей со статьями или None
"""
response = self.get_page(url)
if not response:
return None
soup = self.parse_html(response.text)
articles = self.extract_articles(soup)
return articles
def scrape_multiple_pages(self, urls: List[str]) -> List[Dict]:
"""
Скрапинг нескольких страниц
Args:
urls: Список URL
Returns:
Список всех статей
"""
all_articles = []
for i, url in enumerate(urls, 1):
print(f"Обработка страницы {i}/{len(urls)}: {url}")
articles = self.scrape_page(url)
if articles:
all_articles.extend(articles)
# Задержка между запросами
if i < len(urls):
time.sleep(1)
return all_articles
def save_to_json(self, data: List[Dict], filename: str) -> None:
"""
Сохранение данных в JSON
Args:
data: Список данных
filename: Имя файла
"""
with open(filename, "w", encoding="utf-8") as file:
json.dump(data, file, ensure_ascii=False, indent=4)
print(f"Данные сохранены в {filename}")
def save_to_database(self, data: List[Dict], db_name: str, table_name: str) -> None:
"""
Сохранение данных в базу данных SQLite
Args:
data: Список данных
db_name: Имя базы данных
table_name: Имя таблицы
"""
import sqlite3
if not data:
print("Нет данных для сохранения")
return
# Создание таблицы
columns = list(data[0].keys())
column_defs = ", ".join([f"{col} TEXT" for col in columns])
connection = sqlite3.connect(db_name)
cursor = connection.cursor()
cursor.execute(f"""
CREATE TABLE IF NOT EXISTS {table_name} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
{column_defs}
)
""")
# Вставка данных
placeholders = ", ".join(["?"] * len(columns))
insert_query = f"INSERT INTO {table_name} ({', '.join(columns)}) VALUES ({placeholders})"
for article in data:
values = [str(article.get(col, "")) for col in columns]
cursor.execute(insert_query, values)
connection.commit()
connection.close()
print(f"Данные сохранены в {db_name} (таблица: {table_name})")
def run(self, urls: List[str], output_file: str = None) -> List[Dict]:
"""
Основной метод запуска скрапера
Args:
urls: Список URL для скрапинга
output_file: Имя файла для сохранения (опционально)
Returns:
Список собранных данных
"""
print(f"Запуск скрапера для {len(urls)} страниц")
data = self.scrape_multiple_pages(urls)
print(f"Собрано {len(data)} статей")
if output_file:
self.save_to_json(data, output_file)
return data
from typing import List, Dict
from .base_scraper import BaseScraper
class HabrScraper(BaseScraper):
"""
Скрапер для сайта Habr
"""
def __init__(self):
super().__init__("https://habr.com")
def extract_articles(self, soup) -> List[Dict]:
"""
Извлечение статей с Habr
Args:
soup: BeautifulSoup объект
Returns:
Список словарей со статьями
"""
articles = []
# Поиск статей по CSS-селектору
article_elements = soup.find_all("article", class_="tm-articles-list__item")
for element in article_elements:
try:
# Заголовок
title_element = element.find("a", class_="tm-title__link")
title = title_element.text.strip() if title_element else "Без заголовка"
# Ссылка
url = title_element["href"] if title_element else ""
full_url = f"{self.base_url}{url}" if url else ""
# Автор
author_element = element.find("a", class_="tm-user-info__username")
author = author_element.text.strip() if author_element else "Неизвестный автор"
# Рейтинг
rating_element = element.find("span", class_="tm-votes-meter__value")
rating = rating_element.text.strip() if rating_element else "0"
# Дата
date_element = element.find("time")
date = date_element["datetime"] if date_element else ""
articles.append({
"источник": "Habr",
"заголовок": title,
"ссылка": full_url,
"автор": author,
"рейтинг": rating,
"дата": date
})
except Exception as e:
print(f"Ошибка при извлечении статьи: {e}")
continue
return articles
def get_hub_articles(self, hub_name: str, pages: int = 1) -> List[Dict]:
"""
Получение статей из хаба
Args:
hub_name: Название хаба
pages: Количество страниц
Returns:
Список статей
"""
urls = [f"{self.base_url}/ru/hub/{hub_name}/page{i}/" for i in range(1, pages + 1)]
return self.run(urls)
from typing import List, Dict
from .base_scraper import BaseScraper
class DzenScraper(BaseScraper):
"""
Скрапер для сайта Dzen
"""
def __init__(self):
super().__init__("https://dzen.ru")
def extract_articles(self, soup) -> List[Dict]:
"""
Извлечение статей с Dzen
Args:
soup: BeautifulSoup объект
Returns:
Список словарей со статьями
"""
articles = []
# Поиск статей по CSS-селектору
article_elements = soup.find_all("div", class_="card-compact-view")
for element in article_elements:
try:
# Заголовок
title_element = element.find("a", class_="card-compact-view__title-link")
title = title_element.text.strip() if title_element else "Без заголовка"
# Ссылка
url = title_element["href"] if title_element else ""
# Автор
author_element = element.find("span", class_="card-compact-view__author")
author = author_element.text.strip() if author_element else "Неизвестный автор"
# Время
time_element = element.find("span", class_="card-compact-view__time")
time = time_element.text.strip() if time_element else ""
articles.append({
"источник": "Dzen",
"заголовок": title,
"ссылка": url,
"автор": author,
"время": time
})
except Exception as e:
print(f"Ошибка при извлечении статьи: {e}")
continue
return articles
def get_tag_articles(self, tag: str, pages: int = 1) -> List[Dict]:
"""
Получение статей по тегу
Args:
tag: Тег для поиска
pages: Количество страниц
Returns:
Список статей
"""
urls = [f"{self.base_url}/news/{tag}/page{i}/" for i in range(1, pages + 1)]
return self.run(urls)

Создайте файл main.py:

from scrapers import HabrScraper, DzenScraper
# Скрапинг Habr
print("=== Скрапинг Habr ===")
habr_scraper = HabrScraper()
habr_data = habr_scraper.get_hub_articles("python", pages=1)
if habr_data:
habr_scraper.save_to_json(habr_data, "habr_articles.json")
habr_scraper.save_to_database(habr_data, "articles.db", "habr")
# Скрапинг Dzen
print("\n=== Скрапинг Dzen ===")
dzen_scraper = DzenScraper()
dzen_data = dzen_scraper.get_tag_articles("python", pages=1)
if dzen_data:
dzen_scraper.save_to_json(dzen_data, "dzen_articles.json")
dzen_scraper.save_to_database(dzen_data, "articles.db", "dzen")

Скрытие внутренней реализации и предоставление интерфейса:

class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.__balance = balance # Приватный атрибут
def deposit(self, amount):
if amount > 0:
self.__balance += amount
return True
return False
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
return True
return False
def get_balance(self):
return self.__balance
account = BankAccount("Иван", 1000)
print(account.get_balance()) # 1000
account.deposit(500)
print(account.get_balance()) # 1500

Классы, которые не могут быть созданы напрямую:

from abc import ABC, abstractmethod
class Vehicle(ABC):
@abstractmethod
def move(self):
pass
@abstractmethod
def stop(self):
pass
class Car(Vehicle):
def move(self):
print("Машина едет")
def stop(self):
print("Машина остановилась")
car = Car()
car.move()
car.stop()
  1. Создайте новый скрапер для сайта по вашему выбору, наследующий от BaseScraper.

  2. Добавьте в BaseScraper метод для экспорта данных в CSV формат.

  3. Реализуйте кэширование запросов в BaseScraper для избежания повторных запросов.

  4. Создайте класс Article для хранения данных статьи и используйте его в скраперах.

  5. Добавьте логирование в BaseScraper для отслеживания процесса скрапинга.

class Article:
def __init__(self, title, url, author, date, source):
self.title = title
self.url = url
self.author = author
self.date = date
self.source = source
def to_dict(self):
return {
"заголовок": self.title,
"ссылка": self.url,
"автор": self.author,
"дата": self.date,
"источник": self.source
}
def __repr__(self):
return f"Article({self.title})"
# Использование в скрапере
class HabrScraper(BaseScraper):
def extract_articles(self, soup) -> List[Dict]:
articles = []
article_elements = soup.find_all("article", class_="tm-articles-list__item")
for element in article_elements:
# ... извлечение данных ...
article = Article(title, full_url, author, date, "Habr")
articles.append(article.to_dict())
return articles

На следующем занятии мы изучим библиотеку transformers и выполним анализ тональности текста.