- 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>
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
📋 Содержание
- Установка
- Smoke-тесты
- Команды
- География: город / район / регион
- Pipeline и его шаги
- Скоринг лидов
- Что в БД
- CRM-приложение (Streamlit)
- Структура проекта
- PyCharm флоу
- Troubleshooting
- Прогресс по фазам
Установка
# Создать 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). Делает:
- Парсинг одного источника (HH или Я.Карты)
- HH employer-pages (если HH) — заходит на страницу работодателя, тащит website + контакты
- DDG find-sites — для лидов без website ищет сайт через DuckDuckGo + верифицирует
- Tier 2 — анализ сайтов (CMS, чат, запись, аналитика) + извлечение ИНН/ОГРН/КПП из footer (152-ФЗ)
- Tier 3 — ЕГРЮЛ через DaData (primary) → Rusprofile (fallback). Если на сайте нашли ИНН — обогащение по ИНН (точно).
- Blacklist filter — автоматически отсекает крупные публичные компании (банки, ритейл-сети, госструктуры)
- Rescore всех лидов в БД (включая старых)
- 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 — возьмёт defaultyandex)--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 пуст), парсер автоматически:
- Использует
Москва и МО(geo_id 1) как базу - Город уходит в
--district - В лог пишет ⚠️ предупреждение
# Указано "Жуковский" — нет в config → fallback на "Москва и МО" + район "Жуковский"
python main.py --full --city "Жуковский" --category "автосервис"
⚠️ Если указать и неизвестный city, и district одновременно — будет ошибка с подсказкой.
Как добавить точный город (5 минут)
- Открыть https://yandex.ru/maps/ → найти город в поиске
- Кликнуть на первый результат ("Мытищи, городской округ")
- URL станет
https://yandex.ru/maps/10743/mytishchi/?... - Вставить в
config.CITIES:"Мытищи": {"yandex_id": 10743, "yandex_slug": "mytishchi", "vk_id": 1, "region": "Московская область"}, - Готово:
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 0–10)
- Поиск по имени
Сверху — 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
- Run → Edit Configurations → main
- Script parameters: например
--full --category "кафе" --limit 15 - Working directory:
parser_v1(полный путь). Важно — иначеleads.dbсоздастся не там - 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, junctionlead_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 начнёт лочить.
Внешние ссылки
- Botasaurus: https://github.com/omkarcloud/botasaurus
- Yandex.Maps URL формат:
https://yandex.ru/maps/{geo_id}/{slug}/search/{query}/ - Rusprofile (ЕГРЮЛ fallback): https://www.rusprofile.ru/
- DaData Suggestions API (primary ЕГРЮЛ): https://dadata.ru/api/suggest/party/ (10K/день free)
- DuckDuckGo HTML (find-sites): https://html.duckduckgo.com/html/
- VK API: https://dev.vk.com/reference (постпонировано)
- 2ГИС API: https://docs.2gis.com/ru/api/search/3.0 (постпонировано)
- HH: через Botasaurus браузер (api.hh.ru 403'ит)
Версия: 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).