scraper: новая архитектура — core/ + forums/ (BaseAdapter, XenForo, phpBB)
- core/ — общий слой: fetch, throttle, output, state, runner - forums/base.py — абстрактный BaseAdapter - forums/xenforo/ — адаптер для XenForo (vwts.ru) - forums/phpbb/ — адаптер для phpBB (нива-лада.рф) - __main__.py — точка входа CLI - history/001-initial-structure.md — журнал изменений Также на ВМ (не в этом коммите): - lada_scraper/parse.py — пропуск sticky-тем при многопоточном парсинге
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
"""Абстрактный базовый класс адаптера форума.
|
||||
|
||||
Каждый движок (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 []
|
||||
Reference in New Issue
Block a user