Files
Aks 98309dcc96 fix: устранены все найденные аудитом баги и тихие падения
- SQL injection паттерн → параметризованные запросы во всех местах
- except: pass/continue → logger.warning() везде, ничего не тонет молча
- WAL mode + индекс domain_dedup_key в database.py
- try/finally для conn в main.py, утечка соединения устранена
- backoff 30с при 403/429 от Rusprofile/ЕГРЮЛ
- ликвидированные компании → egrul_status="liquidated"
- max_candidates в contacts_finder считает только реальных кандидатов
- DB_PATH абсолютный (Path(__file__).parent), HH_PAUSE_BETWEEN_QUERIES в config
- HH_SIGNAL_QUERIES дубль убран из launcher.py → импорт из config
- path traversal защита в egrul_enricher debug_dump_html

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

283 lines
21 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.
"""Конфигурация парсера лидов.
Все настройки в одном месте. Меняем здесь — отражается во всех парсерах.
"""
from pathlib import Path
# ───────────────────────────────────────────────────────────────────────
# Города и их Yandex.Maps ID
# ───────────────────────────────────────────────────────────────────────
# Каждая точка парсинга (город / городской округ / регион):
# yandex_id — geo ID в Яндекс.Картах (https://yandex.ru/maps/{yandex_id}/{slug}/...)
# yandex_slug — slug в URL (moscow, mytishchi, ...). Декоративный — главное yandex_id
# vk_id — city ID в VK API (1=Москва, 2=СПб, ...)
# region — агрегатор для группировки в БД (Москва / Московская область / ...)
#
# Активный регион выбирается через CLI: python main.py --city "Мытищи"
# По умолчанию используется ACTIVE_CITY.
#
# Geo ID можно проверить, открыв https://yandex.ru/maps/{geo_id}/ в браузере.
CITIES = {
# ── Только проверенные значения (geo_id подтверждены) ───────────
"Москва": {"yandex_id": 213, "yandex_slug": "moscow", "vk_id": 1, "region": "Москва"},
"Москва и МО": {"yandex_id": 1, "yandex_slug": "moscow-and-moscow-oblast", "vk_id": 1, "region": "Москва и МО"},
"Санкт-Петербург": {"yandex_id": 2, "yandex_slug": "saint-petersburg", "vk_id": 2, "region": "Санкт-Петербург"},
# ── Любой другой город / район — через --district ───────────────
# Парсер автоматически использует "Москва и МО" + название как район.
# Пример:
# python main.py --full --city "Москва и МО" --district "Химки" --category "стоматология"
# python main.py --full --city "Москва и МО" --district "Мытищи" --category "автосервис"
# python main.py --full --city "Москва" --district "Митино" --category "салон красоты"
#
# Чтобы добавить точный город — найди его geo_id через браузер:
# 1. Открой https://yandex.ru/maps/
# 2. Найди город в поиске → кликни на первый результат
# 3. URL станет вида: https://yandex.ru/maps/{geo_id}/{slug}/?...
# 4. Скопируй сюда:
#
# "Мытищи": {"yandex_id": 10743, "yandex_slug": "mytishchi", "vk_id": 1, "region": "Московская область"},
}
# Активный регион (можно переопределить через CLI --city)
ACTIVE_CITY = "Москва"
# Категории — фокус-ЦА 44AS: локальный сервисный бизнес с онлайн-записью и
# отзывами. Бьёт прямо в наши продукты: P3 AI Reputation (отзывы Я.Карт/2ГИС),
# P4 AI Consultant (входящие + онлайн-запись), P12 AI SMM (Instagram/контент).
CATEGORIES = [
# 🎯 Бьюти — главная цель (онлайн-запись = боль, Instagram = канал продаж, отзывы решают)
"салон красоты",
"барбершоп",
"ногтевой сервис",
"студия массажа",
"косметология",
"спа-салон",
# 🍽 HoReCa — отзывы = выручка, бронь столиков, визуал блюд
"кафе",
"ресторан",
# 🏥 Клиники / запись — доверие через отзывы + онлайн-запись
"стоматология",
"фитнес-клуб",
]
# ───────────────────────────────────────────────────────────────────────
# HH.ru — signal-запросы (компании ищут "руки" = нет автоматизации)
# ───────────────────────────────────────────────────────────────────────
# Принцип: если компания ищет ЭТИ должности — у неё нет CRM / нет
# онлайн-записи / ручная коммуникация. Это +3 к hh_signal в скоринге.
HH_SIGNAL_QUERIES = [
# Ручная коммуникация → нет CRM / нет автоответов
"оператор ПК",
"оператор колл-центра",
"оператор технической поддержки",
"менеджер чата",
"менеджер по продажам без CRM",
# Личный помощник / административка → "помоги мне разобраться"
"помощник руководителя",
"ассистент руководителя",
"офис-менеджер",
# Запись клиентов руками → нет онлайн-booking
"администратор записи",
"администратор салона красоты",
"администратор клиники",
"ресепшн",
# Бухгалтерия "руками" → не автоматизирован документооборот
"бухгалтер 1С",
"помощник бухгалтера",
]
# Период поиска (дней) — свежие вакансии
HH_PERIOD_DAYS = 30
HH_MAX_PAGES_PER_QUERY = 5 # 5 страниц × 100 = до 500 вакансий на запрос
# ───────────────────────────────────────────────────────────────────────
# Anti-bot задержки (секунды)
# 2026-05-18: снижены ~30% после успешного прогона 2634 лидов без блокировок.
# Если домашний IP начнёт ловить captcha — поднять обратно к 2.0/7.0 + 30/60.
# ───────────────────────────────────────────────────────────────────────
MIN_DELAY = 1.5 # минимум между запросами
MAX_DELAY = 4.0 # максимум между запросами
CATEGORY_PAUSE_MIN = 15 # пауза между категориями
CATEGORY_PAUSE_MAX = 30
# ───────────────────────────────────────────────────────────────────────
# Лимиты безопасности
# ───────────────────────────────────────────────────────────────────────
MAX_BLOCKED_TRIES = 3 # сколько раз получить captcha → останавливаемся
MAX_SCROLLS = 15 # максимум скроллов списка Я.Карт
MAX_CARDS_PER_CATEGORY = 100 # сколько карточек открывать на 1 категорию
# ───────────────────────────────────────────────────────────────────────
# Пути
# ───────────────────────────────────────────────────────────────────────
DB_PATH = Path(__file__).parent / "leads.db"
EXPORT_DIR = "exports"
# Пауза между HH-запросами (секунды) — задаётся здесь и используется в run_hh
HH_PAUSE_BETWEEN_QUERIES = 3
# ───────────────────────────────────────────────────────────────────────
# Скоринг лидов v5 — «решаемая нами боль» (шкала 0-10)
# ───────────────────────────────────────────────────────────────────────
# Семантика (решение 2026-06-01): score = есть ли у компании проблемы,
# которые закрывают НАШИ продукты, и насколько остро.
# score = pain(решаемая боль) × icp_fit(наш ли это размер)
# Не дозвонибельность, не «качество лида» — только боль под продукты 44AS.
#
# Логика в scoring.py:
# • каждый детектор боли привязан к продукту (P-код) и теме;
# • внутри темы сигналы агрегируются с насыщением (max + k·остальные),
# чтобы коррелированные признаки не складывались линейно;
# • сумма тем → raw_pain, нормируется к 0-10 через PAIN_NORM;
# • ICP-гейт множителем топит крупняк/премиум (см. ICP_* ниже);
# • «уже автоматизирован» отдельно НЕ гейтим — у такого лида просто нет
# болевых дыр, severity→0 естественно.
#
# Веса = бизнес-приоритет (severity в «сырых» баллах). Крутим здесь.
SCORE_WEIGHTS = {
# ── Запись / входящие → P4 AI-Consultant, P1 Text Agent ──────────
"no_online_booking": 2.0, # нет онлайн-записи (для услуг — критично)
"no_live_chat": 1.0, # нет онлайн-чата → входящие теряются
# ── Репутация → P3 AI-Reputation (континуум по рейтингу) ─────────
"rating_very_low": 2.0, # avg < 3.5
"rating_low": 1.5, # 3.5 ≤ avg < 4.0
"rating_mid": 1.0, # 4.0 ≤ avg < 4.5 (4.5+ = здоровая репутация, не боль)
"few_reviews": 1.0, # < 10 отзывов — репутацией не занимаются
"some_reviews": 0.5, # 10..30 отзывов
# ── Веб → P10 Smart Web ──────────────────────────────────────────
"no_website": 1.5, # нет сайта вовсе
"site_dead": 2.5, # сайт не отвечает / 404 (платят, не работает)
"site_constructor": 0.5, # tilda/wix И только для малого бизнеса (см. scoring)
# ── Маркетинг → P12 AI SMM ───────────────────────────────────────
"no_social": 1.0, # ни vk, ни telegram, ни instagram
"no_analytics": 0.5, # не меряют трафик
# ── Инфраструктура → P2 AI-Office ────────────────────────────────
"free_email": 0.5, # корп.почта на mail.ru/gmail — нет своей
}
# Каждый детектор → тема (для тематической агрегации с насыщением).
PAIN_THEME = {
"no_online_booking": "booking", "no_live_chat": "booking",
"rating_very_low": "reputation", "rating_low": "reputation",
"rating_mid": "reputation",
"few_reviews": "reputation", "some_reviews": "reputation",
"no_website": "web", "site_dead": "web", "site_constructor": "web",
"no_social": "marketing", "no_analytics": "marketing",
"free_email": "infra",
}
# Детектор → наш продукт (для подсказки CRM «с чем заходить»).
PAIN_PRODUCT = {
"no_online_booking": "P4", "no_live_chat": "P4",
"rating_very_low": "P3", "rating_low": "P3", "rating_mid": "P3",
"few_reviews": "P3", "some_reviews": "P3",
"no_website": "P10", "site_dead": "P10", "site_constructor": "P10",
"no_social": "P12", "no_analytics": "P12",
"free_email": "P2",
}
# Человекочитаемые причины (для reasons в breakdown и CRM).
PAIN_REASON = {
"no_online_booking": "нет онлайн-записи",
"no_live_chat": "нет онлайн-чата",
"rating_very_low": "низкий рейтинг (<3.5)",
"rating_low": "слабый рейтинг (<4.0)",
"rating_mid": "средний рейтинг (<4.5)",
"few_reviews": "мало отзывов (<10)",
"some_reviews": "немного отзывов (<30)",
"no_website": "нет сайта",
"site_dead": "сайт не отвечает",
"site_constructor": "сайт на конструкторе",
"no_social": "нет соцсетей (VK/Telegram)",
"no_analytics": "нет веб-аналитики",
"free_email": "почта на бесплатном домене",
}
# ════════════════════════════════════════════════════════════════════
# 📌 КАТЕГОРИЙНАЯ РЕЛЕВАНТНОСТЬ ДЕТЕКТОРОВ — ЧИТАЙ ПРИ ДОБАВЛЕНИИ КАТЕГОРИИ
# ════════════════════════════════════════════════════════════════════
# Часть болей применима НЕ ко всем типам бизнеса:
# • «нет онлайн-записи» (P4) — только услуги с записью (салон, клиника,
# кафе, автосервис). Магазину / опту / бухгалтерии запись не нужна.
# • «нет соцсетей» (P12) — только B2C, где соцсети = канал продаж
# (бьюти, HoReCa, розница, фитнес). У B2B (бухгалтерия, стройка) — не боль.
# Универсальные боли (сайт, репутация, чат, аналитика, почта) применяются ВСЕГДА.
#
# Модель — БЕЛЫЕ СПИСКИ по ключевым словам (матч по подстроке в lead.category):
# детектор срабатывает ТОЛЬКО если категория попала в его множество.
# Неизвестная / новая категория по умолчанию НЕ получает booking/social —
# консервативно: лучше не начислить, чем ложно завысить (как было с кейсом
# «магазин одежды → нет онлайн-записи»).
#
# 👉 ДОБАВЛЯЕШЬ НОВУЮ КАТЕГОРИЮ (в CATEGORIES выше или новым --category)?
# Впиши её ключевое слово сюда:
# — есть запись клиентов? → в APPT_CATEGORIES
# — продаётся через соцсети? → в SOCIAL_SALES_CATEGORIES
# Чистый B2B без записи и соцпродаж — НЕ добавляй никуда (получит только
# универсальные боли — это правильно).
# ════════════════════════════════════════════════════════════════════
# Услуги с записью клиентов → применяется детектор «нет онлайн-записи» (P4).
APPT_CATEGORIES = {
"салон", "барбершоп", "ногт", "маникюр", "педикюр", "массаж", "косметолог",
"спа", "парикмахер", "эпиляц", "депиляц", "тату", "броу", "ресниц",
"стоматолог", "клиник", "медицин", "врач", "ветеринар", "груминг",
"фитнес", "йога", "пилатес", "танц", "бассейн", "студи",
"кафе", "ресторан", "бар", "кофейн", "пиццери", "суши", "кальян", "банкет",
"автосервис", "автомойка", "шиномонтаж", "детейлинг", "сервис", "ремонт",
"юридическ", "нотариус", "адвокат", "консультац",
}
# B2C, где соцсети = канал продаж → применяется детектор «нет соцсетей» (P12).
SOCIAL_SALES_CATEGORIES = {
"салон", "барбершоп", "ногт", "маникюр", "массаж", "косметолог", "спа",
"парикмахер", "тату", "броу", "ресниц", "студи",
"кафе", "ресторан", "бар", "кофейн", "пиццери", "суши", "кальян",
"магазин", "розниц", "бутик", "шоурум", "одежд", "обув", "цвет",
"украшен", "подарк", "парфюм",
"фитнес", "йога", "танц", "бассейн",
"стоматолог", "клиник", "космет",
"фото", "видео", "свадьб", "ивент", "праздник", "декор",
"автосервис", "детейлинг",
}
# Насыщение внутри темы: theme_value = max(severities) + k·sum(остальные).
# k < 1 → коррелированные сигналы одной темы не складываются линейно.
THEME_SATURATION = 0.4
# Нормировка raw_pain → шкала 0-10. PAIN_NORM = «практический максимум боли»
# у сильного малого лида (нет записи+чата + слабый сайт + нет соцсетей ≈ 4-5).
# Делим на него, чтобы такой лид попадал в hot, а шкала растягивалась.
PAIN_NORM = 5.0
# Hard cap для финального score
SCORE_MAX = 10
# ── ICP-гейт: ПРОГРЕССИВНЫЙ штраф за «зрелость» (отзывы × рейтинг) ────
# ЦА = малый/средний бизнес, которому нужна автоматизация. Чем больше отзывов
# И выше рейтинг — тем сильнее снижаем балл (процветающим/раскрученным мы менее
# нужны и труднее продать). Формула в scoring.icp_fit:
# icp = 1 ICP_PMAX · rf · (ICP_BASE + (1ICP_BASE)·gf)
# rf = min(1, отзывы / ICP_REVIEWS_FULL) — линейно по объёму
# gf = clamp((avg ICP_RATING_MIN)/(5 ICP_RATING_MIN)) — по рейтингу
# Отзывы штрафуют ВСЕГДА (доля ICP_BASE), высокий рейтинг усиливает до полного.
# Мало отзывов → множитель ≈1 (новый/борющийся бизнес сохраняет балл).
# На Я.Картах рейтинги зажаты 4.5-5.0, поэтому главный рычаг — объём отзывов,
# рейтинг лишь усиливает. Заменил прежний ступенчатый гейт (D18).
ICP_PMAX = 0.85 # макс. доля снижения (при отзывы≥FULL и avg=5.0)
ICP_REVIEWS_FULL = 6000 # отзывов для максимального review-фактора (усилен 2026-06-05: 10000→6000)
ICP_BASE = 0.65 # доля штрафа от объёма, не зависящая от рейтинга (усилен: 0.5→0.65)
ICP_RATING_MIN = 4.0 # ниже этого рейтинг не усиливает штраф
# ── Бэнды (для CRM-сортировки и outreach-очереди) ───────────────────
BAND_HOT = 6 # score >= 6 → 🔥 hot
BAND_WARM = 4 # 4..5 → 🟡 warm, ниже → ⚪ cold
HOT_LEAD_THRESHOLD = BAND_HOT # совместимость: get_stats / csv_export / CRM
# Полнота диагностики ниже порога → лид помечается «нужно обогащение».
MIN_COVERAGE = 0.5