Files
Aks f78f35fb3f init: Parser v1 — Lead Generation Engine
Парсер лидов МБ РФ: Яндекс.Карты + HH.ru + обогащение DaData/ЕГРЮЛ/Rusprofile + Streamlit CRM.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 12:56:06 +03:00

81 lines
3.8 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.
"""Базовый класс для парсеров — общая логика retry, sleep, счётчики ошибок.
Конкретные парсеры (yandex_maps.py, two_gis.py, vk.py, ...) наследуют от него,
переопределяют parse_category() и используют общие хелперы.
"""
import logging
import random
import time
from typing import Callable, TypeVar
from fake_useragent import UserAgent
import config
T = TypeVar("T")
logger = logging.getLogger(__name__)
class BaseParser:
"""Общая основа для всех парсеров.
Subclasses переопределяют:
- source_name: ключ в БД ('yandex_maps', '2gis', ...)
- parse_category(query, city) -> list[dict]
"""
source_name: str = "base"
def __init__(self):
self._ua_pool = UserAgent()
self.session_errors = 0
self.captcha_count = 0
# ─── Anti-bot хелперы ───────────────────────────────────────────────
def random_user_agent(self) -> str:
return self._ua_pool.random
def sleep_random(self, min_s: float | None = None, max_s: float | None = None) -> None:
"""Случайная пауза между запросами."""
lo = min_s if min_s is not None else config.MIN_DELAY
hi = max_s if max_s is not None else config.MAX_DELAY
delay = random.uniform(lo, hi)
time.sleep(delay)
def sleep_between_categories(self) -> None:
"""Длинная пауза между категориями (~30-60 сек)."""
delay = random.uniform(config.CATEGORY_PAUSE_MIN, config.CATEGORY_PAUSE_MAX)
logger.info(f"⏸ Пауза между категориями: {delay:.0f} сек")
time.sleep(delay)
# ─── Retry-обёртка с экспоненциальной задержкой ─────────────────────
def retry(self, func: Callable[[], T], retries: int = 3) -> T | None:
"""Запустить func() с N попытками. Между попытками — exp.backoff."""
for attempt in range(retries):
try:
return func()
except Exception as e:
wait = (2 ** attempt) * 5
logger.warning(f"Попытка {attempt + 1}/{retries} упала: {e}. Ждём {wait}с.")
if attempt < retries - 1:
time.sleep(wait)
self.session_errors += 1
return None
# ─── Захват captcha и эскалация ─────────────────────────────────────
def register_captcha(self) -> bool:
"""Зарегистрировать инцидент captcha. Возвращает True если надо остановиться."""
self.captcha_count += 1
logger.warning(f"⚠️ Captcha #{self.captcha_count}/{config.MAX_BLOCKED_TRIES}")
if self.captcha_count >= config.MAX_BLOCKED_TRIES:
logger.error(
"🛑 Превышен лимит captcha. Останавливаемся — нужна смена тактики."
)
return True
return False
# ─── Контракт для подклассов ────────────────────────────────────────
def parse_category(self, query: str, city: str = "Москва") -> list[dict]:
"""Должен вернуть list[dict] согласно схеме leads (database.py)."""
raise NotImplementedError("Subclass must implement parse_category()")