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:
2026-06-06 11:46:57 +04:00
parent 673bfe5e2a
commit 5c142096de
18 changed files with 2118 additions and 0 deletions
+109
View File
@@ -0,0 +1,109 @@
"""Пагинация XenForo (vwts.ru и другие форумы на XenForo).
Страницы раздела: /forum/vag/benzinovye-dvigateli/page-2
Страницы темы: /forum/topic/12345/page-2
HTML-структура пагинации:
<div class="pageNav">
<a>1</a> ← текущая (без href)
<a href="page-2">2</a> ← соседняя
<a>…</a> ← эллипсис (пропускаем)
<a href="page-218">218</a> ← последняя
</div>
Все номера страниц видны на любой странице раздела.
Кнопки <button> не трогаем — ищем только <a>.
"""
import re
import logging
from bs4 import BeautifulSoup
log = logging.getLogger(__name__)
def count_pages(soup: BeautifulSoup) -> int:
"""Определить количество страниц в пагинации XenForo.
Ищем:
<div class="pageNav"> → все <a>
Из href вытаскиваем /page-N
Из текста — числа
Возвращаем максимум
Args:
soup: BeautifulSoup страницы раздела или темы.
Returns:
int: количество страниц (минимум 1).
"""
nav = soup.select_one("div.pageNav")
# Нет пагинации — всего 1 страница
if nav is None:
return 1
nums = set()
# Проходим по всем ссылкам внутри пагинации
for link in nav.select("a"):
href = link.get("href", "")
text = link.text.strip()
# Из href вытаскиваем page-N
match = re.search(r"page-(\d+)", href)
if match:
nums.add(int(match.group(1)))
# Из текста — тоже числа (текущая страница без href)
try:
nums.add(int(text))
except ValueError:
# "…", "Назад", "Вперёд" — пропускаем
pass
# Защита от пустой пагинации
return max(nums) if nums else 1
def page_url(section_url: str, page_num: int) -> str:
"""Сформировать URL страницы раздела XenForo.
Формат:
page-1 (первая) → сам section_url
page-N (N>1) → section_url + page-N
Args:
section_url: URL раздела (с / на конце).
page_num: Номер страницы (1-based).
Returns:
str: полный URL страницы.
"""
if page_num <= 1:
return section_url
# Убеждаемся что URL заканчивается на /
base = section_url.rstrip("/") + "/"
return f"{base}page-{page_num}"
def topic_page_url(topic_url: str, page_num: int) -> str:
"""Сформировать URL страницы темы XenForo.
Формат:
page-1 → сам topic_url
page-N → topic_url + page-N
Args:
topic_url: Полный URL темы.
page_num: Номер страницы (1-based).
Returns:
str: URL страницы темы.
"""
if page_num <= 1:
return topic_url
base = topic_url.rstrip("/") + "/"
return f"{base}page-{page_num}"