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

774 lines
56 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`): таблица лидов, фильтры, форма касания.
```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_<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 |
---
### 💡 Какие флаги можно комбинировать
#### ✅ Типичные комбинации
```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 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 активируется автоматически. Удобно для произвольных команд:
```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).*