Практическое занятие 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__init__.py
Заголовок раздела «__init__.py»from .base_scraper import BaseScraperfrom .habr_scraper import HabrScraperfrom .dzen_scraper import DzenScraper
__all__ = ["BaseScraper", "HabrScraper", "DzenScraper"]base_scraper.py
Заголовок раздела «base_scraper.py»import requestsfrom bs4 import BeautifulSoupimport jsonfrom abc import ABC, abstractmethodfrom typing import List, Dict, Optionalimport 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 datahabr_scraper.py
Заголовок раздела «habr_scraper.py»from typing import List, Dictfrom .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)dzen_scraper.py
Заголовок раздела «dzen_scraper.py»from typing import List, Dictfrom .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
# Скрапинг Habrprint("=== Скрапинг 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")
# Скрапинг Dzenprint("\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()) # 1000account.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()Задания для самостоятельной работы
Заголовок раздела «Задания для самостоятельной работы»-
Создайте новый скрапер для сайта по вашему выбору, наследующий от
BaseScraper. -
Добавьте в
BaseScraperметод для экспорта данных в CSV формат. -
Реализуйте кэширование запросов в
BaseScraperдля избежания повторных запросов. -
Создайте класс
Articleдля хранения данных статьи и используйте его в скраперах. -
Добавьте логирование в
BaseScraperдля отслеживания процесса скрапинга.
Пример решения задания 4
Заголовок раздела «Пример решения задания 4»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 и выполним анализ тональности текста.