▸ // rozdziały lekcji


Dobre mieszkanie w rozsądnej cenie znika z portalu w dzień, czasem w kilka godzin. Jeśli sprawdzasz ogłoszenia wieczorem, oglądasz to, co zostało po innych. Odświeżanie tej samej strony pięć razy dziennie to robota, którą n8n zrobi co godzinę i nie zapomni.
Ten workflow pilnuje jednego wyszukiwania na Otodom. W pliku jest przykład: mieszkania na sprzedaż w Częstochowie, dzielnica Tysiąclecie, do 5400 zł za metr. Nowe ogłoszenie trafia do tabeli i na Telegram jako krótka wiadomość z ceną, metrażem, piętrem, ulicą i linkiem.
Poprzednia wersja miała 28 węzłów: dwie zdublowane gałęzie, Jinę do pobierania strony i dwa agenty AI, które robiły z tekstu tabelę. Wyleciało wszystko. Otodom trzyma dane ogłoszeń w kodzie strony jako gotowy JSON, więc wystarczy je stamtąd wyjąć.
Co robi ten workflow
- Co godzine - harmonogram, co godzinę w 5. minucie.
- Przygotuj tabele - Data Table z operacją Create zakłada tabelę ogloszenia z kolumnami url, tytul, cena, cena_m2, metraz, pokoje, pietro, ulica i znaleziono. Jeśli tabela już jest, używa istniejącej.
- Ustawienia - pole
wyszukiwaniez adresem wyników z Otodom. - Pobierz wyniki - HTTP Request z nagłówkami przeglądarki, odpowiedź jako tekst, ponawianie 3 razy co 5 sekund.
- Dane z kodu strony - węzeł HTML wyciąga skrypt
script#__NEXT_DATA__. - Lista ogloszen - Edit Fields zamienia tekst skryptu na JSON i wyjmuje z niego listę ogłoszeń.
- Rozbij ogloszenia - Split Out, jeden item na ogłoszenie.
- Dane ogloszenia - Edit Fields wybiera i tłumaczy pola.
- Czy nowe ogloszenie? - Row Not Exists po kolumnie
url. - Zapisz ogloszenie - Insert do tabeli.
- Wyslij na Telegram - wiadomość o każdym nowym ogłoszeniu.
Czego potrzebujesz
- adresu wyszukiwania z Otodom,
- bota na Telegramie założonego przez @BotFather i poświadczenia Telegram w n8n z tokenem bota (Credentials Telegram),
- ID czatu, na który bot ma pisać. W pliku stoi zaślepka
TWOJE_ID_CZATU. Jak odczytać ID, pokazuję w tej samej lekcji o Telegramie.
Jak uruchomić
- Zaimportuj plik z dołu lekcji (jak importować workflow).
- Na stronie Otodom ustaw filtry wyszukiwania: miasto, dzielnicę, cenę, metraż. Skopiuj adres z paska przeglądarki.
- Wklej go w pole
wyszukiwaniew węźle Ustawienia. - W węźle Wyslij na Telegram wybierz poświadczenie i wpisz ID czatu w miejsce
TWOJE_ID_CZATU. - Usuń połączenie między Zapisz ogloszenie a Wyslij na Telegram i kliknij Execute workflow. Powód jest w haczyku niżej.
- Sprawdź tabelę: Overview > zakładka Data tables > ogloszenia.
- Podłącz Telegram z powrotem i kliknij Publish (interfejs i publikacja).
Haczyk. Przy pierwszym uruchomieniu tabela jest pusta, więc każde ogłoszenie z listy jest „nowe". Z podłączonym Telegramem dostaniesz naraz tyle wiadomości, ile ogłoszeń jest na stronie wyników. Dlatego pierwszy przebieg robisz bez Telegrama: tabela się wypełnia, a od drugiego przebiegu na telefon idą tylko nowości.
Dane leżą w kodzie strony
Otodom zbudowany jest na frameworku Next.js. Taka strona wysyła w kodzie skrypt __NEXT_DATA__, a w nim JSON z danymi, z których przeglądarka rysuje listę ogłoszeń. Cena, metraż, piętro, ulica, wszystko w polach o stałych nazwach.
Węzeł Dane z kodu strony wyciąga ten skrypt selektorem script#__NEXT_DATA__, a Lista ogloszen robi z niego obiekt i schodzi do listy:
{{ JSON.parse($json.dane).props.pageProps.data.searchAds.items }}JSON.parse zamienia tekst na obiekt, reszta to ścieżka do listy ogłoszeń. Dalej Split Out i zwykłe wyrażenia.

