"""Адаптер для форумов на phpBB (нива-лада.рф и др.). Настройки задаются в конструкторе: data_dir: куда сохранять. forum_base: базовый URL форума (напр. https://нива-лада.рф/n/). topics_per_page: сколько тем на странице раздела. posts_per_page: сколько постов на странице темы. delay: задержка между запросами. ban_test: сколько запросов для проверки бана. topics_file: сколько тем в один JSONL. """ import re from pathlib import Path from urllib.parse import urljoin from ..base import BaseAdapter from . import paginate, parse class PhpBBAdapter(BaseAdapter): """Адаптер для форума phpBB. Параметризуется под любой phpBB-форум: достаточно указать forum_base и per_page-значения. """ def __init__(self, data_dir: str | Path = "lada_data", forum_base: str = "https://нива-лада.рф/n/", topics_per_page: int = 25, posts_per_page: int = 10): """Инициализация адаптера phpBB. Args: data_dir: Папка для данных. forum_base: Базовый URL форума (с / на конце). topics_per_page: Тем на одной странице раздела. posts_per_page: Постов на одной странице темы. """ self._data_dir = Path(data_dir) self._forum_base = forum_base.rstrip("/") + "/" self._topics_per_page = topics_per_page self._posts_per_page = posts_per_page # ─── Атрибуты конфигурации ────────────────────────────────────────── @property def data_dir(self) -> Path: return self._data_dir @property def delay(self) -> float: return 1.5 @property def ban_test_count(self) -> int: return 5 @property def topics_per_file(self) -> int: return 100 @property def topics_per_page(self) -> int: return self._topics_per_page @property def posts_per_page(self) -> int: return self._posts_per_page # ─── Вспомогательные ──────────────────────────────────────────────── def _abs_url(self, path_or_url: str) -> str: """Преобразовать относительный URL в абсолютный. Если path_or_url уже абсолютный — возвращаем как есть. Иначе — присоединяем к forum_base. Args: path_or_url: URL или относительный путь. Returns: str: абсолютный URL. """ if path_or_url.startswith("http"): # Убираем sid из URL clean = re.sub(r"[?&]sid=[^&]+", "", path_or_url) # Нормализуем http → https clean = clean.replace("http://", "https://") return clean # Относительный — присоединяем к базе clean = re.sub(r"[?&]sid=[^&]+", "", path_or_url) return urljoin(self._forum_base, clean) # ─── Пагинация раздела ────────────────────────────────────────────── def count_pages(self, soup) -> int: """Количество страниц в разделе. Args: soup: BeautifulSoup страницы раздела. Returns: int: количество страниц. """ return paginate.count_pages(soup, self._topics_per_page) def page_url(self, section_url: str, page_num: int) -> str: """URL страницы раздела. section_url — это viewforum.php?f=ID (первая страница). Args: section_url: URL раздела (первая страница). page_num: Номер страницы. Returns: str: URL страницы с ?start= (если page>1). """ return paginate.page_url(section_url, page_num, self._topics_per_page) # ─── Пагинация темы ───────────────────────────────────────────────── def topic_url(self, section_url: str, topic: dict) -> str: """Полный URL темы. Если у темы есть url — нормализуем его. Иначе формируем из ID. Args: section_url: URL раздела. topic: Словарь темы. Returns: str: полный URL темы. """ if topic.get("url"): return self._abs_url(topic["url"]) # Вытаскиваем f=ID из section_url f_match = re.search(r"[?&]f=(\d+)", section_url) f_id = f_match.group(1) if f_match else "0" return self._abs_url(f"./viewtopic.php?f={f_id}&t={topic['id']}") def topic_page_url(self, topic_url: str, page_num: int) -> str: """URL страницы темы. Args: topic_url: Полный URL темы. page_num: Номер страницы. Returns: str: URL страницы с ?start=. """ return paginate.topic_page_url(topic_url, page_num, self._posts_per_page) # ─── Парсинг ──────────────────────────────────────────────────────── def parse_section_name(self, soup) -> str | None: """Название раздела. Args: soup: BeautifulSoup страницы раздела. Returns: str | None: название. """ return parse.parse_section_name(soup) def parse_topics(self, soup) -> list[dict]: """Список тем со страницы раздела. Args: soup: BeautifulSoup. Returns: list[dict]: [{id, title, url}]. """ return parse.parse_topics(soup) def parse_posts(self, soup) -> list[dict]: """Посты со страницы темы. Args: soup: BeautifulSoup страницы темы. Returns: list[dict]: [{author, date, num, text}]. """ return parse.parse_posts(soup)