init: Parser v1 — Lead Generation Engine
Парсер лидов МБ РФ: Яндекс.Карты + HH.ru + обогащение DaData/ЕГРЮЛ/Rusprofile + Streamlit CRM. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
"""Базовый класс для парсеров — общая логика 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()")
|
||||
Reference in New Issue
Block a user