"""Основной цикл: обход страниц раздела → темы → посты → сохранение.""" import sys, logging from pathlib import Path from urllib.parse import urlparse from .config import OUTPUT_DIR from .fetch import get, session from .paginate import count, verify_last from .parse import parse_topics, parse_posts, parse_tags from .state import load, save from .output import save_topic log = logging.getLogger(__name__) def run(section_url: str): """Главный цикл скрейпера. Args: section_url: полный URL раздела, напр. https://vwts.ru/forum/vag/benzinovye-dvigateli/ """ section_slug = section_url.rstrip("/").split("/")[-1] # загружаем состояние (если было прервано — продолжим) st = load(section_slug) if st["topics_done"]: log.info(f"🔄 Продолжаем: {len(st['topics_done'])} тем, " f"part_{st['file_index']:04d}.jsonl") # ── Страница 1: название раздела + количество страниц ──────────────── soup = get(section_url) if not soup: log.error("❌ Сайт недоступен") sys.exit(1) section_name = soup.select_one("h1") section_name = section_name.text.strip() if section_name else section_slug total_pages = count(soup) # Проверяем: последняя страница реально существует? total_pages = verify_last(section_url, total_pages, session) log.info(f"📋 {section_name} | страниц: {total_pages}") # ── Обход страниц раздела ─────────────────────────────────────────── consecutive_404 = 0 for pg in range(1, total_pages + 1): if pg in st["pages_done"]: consecutive_404 = 0 continue # page-1 = базовый URL (чтобы не было лишнего редиректа) url = section_url if pg == 1 else f"{section_url}page-{pg}" log.info(f"📄 [{pg}/{total_pages}]") page_soup = get(url) if not page_soup: consecutive_404 += 1 # 3 пустых страницы подряд = раздел закончился раньше if consecutive_404 >= 3: log.info(f" ⏹️ {consecutive_404} пустых подряд — завершаем") break log.warning(f" ⚠️ Пропущена стр.{pg} " f"(пустых подряд: {consecutive_404})") continue consecutive_404 = 0 topics = parse_topics(page_soup) log.info(f" Тем: {len(topics)}") # ── Обход тем на странице ────────────────────────────────────── for i, tp in enumerate(topics, 1): tid = tp["id"] if str(tid) in st["topics_done"]: continue log.info(f" [{i}/{len(topics)}] #{tid}: {tp['title'][:80]}") topic_soup = get(tp["url"]) if not topic_soup: # не удалось — помечаем чтобы не ретраить бесконечно st["topics_done"][str(tid)] = tp["title"] save(section_slug, st) continue topic_pages = count(topic_soup) posts, tags = [], parse_tags(topic_soup) # ── Обход страниц темы ──────────────────────────────────── base = f"{urlparse(section_url).scheme}://{urlparse(section_url).netloc}" for tpp in range(1, topic_pages + 1): if tpp == 1: posts += parse_posts(topic_soup) else: tp2 = get(f"{base}/forum/topic/{tid}/page-{tpp}") if tp2: posts += parse_posts(tp2) log.info(f" {len(posts)} постов ({topic_pages} стр.), " f"теги: {tags}") save_topic(section_slug, section_name, tp, posts, tags, st) st["topics_done"][str(tid)] = tp["title"] save(section_slug, st) st["pages_done"].append(pg) save(section_slug, st) # ── Итог ──────────────────────────────────────────────────────────── log.info("=" * 60) log.info(f"✅ '{section_name}' — {st['topic_count']} тем " f"в {st['file_index']+1} частях") for f in sorted((OUTPUT_DIR / section_slug).glob("part_*.jsonl")): log.info(f" 📄 {f.name} ({f.stat().st_size/1024/1024:.1f} MB)") log.info("=" * 60)