Lead Radar: исходный код

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
yuriandClaude Opus 5.5 committed 2026-10-25 23:11:04 +04:00
commit 9749da5ae7
201 files changed
+97685

No files matched your search

+101
View File
@@ -0,0 +1,101 @@
# Парсер List.am
Модуль взят из прежней CRM (`apps/api/src/import` → `apps/api/src/parser`) и доработан.
Конвейер, который им пользуется, — `apps/api/src/ingest/service.ts`.
## Как это работает
1. **Все категории недвижимости** list.am читаются по кругу: квартиры, дома, коммерческая,
участки, гаражи — продажа и аренда (10 категорий). Напрямую на list.am сервис не ходит:
сайт закрыт Cloudflare, страницы читаются через публичный ридер Jina (`r.jina.ai`).
2. **Свежесть.** Список list.am отсортирован по «поднятию», а блок «Топ» ротируется, поэтому
незнакомое объявление ещё не значит новое. Возраст определяется по дате «Размещено» на
странице объявления. В базу попадают объявления, размещённые **не раньше чем 3 дня назад**
(`FRESH_DAYS`). Более старые запоминаются только по ID (`seen_listings`), чтобы не
перечитывать их при каждом «поднятии».
3. **Общая база.** Свежее объявление сохраняется в таблицу `listings` один раз, независимо от
того, есть ли под него фильтр у клиентов.
4. **Раздача.** Объявление из базы сопоставляется с фильтрами всех компаний с действующей
подпиской; где подошло — создаётся лид в пуле и уходят уведомления в Telegram.
Новый или изменённый фильтр сразу получает подходящие объявления из базы за последние
3 дня — в пул, без уведомлений.
5. **Первый проход** по категории наполняет базу без лидов: эти объявления висели на сайте
до подключения.
## Что собирается
ID, ссылка, тип сделки, тип объекта, область и район (или населённый пункт), адрес, цена и
валюта, комнаты, площадь, этаж и этажность, описание, обложка фото, метка «Агентство»,
дата размещения.
## Ключ ридера Jina (`JINA_API_KEY`)
Ключ оплачивается по объёму ответа (токенам), поэтому используется экономно:
| Что читается | Как | Объём |
| --- | --- | --- |
| Страница категории (10 штук за круг) | **без ключа**, бесплатно, не чаще ~20 запросов в минуту | ~22 000 токенов за страницу — с ключом это сожгло бы баланс за часы |
| Дата размещения незнакомого объявления | с ключом: HTML-вырезка подвала страницы | ~330 токенов |
| Детали свежего объявления | с ключом: HTML-вырезка блоков с адресом, характеристиками, описанием | 1 000–6 000 токенов |
Если ключ отклонён или баланс кончился, парсер на час переходит на режим без ключа и продолжает
работать. Без ключа совсем: дата известна с точностью до дня, детали подтягиваются примерно у
половины объявлений, круг длится 2–3 минуты.
## Замеры (08.10.2026, локальный стенд)
| Что | С ключом | Без ключа |
| --- | --- | --- |
| Проход парсера по 10 категориям | 31–35 с | 107–200 с |
| Полный круг (проход + пауза 5 с) | около 37 с | 2–3,5 мин |
| Ошибки чтения за проход | 0 | 0–2 |
| Первый проход (наполнение базы) | — | около 10 минут |
| Задержка «размещено → лид» | 41 с (один замер: размещено в 20:50:05, лид в 20:50:46) | не измеряется: нет времени размещения |
Объявление попадает в базу в течение одного круга после появления на первой странице категории,
то есть обычно быстрее минуты. Круг ограничен бесплатным чтением страниц категорий: 10 страниц
с паузой ~3 с. Замер задержки пока единичный — медиана за сутки видна суперадмину в разделе «Парсер».
Расход ключа на длинном отрезке не измерен: баланс в кабинете Jina обновляется с задержкой.
Оценка по объёму ответов — несколько миллионов токенов в сутки.
## Ограничения
| Что | Состояние |
| --- | --- |
| Телефон и имя автора | Не собираются: list.am отдаёт телефон отдельным запросом по нажатию «Позвонить». Решение заказчика: агент берёт номер по ссылке на объявление |
| Фото | Только обложка |
| Область вне Еревана | Определяется по названию населённого пункта в конце карточки и справочнику городов (`packages/shared/src/regions.ts`). Село, которого нет в справочнике, остаётся без области и не проходит фильтр по области |
| Площадь домов | Иногда берётся площадь участка вместо площади дома |
| Устойчивость | Своей защиты от блокировок нет: всё зависит от ридера. Если страница объявления не прочиталась, объявление не теряется — попытка повторится на следующем круге. При сбое всех категорий суперадмин получает алерт в Telegram (не чаще раза в час) |
## Наблюдения о list.am
- ID объявлений растут по порядку, около 4 000 в день. Это используется как дешёвый
предварительный отсев: ID, отстающий от последних больше чем на 20 000, заведомо старше 3 дней.
- У list.am две нумерации: старая (до ~24,3 млн, закончилась в сентябре 2026) и новая «995…».
## Запасной источник: фид (`LISTING_SOURCE=feed`)
`FeedListingSource` читает JSON-фид по `LISTING_FEED_URL` (официальный API или провайдер данных).
К конвейеру он пока не подключён — понадобится, если появится источник с телефонами.
Формат: `GET {url}?city=…&category=…&ownerOnly=…[&cursor=…]`, заголовок
`Authorization: Bearer {LISTING_FEED_TOKEN}`; ответ `{ "items": [...], "next": "курсор или null" }`.
Обязательны только `id` и `url`; остальные поля: `title`, `description`, `price`, `currency`,
`dealType`, `propertyType`, `region`, `district`, `address`, `rooms`, `floor`, `floorsTotal`,
`area`, `ownerName`, `phone`, `isAgency`, `publishedAt`, `photos`.
## Снятые и повторно опубликованные объявления
- Раз в 10 минут проверяется пачка из 20 объявлений, по которым есть лиды; каждое объявление —
раз в сутки, сначала те, что кто-то брал в работу. Запросы идут без ключа ридера.
- Признак снятия (замер 08.10.2026): у страницы нет подвала с номером объявления (ридер отвечает
422), а заголовок — «Данного объявления не существует». Если подвала нет, но и такого заголовка
нет, решение откладывается на час.
- Снятое объявление снова появилось в списке категории под тем же номером — оно становится
активным, и компании, у которой прошлый лид закрыт сделкой или отказом, приходит новый лид.
- Объект под новым номером связывается с прошлым лидом, если совпали адрес и телефон автора.
Телефон парсер не собирает, поэтому на практике работает запасное правило: совпали адрес,
тип, комнаты, площадь и этаж, а прошлое объявление снято. Возможна ложная связь для одинаковых
квартир в одном доме — она влияет только на блок «Уже был в работе».
- 08.10.2026 пробный баланс ключа Jina закончился (ответ 402): парсер сам работает без ключа.