Files
LLM-UI/scraper/core/state.py
T
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

115 lines
4.5 KiB
Python

"""Управление состоянием: state.json в папке раздела.
Позволяет:
- Сохранять прогресс (какие темы обработаны, какие страницы пройдены).
- Продолжать парсинг с места остановки (той же командой).
- Мержить состояния воркеров в единый state.json (после завершения).
Файл state.json (раздел):
{
"topics_done": {"123": "Название темы", "456": "..."},
"pages_done": [1, 2, 3],
"file_index": 5,
"topic_count": 432
}
Файл worker_N/state.json:
Аналогичная структура, но только для одного воркера.
"""
import json
import logging
from pathlib import Path
log = logging.getLogger(__name__)
def _state_file(data_dir: Path, section_slug: str) -> Path:
"""Полный путь к state.json для раздела.
Args:
data_dir: Корневая папка с данными (напр. ./vwts_data).
section_slug: Короткое имя раздела.
Returns:
Path к state.json.
"""
section_dir = data_dir / section_slug
section_dir.mkdir(parents=True, exist_ok=True)
return section_dir / "state.json"
def load(data_dir: Path, section_slug: str) -> dict:
"""Загрузить state.json раздела.
Args:
data_dir: Корневая папка с данными.
section_slug: Короткое имя раздела.
Returns:
Словарь состояния. Если файла нет — пустой state.
"""
path = _state_file(data_dir, section_slug)
if path.exists():
try:
return json.loads(path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError) as exc:
log.warning("⚠️ Ошибка чтения %s: %s", path, exc)
# Пустой state по умолчанию
return {"topics_done": {}, "pages_done": [], "file_index": 0, "topic_count": 0}
def save(data_dir: Path, section_slug: str, state: dict):
"""Сохранить state.json раздела.
Args:
data_dir: Корневая папка с данными.
section_slug: Короткое имя раздела.
state: Словарь состояния для сохранения.
"""
path = _state_file(data_dir, section_slug)
path.write_text(
json.dumps(state, ensure_ascii=False, indent=2),
encoding="utf-8",
)
def merge_workers(data_dir: Path, section_slug: str):
"""Собрать состояния всех воркеров в единый state.json раздела.
Проходит по worker_*/state.json, сливает topics_done, pages_done,
суммирует topic_count.
Args:
data_dir: Корневая папка с данными.
section_slug: Короткое имя раздела.
"""
root = data_dir / section_slug
# ── Собираем все worker_*/state.json ────────────────────────────────
merged = {"topics_done": {}, "pages_done": [], "file_index": 0, "topic_count": 0}
for worker_state_path in sorted(root.glob("worker_*/state.json")):
try:
worker_state = json.loads(
worker_state_path.read_text(encoding="utf-8"),
)
merged["topics_done"].update(worker_state.get("topics_done", {}))
merged["pages_done"].extend(worker_state.get("pages_done", []))
merged["topic_count"] += worker_state.get("topic_count", 0)
except (json.JSONDecodeError, OSError) as exc:
log.warning("⚠️ Ошибка чтения %s: %s", worker_state_path, exc)
# Чистим дубликаты и сортируем
merged["pages_done"] = sorted(set(merged["pages_done"]))
# Определяем следующий file_index по количеству part_*.jsonl
existing_parts = list(root.glob("part_*.jsonl"))
merged["file_index"] = max(0, len(existing_parts) - 1)
# ── Пишем ───────────────────────────────────────────────────────────
save(data_dir, section_slug, merged)
log.info("🔗 Мерж состояний: %d воркеров → state.json", len(list(root.glob("worker_*"))))