Pro-tip. Pobierz wyniki wysyła nagłówek User-Agent przeglądarki i Accept-Language z językiem polskim. Serwis dostaje zapytanie, które wygląda jak zwykła wizyta, i oddaje zwykłą stronę. Ponawianie 3 razy co 5 sekund łapie chwilowe czkawki serwera.
Tłumaczenie pól
Węzeł Dane ogloszenia wyciąga osiem pól. Większość to proste odwołania: totalPrice.value to cena, pricePerSquareMeter.value cena za metr (zaokrąglona), areaInSquareMeters metraż. Link składa się z https://www.otodom.pl/pl/oferta/ i pola slug.
Dwa pola wymagają tłumaczenia. Otodom zapisuje liczbę pokoi słowem, na przykład TWO, a piętro jako FOURTH albo GROUND. Wyrażenie ma w sobie mapę i bierze z niej wartość:
{{ ({ONE:1,TWO:2,THREE:3,FOUR:4,FIVE:5,SIX:6,SEVEN:7,EIGHT:8,NINE:9,TEN:10})[$json.o.roomsNumber] ?? 0 }}TWO zamienia się w 2. Piętro działa tak samo: FOURTH daje „4", GROUND daje „parter", GARRET „poddasze". Jeśli wartości nie ma w mapie, wpada „brak danych".
Ulica bierze location.address.street.name. Ogłoszeniodawca nie zawsze ją podaje, więc wyrażenie ma zapas: „brak ulicy w ogłoszeniu".
Co naprawiłem względem starej wersji
- cena za metr szła w wiadomości dwa razy, a ulicy nie było wcale. Teraz wiadomość ma cenę, cenę za metr i metraż w jednej linii, a ulicę w osobnej,
- deduplikacja to jeden węzeł Row Not Exists po
urlzamiast porównywania dwóch zbiorów, - zniknęły klucze do Jiny i modeli AI. Workflow nie ma żadnego płatnego elementu.
Test i czego nie sprawdziłem
Na przykładowym wyszukiwaniu (Częstochowa, Tysiąclecie) workflow znalazł 12 ogłoszeń i zapisał je w tabeli. Wysyłki na Telegram nie testowałem, bo nie zakładałem bota. Wiadomość składa się z pól zapisanego wiersza, ale pierwszą prawdziwą wiadomość obejrzyj krytycznie.

