Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
102 lines
10 KiB
Markdown
102 lines
10 KiB
Markdown
# Парсер 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): парсер сам работает без ключа.
|