Files
parser-v1/README.md
T
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

56 KiB
Raw Blame History

Parser v1 — Lead Generation Engine

Парсер лидов малого бизнеса РФ с многоуровневым обогащением, скорингом и CRM-приложением для ручной работы с лидами.

Источники:

  • 🗺 Яндекс.Карты — компании по нишам (кафе, автосервис, стоматология...). Сканирует карточку на email и доп.телефоны (в описаниях / «О компании»).
  • 💼 HH.ru — компании, ищущие «руки» по signal-запросам (оператор, администратор записи) = нет автоматизации.
  • 🔍 HH employer-pages — для лидов от HH парсит страницу работодателя → website + доп.контакты.
  • 🔎 DDG-find-sites — для лидов без website ищет сайт через DuckDuckGo (бесплатно, без капчи) + верифицирует по содержимому (name fuzzy-match + blocklist 80+ агрегаторов).
  • 🌐 Tier 2 enrichment — анализ сайта: CMS, чат, онлайн-запись, аналитика + email/телефоны из <a href="mailto:..."> / <a href="tel:..."> + ИНН/ОГРН/КПП из footer (152-ФЗ disclosure, пробует 7 типичных contact-страниц).
  • 🏛 Tier 3 ЕГРЮЛDaData (primary, 10К запросов/день бесплатно, индексирует бренды) → Rusprofile (fallback). Возвращает ИНН/ОГРН/КПП/директор/адрес/дата регистрации/ОКВЭД.
  • 🚫 Blacklist крупных компаний — автоматически отсекает банки (Газпром/Альфа/Сбер), ритейл-сети (ВкусВилл/X5/Магнит), госструктуры (Правительство Москвы/ФГАОУ) и пр. публичные компании ПАО которым outreach бесполезен.

Два режима работы:

  • 🤖 Парсер — собирает лидов в leads.db через launch.bat (TUI-меню) или python main.py …
  • 🎯 CRM-приложение (Streamlit) — открывается через launch_crm.bat, читает/пишет ту же leads.db: фильтры по статусу/региону/score, форма касания (звонок/email/VK/TG → реакция), история всех касаний, заметки о лиде.

Стек: Python 3.12 + Botasaurus + SQLite + requests + colorlog + Streamlit (для CRM) + DaData Suggestions API Текущая БД: 2874 лида (2676 HH + 198 Я.Карты), из них 143 excluded (blacklist крупных компаний), 2731 доступных для outreach, 1587 hot (score≥5). У ~520 лидов есть директор/ИНН.


Быстрый старт

Два двойных клика:

  • launch.bat — запускает парсер через TUI-меню (questionary): источники, категории, локация, опции pipeline.
  • launch_crm.bat — открывает CRM-приложение в браузере (localhost:8501): таблица лидов, фильтры, форма касания.
# 1. Один раз: установка
python -m venv .venv
.venv\Scripts\activate                 # Windows
pip install -r requirements.txt
playwright install chromium

# 2a. Парсер через TUI-меню (рекомендуется)
launch.bat
# или: python launcher.py

# 2b. Или напрямую через CLI
python main.py --full --category "автосервис,салон красоты,стоматология" --limit 20

# 3. Работать с собранными лидами в CRM
launch_crm.bat
# или: streamlit run app/app.py

# 4. CSV-отчёты прогонов
explorer exports

📋 Содержание

  1. Установка
  2. Smoke-тесты
  3. Команды
  4. География: город / район / регион
  5. Pipeline и его шаги
  6. Скоринг лидов
  7. Что в БД
  8. CRM-приложение (Streamlit)
  9. Структура проекта
  10. PyCharm флоу
  11. Troubleshooting
  12. Прогресс по фазам

Установка

# Создать venv и активировать
python -m venv .venv
.venv\Scripts\activate                 # Windows
# source .venv/bin/activate            # Linux/Mac

# Зависимости
pip install -r requirements.txt
playwright install chromium             # обязательно — это браузер для Botasaurus

