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,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}"
|
||||
Reference in New Issue
Block a user