Версія: 3.0 (оновлено після реалізації MVP 2026-06-12) Статус: MVP реалізовано та протестовано
Створити сервіс-парсер, який збирає актуальні оголошення про нерухомість з порталу DOM.RIA (українська версія) та передає їх до локального сервісу data_collector (http://localhost:8020).
Важливо: на поточному етапі ми лише збираємо дані (one-shot або періодичний crawl). Інкрементальне оновлення, перевірка архівів та історія цін — не входять у MVP; ці задачі делеговані
data_collectorабо відкладені (див. розділ 7).
curl_cffi (імітація TLS + cookies)Офіційний REST API (developers.ria.com) має жорсткі ліміти безкоштовного пакету: 30 запитів/годину, 1000/місяць. Цього недостатньо для обходу всієї України.
В ході дослідження знайдено робочий спосіб скрейпингу без браузера:
curl_cffi із параметром impersonate="chrome124" імітує повний TLS/JA3 fingerprint, заголовки та поведінку реального Chrome 124.requests.Session — перед запитом каталогу необхідно відкрити головну сторінку https://dom.ria.com/uk/, щоб отримати session-cookies./{operation}-{type}/{city}/ (наприклад, /uk/prodazha-kvartir/kiev/). URL із дефісом перед містом (prodazha-kvartir-kiev/) повертає 404.undetected-chromedriver, Playwright (headless та non-headless) — усі повертають serverStatus: 404 і порожній каталог. Сайт використовує додатковий рівень детекції (ймовірно, IP/cookie-based + fingerprint), який блокує автоматизовані браузери навіть при повній емуляції. curl_cffi обходить цей бар’єр, оскільки не є браузером, а імітує його на мережевому рівні.
Після успішного запиту HTML-сторінки в тілі міститься inline-скрипт із глобальною змінною window.__INITIAL_STATE__:
catalog.realtyForCatalog[] — масив об’єктів оголошень на поточній сторінці (20–27 шт.).listing.data.realty — повний об’єкт оголошення.catalog.realtyCountCatalog — загальна кількість результатів.?page=N. Перевірено: page=1, page=2 — працюють.Важливі нюанси структури даних (виявлені емпірично):
priceArr у каталозі — це dict {"1": "130 000", "2": "112 317", "3": "5 863 000"} (USD / EUR / UAH відповідно), а не список об’єктів.price_USD часто відсутній у каталозі; детальна сторінка містить явний priceObj.priceUSD.file (відносний шлях на cdn.riastatic.com).window.__INITIAL_STATE__ зустрічається у форматі window.__INITIAL_STATE__={...} без пробілів навколо =. Парсер повинен бути толерантним до цього.| Категорія / Операція | Київ | Львів | Одеса | Харків |
|---|---|---|---|---|
| Продаж квартир | prodazha-kvartir/kiev/ |
prodazha-kvartir/lvov/ |
prodazha-kvartir/odessa/ |
prodazha-kvartir/kharkov/ |
| Продаж будинків | prodazha-domov/kiev/ |
prodazha-domov/lvov/ |
prodazha-domov/odessa/ |
prodazha-domov/kharkov/ |
| Оренда квартир | arenda-kvartir/kiev/ |
arenda-kvartir/lvov/ |
arenda-kvartir/odessa/ |
arenda-kvartir/kharkov/ |
Важливо: slug міста може відрізнятися від очікуваного (наприклад,
lvovзамістьlviv,odessaзамістьodesa,kharkovзамістьkharkiv). Повний перелік слід витягти з головної сторінки або sitemap.
developers.ria.com.search-engine. Якщо curl_cffi перестане працювати, можна дослідити endpoint /uk/search-engine/ (вимагає apiKey з __INITIAL_STATE__).Вся Україна. Парсер повинен обходити всі доступні міста/області, а не обмежуватися Києвом.
category |
Тип об’єкта | operation_type |
|---|---|---|
1 |
Квартири / кімнати | 1 — Продаж, 2 — Довгострокова оренда, 3 — Подобова оренда |
2 |
Будинки / дачі / котеджі | 1, 2, 3 |
3 |
Земельні ділянки | 1, 2 |
4 |
Комерційна нерухомість | 1, 2, 3 |
5 |
Гаражі / парковки | 1, 2 |
Для кожної комбінації
category × realty_type × operation_type × city_idформується окремий crawl.
Зібрати максимально можливий набір полів. Прості та універсальні поля нормалізувати; рідкісні, вкладені або змінні — залишити в сирому вигляді в підоб’єкті characteristics_raw.
| Поле | Джерело в __INITIAL_STATE__ |
Тип |
|---|---|---|
external_id |
listing.data.realty_id |
string |
url |
Сформувати з beautiful_url або шаблону |
string |
title |
advert_title (якщо є) або згенерувати з адреси |
string |
operation |
advert_type_name |
string (продаж / оренда) |
category |
realty_type_parent_name |
string |
realty_type |
realty_type_name |
string |
price.amount |
price або price_total |
number |
price.currency |
currency_type |
string |
price.price_per_sqm |
price_item |
number |
address.city |
city_name |
string |
address.district |
district_name |
string |
address.street |
street_name |
string |
address.building |
building_number_str |
string |
location.lat |
latitude |
number |
location.lon |
longitude |
number |
area.total |
total_square_meters |
number |
area.living |
living_square_meters |
number |
area.kitchen |
kitchen_square_meters |
number |
area.plot |
ares_count або lot_unit (для будинків/землі) |
number |
rooms |
rooms_count |
number |
floor.current |
floor |
number |
floor.total |
floors_count |
number |
year_built |
characteristics_values[443] (з мапінгом) |
number |
wall_type |
wall_type (текст) або characteristics_values[118/149] |
string |
offer_type |
characteristics_values[1437] (з мапінгом) |
string |
description |
description_uk (пріоритет) або description |
string |
photos[] |
photos[].file з префіксом xl перед .jpg |
string[] |
contact.name |
user.name |
string |
published_at |
publishing_date |
ISO8601 |
expires_at |
date_end |
ISO8601 |
created_at |
created_at |
ISO8601 |
inspected |
inspected |
boolean |
verified |
inspected_at присутній |
boolean |
Весь об’єкт characteristics_values (ID → значення) залишити в characteristics_raw для downstream-аналітики, якщо data_collector або аналітик вирішить розпарсити додаткові поля.
Локальний сервіс FastAPI (http://localhost:8020). Відкрита документація: /docs, OpenAPI: /openapi.json.
| Endpoint | Метод | Призначення для парсера |
|---|---|---|
POST /api/v1/ingest |
Ingest | Головний. Приймає "сирий" запис про оголошення. |
POST /api/v1/listings/{listing_id}/archive-check |
Archive Check | Відкладено до майбутнього апдейту (див. розділ 7). |
GET /api/v1/health |
Health | Перевірка доступності сервісу перед роботою. |
GET /metrics |
Metrics | Prometheus-метрики (моніторинг). |
RawDataIngestRequest{
"source_slug": "dom_ria",
"external_id": "13825265",
"payload": { }
}
source_slug — ідентифікатор джерела. Константа: dom_ria.external_id — унікальний ID оголошення на DOM.RIA (realty_id).payload — змішана структура: нормалізовані полі + сирі дані.IngestResponse{
"job_id": 1,
"property_id": 123,
"status": "accepted",
"reason": null,
"message": "...",
"snapshot_id": 1
}
job_id — логувати для трасування.snapshot_id — data_collector сам відстежує зміни між версіями.category, realty_type, operation, city_id) відкрити сторінку каталогу.__INITIAL_STATE__ заповнений).catalog.realtyForCatalog.catalog.realtyCountCatalog та catalog.nextPageRealtyCount.?page=N, клік "Далі", або нескінченний скролл — з’ясувати емпірично).listing.data.5xx, таймаутах або порожній відповіді — exponential backoff (1с, 2с, 4с, 8с), макс. 3 спроби.data_collector — сервіс сам створює новий snapshot_id, якщо payload змінився.┌─────────────────┐ Браузерний рендеринг ┌──────────────┐
│ Crawl Runner │ ───────────────────────────> │ dom.ria.com │
│ (Python │ <─────────────────────────── │ (JS SPA) │
│ script) │ __INITIAL_STATE__ └──────────────┘
└────────┬────────┘
│
│ POST /api/v1/ingest
v
┌─────────────────┐
│ data_collector │
│ (localhost:8020)│
└─────────────────┘
| Компонент | Бібліотека | Призначення |
|---|---|---|
| HTTP-скрейпинг | curl_cffi (requests.Session) |
Імітація TLS fingerprint Chrome + session cookies |
| Парсинг HTML/JSON | beautifulsoup4 + lxml |
Пошук window.__INITIAL_STATE__ у відповіді |
| HTTP-клієнт (якщо API розкриється) | httpx (async) |
Швидкі запити без браузера |
| Конфігурація | pydantic-settings + .env |
DATA_COLLECTOR_URL, DELAY_MIN, DELAY_MAX, CITY_SLUGS |
| Логування | structlog + rich |
Читабельні логи з прогресом |
| База стану (опціонально) | SQLite |
Зберігання external_id, last_seen_at, page для resume |
| Планувальник | systemd timer / cron / ручний запуск |
MVP — ручний запуск, потім таймер |
data_collector{
"source_slug": "dom_ria",
"external_id": "13825265",
"payload": {
"url": "https://dom.ria.com/uk/realty-perevireno-prodaja-kvartira-kiev-goloseevskiy-onufriya-trutenko-ulitsa-13825265.html",
"title": "Продаж квартири, Київ, Голосіївський, вул. Онуфрія Трутенка",
"operation": "продажа",
"category": "Квартири",
"realty_type": "Квартира",
"price": {
"amount": 60000,
"currency": "$",
"total": 60000,
"price_per_sqm": 645
},
"address": {
"city": "Київ",
"district": "Голосеевский",
"street": "Онуфрія Трутенка вулиця",
"building": "3 Г"
},
"location": {
"lat": 50.39161687881484,
"lon": 30.480281005166944
},
"area": {
"total": 93,
"living": 50,
"kitchen": 20
},
"rooms": 2,
"floor": { "current": 23, "total": 23 },
"year_built": 2015,
"wall_type": "кирпич",
"offer_type": "власник",
"description": "Світла квартира...",
"photos": [
"https://cdn.riastatic.com/photos/dom/photo/8070/807045/80704539/80704539xl.jpg"
],
"contact": { "name": "Владимир" },
"published_at": "2017-10-02T14:51:07",
"expires_at": "2018-03-02T14:51:07",
"created_at": "2017-09-20T21:12:42",
"inspected": true,
"verified": true,
"characteristics_raw": {
"118": 108,
"209": 2,
"214": 93,
"216": 50,
"218": 20,
"227": 23,
"228": 23,
"234": 60000,
"242": 239,
"443": 1449,
"1437": 1436,
"...": "..."
},
"_raw_listing": { }
}
}
_raw_listing— необов’язковий підоб’єкт. Якщо якісь поля складно нормалізувати на поточному етапі, їх можна залишити тут у сирому вигляді.
| # | Задача | Пріоритет | Примітки |
|---|---|---|---|
| 1 | Повний перелік slug міст | 🟡 Дослідження | Потрібно витягти всі доступні city-slug з головної сторінки / sitemap / меню. Перевірено: kiev, lvov, odessa, kharkov, dnepr, cherkassy, ivano-frankovsk, lutsk, nikolaev, khmelnytskyi. |
| 2 | Пагінація каталогу | 🟢 Рішено | Працює через ?page=N. Перевірено до page=2. |
| 3 | Земля, комерція, гаражі | 🟡 Дослідження | URL prodazha-zemli/kiev/, prodazha-kommercheskoj-nedvizhimosti/kiev/, prodazha-garazhej/kiev/ повертають 404. Можливо, для цих категорій slug інший або вони недоступні в окремому розділі. |
| 4 | Архівація (archive-check) | 🟢 Future | Реалізувати після налагодження основного crawl. Порівнювати last_seen_at з поточним запуском та викликати POST /api/v1/listings/{id}/archive-check. |
| 5 | Історія цін / статистика | 🟢 Future | Залишається на боці data_collector (він створює snapshot_id при зміні payload). |
| 6 | Паралелізація | 🟢 Future | На поточному етапі — послідовний crawl. Паралельність може підвищити ризик блокування. |
| 7 | Моніторинг curl_cffi | 🟡 Дослідження | Якщо DOM.RIA почне блокувати curl_cffi, потрібен fallback на інший impersonate (chrome123, safari) або на справжній браузер. |
.env.ok, error, skipped), часом обробки.page / city_id / external_id в SQLite).data_collector; локальні HTML-файли з __INITIAL_STATE__ для unit-тестів парсера.Dockerfile з curl_cffi (залежить лише від libcurl, легкий образ).| Етап | Термін | Що робимо |
|---|---|---|
| PoC | ✅ Виконано | Досліджено та знайдено робочий спосіб: curl_cffi + impersonate=chrome124 + session cookies + URL /{op}-{type}/{city}/. Перевірено каталог та детальну сторінку. |
| MVP Crawl | ✅ Реалізовано | Скрипт на curl_cffi: обхід 1 міста + 1 категорії (Київ, квартири, продаж). Пагінація (?page=N), детальні сторінки, нормалізація payload, інжест у data_collector. Протестовано: 23 оголошення успішно зібрано та надіслано. |
| Full Scope | 🔄 Наступний етап | Розширити на всі підтверджені міста та категорії. Додати конфігурацію slug’ів. Rate limiting (10с між сторінками, 2 хв кожні 50). Resume з SQLite вже реалізовано. |
| Polish | 🔄 Наступний етап | Тести, обробка крайових випадків (404 на детальній, зняті оголошення), Docker-образ, моніторинг. |
data_collector: http://localhost:8020/openapi.json