Python: 3.12 (3.13 тоже работает; 3.14 — рискованно, не все wheel'ы есть).


Smoke-тесты

Перед первым запуском проверь что модули работают:

python normalization.py    # тесты телефона / домена / рейтинга
python database.py         # тест дедупликации
python scoring.py          # тест скоринга

Все 3 должны напечатать ✅ ... — пройдены. Если упало — баг.


Команды

💡 Можно вообще не помнить флаги — двойной клик по launch.bat откроет TUI-меню (launcher.py) с навигацией стрелками + чекбоксами + 50+ категорий и 40+ локаций. Описание ниже — для запуска через PowerShell/Terminal или PyCharm Run Configuration.

Полный pipeline (всё одной командой)

--full эквивалент --source X --find-sites --enrich --enrich-egrul --rescore --export (+ --hh-enrich-websites если source=hh). Делает:

  1. Парсинг одного источника (HH или Я.Карты)
  2. HH employer-pages (если HH) — заходит на страницу работодателя, тащит website + контакты
  3. DDG find-sites — для лидов без website ищет сайт через DuckDuckGo + верифицирует
  4. Tier 2 — анализ сайтов (CMS, чат, запись, аналитика) + извлечение ИНН/ОГРН/КПП из footer (152-ФЗ)
  5. Tier 3 — ЕГРЮЛ через DaData (primary) → Rusprofile (fallback). Если на сайте нашли ИНН — обогащение по ИНН (точно).
  6. Blacklist filter — автоматически отсекает крупные публичные компании (банки, ритейл-сети, госструктуры)
  7. Rescore всех лидов в БД (включая старых)
  8. Export CSV
# Я.Карты — нужно явно указать --source yandex (default)
python main.py --full --category "автосервис" --limit 30

# HH — компании которые ищут "руки" (нет CRM)
python main.py --full --source hh --category "оператор колл-центра" --limit 5

# По всем signal-запросам HH (~30-40 минут)
python main.py --full --source hh

Источники запускаются раздельно — БД одна, но каждый источник имеет свои параметры, скорость, риски блокировок. Так удобнее контролировать прогоны.

БД общая (leads.db) — после двух раздельных прогонов компании из Я.Карт и HH автоматически смерджатся через дедуп по ИНН/телефону/домену (после ЕГРЮЛ-обогащения).

По шагам

Команда Что делает
--source yandex --category "кафе" --limit 30 Парсинг 1 категории на Я.Картах
--source yandex (без --category) Все 10 категорий из config.CATEGORIES
--source hh HH.ru — все signal-запросы (оператор, администратор записи, ...)
--source hh --category "оператор колл-центра" HH.ru — только один запрос
--hh-enrich-websites Для HH-лидов без сайта зайти на employer-страницу, взять website + контакты
--find-sites Для лидов без website искать сайт через DuckDuckGo (бесплатно) + верифицировать по INN/name
--enrich Tier 2 — обогатить непроверенные сайты (CMS / контакты / footer ИНН)
--enrich-egrul Tier 3 — обогатить через DaData (primary) → Rusprofile (fallback)
--rescore Пересчитать score у всех лидов
--export --min-score 5 Только экспорт горячих лидов
--stats Показать статистику БД
--cleanup-directors Очистить лиды где director_name = "Генеральный директор" / "Председатель" / etc. (запустится повторное обогащение)

📖 Справочник по всем флагам

Каждый флаг можно использовать отдельно или комбинировать в любом порядке. Pipeline запускается последовательно: parse → enrich → enrich-egrul → rescore → export.

🎯 Pipeline-флаги (запускают шаги обработки)

Флаг Default Что делает Зависит от
--source {yandex,hh} Какой источник парсить. Без него парсинг не идёт. (2GIS/VK/WB — отключены, см. DECISIONS D13/D17)
--hh-enrich-websites false Для HH-лидов без сайта: зайти на employer-страницу, забрать website + доп.контакты Лиды source=hh без website
--find-sites false Для лидов без website: искать сайт через DuckDuckGo HTML search + верифицировать (INN на странице ИЛИ fuzzy-match названия + blocklist 80+ доменов агрегаторов) Лиды без website
--find-sites-source {yandex,hh,all} all Ограничить --find-sites лидами из конкретного источника используется с --find-sites
--find-sites-limit N Ограничить --find-sites первыми N лидами (для теста) используется с --find-sites
--enrich false Tier 2: анализ сайта (CMS, чат, запись, аналитика) + сбор email/доп.телефонов + ИНН/ОГРН/КПП из footer (152-ФЗ) для лидов где website есть, но site_checked_at ещё пуст. Если на сайте нашли ИНН — авто-запуск ЕГРЮЛ по ИНН. Лиды с website в БД
--enrich-egrul false Tier 3: ЕГРЮЛ через DaData (primary, 10K/день бесплатно) → Rusprofile (fallback). Для лидов где egrul_checked_at пуст Лиды с name в БД
--rescore false Пересчитать score у всех лидов в БД (нужно после изменения config.SCORE_WEIGHTS)
--export false CSV каждого прогона сессии в exports/YYYY-MM/leads_<src>_<query>_<city>_<ts>.csv с шапкой-метаданными. Если парсинга в сессии не было — плоский CSV по min_score.
--export-master false Snapshot всей БД в exports/_master/all_leads.csv (перезапись). Можно вызвать в любой момент.
--export-run N Пересоздать CSV конкретного прогона по его ID из sources_log (если файл удалили).
--full false Shortcut — раскрывается в --source X --find-sites --enrich --enrich-egrul --rescore --export (+ --hh-enrich-websites если --source hh). Default source = yandex.
--stats false Показать сводку БД и выйти (другие флаги игнорируются)
--cleanup-directors false Утилита: очистить кривые ФИО директоров ("Генеральный директор" и т.п.) и сбросить egrul_checked_at чтобы --enrich-egrul повторно их прогонял
--rescan-sites false Одноразовая утилита: сбросить site_checked_at у всех лидов → следующий --enrich пройдёт по сайтам заново. Нужно после расширения сбора контактов / обновления analyze_website.

🔍 Параметры парсинга (модификаторы для --source)

Флаг Default Что делает Где работает
--category "X" Конкретный поисковый запрос. Можно несколько через запятую: "автосервис,салон красоты,стоматология". Если не указано — берётся профильный список (CATEGORIES для Я.Карт, HH_SIGNAL_QUERIES для HH) yandex, hh
--city "X" "Москва" Город из config.CITIES (Москва / Москва и МО / СПб). Если указан незнакомый — fallback на "Москва и МО" + город как район yandex, hh
--district "X" Район Москвы или другой топоним для уточнения. Свободная строка (Митино, Бутово, Зеленоград). Добавляется к поисковому запросу + сохраняется в БД отдельной колонкой yandex
--limit N 100 Для Я.Карт — максимум карточек на категорию. Для HH — максимум страниц (1 страница ≈ 50 вакансий). Не действует на enrichment (он всегда обрабатывает всех непроверенных) yandex, hh

📤 Параметры экспорта

Флаг Default Что делает
--min-score N 0 В CSV попадут только лиды с score >= N. Например --min-score 5 оставит только hot leads

💡 Какие флаги можно комбинировать

Типичные комбинации

# Только парсинг, без обогащения
python main.py --source yandex --category "кафе" --limit 20

# Полная цепочка по Я.Картам — парсинг + find-sites + Tier2 + DaData + score + CSV
python main.py --full --source yandex --category "кафе" --limit 20

# Полная цепочка по HH (включая employer-pages)
python main.py --full --source hh --category "оператор колл-центра" --limit 5

# Обогатить и пересчитать без нового парсинга (например после правки скоринга)
python main.py --find-sites --enrich --enrich-egrul --rescore --export

# Только найти сайты для HH-лидов (лимит 50 для теста)
python main.py --find-sites --find-sites-source hh --find-sites-limit 50

# Только пересчитать score (быстро — секунды)
python main.py --rescore --export

# Утилита очистки + повторный enrichment + rescore + CSV
python main.py --cleanup-directors --enrich-egrul --rescore --export

# Только посмотреть текущее состояние
python main.py --stats

# Перепрогон enricher на ВСЕХ лидах с website (после расширения сбора контактов)
python main.py --rescan-sites --enrich --rescore --export-master

# Master-файл всех лидов в один CSV
python main.py --export-master

# Восстановить удалённый CSV прогона #5
python main.py --export-run 5

Бессмысленные комбинации

  • --stats + что угодно — --stats всегда выходит сразу
  • --full без --source (точнее, без явного source — возьмёт default yandex)
  • --enrich без лидов с website в БД — нечего обогащать
  • --enrich-egrul без лидов в БД — то же самое
  • --district "X" для --source hh — игнорируется (HH парсит по area_id)

HH.ru — отдельный workflow

# Полный цикл одной командой (Парсинг → Tier 2 → ЕГРЮЛ → Rescore → CSV)
python main.py --full --source hh

# Только парсинг по одному signal-запросу (для теста)
python main.py --source hh --category "оператор колл-центра" --limit 2

# Без --category — пройдёт по всем 13 signal-запросам из config.HH_SIGNAL_QUERIES
python main.py --source hh

Принцип: если компания ищет "оператора ПК" / "администратора записи" / "менеджера без CRM" — у них нет автоматизации. Это прямой сигнал боли = +3 hh_signal в скоринг. Дедупликация по ИНН автоматически свяжет их с лидами из Я.Карт если совпадут (обычно не пересекаются — разные срезы рынка).

Важно: HH парсер работает через Botasaurus браузер (не api.hh.ru — он 403'ит даже с Chrome TLS-fingerprint). Открывает страницы поиска hh.ru/search/vacancy как реальный пользователь. Региональный модал "Вы из Москвы?" закрывается автоматически.

Что HH даёт vs что нет:

Поле HH Что компенсирует
name
employer_id, source_url
phone HH публично не показывает
website
ИНН + директор + дата регистрации через --enrich-egrul Rusprofile по name (~65% покрытие)

Геолокация — --city и --district

См. подробно в разделе ниже. Кратко:

# Известный город (есть в config.CITIES)
python main.py --full --city "Москва" --category "стоматология" --limit 15

# Любой город / район — через district + общая зона
python main.py --full --city "Москва и МО" --district "Жуковский" --category "автосервис"
python main.py --full --city "Москва" --district "Митино" --category "салон красоты"

Категории — несколько через запятую

python main.py --full --category "автосервис,салон красоты,стоматология" --limit 20

Парсятся последовательно. Между категориями — пауза random(30, 60) сек (anti-bot).


География

3 уровня геолокации

Уровень CLI Поле в БД Пример
Регион (агрегатор) автоматом из конфига region "Москва", "Московская область"
Город --city "Москва" city "Москва", "Мытищи"
Район --district "Митино" district "Митино", "Жуковский"

Что в config.CITIES

CITIES = {
    "Москва":          {"yandex_id": 213, ...},   # ✓ проверено
    "Москва и МО":     {"yandex_id": 1,   ...},   # ✓ проверено
    "Санкт-Петербург": {"yandex_id": 2,   ...},   # ✓ проверено
}

Fallback для незнакомого города

Если указать --city которого нет в config.CITIES (и при этом --district пуст), парсер автоматически:

  1. Использует Москва и МО (geo_id 1) как базу
  2. Город уходит в --district
  3. В лог пишет ⚠️ предупреждение
# Указано "Жуковский" — нет в config → fallback на "Москва и МО" + район "Жуковский"
python main.py --full --city "Жуковский" --category "автосервис"

⚠️ Если указать и неизвестный city, и district одновременно — будет ошибка с подсказкой.

Как добавить точный город (5 минут)

  1. Открыть https://yandex.ru/maps/ → найти город в поиске
  2. Кликнуть на первый результат ("Мытищи, городской округ")
  3. URL станет https://yandex.ru/maps/10743/mytishchi/?...
  4. Вставить в config.CITIES:
    "Мытищи": {"yandex_id": 10743, "yandex_slug": "mytishchi",
               "vk_id": 1, "region": "Московская область"},
    
  5. Готово: python main.py --full --city "Мытищи" --category "автосервис"

Pipeline

┌─────────┐  ┌─────────┐  ┌──────────┐  ┌─────────┐  ┌──────────┐  ┌──────────┐  ┌────────┐  ┌──────┐
│1. Parse │→ │2. HH-emp│→ │3. Find-  │→ │4. Tier2 │→ │5. Tier 3 │→ │6. Black- │→ │7. Re-  │→ │8.CSV │
│ Я.Карты │  │   pages │  │  Sites   │  │ website │  │ DaData → │  │ list     │  │ score  │  │      │
│ + HH    │  │ employer│  │ DuckDuck │  │ analyzer│  │ Rusprof  │  │  filter  │  │ all    │  │      │
│ + body  │  │website+ │  │ + verify │  │ + footer│  │ fallback │  │ 143 leads│  │ leads  │  │      │
│ emails  │  │contacts │  │ by INN/  │  │ INN/OGRN│  │ + INN-by-│  │ excluded │  │        │  │      │
│         │  │         │  │ name     │  │   /KPP  │  │ site     │  │          │  │        │  │      │
└─────────┘  └─────────┘  └──────────┘  └─────────┘  └──────────┘  └──────────┘  └────────┘  └──────┘
 --source     --hh-enrich  --find-     --enrich      --enrich-       (auto в     --rescore   --export
              -websites    sites                     egrul           upsert)

Каждый шаг можно запустить независимо или в комбинации. Флаг --full запускает всю цепочку.

Сбор контактов в pipeline (важно):

  • Шаг 1 парсит карточки Я.Карт + HH-вакансии. Я.Карты помимо .orgpage-phones-view__phone-number сканирует весь видимый текст карточки через document.body.innerText → email/доп.телефоны из описания / «О компании». Технические домены Яндекса фильтруются.
  • Шаг 2 (HH employer-pages) для HH-лидов без сайта — заходит на страницу работодателя на hh.ru, тащит website + социальные сети + телефоны.
  • Шаг 3 (Find-Sites) для всех лидов без website — ищет сайт через DuckDuckGo HTML search. Верифицирует двумя способами: (а) ИНН лида должен встречаться на странице, или (б) название компании fuzzy-match (с очисткой от ОПФ-префиксов). Blocklist 80+ доменов агрегаторов (hh.ru, 2gis.ru, yell.ru, rusprofile.ru и т.п.).
  • Шаг 4 (Tier 2) analyze_website запрашивает сайт компании: CMS-сигнатуры (Tilda/Wix/1С-Bitrix/InSales), online-чат, online-запись, аналитика. Извлекает email/телефоны из mailto:/tel:. Сканирует footer + пробует 7 типичных contact-страниц (/contacts/, /kontakty/, /o-kompanii/, /rekvizity/, ...) на наличие ИНН/ОГРН/КПП (152-ФЗ disclosure). Если ИНН найден — авто-запуск enrich_egrul_by_inn(inn) (точное обогащение по ИНН без fuzzy-поиска).
  • Шаг 5 (Tier 3) ЕГРЮЛ-обогащение: сначала DaData Suggestions API (10K запросов/день бесплатно, индексирует бренды → находит «ВкусВилл» / «Кофемания» / «The Бык»), при неудаче — Rusprofile fallback. С учётом kladr_id для приоритета по городу (Москва/СПб). Возвращает: ИНН, ОГРН, КПП, директор (ФИО), адрес, дата регистрации, ОКВЭД.
  • Шаг 6 (Blacklist) при upsert_lead проверяется через blacklist.is_blacklisted(name) — три стратегии: точное имя (~140 компаний), keyword-fragments (~30 паттернов), prefix-matching. Лид помечается outreach_status='excluded' и пропускается фильтрами CRM.
  • Если расширил analyze_website (добавил новые сигнатуры/детекторы) — прогони --rescan-sites --enrich чтобы обогатить уже собранных лидов заново.

Скоринг

Формула v5«решаемая нами боль» × ICP, шкала 0-10 (логика в scoring.py, веса в config.py). Полное обоснование и история — D17 в ../DECISIONS.md.

Семантика: score отвечает на ОДИН вопрос — есть ли у компании проблемы, которые закрывают НАШИ продукты, и насколько остро. Не «качество лида», не «дозвонибельность».

score = pain(решаемая боль) × icp_fit(наш ли это размер)

Детекторы боли → продукт (config.SCORE_WEIGHTS)

Тема Сигнал Вес Продукт Применяется к
Запись/входящие нет онлайн-записи 2.0 P4 только услуги с записью (APPT_CATEGORIES)
нет онлайн-чата 1.0 P4 всем
Репутация рейтинг <3.5 / <4.0 / <4.5 2.0 / 1.5 / 1.0 P3 всем (4.5+ = не боль)
мало отзывов <10 / <30 1.0 / 0.5 P3 всем
Веб нет сайта / сайт мёртв 1.5 / 2.5 P10 всем
сайт на конструкторе (вкл. авто-визитки Я.Бизнес) 0.5 P10 всем (премиум топит ICP)
Маркетинг нет соцсетей (VK/Telegram) 1.0 P12 только B2C (SOCIAL_SALES_CATEGORIES)
нет веб-аналитики 0.5 P12 всем
Инфра почта на бесплатном домене 0.5 P2 всем

Внутри темы — насыщение max + 0.4·остальные (коррелированные сигналы не стакаются линейно). Сумма тем → raw_pain, нормировка /PAIN_NORM·10, cap 10. pain_products (JSON) = «с чем заходить» для CRM.

ICP-гейт — прогрессивный штраф за «зрелость» (множитель 0..1)

ЦА = малый/средний бизнес, которому нужна автоматизация. Чем больше отзывов и выше рейтинг — тем сильнее снижаем балл (процветающим/раскрученным труднее продать):

icp = 1  0.85 · rf · (0.65 + 0.35·gf)
  rf = min(1, отзывы / 6000)         # линейно по объёму (усилен 2026-06-05)
  gf = clamp((avg  4.0)/(5.0  4.0))  # по рейтингу

Отзывы штрафуют всегда (база 0.5), высокий рейтинг усиливает до полного. Мало отзывов → ≈1 (новый/борющийся бизнес сохраняет балл). На Я.Картах рейтинги зажаты 4.5-5.0 → главный рычаг это объём отзывов. Параметры — ICP_* в config.py.

«Уже автоматизирован» отдельно не режем — у такого лида просто нет болевых дыр (severity→0).

⚠️ Категорийная релевантность (читай при добавлении категории)

Часть болей применима не ко всем типам бизнеса. Подробная инструкция — в блоке config.py:

  • booking (P4) — только APPT_CATEGORIES (салон, клиника, кафе, автосервис…)
  • social (P12) — только SOCIAL_SALES_CATEGORIES (бьюти, HoReCa, розница…)
  • Новая/неизвестная категория по умолчанию НЕ получает booking/social, пока не впишешь ключевое слово в нужный список (консервативно — чтобы не завышать ложно).
  • Instagram в РФ забанен → в соцсетях учитываются только VK/Telegram.

Полнота диагностики

diagnostic_coverage (0..1) — доля проверенных тем. «Не проверяли» ≠ «нет боли»: при coverage < MIN_COVERAGE (0.5) лид помечается «нужно обогащение», а не ранжируется фальшиво.

Бэнды и пороги

  • 🔥 hot — score ≥ BAND_HOT (6) · 🟡 warm — 4-5 · cold — ≤3
  • HOT_LEAD_THRESHOLD = BAND_HOT (совместимость с CRM / экспортом)
  • PAIN_NORM = 5.0 — нормировочный делитель · SCORE_MAX = 10 — hard cap

Аудит скоринга

python audit_scores.py — независимая проверка всей БД (пересчёт каждого лида + санитарные правила: боль vs данные, устаревший score, целостность). Код возврата 1 при HARD-аномалиях — можно вешать на хук/CI.


Что в БД

Таблица leads — все поля:

Идентичность:    id, name, inn, ogrn, director_name
Контакты:        phones (доверенные — Я.Карты), phones_extra (с сайта, к проверке),
                 emails (JSON), phone_primary, email_primary
Онлайн:          website, vk_url, telegram_url, instagram_url, youtube_url
Гео:             address, city, region, district
Бизнес:          category
Сигналы:         reviews_count, reviews_avg, has_website, has_vk, has_telegram
Скоринг:         score, score_breakdown (JSON)
Email валидация: email_valid, email_checked_at
Tier 2:          site_alive, site_status_code, cms_type,
                 has_live_chat, has_online_booking, has_analytics,
                 email_domain_type, site_checked_at
Tier 3:          registration_date, egrul_checked_at, egrul_status
Outreach (auto): outreach_status, outreach_channel, outreach_sent_at, outreach_replied_at
CRM (ручной):    comments, last_action, last_reaction, last_touched_at
Системные:       source, source_id, source_url, parsed_at, updated_at,
                 phone_dedup_key, domain_dedup_key

Дополнительные таблицы:

  • sources_log — история прогонов (source, query, city, started_at, finished_at, счётчики, статус)
  • outreach_events — история всех касаний (звонок/email/VK/TG → реакция → комментарий). Пишется из CRM-приложения и в будущем — из Phase 3 auto-pipeline.
  • lead_in_run — junction-таблица «прогон ↔ лид» с пометкой inserted/merged. Нужна для --export-run N — пересоздания CSV конкретного прогона.

Статусы лида (outreach_status):

  • inbox / new — не разобран (default для свежих лидов)
  • triaged — посмотрел, оставил в работе
  • in_work — идёт работа (звоним, переписываемся)
  • done — закрыт (стал клиентом или отказался окончательно)
  • skip — пропустить (не наша ЦА)
  • queued / sent / replied / converted — для будущего auto-pipeline (Phase 3)

Дедупликация

upsert_lead() ищет дубль в порядке: ИНН → телефон (10 цифр) → домен. Найденный — мержит (объединяет phones/emails, заполняет пустые поля).

UNIQUE constraints на inn и phone_dedup_key.


CRM-приложение

Streamlit-приложение для работы со звонящего/писавшего человека: фильтр лидов, форма касания, история всех взаимодействий, заметки.

Запуск

# Двойной клик по launch_crm.bat либо:
streamlit run app/app.py

При первом запуске launch_crm.bat ставит streamlit>=1.35.0 через pip (~30 сек) и создаёт пустой ~/.streamlit/credentials.toml чтобы Streamlit не задавал вопрос про email.

Откроется браузер на http://localhost:8501. Работает на той же leads.db что и парсер — изменения видны сразу с обеих сторон.

Интерфейс

Левая панель — фильтры:

  • Быстрый фильтр: 📥 Inbox / 🔥 Hot (≥6) / ⚙️ В работе / Готовые / 🚫 Пропущенные / 📋 Все
  • Источник (multiselect)
  • Регион (multiselect)
  • Район содержит (текстовый поиск)
  • Категория (multiselect)
  • Статус (multiselect)
  • Диапазон score (slider 010)
  • Поиск по имени

Сверху — 5 метрик: Всего в БД, Inbox, В работе, Готовых, Под текущий фильтр.

Таблица лидов — клик по строке открывает детальную карточку под таблицей.

Карточка лида:

  • 3 колонки: контакты (телефон/email/сайт/VK/TG/Instagram), бизнес (категория/отзывы/директор/ИНН), статус (score / последнее касание).
  • Форма « Записать касание»: действие (звонок/email/VK/TG/WhatsApp/SMS) + реакция (не ответили / отказ / согласились / перешли в TG / перезвонить / не наша ЦА / спам) + комментарий + новый статус лида.
  • «📝 Заметки» — свободные заметки о лиде (отдельно от истории касаний).
  • «📜 История касаний» — все события по этому лиду из таблицы outreach_events.

Автомиграция БД

При запуске app.py вызывается database.init_db(...) — это автоматически догоняет схему: добавляет 4 CRM-колонки, таблицы outreach_events и lead_in_run если их ещё нет. То есть приложение работает на старой БД без подготовки.

Файлы

  • app/app.py — Streamlit UI (~380 строк)
  • app/db_layer.py — слой работы с БД (~270 строк): фильтры, детали, история, запись касаний, метрики
  • launch_crm.bat — Windows-launcher с автоустановкой streamlit и автокредами

Структура

parser_v1/
├── README.md                      ← этот файл
├── launch.bat                     ← 🚀 запуск парсера через TUI-меню (двойной клик)
├── launch_crm.bat                 ← 🎯 запуск CRM-приложения в браузере (двойной клик)
├── launcher.py                    ← TUI-меню на questionary (запуск парсера)
├── requirements.txt
├── .gitignore
│
├── config.py                      # CITIES, CATEGORIES, веса скоринга, задержки
├── database.py                    # SQLite: схема, миграции, upsert, start/finish_source_run, update_lead_contacts
├── normalization.py               # Телефоны (E.164), домены, ratings, extract_emails/phones_from_text
├── scoring.py                     # Формула скоринга v4 (0-10, hard cap)
├── logger_setup.py                # Цветной вывод (colorlog)
├── main.py                        # CLI — точка входа парсера
├── leads.db                       # ⚠️ создаётся при первом запуске
│
├── parsers/
│   ├── base.py                    # BaseParser: retry, sleep, captcha-counter
│   ├── yandex_maps.py             # Я.Карты (Botasaurus + JS-скролл + body-text email/phones scan)
│   └── hh.py                      # HH.ru — signal-запросы через Botasaurus → employers как лиды
│
├── enricher/
│   ├── website_analyzer.py        # Tier 2: CMS, чат, запись, аналитика + email/phones + ИНН/ОГРН/КПП из footer (152-ФЗ, 7 contact-страниц)
│   ├── contacts_finder.py         # 🔎 Поиск сайта через DuckDuckGo HTML + верификация (INN на странице или fuzzy-match имени + blocklist 80+ агрегаторов)
│   ├── dadata_enricher.py         # 🏛 ЕГРЮЛ через DaData Suggestions API (primary, 10K/день free, kladr_id для Москва/СПб приоритета)
│   ├── egrul_enricher.py          # 🏛 ЕГРЮЛ через Rusprofile (fallback) + enrich_egrul_by_inn (точное обогащение)
│   └── blacklist.py               # 🚫 Чёрный список крупных компаний — 143 лида исключено (банки/ритейл/гос)
│
├── export/
│   └── csv_export.py              # CSV: export_run (per-прогон), export_master (snapshot всей БД), export_to_csv (плоский по min_score)
│
├── app/                           ← 🎯 CRM-приложение (Streamlit)
│   ├── app.py                     #    UI: фильтры + таблица + карточка лида + форма касания
│   └── db_layer.py                #    Слой работы с БД (фильтры, история, запись касаний)
│
├── exports/                       # ⚠️ создаётся при первом экспорте
│   ├── 2026-05/                   #    Файлы прогонов сгруппированы по месяцу
│   │   └── leads_<src>_<query>_<city>_<timestamp>.csv
│   └── _master/
│       └── all_leads.csv          #    Snapshot всей БД (--export-master, перезапись)
│
└── _archive/
    └── phase0/                    # тесты Phase 0 (Botasaurus discovery)
        ├── test_yandex.py
        ├── test_yandex_v2.py
        └── test_yandex_v3.py

PyCharm

Run Configuration

  1. Run → Edit Configurations → main
  2. Script parameters: например --full --category "кафе" --limit 15
  3. Working directory: parser_v1 (полный путь). Важно — иначе leads.db создастся не там
  4. Apply → Run

Terminal

Alt+F12 (или View → Tool Windows → Terminal). venv активируется автоматически. Удобно для произвольных команд:

python main.py --full --city "Москва" --district "Митино" --category "автосервис" --limit 10

Troubleshooting

Скролл подгружает мало карточек

Симптом: в логе видно Скролл #1: карточек 5 (+0) подряд → стоп через 3 итерации.

Причина: Яндекс кеширует страницу при reuse_driver=True или замедляется.

Что делать:

  • Подожди 30 сек, попробуй заново — обычно помогает
  • Закрой все окна Chrome, перезапусти

404 на URL вида /maps/{geo_id}/{slug}/

Причина: неправильный yandex_id или yandex_slug в config.CITIES.

Что делать: удали запись для этого города из config.CITIES — fallback автоматически переключит на "Москва и МО" + район. Или добавь точные значения через инструкцию выше.

Captcha от Яндекса

Симптом: ⚠️ CAPTCHA. Останавливаемся. или ⚠️ Бот-детектор: cloudflare.

Что делать:

  • Подожди 1-2 часа (Яндекс снимает блок сам)
  • Уменьши --limit (например 10 вместо 50)
  • Увеличь задержки в config.MIN_DELAY / config.MAX_DELAY
  • Если повторяется — переключиться на VPS или использовать Tor

DaData/Rusprofile не находит компанию (не найдено в ЕГРЮЛ)

Это нормально для ~70% компаний (текущий success rate ~29.6%). Причины:

  • Юр.название отличается от названия в Я.Картах / HH (например, бренд «Кофемания» = ООО «КАФЕ К» — DaData находит, Rusprofile нет)
  • Кавычки или специальные символы (буква "ъ" в "Кафе Пушкинъ" мешает)
  • Это ИП без публичного бренда

Что помогает:

  • На сайте может быть ИНН в footer — analyze_website его извлечёт автоматически (152-ФЗ), и тогда обогащение идёт по ИНН (точно).
  • Запустить --find-sites чтобы сначала найти сайты, потом --enrich (поднимет много ИНН с footer'ов).

DaData 401/403

Симптом: DaData error: 401 Unauthorized.

Причина: не задан DADATA_API_KEY / DADATA_SECRET_KEY в .env.

Решение: скопировать .env.example.env, вставить ключи с https://dadata.ru/profile/#info. Файл .env — в .gitignore, в git не попадёт.

IntegrityError: UNIQUE constraint failed: leads.inn

Симптом: при ЕГРЮЛ-обогащении одного из лидов.

Причина: этот ИНН уже в БД у другого лида (одна сеть с разными филиалами).

Что делает парсер: ловит исключение → пишет остальные поля без ИНН → помечает в логе (DUP по ИНН). Прогон не падает.

Connection to remote host was lost. - goodbye

Не ошибка — это сообщение когда Botasaurus закрывает CDP-соединение с Chrome в конце прогона.

"Должность" вместо ФИО директора

Симптом: в БД director_name = "Генеральный директор" или похожее.

Причина: старые записи, обогащённые до фикса валидатора ФИО.

Решение: запустить

python main.py --cleanup-directors --enrich-egrul --rescore --export

Это очистит мусорные ФИО + повторно прогонит ЕГРЮЛ для очищенных + пересчитает.

HH 403 Forbidden для api.hh.ru

Симптом: в логе HH 403 даже с TLS-fingerprint Chrome.

Причина: Cloudflare защищает api.hh.ru от bulk-запросов. Не помогают ни requests, ни botasaurus_requests с Chrome TLS.

Решение: парсер уже использует Botasaurus браузер (через сайт hh.ru/search/vacancy). См. D10 в ../DECISIONS.md. Если ошибка осталась после этого — закрой Chrome и перезапусти.

HH модал "Вы из Москвы?"

Симптом: окно Chrome зависло на регионального модале.

Что делает парсер: автоматически закрывает через _dismiss_hh_modals() (JS клик на "Да, верно"). После первого закрытия cookie сохраняется в user_data/ — модал больше не появляется.

Если всё равно показывается: закрой руками в окне Chrome — парсер продолжит.

HH парсинг карточек "зависает" на каждой странице

Симптом: Sleeping for 3 seconds... потом тишина 1-2 минуты, потом результат.

Причина: Botasaurus select_all без wait=None ждёт 5 сек на каждом элементе (50 карточек × 3 поля = 12 минут зависа).

Что делает парсер: в parsers/hh.py все select внутри циклов карточек идут с wait=None. На странице 5-10 секунд вместо 12 минут.


Прогресс по фазам

Phase 1 — MVP DONE (2026-05-03)

  • Скелет (config, normalization, database, base, scoring)
  • Yandex.Maps парсер (Botasaurus + JS-скролл)
  • CSV экспорт (utf-8-sig для Excel)
  • CLI (main.py)
  • Tier 2 enrichment (website analyzer — CMS / чат / запись / аналитика)
  • Tier 3 enrichment (ЕГРЮЛ / Rusprofile + валидатор ФИО)
  • Pipeline --full
  • Геолокация (--city + --district + fallback на "Москва и МО")
  • Цветные логи (colorlog)
  • DoD: 100+ уникальных лидов в CSV (116 лидов)

Phase 2 — Scale 🟢 ACTIVE

  • HH.ru парсер через Botasaurus DONE 2026-05-04 — signal-запросы → employers как лиды
  • CRM-блок DONE 2026-05-17 — БД-поля статусов/действий/реакций, таблица outreach_events, junction lead_in_run, Streamlit-приложение (app/), launch_crm.bat. Подробнее — D11 в ../DECISIONS.md.
  • Расширенный сбор контактов DONE 2026-05-17 — Я.Карты сканируют тело карточки на email/доп.телефоны, website-analyzer сохраняет найденные email/телефоны в лида через update_lead_contacts. Флаг --rescan-sites для перепрогона старых лидов. Подробнее — D12.
  • CSV экспорт двух уровней DONE 2026-05-17 — export_run() (per-прогон с шапкой в exports/YYYY-MM/) + export_master() (snapshot всей БД).
  • DDG Find-Sites DONE 2026-05-20 — contacts_finder.py: поиск сайта через DuckDuckGo HTML + верификация (INN/fuzzy-name + blocklist 80+ агрегаторов). Подробнее — D16.
  • DaData primary + Rusprofile fallback DONE 2026-05-20 — dadata_enricher.py: индексирует бренды (находит «ВкусВилл»/«Кофемания»), kladr_id для приоритета по городу. Success rate вырос с ~10% до ~29.6%. Подробнее — D14.
  • INN из footer сайта (152-ФЗ) DONE 2026-05-20 — analyze_website пробует 7 contact-страниц, тащит ИНН/ОГРН/КПП → авто-enrich_egrul_by_inn (точно). Подробнее — D16.
  • Blacklist крупных компаний DONE 2026-05-20 — blacklist.py: 143 лида с outreach_status='excluded'. Три стратегии: точные имена / keyword-fragments / prefix. Подробнее — D15.
  • WB парсер удалён DONE 2026-05-20 — селлеры маркетплейса не наша ЦА. Подробнее — D17.
  • 🔔 Скоринг: SPA-слепотаwebsite_analyzer (requests) не исполняет JS → у JS-SPA сайтов (React/Vue/Next, напр. niqa.ru = 759 байт каркас) видит пустышку → ложные «всё отсутствует» → завышенный балл. Фикс: тонкий каркас → сигналы unknown, не 0 (+ опц. вес авто-визитки 0.5→1.5). Кейс: Niqa должен быть < Кафе Гурмэ. Решения ①②③ — см. DECISIONS/память.
  • 🔔 Финансы (оборот + численность) — обвяз готов (колонки employee_count/revenue, шаг --enrich-finance, CRM-вывод), но бесплатный DaData финансы НЕ отдаёт (проверено: всё null). Нужен источник: Rusprofile-добор (проверить первым) / ФНС открытые данные bulk / платный DaData. bo.nalog.ru = JS-SPA с токеном (простой REST не идёт). При рабочем источнике — сбросить finance_checked_at.
  • Свежие ИП — Rusprofile «Новые компании» как самостоятельный источник лидов
  • 2ГИС парсер — в доработке (постпонировано на потом)
  • VK парсер — в доработке (постпонировано на потом)
  • Avito парсер (anti-bot самый агрессивный)
  • Telegram парсер (TGStat API)
  • SMTP-валидация email
  • LLM Pain Detection (gpt-4o-mini анализ отзывов Я.Карт)
  • Migration SQLite → Supabase

Phase 3 — Outreach pending

  • Ручной CRM-режим через Streamlit-приложение DONE в Phase 2
  • n8n pipeline для авто-рассылок
  • Telegram bot для управления

Phase 4 — Monitor pending

  • Cron еженедельного парсинга
  • Дельта-синхронизация (только новые)
  • Telegram-отчёты Теме

Anti-bot настройки

  • Задержки между запросами: random.uniform(MIN_DELAY, MAX_DELAY) — по умолчанию 2-7 сек
  • Между категориями: random.uniform(CATEGORY_PAUSE_MIN, CATEGORY_PAUSE_MAX) — по умолчанию 30-60 сек
  • Botasaurus имитирует браузер на уровне fingerprint
  • При срабатывании captcha N раз — стоп с алертом (config.MAX_BLOCKED_TRIES = 3)
  • Tor / прокси не используем (D4 в ../DECISIONS.md). Подключим если домашний IP начнёт лочить.

Внешние ссылки


Версия: v7.1 (Прогрессивный ICP-штраф за зрелость) | Обновлено: 2026-06-01 Changelog: - v7.1 (2026-06-01): ICP-гейт стал прогрессивным — штраф за отзывы×рейтинг (D18): линейная шкала отзывов, рейтинг усиливает, база гарантирует штраф сетям. Порог hot 7→6. Калибровка под цель «Кафе Гурмэ=6». - v7 (2026-06-01): Скоринг переписан на v5 — «решаемая нами боль» × ICP (D17). Категорийная релевантность детекторов (booking/social по белым спискам, дефолт консервативный). Фикс потери URL сайта в ЕГРЮЛ-обогащении + восстановление 29 лидов. Рейтинг 4.5+ больше не боль. Instagram исключён (бан РФ). Новые колонки pain_products/band/diagnostic_coverage. CRM: бэнды + «с чем заходить» + фильтр по продукту. Детект конструкторов по домену (Я.Бизнес .clients.site). Инструмент audit_scores.py. - v6 (2026-05-21): WB парсер удалён (D17). DaData primary + Rusprofile fallback (D14). DDG Find-Sites (contacts_finder.py D16). INN из footer сайта (152-ФЗ). Blacklist крупных компаний (blacklist.py D15, 143 leads excluded). --full теперь включает --find-sites + --hh-enrich-websites если source=hh. - v5 (2026-05-17): CRM-блок + расширенный сбор контактов (Я.Карты body-text + website email/phones).