Haczyk. To nieoficjalne źródło. Otodom nie obiecuje, że struktura
__NEXT_DATA__zostanie taka sama. Po zmianie ścieżkaprops.pageProps.data.searchAds.itemsprzestanie trafiać, a wyrażenie w węźle Lista ogloszen nie znajdzie listy. Takiej awarii nie testowałem, ale spodziewaj się błędu właśnie w tym węźle. Podepnij alert błędów na Discord, żeby wiedzieć o tym od razu.
Haczyk. Nie odpalaj częściej niż co godzinę i przed uruchomieniem sprawdź regulamin serwisu. Jedno wyszukiwanie raz na godzinę to skala kupującego, nie hurtowego zbierania danych.
Podsumowanie
Jedenaście węzłów zamiast dwudziestu ośmiu, zero AI, zero płatnych usług. Dane leżą w kodzie strony jako JSON, węzeł HTML je wyjmuje, a mapy w wyrażeniach tłumaczą TWO na 2 i GROUND na parter. Row Not Exists po adresie pilnuje, żeby na Telegram szły tylko nowości. Ten sam układ przeniesiesz na każdy serwis, który trzyma dane w __NEXT_DATA__.
Tu kończy się dział z gotowymi workflow. W następnym zaczynamy serię SEO: NawiSEO 1 - parser mapy strony zbiera wszystkie adresy twojej strony do tabeli.
Pokaż JSON do skopiowania - scraper-ogloszen-z-powiadomieniem.json
{
"name": "Scraper ogloszen Otodom z powiadomieniem na Telegram",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"triggerAtMinute": 5
}
]
}
},
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
0,
0
],
"id": "9e0586db-a7b1-39b5-a485-07f10da8a179",
"name": "Co godzine"
},
{
"parameters": {
"resource": "table",
"operation": "create",
"tableName": "ogloszenia",
"columns": {
"column": [
{
"name": "url",
"type": "string"
},
{
"name": "tytul",
"type": "string"
},
{
"name": "cena",
"type": "number"
},
{
"name": "cena_m2",
"type": "number"
},
{
"name": "metraz",
"type": "number"
},
{
"name": "pokoje",
"type": "number"
},
{
"name": "pietro",
"type": "string"
},
{
"name": "ulica",
"type": "string"
},
{
"name": "znaleziono",
"type": "date"
}
]
},
"options": {
"createIfNotExists": true
}
},
"type": "n8n-nodes-base.dataTable",
"typeVersion": 1.1,
"position": [
220,
0
],
"id": "be3c808f-9b42-baed-8c47-d715e9659114",
"name": "Przygotuj tabele"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "92c967a0-c641-96a5-0828-659fb32efdbb",
"name": "wyszukiwanie",
"value": "https://www.otodom.pl/pl/wyniki/sprzedaz/mieszkanie/slaskie/czestochowa/czestochowa/czestochowa/tysiaclecie?limit=36&ownerTypeSingleSelect=ALL&pricePerMeterMax=5400&by=DEFAULT&direction=DESC",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
440,
0
],
"id": "3e9d0a59-f7b1-5b26-53ea-caa88cb501b9",
"name": "Ustawienia"
},
{
"parameters": {
"url": "={{ $json.wyszukiwanie }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "User-Agent",
"value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0 Safari/537.36"
},
{
"name": "Accept-Language",
"value": "pl-PL,pl;q=0.9"
}
]
},
"options": {
"response": {
"response": {
"responseFormat": "text"
}
}
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
660,
0
],
"id": "8c02797b-6764-7030-935b-1a9aca91bc9f",
"name": "Pobierz wyniki",
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000
},
{
"parameters": {
"operation": "extractHtmlContent",
"dataPropertyName": "data",
"extractionValues": {
"values": [
{
"key": "dane",
"cssSelector": "script#__NEXT_DATA__",
"returnValue": "html"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.html",
"typeVersion": 1.2,
"position": [
880,
0
],
"id": "6034d858-f78d-f2ea-2592-5fa2019a7987",
"name": "Dane z kodu strony"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "1a363461-2935-5b21-66fb-fb49028f0caa",
"name": "ogloszenia",
"value": "={{ JSON.parse($json.dane).props.pageProps.data.searchAds.items }}",
"type": "array"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1100,
0
],
"id": "ff62f86f-1710-d1c7-ff8d-03a7c8af3e6b",
"name": "Lista ogloszen"
},
{
"parameters": {
"fieldToSplitOut": "ogloszenia",
"options": {
"destinationFieldName": "o"
}
},
"type": "n8n-nodes-base.splitOut",
"typeVersion": 1,
"position": [
1320,
0
],
"id": "7d94dc58-2dea-2ff0-3767-0e474437f52c",
"name": "Rozbij ogloszenia"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "51baf8e5-969e-9a51-26fd-a43f07af1b88",
"name": "url",
"value": "={{ 'https://www.otodom.pl/pl/oferta/' + $json.o.slug }}",
"type": "string"
},
{
"id": "13f880f2-0d6d-bc7b-f78e-d6c6a1703633",
"name": "tytul",
"value": "={{ $json.o.title }}",
"type": "string"
},
{
"id": "b58d22fc-7f69-b2dc-18ef-6a907a320326",
"name": "cena",
"value": "={{ $json.o.totalPrice?.value ?? 0 }}",
"type": "number"
},
{
"id": "9f59a87c-8b10-927f-b227-b254b9834258",
"name": "cena_m2",
"value": "={{ Math.round($json.o.pricePerSquareMeter?.value ?? 0) }}",
"type": "number"
},
{
"id": "2ae11e1a-c4e0-6c03-7db0-8e84536608a4",
"name": "metraz",
"value": "={{ $json.o.areaInSquareMeters }}",
"type": "number"
},
{
"id": "e82982a0-6cfb-4fe4-401e-ca197d5f3f96",
"name": "pokoje",
"value": "={{ ({ONE:1,TWO:2,THREE:3,FOUR:4,FIVE:5,SIX:6,SEVEN:7,EIGHT:8,NINE:9,TEN:10})[$json.o.roomsNumber] ?? 0 }}",
"type": "number"
},
{
"id": "88e9338e-04e4-cd27-d2c9-ce4a7540c3f9",
"name": "pietro",
"value": "={{ ({CELLAR:'suterena',GROUND:'parter',FIRST:'1',SECOND:'2',THIRD:'3',FOURTH:'4',FIFTH:'5',SIXTH:'6',SEVENTH:'7',EIGHTH:'8',NINTH:'9',TENTH:'10',ABOVE_TENTH:'powyżej 10',GARRET:'poddasze'})[$json.o.floorNumber] ?? 'brak danych' }}",
"type": "string"
},
{
"id": "2d7be0bd-d01d-b2e8-d464-965a37fcab68",
"name": "ulica",
"value": "={{ $json.o.location?.address?.street?.name || 'brak ulicy w ogłoszeniu' }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1540,
0
],
"id": "2c7d1e85-9549-1f7e-5b0d-281304808fb6",
"name": "Dane ogloszenia"
},
{
"parameters": {
"operation": "rowNotExists",
"dataTableId": {
"__rl": true,
"value": "ogloszenia",
"mode": "name"
},
"filters": {
"conditions": [
{
"keyName": "url",
"condition": "eq",
"keyValue": "={{ $json.url }}"
}
]
}
},
"type": "n8n-nodes-base.dataTable",
"typeVersion": 1.1,
"position": [
1760,
0
],
"id": "776a82d6-dadc-eb79-5f38-128a422159fe",
"name": "Czy nowe ogloszenie?"
},
{
"parameters": {
"operation": "insert",
"dataTableId": {
"__rl": true,
"value": "ogloszenia",
"mode": "name"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"url": "={{ $json.url }}",
"tytul": "={{ $json.tytul }}",
"cena": "={{ $json.cena }}",
"cena_m2": "={{ $json.cena_m2 }}",
"metraz": "={{ $json.metraz }}",
"pokoje": "={{ $json.pokoje }}",
"pietro": "={{ $json.pietro }}",
"ulica": "={{ $json.ulica }}",
"znaleziono": "={{ $now.toISO() }}"
},
"matchingColumns": [],
"schema": [
{
"id": "url",
"displayName": "url",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"readOnly": false,
"removed": false
},
{
"id": "tytul",
"displayName": "tytul",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"readOnly": false,
"removed": false
},
{
"id": "cena",
"displayName": "cena",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"readOnly": false,
"removed": false
},
{
"id": "cena_m2",
"displayName": "cena_m2",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"readOnly": false,
"removed": false
},
{
"id": "metraz",
"displayName": "metraz",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"readOnly": false,
"removed": false
},
{
"id": "pokoje",
"displayName": "pokoje",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"readOnly": false,
"removed": false
},
{
"id": "pietro",
"displayName": "pietro",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"readOnly": false,
"removed": false
},
{
"id": "ulica",
"displayName": "ulica",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"readOnly": false,
"removed": false
},
{
"id": "znaleziono",
"displayName": "znaleziono",
"required": false,
"defaultMatch": false,
"display": true,
"type": "date",
"readOnly": false,
"removed": false
}
]
},
"options": {}
},
"type": "n8n-nodes-base.dataTable",
"typeVersion": 1.1,
"position": [
1980,
0
],
"id": "2175dd00-c247-59d7-c1ad-02d05fa1cb41",
"name": "Zapisz ogloszenie"
},
{
"parameters": {
"chatId": "TWOJE_ID_CZATU",
"text": "={{ $json.tytul }}\n{{ $json.cena.toLocaleString('pl-PL') }} zł | {{ $json.cena_m2.toLocaleString('pl-PL') }} zł/m² | {{ $json.metraz }} m²\n{{ $json.pokoje }} pok. | piętro: {{ $json.pietro }}\n{{ $json.ulica }}\n{{ $json.url }}",
"additionalFields": {
"appendAttribution": false
}
},
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
2200,
0
],
"id": "2d480577-5b76-d738-0523-dd1ed6e6ec38",
"name": "Wyslij na Telegram"
}
],
"connections": {
"Co godzine": {
"main": [
[
{
"node": "Przygotuj tabele",
"type": "main",
"index": 0
}
]
]
},
"Przygotuj tabele": {
"main": [
[
{
"node": "Ustawienia",
"type": "main",
"index": 0
}
]
]
},
"Ustawienia": {
"main": [
[
{
"node": "Pobierz wyniki",
"type": "main",
"index": 0
}
]
]
},
"Pobierz wyniki": {
"main": [
[
{
"node": "Dane z kodu strony",
"type": "main",
"index": 0
}
]
]
},
"Dane z kodu strony": {
"main": [
[
{
"node": "Lista ogloszen",
"type": "main",
"index": 0
}
]
]
},
"Lista ogloszen": {
"main": [
[
{
"node": "Rozbij ogloszenia",
"type": "main",
"index": 0
}
]
]
},
"Rozbij ogloszenia": {
"main": [
[
{
"node": "Dane ogloszenia",
"type": "main",
"index": 0
}
]
]
},
"Dane ogloszenia": {
"main": [
[
{
"node": "Czy nowe ogloszenie?",
"type": "main",
"index": 0
}
]
]
},
"Czy nowe ogloszenie?": {
"main": [
[
{
"node": "Zapisz ogloszenie",
"type": "main",
"index": 0
}
]
]
},
"Zapisz ogloszenie": {
"main": [
[
{
"node": "Wyslij na Telegram",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"active": false
}