"""Абстрактный базовый класс адаптера форума. Каждый движок (XenForo, phpBB, ...) реализует этот интерфейс. core/runner.py вызывает эти методы, ничего не зная о конкретном движке. Методы для обязательной реализации: count_pages(soup) -> int parse_topics(soup) -> list[dict] parse_posts(soup) -> list[dict] parse_section_name(soup) -> str | None page_url(section_url, page_num) -> str topic_url(section_url, topic) -> str topic_page_url(topic_url, page_num) -> str Опционально: parse_tags(soup) -> list[str] Атрибуты (должны быть определены в конструкторе адаптера): data_dir: Path — куда сохранять данные. delay: float — задержка между запросами (сек). ban_test_count: int — сколько запросов для проверки бана. topics_per_file: int — сколько тем в один JSONL-файл. """ from abc import ABC, abstractmethod class BaseAdapter(ABC): """Интерфейс адаптера форума. Все методы, кроме data_dir, delay и т.д., получают BeautifulSoup-объект и возвращают структурированные данные. """ # ─── Атрибуты конфигурации ────────────────────────────────────────── @property @abstractmethod def data_dir(self): """Path: корневая директория для сохранения данных.""" ... @property @abstractmethod def delay(self) -> float: """float: задержка между запросами в секундах.""" ... @property @abstractmethod def ban_test_count(self) -> int: """int: количество быстрых запросов для проверки бана.""" ... @property @abstractmethod def topics_per_file(self) -> int: """int: сколько тем писать в один JSONL-файл.""" ... # ─── Определение количества страниц ───────────────────────────────── @abstractmethod def count_pages(self, soup) -> int: """Сколько страниц в разделе (или теме). Args: soup: BeautifulSoup первой страницы раздела/темы. Returns: int: количество страниц (минимум 1). """ ... # ─── Парсинг списка тем ───────────────────────────────────────────── @abstractmethod def parse_topics(self, soup) -> list[dict]: """Распарсить список тем со страницы раздела. Args: soup: BeautifulSoup страницы раздела. Returns: list[dict]: каждая тема — словарь с ключами: - id: int — ID темы на форуме. - title: str — заголовок темы. - url: str | None — URL темы (если известен). """ ... # ─── Парсинг постов ───────────────────────────────────────────────── @abstractmethod def parse_posts(self, soup) -> list[dict]: """Распарсить посты со страницы темы. Args: soup: BeautifulSoup страницы темы. Returns: list[dict]: каждый пост — словарь с ключами: - author: str — имя автора. - date: str — дата/время поста. - num: str — номер поста (#1, #2...). - text: str — текст поста. """ ... # ─── Парсинг названия раздела ─────────────────────────────────────── @abstractmethod def parse_section_name(self, soup) -> str | None: """Извлечь название раздела из HTML. Args: soup: BeautifulSoup страницы раздела. Returns: str | None: название (напр. "Бензиновые двигатели") или None, если не удалось определить. """ ... # ─── Формирование URL ─────────────────────────────────────────────── @abstractmethod def page_url(self, section_url: str, page_num: int) -> str: """Сформировать URL страницы раздела. Args: section_url: URL раздела (первая страница). page_num: Номер страницы (1-based). Returns: str: полный URL страницы. """ ... @abstractmethod def topic_url(self, section_url: str, topic: dict) -> str: """Сформировать полный URL темы. Args: section_url: URL раздела. topic: Словарь темы из parse_topics(). Returns: str: полный URL темы. """ ... @abstractmethod def topic_page_url(self, topic_url: str, page_num: int) -> str: """Сформировать URL страницы внутри темы. Args: topic_url: Полный URL темы. page_num: Номер страницы (1-based, начиная с 2). Returns: str: полный URL страницы темы. """ ... # ─── Опционально: теги ────────────────────────────────────────────── def parse_tags(self, soup) -> list[str]: """Распарсить теги темы (если движок поддерживает). По умолчанию — пустой список. Args: soup: BeautifulSoup страницы темы. Returns: list[str]: список тегов. """ return []