Files
LLM-UI/scraper/forums/phpbb/adapter.py
naeel 5c142096de 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-тем при многопоточном парсинге
2026-06-06 11:46:57 +04:00

194 lines
6.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Адаптер для форумов на 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)