# 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/телефоны из `` / `` + **ИНН/ОГРН/КПП из 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`): таблица лидов, фильтры, форма касания. ```bash # 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-тесты](#smoke-тесты) 3. [Команды](#команды) - [📖 Справочник по всем флагам](#-справочник-по-всем-флагам) 4. [География: город / район / регион](#география) 5. [Pipeline и его шаги](#pipeline) 6. [Скоринг лидов](#скоринг) 7. [Что в БД](#что-в-бд) 8. [CRM-приложение (Streamlit)](#crm-приложение) 9. [Структура проекта](#структура) 10. [PyCharm флоу](#pycharm) 11. [Troubleshooting](#troubleshooting) 12. [Прогресс по фазам](#прогресс-по-фазам) --- ## Установка ```bash # Создать 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-тесты Перед первым запуском проверь что модули работают: ```bash 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 ```bash # Я.Карты — нужно явно указать --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____.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 | --- ### 💡 Какие флаги можно комбинировать #### ✅ Типичные комбинации ```bash # Только парсинг, без обогащения 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 ```bash # Полный цикл одной командой (Парсинг → 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` См. подробно в [разделе ниже](#география). Кратко: ```bash # Известный город (есть в 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 "салон красоты" ``` ### Категории — несколько через запятую ```bash python main.py --full --category "автосервис,салон красоты,стоматология" --limit 20 ``` Парсятся последовательно. Между категориями — пауза `random(30, 60)` сек (anti-bot). --- ## География ### 3 уровня геолокации | Уровень | CLI | Поле в БД | Пример | |---|---|---|---| | **Регион** (агрегатор) | автоматом из конфига | `region` | "Москва", "Московская область" | | **Город** | `--city "Москва"` | `city` | "Москва", "Мытищи" | | **Район** | `--district "Митино"` | `district` | "Митино", "Жуковский" | ### Что в `config.CITIES` ```python CITIES = { "Москва": {"yandex_id": 213, ...}, # ✓ проверено "Москва и МО": {"yandex_id": 1, ...}, # ✓ проверено "Санкт-Петербург": {"yandex_id": 2, ...}, # ✓ проверено } ``` ### Fallback для незнакомого города Если указать `--city` которого **нет** в `config.CITIES` (и при этом `--district` пуст), парсер автоматически: 1. Использует `Москва и МО` (geo_id 1) как базу 2. Город уходит в `--district` 3. В лог пишет ⚠️ предупреждение ```bash # Указано "Жуковский" — нет в 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`: ```python "Мытищи": {"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`](../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-приложение для работы со звонящего/писавшего человека: фильтр лидов, форма касания, история всех взаимодействий, заметки. ### Запуск ```powershell # Двойной клик по 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____.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 активируется автоматически. Удобно для произвольных команд: ```bash 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 автоматически переключит на "Москва и МО" + район. Или добавь точные значения через [инструкцию выше](#как-добавить-точный-город-5-минут). ### 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 = "Генеральный директор"` или похожее. **Причина:** старые записи, обогащённые до фикса валидатора ФИО. **Решение:** запустить ```bash 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) - [x] Скелет (config, normalization, database, base, scoring) - [x] Yandex.Maps парсер (Botasaurus + JS-скролл) - [x] CSV экспорт (utf-8-sig для Excel) - [x] CLI (main.py) - [x] Tier 2 enrichment (website analyzer — CMS / чат / запись / аналитика) - [x] Tier 3 enrichment (ЕГРЮЛ / Rusprofile + валидатор ФИО) - [x] Pipeline `--full` - [x] Геолокация (`--city` + `--district` + fallback на "Москва и МО") - [x] Цветные логи (colorlog) - [x] **DoD: 100+ уникальных лидов в CSV** ✅ (116 лидов) ### Phase 2 — Scale 🟢 ACTIVE - [x] **HH.ru парсер через Botasaurus** ✅ DONE 2026-05-04 — signal-запросы → employers как лиды - [x] **CRM-блок** ✅ DONE 2026-05-17 — БД-поля статусов/действий/реакций, таблица `outreach_events`, junction `lead_in_run`, Streamlit-приложение (`app/`), `launch_crm.bat`. Подробнее — D11 в `../DECISIONS.md`. - [x] **Расширенный сбор контактов** ✅ DONE 2026-05-17 — Я.Карты сканируют тело карточки на email/доп.телефоны, website-analyzer сохраняет найденные email/телефоны в лида через `update_lead_contacts`. Флаг `--rescan-sites` для перепрогона старых лидов. Подробнее — D12. - [x] **CSV экспорт двух уровней** ✅ DONE 2026-05-17 — `export_run()` (per-прогон с шапкой в `exports/YYYY-MM/`) + `export_master()` (snapshot всей БД). - [x] **DDG Find-Sites** ✅ DONE 2026-05-20 — `contacts_finder.py`: поиск сайта через DuckDuckGo HTML + верификация (INN/fuzzy-name + blocklist 80+ агрегаторов). Подробнее — D16. - [x] **DaData primary + Rusprofile fallback** ✅ DONE 2026-05-20 — `dadata_enricher.py`: индексирует бренды (находит «ВкусВилл»/«Кофемания»), kladr_id для приоритета по городу. Success rate вырос с ~10% до ~29.6%. Подробнее — D14. - [x] **INN из footer сайта (152-ФЗ)** ✅ DONE 2026-05-20 — `analyze_website` пробует 7 contact-страниц, тащит ИНН/ОГРН/КПП → авто-`enrich_egrul_by_inn` (точно). Подробнее — D16. - [x] **Blacklist крупных компаний** ✅ DONE 2026-05-20 — `blacklist.py`: 143 лида с `outreach_status='excluded'`. Три стратегии: точные имена / keyword-fragments / prefix. Подробнее — D15. - [x] **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 - [x] Ручной 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).*