Wprowadzenie: co osiągniesz w rezultacie

Rynek nieruchomości żyje liczbami. Ktoś szuka mieszkania poniżej ceny rynkowej, ktoś ocenia konkurencję na rynku nowych inwestycji, ktoś buduje raporty dla inwestorów. Łączy ich jedno: potrzebują aktualnych danych o ofertach i cenach, a ręczne ich zebranie jest niemożliwe. I właśnie tutaj przyda się własny parser Cian.

W tym przewodniku zbudujesz od zera działające narzędzie, które potrafi trzy rzeczy. Po pierwsze: przechodzić strony wyników według zadanego filtra i zbierać listę ofert z ceną, adresem, powierzchnią i linkiem. Po drugie: wchodzić do karty każdej oferty i wyciągać szczegóły, takie jak piętro, rok budowy i typ domu. Po trzecie, najcenniejsze: zapisywać dane do bazy i dzień po dniu gromadzić historię cen, żebyś widział, które oferty potaniały, które zostały wycofane ze sprzedaży i jak zmienia się rynek w konkretnej dzielnicy.

Efekt końcowy wygląda tak: masz folder ze skryptami w Pythonie, plik bazy danych SQLite i tabelę, którą możesz otworzyć w Excelu lub Google Sheets. Uruchamiasz skrypt rano, otrzymujesz świeży przekrój danych. Po tygodniu uruchomień masz już dynamikę.

Dla kogo jest ten przewodnik

  • Dla marketerów i analityków agencji nieruchomości, którym potrzebny jest monitoring cen w dzielnicach bez kupowania drogich raportów.
  • Dla arbitrażystów i właścicieli firm, którzy szukają nisz i chcą rozumieć popyt oraz podaż w liczbach.
  • Dla początkujących programistów, którzy chcą opanować parsowanie na żywym i zrozumiałym przykładzie.
  • Dla inwestorów i prywatnych kupujących, którzy chcą łapać oferty z obniżką ceny przed innymi.

Co warto wiedzieć wcześniej

Doświadczenie w programowaniu nie jest konieczne. Omówimy każdą linię kodu i wyjaśnimy, po co jest potrzebna. Wystarczy umieć instalować programy, otwierać wiersz poleceń i kopiować tekst. Jeśli choć raz otwierałeś narzędzia deweloperskie w przeglądarce, będzie naprawdę łatwo. Jeśli nie, pokażemy, gdzie się znajdują.

Jedyny wymóg: uważność. Parsowanie jest wrażliwe na literówki w nazwach klas i adresach. Jedna zbędna litera i skrypt zwróci pustą listę. Nie przerażaj się: każdy krok ma punkt kontrolny, w którym upewnisz się, że wszystko idzie zgodnie z planem.

Ile czasu to zajmie

Przygotowanie środowiska zajmuje około 30 minut. Pierwszy działający parser wyników napiszesz w godzinę. Karty ofert, proxy i baza danych pochłoną kolejne półtorej do dwóch godzin. Razem od trzech do czterech godzin czystego czasu, jeśli idziesz bez pośpiechu. Historia cen zacznie gromadzić się sama, począwszy od drugiego uruchomienia.

Przygotowanie wstępne: narzędzia i środowisko

Zanim zaczniesz pisać kod, zbierzmy wszystko, co potrzebne. Nie warto pomijać tego rozdziału: połowa problemów początkujących wynika z nieprawidłowo zainstalowanego Pythona lub brakujących bibliotek.

Wymagania systemowe

  • Komputer z Windows 10 lub 11, macOS albo Linux. Wystarczy dowolny laptop z ostatnich ośmiu lat.
  • Minimum 4 GB pamięci RAM. W wariancie z automatyzacją przeglądarki zalecane 8 GB.
  • Około 2 GB wolnego miejsca na dysku na Pythona, biblioteki i bazę danych.
  • Stabilne połączenie internetowe.

Co trzeba zainstalować

  1. Python 3.11 lub nowszy. Pobierz instalator z oficjalnej strony python.org. W Windowsie podczas instalacji koniecznie zaznacz pole Add Python to PATH na dole pierwszego ekranu. Bez tego polecenie python nie będzie działać w terminalu. W macOS Python często już jest, ale lepiej zainstalować świeżą wersję.
  2. Edytor kodu. Polecamy Visual Studio Code: darmowy, podświetla składnię i pokazuje błędy. Zainstaluj rozszerzenie Python z wbudowanego sklepu rozszerzeń (ikona z czterema kwadratami na lewym pasku).
  3. Przeglądarka Chrome lub Edge. Będą potrzebne narzędzia deweloperskie do badania struktury stron.
  4. Biblioteki Pythona. Zainstalujemy je przez terminal nieco niżej.
  5. Dostęp do mobilnych proxy. Przyda się na piątym kroku. Potrzebujesz adresu serwera, portu, loginu, hasła i linku do zmiany adresu IP. Wszystko to otrzymasz w panelu klienta serwisu przy zakupie. Jeśli nie masz jeszcze proxy, pierwsze kroki możesz wykonać bez nich.

Tworzymy folder roboczy i środowisko wirtualne

  1. Utwórz na dysku folder o nazwie cian_parser. Unikaj polskich znaków i spacji w ścieżce: czasem psują narzędzia.
  2. Otwórz terminal. W Windowsie naciśnij Win+R, wpisz cmd i naciśnij Enter. W macOS otwórz Terminal przez Spotlight.
  3. Przejdź do folderu poleceniem cd i podaj ścieżkę, na przykład: cd C:\projects\cian_parser w Windowsie lub cd ~/projects/cian_parser w macOS.
  4. Utwórz środowisko wirtualne poleceniem python -m venv venv. To izolowana kopia Pythona, żeby biblioteki projektu nie kolidowały z systemowymi.
  5. Aktywuj środowisko. W Windowsie: venv\Scripts\activate. W macOS i Linux: source venv/bin/activate. Na początku wiersza terminala pojawi się napis (venv).
  6. Zainstaluj biblioteki jednym poleceniem: pip install requests beautifulsoup4 lxml pandas openpyxl. Instalacja zajmie jedną-dwie minuty.

Test: wpisz w terminalu python -c "import requests, bs4, pandas; print('ok')". Jeśli na ekranie pojawiło się słowo ok bez błędów, środowisko jest gotowe. Jeśli widzisz ModuleNotFoundError, środowisko nie jest aktywne albo instalacja została przerwana. Aktywuj venv ponownie i powtórz pip install.

Kopie zapasowe

W tym projekcie najcenniejszy nie jest kod, a zgromadzona baza z historią cen. Nie da się jej odtworzyć: przeszłe ceny nigdzie więcej się nie pojawią. Dlatego od pierwszego dnia ustal ze sobą: plik bazy danych kopiuj do chmury lub na dysk zewnętrzny minimum raz w tygodniu. Później dodamy automatyczne kopiowanie do skryptu.

Podstawowe pojęcia: co trzeba rozumieć przed startem

Omówmy terminy, które będą się pojawiać dalej. Jeśli znasz już parsowanie, przejrzyj rozdział, ale zwróć uwagę na część prawną.

Kluczowe terminy prostym językiem

  • Parsowanie (scraping): automatyczne pobieranie strony internetowej przez program i wyciąganie z niej potrzebnych danych. To samo, co robisz oczami, ale robi to skrypt i tysiąc razy szybciej.
  • HTML: język znaczników, z którego składa się każda strona internetowa. Cena mieszkania na Cian leży wewnątrz znacznika HTML z określonymi atrybutami, a naszym zadaniem jest znaleźć ten znacznik.
  • Selektor: adres elementu wewnątrz HTML. Na przykład span z atrybutem data-mark równym MainPrice. Po selektorze parser wie, skąd wziąć cenę.
  • Żądanie HTTP: zwrócenie się do serwera strony. Przeglądarka robi to, gdy otwierasz stronę. Biblioteka requests robi to samo z kodu.
  • Nagłówki żądania (headers): informacje pomocnicze, które przeglądarka wysyła razem z żądaniem: typ przeglądarki, język, formaty danych. Serwer na ich podstawie decyduje, co oddać.
  • Proxy: serwer pośredniczący, przez który przechodzą twoje żądania. Mobilne proxy korzystają z adresów IP operatorów komórkowych i pozwalają zmieniać adres na polecenie.
  • Paginacja: podział wyników na strony. Żeby zebrać wszystkie oferty, parser musi przejść strony od pierwszej do ostatniej.
  • SQLite: lekka baza danych w jednym pliku. Nie wymaga instalacji serwera, jest wbudowana w Pythona. Idealna do historii cen.

Jak zbudowane są wyniki na portalach nieruchomości

Cian, Domclick, Yandex Nieruchomości i inne portale działają na podobnej zasadzie. Jest strona wyszukiwania z filtrami: miasto, typ transakcji, liczba pokoi, zakres ceny. Każdy filtr zamienia się w parametr w pasku adresu. Na przykład parametr deal_type o wartości sale oznacza sprzedaż, a room1 równy 1 dodaje jednopokojowe mieszkania. Zrozumienie tych parametrów daje ci potężne narzędzie: zamiast klikać po stronie, po prostu tworzysz potrzebny adres.

W wynikach każda oferta jest przedstawiona jako karta: tytuł, cena, adres, kilka zdjęć, link do strony szczegółowej. Strona szczegółowa zawiera pełne charakterystyki i często duplikuje wszystkie dane w ukrytym bloku JSON, którego strona używa do rysowania interfejsu. Ten blok parsuje się znacznie wygodniej niż HTML.

Ramy prawne i etyczne

Uwaga: zbieraj wyłącznie publicznie dostępne informacje o ofertach: cenę, powierzchnię, adres, charakterystyki domu. Nie zbieraj i nie przechowuj telefonów, imion i innych danych osobowych sprzedających i agentów: reguluje to ustawa o ochronie danych osobowych, a naruszenie grozi realną odpowiedzialnością. Przeczytaj regulamin portalu przed rozpoczęciem pracy i wykorzystuj dane do własnej analityki, a nie do odsprzedaży lub tworzenia kopii strony. Zachowuj rozsądną częstotliwość żądań: twój parser nie powinien tworzyć obciążenia, które przeszkadza w działaniu serwisu.

Takie podejście jest nie tylko legalne, ale i praktyczne. Staranny parser z pauzami i rotacją adresów działa miesiącami, a agresywny dostaje tymczasowe ograniczenia już po godzinie.

Krok 1: Określamy cel i strukturę danych

Cel etapu: jasno opisać, co dokładnie zbieramy i jak to będzie przechowywane. Bez tego kroku napiszesz parser, który ciągnie wszystko pod rząd, a potem tydzień będziesz się grzebać w śmietniku danych.

  1. Sformułuj pytanie biznesowe. Przykłady: które jednopokojowe mieszkania w Krakowie potaniały w miesiąc o więcej niż 5 procent; ile kosztuje metr kwadratowy w nowych inwestycjach w konkretnej dzielnicy; jak szybko znikają oferty tańsze niż określona kwota.
  2. Określ filtr wyników. Dla przykładu w tym przewodniku weźmiemy: sprzedaż, rynek wtórny, mieszkania jedno- i dwupokojowe, Warszawa, cena do 1,5 miliona złotych. Ty podstawisz swoje parametry.
  3. Zrób listę pól. Dla każdej oferty potrzebujemy: unikalnego identyfikatora ogłoszenia, linku, tytułu, ceny, adresu, powierzchni całkowitej, piętra i liczby pięter, typu domu, roku budowy, daty pierwszego wykrycia, daty ostatniego sprawdzenia. Dla historii cen: identyfikatora ogłoszenia, daty, ceny.
  4. Otwórz edytor kodu i utwórz w folderze projektu plik config.py. Zapisz w nim parametry, które będziemy zmieniać najczęściej:
BASE_URL = 'https://www.cian.ru/cat.php'
SEARCH_PARAMS = {'deal_type': 'sale', 'engine_version': 2, 'offer_type': 'flat', 'region': 1, 'room1': 1, 'room2': 1, 'maxprice': 15000000}
MAX_PAGES = 5
PAUSE_MIN = 4
PAUSE_MAX = 9
DB_PATH = 'realty.db'

Zwróć uwagę: w przykładzie kodu przejścia do nowej linii oznaczono symbolami nowej linii, w edytorze po prostu pisz każdą zmienną od nowej linii. Parametr region równy 1 odpowiada Moskwie, 2 odpowiada Sankt Petersburgowi. Kody innych regionów znajdziesz, stosując filtr na stronie i patrząc na pasek adresu.

Rada: zacznij od MAX_PAGES równego 2-3. Na każdej stronie wyników jest około 28 ofert, do debugowania to wystarczy. Pełne zbieranie uruchom, gdy upewnisz się, że wszystkie pola są poprawnie wyciągane.

Test: masz plik config.py, a w notatniku lub w głowie zapisaną listę 12 pól i jedno konkretne pytanie biznesowe. Jeśli pytanie brzmi chcę wszystkie dane z całej Polski, wróć i je zawęź: pełne zbieranie po całym kraju to setki tysięcy ofert i zupełnie inna infrastruktura.

Możliwe problemy

Nie udaje się zrozumieć, który parametr odpowiada za potrzebny filtr. Rozwiązanie: otwórz stronę, ustaw filtr ręcznie, skopiuj adres z paska adresu i rozłóż go po znakach ampersanda. Każda para klucz równa się wartość to parametr.

Krok 2: Badamy strukturę strony wyników

Cel etapu: znaleźć w HTML te elementy, z których będziemy brać cenę, tytuł, adres i link. To najbardziej badawczy krok i właśnie tutaj początkujący najczęściej się gubią, dlatego idziemy bardzo powoli.

  1. Otwórz przeglądarkę i przejdź na stronę wyników Cian z twoimi filtrami. Upewnij się, że widzisz listę mieszkań.
  2. Najedź kursorem na cenę dowolnego mieszkania, naciśnij prawy przycisk myszy i wybierz opcję Zbadaj (w Edge nazywa się Sprawdź). Otworzy się panel narzędzi deweloperskich, a w nim podświetli się element z ceną.
  3. Spójrz na podświetloną linię. W momencie pisania przewodnika jest to znacznik span z atrybutem data-mark równym MainPrice. Zapisz ten atrybut: to będzie selektor dla ceny.
  4. Idź w górę drzewa elementów, klikając na znaczniki nadrzędne, aż znajdziesz znacznik obejmujący całą kartę oferty. Zwykle jest to article z atrybutem data-name równym CardComponent. Gdy najedziesz na niego kursorem w panelu, na stronie podświetli się cała karta ze zdjęciem i ceną.
  5. Wewnątrz karty znajdź tytuł (span z data-mark równym OfferTitle), adres (kilka linków a z data-name równym GeoLabel, z których składa się adres) i link do oferty (znacznik a z href prowadzącym do adresu postaci cian.ru/sale/flat/numer). Zapisz wszystkie cztery selektory.
  6. Znajdź blok paginacji na dole strony. Przewiń wyniki do końca, kliknij prawym przyciskiem na numer drugiej strony i zobacz, jak wygląda jej adres. Zobaczysz parametr p równy 2. Czyli do przechodzenia po stronach wystarczy zmieniać ten parametr.

Uwaga: nazwy atrybutów data-mark i data-name na portalach okresowo się zmieniają przy aktualizacji designu. Nie kopiuj selektorów z tego tekstu na ślepo: koniecznie sprawdź je z realną stroną w panelu deweloperskim. Umiejętność samodzielnego znalezienia selektora jest ważniejsza niż każda gotowa lista.

Sprawdzamy, czy jest ukryty JSON

Wiele portali przechowuje dane wyników w gotowej formie wewnątrz znacznika script. To wygodniejsze niż HTML: nie trzeba sklejać adresu z kawałków.

  1. W panelu deweloperskim naciśnij Ctrl+F (w macOS Cmd+F) i wpisz słowo offers lub initialState.
  2. Jeśli wyszukiwanie znalazło znacznik script z dużą ilością tekstu przypominającego słownik z nawiasami klamrowymi, znaczy to, że dane są w JSON. Zapamiętaj nazwę zmiennej na początku tego bloku.
  3. Jeśli nic się nie znalazło, nie szkodzi: podejście HTML z następnego kroku działa w każdym przypadku.

Rada: otwórz zakładkę Network (Sieć) w panelu deweloperskim, odśwież stronę i przefiltruj żądania po typie Fetch/XHR. Czasem strona doładowuje wyniki osobnym żądaniem w formacie JSON. Jeśli widzisz takie żądanie z polem offers, parsowanie go jest najprostsze: dostajesz czyste dane bez HTML.

Test: masz zapisane selektory dla karty, ceny, tytułu, adresu i linku, znasz nazwę parametru paginacji. Klikając na każdy selektor w panelu, widzisz podświetlenie potrzebnego elementu na stronie.

Możliwe problemy

Panel deweloperski pokazuje HTML, ale nie ma tam ceny. Przyczyna: strona rysuje część danych skryptem po załadowaniu. Rozwiązanie: w zakładce Network sprawdź, czy cena przychodzi osobnym żądaniem, albo użyj automatyzacji przeglądarki z rozdziału dla zaawansowanych.

Krok 3: Piszemy pierwszy parser Cian dla strony wyników

Cel etapu: otrzymać skrypt, który pobiera stronę wyników, wyciąga listę ofert i wypisuje je w konsoli. Po tym kroku będziesz mieć działający szkielet, na który będziemy nakładać kolejne funkcje.

  1. Utwórz w folderze projektu plik parser.py.
  2. Zaimportuj biblioteki i ustawienia na początku pliku:
import time
import random
import requests
from bs4 import BeautifulSoup
from config import BASE_URL, SEARCH_PARAMS, MAX_PAGES, PAUSE_MIN, PAUSE_MAX
  1. Opisz nagłówki żądania. Serwer powinien widzieć w nich zwykłą przeglądarkę z rosyjską lokalizacją, inaczej możesz dostać nie tę wersję strony:
HEADERS = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0 Safari/537.36', 'Accept-Language': 'ru-RU,ru;q=0.9', 'Accept': 'text/html,application/xhtml+xml'}
  1. Napisz funkcję pobierania strony. Przyjmuje numer strony, dodaje go do parametrów i zwraca HTML. Koniecznie sprawdzamy kod odpowiedzi: 200 oznacza sukces, wszystko inne to sygnał, żeby się zatrzymać i wyjaśnić sprawę:
def fetch_page(page, session):
params = dict(SEARCH_PARAMS)
params['p'] = page
resp = session.get(BASE_URL, params=params, headers=HEADERS, timeout=30)
if resp.status_code != 200:
print('Статус', resp.status_code, 'на странице', page)
return None
return resp.text
  1. Napisz funkcję rozbioru. Znajduje wszystkie karty i dla każdej wyciąga pola. Zwróć uwagę na konstrukcję z if: jeśli elementu nie ma, nie wywalamy się z błędem, tylko zapisujemy None:
def parse_cards(html):
soup = BeautifulSoup(html, 'lxml')
cards = soup.select('article[data-name=CardComponent]')
result = []
for card in cards:
link_tag = card.select_one('a[href*=/sale/flat/]')
price_tag = card.select_one('span[data-mark=MainPrice]')
title_tag = card.select_one('span[data-mark=OfferTitle]')
geo_tags = card.select('a[data-name=GeoLabel]')
if not link_tag or not price_tag:
continue
url = link_tag.get('href')
offer_id = url.rstrip('/').split('/')[-1]
price_text = price_tag.get_text()
price = int(''.join(ch for ch in price_text if ch.isdigit()))
title = title_tag.get_text(strip=True) if title_tag else None
address = ', '.join(g.get_text(strip=True) for g in geo_tags)
result.append({'offer_id': offer_id, 'url': url, 'title': title, 'price': price, 'address': address})
return result
  1. Złóż główną pętlę. Przechodzi strony, robi losową pauzę między żądaniami i składa wyniki w ogólną listę. Losowa pauza jest ważna: równe odstępy wyglądają nienaturalnie i tworzą szczytowe obciążenie:
def collect_listing():
session = requests.Session()
all_offers = []
for page in range(1, MAX_PAGES + 1):
html = fetch_page(page, session)
if html is None:
break
offers = parse_cards(html)
print('Страница', page, 'объектов:', len(offers))
if not offers:
break
all_offers.extend(offers)
time.sleep(random.uniform(PAUSE_MIN, PAUSE_MAX))
return all_offers
if __name__ == '__main__':
data = collect_listing()
for item in data[:5]:
print(item)
print('Всего собрано:', len(data))
  1. Zapisz plik i uruchom w terminalu poleceniem python parser.py. Upewnij się, że środowisko venv jest aktywne.

Omówmy kluczowe miejsca. Session zachowuje cookies między żądaniami, dzięki czemu strona widzi spójne zachowanie jednego odwiedzającego, a nie dziesięć rozłącznych wejść. Funkcja select_one zwraca pierwszy pasujący element lub None, dlatego zawsze sprawdzamy wynik przed wywołaniem get_text. Identyfikator ogłoszenia bierzemy z linku: to ostatni fragment adresu, liczba w rodzaju 312456789. Właśnie on stanie się kluczem do historii cen.

Rada: na etapie debugowania zapisuj HTML pierwszej strony do pliku poleceniem open('page1.html', 'w', encoding='utf-8').write(html). Wtedy możesz debugować funkcję rozbioru na lokalnej kopii, nie wysyłając zbędnych żądań na stronę.

Test: w konsoli widzisz linie Страница 1 объектов: 28, Страница 2 объектов: 28 i dalej, a na dole pięć słowników z realnymi cenami i adresami. Ceny powinny być liczbami całkowitymi bez spacji i znaku złotego. Jeśli zamiast liczb jest pusto, wróć do selektorów z drugiego kroku.

Możliwe problemy

Skrypt wypisuje объектов: 0 na pierwszej stronie. Przyczyny: zmienił się selektor karty albo serwer oddał stronę-zastępnik. Otwórz zapisany page1.html w przeglądarce i zobacz, co dostałeś. Jeśli to strona z prośbą o potwierdzenie, że nie jesteś robotem, zwiększ pauzy i przejdź do piątego kroku z proxy. Jeśli to normalne wyniki, sprawdź selektory.

Błąd ValueError przy konwersji ceny. Przyczyna: w tekście ceny nie ma cyfr, na przykład napisano Cena na zapytanie. Rozwiązanie: opakuj konwersję w try i zapisuj None dla takich przypadków.

Krok 4: Zbieramy karty ofert

Cel etapu: nauczyć parser wchodzić na stronę każdej oferty i wyciągać szczegółowe charakterystyki: powierzchnię, piętro, typ domu, rok budowy. Te pola są potrzebne do obliczenia ceny za metr kwadratowy i porównania podobnych mieszkań.

  1. Otwórz w przeglądarce stronę dowolnej oferty z wyników. Naciśnij Ctrl+U, żeby zobaczyć kod źródłowy strony.
  2. Naciśnij Ctrl+F i wpisz słowo totalArea. W momencie pisania przewodnika dane karty leżą w znaczniku script wewnątrz obiektu konfiguracji frontendu, w kluczu o nazwie postaci frontend-offer-card. Zobaczysz pola totalArea, floorNumber, floorsCount, buildYear, materialType i inne.
  3. Jeśli wyszukiwanie po totalArea nic nie dało, szukaj charakterystyk w HTML: zwykle to blok z parami nazwa i wartość, na przykład Powierzchnia całkowita i 38,5 m². Zapisz selektor tego bloku.
  4. Dodaj do parser.py funkcję wyciągania JSON z karty. Znajdujemy potrzebny script, wycinamy z niego obiekt po nawiasach klamrowych i rozbieramy modułem json:
import json
import re
def parse_offer_page(html):
soup = BeautifulSoup(html, 'lxml')
details = {}
for script in soup.find_all('script'):
text = script.string or ''
if 'totalArea' in text and 'offerData' in text:
m = re.search(r'totalArea[^0-9]*([0-9.,]+)', text)
if m:
details['area'] = float(m.group(1).replace(',', '.'))
m = re.search(r'floorNumber[^0-9]*([0-9]+)', text)
if m:
details['floor'] = int(m.group(1))
m = re.search(r'floorsCount[^0-9]*([0-9]+)', text)
if m:
details['floors_total'] = int(m.group(1))
m = re.search(r'buildYear[^0-9]*([0-9]{4})', text)
if m:
details['build_year'] = int(m.group(1))
break
return details
  1. Tutaj celowo używamy wyrażeń regularnych zamiast pełnego rozbioru JSON. Przyczyna jest prosta: skrypt na stronie zawiera nie tylko JSON, ale i kod, a wyodrębnienie czystego obiektu bywa trudne. Wyrażenia regularne szukają klucza i pierwszej liczby po nim, co jest wystarczająco niezawodne dla pól numerycznych.
  2. Dodaj funkcję, która bierze listę ofert z wyników i wzbogaca każdą danymi karty. Pauzy są tu jeszcze ważniejsze, bo żądań robi się 28 razy więcej:
def enrich_offers(offers, session):
for i, offer in enumerate(offers, 1):
try:
resp = session.get(offer['url'], headers=HEADERS, timeout=30)
if resp.status_code == 200:
offer.update(parse_offer_page(resp.text))
else:
print('Карточка', offer['offer_id'], 'статус', resp.status_code)
except requests.RequestException as e:
print('Ошибка сети на', offer['offer_id'], e)
if i % 10 == 0:
print('Обработано карточек:', i)
time.sleep(random.uniform(PAUSE_MIN, PAUSE_MAX))
return offers
  1. W bloku if __name__ po collect_listing dodaj wywołanie enrich_offers(data, requests.Session()) i uruchom skrypt od nowa z MAX_PAGES równym 1, żeby nie czekać długo.

Ile to zajmie: 28 kart przy średniej pauzie 6 sekund to około 3 minuty. Pełne zbieranie 5 stron wyników z kartami zajmie mniej więcej 15 minut. To normalne. Parser nieruchomości nie powinien być szybki, powinien być stabilny.

Rada: nie parsuj kart ponownie przy każdym uruchomieniu. Charakterystyki mieszkania się nie zmieniają: powierzchnię i rok budowy wystarczy zebrać raz. Zmienia się tylko cena, a ta jest w wynikach. W szóstym kroku zrobimy tak, żeby karta była pobierana tylko dla nowych ofert. To zmniejszy liczbę żądań dziesiątki razy.

Test: w wypisywanych słownikach pojawiły się klucze area, floor, floors_total i build_year z prawdopodobnymi wartościami: powierzchnia od 15 do 200, piętro nie większe niż liczba pięter, rok od 1900 do 2026. Jeśli u części ofert brakuje pól, to normalne: nie wszyscy sprzedający wypełniają rok budowy.

Możliwe problemy

Wyrażenie regularne znajduje powierzchnię pokoju zamiast całkowitej. Przyczyna: w JSON są podobne klucze w rodzaju livingArea lub kitchenArea. Rozwiązanie: doprecyzuj wzorzec, dodając przed kluczem cudzysłów lub dwukropek, żeby nie pasował do części innego słowa.

Krok 5: Podłączamy mobilne proxy i czynimy zbieranie odpornym

Cel etapu: rozłożyć żądania przez mobilne proxy z rotacją IP, dodać ponowne próby i poprawną obsługę odpowiedzi. Po tym kroku parser będzie mógł pracować regularnie i długo, nie tworząc nadmiernego obciążenia z jednego adresu.

Po co parserowi nieruchomości mobilne proxy

Każdy duży portal ogranicza częstotliwość żądań z jednego adresu IP. To ochrona przed przeciążeniem i działa na każdej automatyce. Adres domowy po kilkuset żądaniach zaczyna dostawać odpowiedzi 429 lub strony z weryfikacją. Mobilne proxy rozwiązują problem inaczej: dostajesz IP z puli operatora komórkowego, a na polecenie lub po timerze adres się zmienia. Twoje żądania rozkładają się między adresami, obciążenie każdego z nich pozostaje niskie, a parser działa równo. Dla regularnego monitoringu cen to kluczowe: potrzebujesz nie jednorazowych danych, a codziennych przekrojów przez miesiące.

Konfiguracja

  1. Otwórz panel klienta swojego serwisu mobilnych proxy i znajdź kupione proxy. Skopiuj cztery wartości: host, port, login, hasło. Skopiuj też link do zmiany IP: zwykle wygląda jak adres z kluczem, a po jego wywołaniu proxy dostaje nowy adres.
  2. Dodaj do config.py parametry proxy. Nigdy nie zapisuj haseł w kodzie, który gdzieś publikujesz: trzymaj je w osobnym pliku lub zmiennych środowiskowych:
PROXY_HOST = 'ваш_хост'
PROXY_PORT = 'ваш_порт'
PROXY_USER = 'ваш_логин'
PROXY_PASS = 'ваш_пароль'
ROTATE_URL = 'ссылка_для_смены_ip'
ROTATE_EVERY = 25
  1. Dodaj do parser.py funkcję tworzenia sesji z proxy. Biblioteka requests przyjmuje słownik z adresami dla http i https:
from config import PROXY_HOST, PROXY_PORT, PROXY_USER, PROXY_PASS, ROTATE_URL, ROTATE_EVERY
def make_session():
session = requests.Session()
proxy_url = 'http://' + PROXY_USER + ':' + PROXY_PASS + '@' + PROXY_HOST + ':' + PROXY_PORT
session.proxies = {'http': proxy_url, 'https': proxy_url}
return session
def rotate_ip():
try:
r = requests.get(ROTATE_URL, timeout=20)
print('Смена IP:', r.status_code)
time.sleep(5)
except requests.RequestException as e:
print('Не удалось сменить IP:', e)
  1. Sprawdź, że proxy działa. Utwórz tymczasowy plik check_proxy.py z kodem, który przez sesję pyta serwis identyfikacji IP i wypisuje odpowiedź:
from parser import make_session
s = make_session()
print(s.get('https://api.ipify.org', timeout=20).text)
  1. Uruchom go. Powinieneś zobaczyć adres IP inny niż domowy. Wywołaj rotate_ip i uruchom sprawdzenie jeszcze raz: adres powinien się zmienić.
  2. Teraz dodaj funkcję żądania z ponownymi próbami. Obsługuje trzy sytuacje: udaną odpowiedź, odpowiedź 429 lub 403 (trzeba poczekać i zmienić adres), błąd sieci (powtórzyć):
def safe_get(session, url, params=None, retries=3):
for attempt in range(1, retries + 1):
try:
resp = session.get(url, params=params, headers=HEADERS, timeout=30)
if resp.status_code == 200:
return resp
if resp.status_code in (429, 403):
print('Статус', resp.status_code, 'попытка', attempt, 'меняем IP и ждем')
rotate_ip()
time.sleep(30 * attempt)
continue
print('Неожиданный статус', resp.status_code)
return None
except requests.RequestException as e:
print('Сетевая ошибка', e, 'попытка', attempt)
time.sleep(10 * attempt)
return None
  1. Zamień wywołania session.get w fetch_page i enrich_offers na safe_get. Dodaj w enrich_offers licznik: co ROTATE_EVERY żądań wywołuj rotate_ip. To planowa rotacja, która nie pozwala adresowi nazbierać zbyt wielu wejść.

Uwaga: jeśli dostałeś odpowiedź 429 lub stronę z weryfikacją, nie próbuj jej przebić częstymi powtórzeniami. To tylko pogorszy sytuację dla obecnego adresu. Właściwa reakcja: zatrzymać się, zwiększyć pauzy, zmienić IP i kontynuować w spokojnym tempie. Parser, który szanuje limity strony, żyje dłużej i zbiera więcej.

Rada: używaj jednego kanału proxy na jeden wątek parsowania. Pokusa uruchomienia dziesięciu wątków przez jeden adres jest duża, ale to prosta droga do ograniczeń. Jeśli potrzebujesz szybkości, kup kilka kanałów i rozłóż na nie różne regiony lub różne filtry.

Test: check_proxy.py pokazuje adres operatora komórkowego, po rotacji adres się zmienia. Parser przechodzi dwie strony wyników z kartami bez ani jednego statusu 429. W logu widać linie Смена IP: 200 co 25 kart.

Możliwe problemy

Błąd ProxyError lub 407. Przyczyna: błędny login lub hasło albo nie ten port. Rozwiązanie: sprawdź dane w panelu klienta, upewnij się, że używasz portu dla proxy HTTP, a nie SOCKS. Jeśli masz SOCKS5, zainstaluj bibliotekę pysocks i użyj przedrostka socks5h zamiast http w proxy_url.

Po rotacji adres się nie zmienia. Przyczyna: operator wydał ten sam adres albo rotacja jeszcze się nie zastosowała. Rozwiązanie: zwiększ pauzę po rotate_ip do 10 sekund i sprawdź, czy częstotliwość zmiany IP nie jest ograniczona w twoim planie.

Krok 6: Zapisujemy dane i budujemy historię cen

Cel etapu: przenieść parser z wypisywania w konsoli na zapis do bazy danych SQLite tak, żeby każde uruchomienie dodawało nowy punkt w historii cen, a nie nadpisywało stary. To serce całego projektu.

Projektujemy tabele

Potrzebujemy dwóch tabel. Pierwsza, offers, przechowuje ofertę: jeden wiersz na ogłoszenie z charakterystykami i datami. Druga, prices, przechowuje cenę na datę: kilka wierszy na ogłoszenie. Podział jest potrzebny, żeby nie duplikować powierzchni i adresu przy każdym zapisie ceny.

  1. Utwórz plik storage.py i opisz tworzenie tabel:
import sqlite3
from datetime import date
from config import DB_PATH
def get_conn():
conn = sqlite3.connect(DB_PATH)
conn.execute('CREATE TABLE IF NOT EXISTS offers (offer_id TEXT PRIMARY KEY, url TEXT, title TEXT, address TEXT, area REAL, floor INTEGER, floors_total INTEGER, build_year INTEGER, first_seen TEXT, last_seen TEXT, is_active INTEGER DEFAULT 1)')
conn.execute('CREATE TABLE IF NOT EXISTS prices (offer_id TEXT, checked_on TEXT, price INTEGER, PRIMARY KEY (offer_id, checked_on))')
conn.commit()
return conn
  1. Dodaj funkcję, która zwraca zbiór znanych już offer_id. Jest potrzebna, żeby pobierać karty tylko dla nowych ofert:
def known_ids(conn):
rows = conn.execute('SELECT offer_id FROM offers').fetchall()
return set(r[0] for r in rows)
  1. Napisz funkcję zapisu. Dla nowej oferty wstawiamy wiersz do offers. Dla każdej oferty aktualizujemy last_seen i zapisujemy cenę na dziś. Konstrukcja INSERT OR REPLACE w prices oznacza: jeśli dzisiejsza cena już była zapisana, zaktualizuj, inaczej dodaj:
def save_offers(conn, offers):
today = date.today().isoformat()
for o in offers:
conn.execute('INSERT OR IGNORE INTO offers (offer_id, url, title, address, area, floor, floors_total, build_year, first_seen, last_seen) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', (o['offer_id'], o['url'], o.get('title'), o.get('address'), o.get('area'), o.get('floor'), o.get('floors_total'), o.get('build_year'), today, today))
conn.execute('UPDATE offers SET last_seen = ?, is_active = 1 WHERE offer_id = ?', (today, o['offer_id']))
if o.get('price'):
conn.execute('INSERT OR REPLACE INTO prices (offer_id, checked_on, price) VALUES (?, ?, ?)', (o['offer_id'], today, o['price']))
conn.commit()
  1. Dodaj funkcję, która oznacza wycofane oferty. Jeśli ogłoszenie nie pojawiało się w wynikach dłużej niż trzy dni, uznajemy je za nieaktywne. To daje ci dane o szybkości sprzedaży:
def mark_inactive(conn, days=3):
conn.execute('UPDATE offers SET is_active = 0 WHERE julianday(?) - julianday(last_seen) > ?', (date.today().isoformat(), days))
conn.commit()
  1. Przepisz główny blok parser.py tak, żeby zbierał wyniki, określał nowe oferty, wzbogacał tylko je i zapisywał wszystkie:
from storage import get_conn, known_ids, save_offers, mark_inactive
if __name__ == '__main__':
conn = get_conn()
session = make_session()
listing = collect_listing(session)
old = known_ids(conn)
new_offers = [o for o in listing if o['offer_id'] not in old]
print('Новых объектов:', len(new_offers), 'из', len(listing))
enrich_offers(new_offers, session)
save_offers(conn, listing)
mark_inactive(conn)
print('Сохранено. Всего в базе:', conn.execute('SELECT COUNT(*) FROM offers').fetchone()[0])
  1. Nie zapomnij zmienić collect_listing tak, żeby przyjmowała session jako parametr, a nie tworzyła własnej. Uruchom skrypt. W folderze projektu pojawi się plik realty.db.

Patrzymy na historię cen

Utwórz plik report.py, który eksportuje zmiany cen do Excela. Zapytanie poniżej znajduje oferty, których ostatnia cena różni się od pierwszej:

import sqlite3
import pandas as pd
from config import DB_PATH
conn = sqlite3.connect(DB_PATH)
query = 'SELECT o.offer_id, o.address, o.area, o.url, MIN(p.checked_on) AS first_date, MAX(p.checked_on) AS last_date, (SELECT price FROM prices WHERE offer_id = o.offer_id ORDER BY checked_on ASC LIMIT 1) AS first_price, (SELECT price FROM prices WHERE offer_id = o.offer_id ORDER BY checked_on DESC LIMIT 1) AS last_price FROM offers o JOIN prices p ON p.offer_id = o.offer_id GROUP BY o.offer_id'
df = pd.read_sql(query, conn)
df['change_pct'] = ((df['last_price'] - df['first_price']) / df['first_price'] * 100).round(1)
df['price_per_m2'] = (df['last_price'] / df['area']).round(0)
df = df.sort_values('change_pct')
df.to_excel('report.xlsx', index=False)
print(df.head(10))

Po pierwszym uruchomieniu kolumna change_pct będzie zerowa: historii jeszcze nie ma. Od drugiego dnia pojawią się pierwsze zmiany. Po dwóch tygodniach zobaczysz obraz: ile ofert obniżyło cenę, o ile średnio, które dzielnice ruszają się szybciej.

Rada: dodaj na końcu parser.py kopiowanie bazy: shutil.copy(DB_PATH, 'backup_' + date.today().isoformat() + '.db'). Jedna linia kodu ochroni tygodnie zgromadzonych danych. Raz w miesiącu usuwaj stare kopie, zostawiając ostatnie pięć.

Test: plik realty.db istnieje, report.xlsx otwiera się w Excelu, ma kolumny z adresem, powierzchnią, cenami i ceną za metr kwadratowy. Uruchom parser.py drugi raz po kilku minutach: linia Новых объектов powinna pokazać 0 lub niewielką liczbę, a karty nie powinny być pobierane ponownie. To potwierdza, że deduplikacja działa.

Możliwe problemy

Błąd database is locked. Przyczyna: baza jest otwarta w innym programie, na przykład w przeglądarce SQLite, albo dwa egzemplarze skryptu pracują jednocześnie. Rozwiązanie: zamknij zbędne programy i nie uruchamiaj skryptu równolegle sam ze sobą.

W report.xlsx wszystkie oferty pokazują tę samą datę. Przyczyna: skrypt był uruchamiany tylko jednego dnia. To oczekiwane, po prostu poczekaj na kolejne uruchomienia.

Krok 7: Automatyzujemy uruchamianie i rozszerzamy na inne portale

Cel etapu: sprawić, żeby parser uruchamiał się sam każdego ranka, i przygotować kod do podłączenia innych portali nieruchomości. Historia cen jest cenna tylko przy regularności, dlatego automatyzacja jest obowiązkowa.

Harmonogram uruchamiania

  1. W Windowsie otwórz Harmonogram zadań przez wyszukiwanie w menu Start. Naciśnij Utwórz zadanie podstawowe. Wpisz nazwę, na przykład Parser nieruchomości.
  2. Wybierz wyzwalacz Codziennie, ustaw godzinę, na przykład 07:30. Wczesny ranek jest wygodny: obciążenie stron jest minimalne, a na początek dnia pracy masz świeże dane.
  3. W akcji wybierz Uruchom program. W polu Program podaj pełną ścieżkę do python.exe wewnątrz folderu venv, na przykład C:\projects\cian_parser\venv\Scripts\python.exe. W polu Argumenty wpisz parser.py. W polu Folder roboczy podaj folder projektu.
  4. Zapisz zadanie i naciśnij Uruchom w prawym panelu, żeby sprawdzić. W folderze projektu powinna zaktualizować się baza.
  5. W macOS i Linux użyj cron. Wpisz w terminalu crontab -e i dodaj linię: 30 7 * * * cd /путь/к/cian_parser && ./venv/bin/python parser.py >> run.log 2>&1. Log wszystkich uruchomień będzie się gromadzić w run.log.

Przygotowanie do innych portali

Domclick, Yandex Nieruchomości, Metr kwadratowy i regionalne portale działają podobnie, ale selektory i parametry filtrów mają swoje. Żeby nie przepisywać parsera pod każdy, wydziel to, co się różni, do osobnych modułów.

  1. Utwórz folder sites wewnątrz projektu. W nim utwórz plik cian.py i przenieś tam funkcje fetch_page i parse_cards razem z parametrami wyszukiwania. Zostaw w nich identyczny interfejs: funkcja parse_cards przyjmuje HTML i zwraca listę słowników z tymi samymi kluczami offer_id, url, title, price, address.
  2. Dla nowego portalu utwórz plik, na przykład domclick.py, i powtórz badanie z drugiego kroku: otwórz wyniki, znajdź kartę, cenę, link i parametr paginacji. Napisz swoje wersje fetch_page i parse_cards.
  3. W offer_id dodawaj przedrostek portalu, na przykład cian_312456789 i domclick_98765. Inaczej identyfikatory z różnych stron mogą się pokryć i wymieszać historię.
  4. W parser.py importuj moduły z sites i uruchamiaj zbieranie po każdym w pętli. Tabele w bazie są wspólne: jedna schemat dla wszystkich źródeł, a kolumna source podpowie, skąd jest oferta.

Ważna uwaga o Domclick i Yandex Nieruchomości: te portale aktywnie używają wewnętrznych API w formacie JSON, które widać w zakładce Network. Ich wyniki często są wygodniejsze do parsowania niż HTML, ale struktura odpowiedzi zmienia się częściej. Sprawdzaj selektory i pola raz w miesiącu.

Rada: dla tej samej oferty wystawionej na kilku portalach ceny mogą się różnić. Porównywanie takich par daje ciekawą analitykę i pomaga znajdować sprzedających, którzy gdzieś obniżyli cenę, ale zapomnieli zaktualizować w innym miejscu. Oferty można dopasowywać po adresie, powierzchni i piętrze.

Test: zadanie w harmonogramie wykonało się ręcznie bez błędów, w run.log lub w historii harmonogramu widać udaną adnotację. Struktura folderów zawiera sites z co najmniej jednym modułem, parser uruchamia się z korzenia projektu i działa jak wcześniej.

Możliwe problemy

Harmonogram pisze, że zadanie wykonane, ale baza się nie zaktualizowała. Przyczyna: skrypt uruchomił się z innego folderu roboczego i utworzył nową pustą bazę gdzieś indziej. Rozwiązanie: wypełnij pole Folder roboczy w zadaniu albo użyj bezwzględnej ścieżki do bazy w config.py.

Sprawdzenie rezultatu: lista kontrolna gotowego parsera

Przejdź po liście i odhacz każdy punkt. Jeśli wszystko wykonane, masz pełnoprawny parser Cian z historią cen.

  • Skrypt parser.py uruchamia się poleceniem z aktywowanego środowiska bez błędów importu.
  • Wyniki są zbierane z kilku stron, liczba ofert na stronie odpowiada temu, co widzisz w przeglądarce.
  • Ceny są zapisywane jako liczby całkowite, adresy czytelne, linki otwierają się.
  • Karty są pobierane tylko dla nowych ofert, w logu widać linię Новых объектов z malejącą liczbą przy kolejnych uruchomieniach.
  • Żądania idą przez mobilne proxy, check_proxy.py pokazuje adres operatora, rotacja go zmienia,
  • Przy statusie 429 parser robi pauzę i zmienia IP, a nie wywala się.
  • Plik realty.db rośnie, tabela prices dostaje nowe wiersze każdego dnia.
  • report.xlsx tworzy się i otwiera, po kilku dniach pojawiają się w nim niezerowe zmiany cen.
  • Autostart jest skonfigurowany, log rejestruje każde uruchomienie.
  • Kopia zapasowa bazy tworzy się automatycznie,

Jak przetestować całość

  1. Usuń lub zmień nazwę realty.db, żeby zacząć od czystego stanu.
  2. Ustaw MAX_PAGES na 2 i uruchom parser.py. Zmierz czas: powinno zająć około 6-8 minut z uwzględnieniem kart.
  3. Uruchom report.py i upewnij się, że w raporcie jest około 56 wierszy.
  4. Otwórz realty.db dowolną przeglądarką SQLite albo wykonaj w Pythonie zapytanie SELECT COUNT(*) FROM prices. Liczba powinna zgadzać się z liczbą ofert.
  5. Uruchom parser.py ponownie. Czas wykonania powinien skrócić się do minuty, bo karty nie są pobierane. Liczba wierszy w prices nie zmieni się, bo data jest ta sama.
  6. Zmień w bazie cenę jednej oferty ręcznie na dowolną inną liczbę, zmień datę zapisu na wczorajszą i uruchom parser. W report.xlsx ta oferta powinna pokazać zmianę ceny. Tak upewnisz się, że logika historii działa, nie czekając na realne zmiany.

Wskaźniki sukcesu

Udział ofert z wypełnioną powierzchnią powyżej 90 procent. Udział żądań ze statusem 200 powyżej 97 procent. Ani jednego nieobsłużonego wyjątku na przebieg. Czas pełnego przebiegu przewidywalny i nie rośnie z uruchomienia na uruchomienie. Jeśli wskaźniki są niższe, wróć do rozdziału z typowymi błędami.

Typowe błędy i rozwiązania

Zebraliśmy problemy, z którymi zderza się praktycznie każdy, kto pisze parser ogłoszeń nieruchomości po raz pierwszy. Format: problem, przyczyna, rozwiązanie.

Parser zwraca 0 ofert, choć wczoraj działał

Przyczyna: portal zaktualizował szatę graficzną i zmienił atrybuty data-mark lub data-name. Rozwiązanie: otwórz wyniki w przeglądarce, powtórz drugi krok i zaktualizuj selektory. Weź w nawyk trzymanie selektorów w jednym miejscu na początku modułu, żeby poprawki zajmowały minutę. Dodaj do parsera sprawdzenie: jeśli na pierwszej stronie jest 0 ofert, wysyłaj sobie powiadomienie na komunikator.

Ceny zapisują się z błędem tysiąckrotnym

Przyczyna: u części ofert cena jest podana w tysiącach lub z dopiskiem za miesiąc, albo w tekst wpadła cena za metr kwadratowy. Rozwiązanie: upewnij się, że bierzesz właśnie MainPrice, a nie sąsiedni element z ceną za metr. Dodaj sprawdzenie rozsądku: cena sprzedaży mieszkania w Warszawie poniżej dziesięciu tysięcy złotych to prawie na pewno błąd rozbioru, loguj takie przypadki.

Status 429 przychodzi już na trzeciej stronie

Przyczyna: pauzy są zbyt krótkie albo proxy nie jest jeszcze podłączone, wszystkie żądania idą z jednego domowego IP. Rozwiązanie: zwiększ PAUSE_MIN i PAUSE_MAX do 6 i 12, podłącz mobilne proxy, włącz planową rotację co 20-25 żądań. Sprawdź, że nie uruchomiłeś kilku egzemplarzy skryptu jednocześnie.

Błąd UnicodeEncodeError przy wypisywaniu w konsoli Windows

Przyczyna: standardowa konsola Windows nie zawsze poprawnie wypisuje polskie znaki. Rozwiązanie: wykonaj w terminalu chcp 65001 przed uruchomieniem albo dodaj na początku skryptu linię z sys.stdout.reconfigure(encoding='utf-8'). Możesz też pisać logi do pliku zamiast do konsoli.

Adres zbiera się niepełny lub z duplikatami

Przyczyna: adres na karcie składa się z kilku linków GeoLabel, część z nich duplikuje miasto i dzielnicę. Rozwiązanie: usuwaj duplikaty, zachowując kolejność, albo bierz adres z karty oferty, gdzie jest przedstawiony jako jeden ciąg. Do analityki po dzielnicach dodaj osobne pole district, wycinając je z adresu po znanej liście dzielnic.

Parser działa przy ręcznym uruchomieniu, ale nie w harmonogramie

Przyczyna: harmonogram używa innego interpretera Pythona bez zainstalowanych bibliotek lub innego folderu roboczego. Rozwiązanie: podawaj bezwzględną ścieżkę do python.exe wewnątrz venv i wypełniaj pole folderu roboczego. Przekierowuj wyjście do pliku logu, żeby widzieć błędy.

Baza waży gigabajty po miesiącu

Przyczyna: zapisujesz pełny HTML stron lub wszystkie pola JSON w bazie. Rozwiązanie: przechowuj tylko potrzebne pola. Jeśli chcesz zapisywać źródłowe strony do ponownego rozbioru, składuj je w spakowanych plikach na dysku, a nie w SQLite. Raz na kwartał wykonuj polecenie VACUUM do zagęszczenia bazy.

Identyczne oferty duplikują się pod różnymi identyfikatorami

Przyczyna: sprzedający zdjął ogłoszenie i wystawił je na nowo, dostając nowy numer. Rozwiązanie: dodaj dodatkowy klucz dopasowania z adresu, powierzchni i piętra. Oferty z tym samym kluczem, ale różnymi offer_id można powiązać w osobnej tabeli i liczyć historię ceny po powiązaniu. To już zaawansowana analityka, ale właśnie ona pokazuje realne obniżki ceny ukryte za ponowną publikacją.

Dodatkowe możliwości dla zaawansowanych

Podstawowy parser gotowy. Jeśli chcesz więcej, oto kierunki, które dają największy zwrot.

Automatyzacja przeglądarki przez Playwright

Niektóre strony doładowują dane skryptami już po załadowaniu, a requests dostaje pusty szkielet. W takich przypadkach użyj Playwright: steruje prawdziwą przeglądarką. Zainstaluj go poleceniami pip install playwright i playwright install chromium. Proxy przekazuje się przy uruchomieniu przeglądarki w parametrze proxy ze słownikiem server, username, password. Poczekaj na pojawienie się kart przez page.wait_for_selector i przekaż page.content() do już napisanej funkcji parse_cards. Pamiętaj, że przeglądarka zużywa dziesięć razy więcej zasobów, więc używaj jej punktowo, tylko dla problematycznych stron.

Równoległe zbieranie po kilku kanałach proxy

Jeśli musisz zbierać kilka regionów, kup osobne mobilne proxy na każdy region i uruchamiaj osobny proces na kanał. Nie używaj wielowątkowości w obrębie jednego kanału: sens rozkładania obciążenia znika. Prosty sposób: parametr regionu przekazywany jest skryptowi argumentem wiersza poleceń, a harmonogram uruchamia kilka zadań z różnymi argumentami i niewielkim przesunięciem w czasie.

Powiadomienia o obniżce ceny

Dodaj na końcu parsera porównanie dzisiejszej ceny z poprzednią dla każdej oferty. Jeśli obniżka przekracza zadany próg, na przykład 3 procent, twórz wiadomość z adresem, starą i nową ceną, linkiem i wysyłaj ją sobie przez bota na komunikatorze. To zamienia parser z narzędzia analityki w narzędzie działania: o korzystnych ofertach dowiesz się w ciągu godziny od zmiany.

Analityka i wizualizacja

Z pandas możesz grupować dane po dzielnicach i liczyć medianę ceny metra kwadratowego, udział ofert z obniżką, średni czas ekspozycji (różnica między first_seen i last_seen dla nieaktywnych ofert). Biblioteka matplotlib zbuduje wykres dynamiki za miesiąc w trzech liniach kodu. Wgraj raport do Google Sheets, a współpracownicy bez umiejętności programowania dostaną żywy panel wskaźników.

Przechowywanie w PostgreSQL

Gdy ofert zrobi się więcej niż sto tysięcy, SQLite zacznie zwalniać na zapytaniach analitycznych. Przeprowadzka na PostgreSQL jest prosta: schemat tabel ten sam, zmienia się tylko ciąg połączenia i biblioteka (psycopg2 zamiast sqlite3). Rób to tylko przy realnej potrzebie: dla jednego miasta SQLite wystarcza na lata.

Monitoring kondycji parsera

Zapisuj w osobnej tabeli runs czas startu, czas zakończenia, liczbę zebranych ofert, liczbę błędów i liczbę rotacji IP. Jeśli ofert gwałtownie ubyło albo błędów jest więcej niż 5 procent, wysyłaj powiadomienie. Taki monitoring pozwala zauważyć zmianę szaty graficznej w dniu jej pojawienia się, a nie po dwóch tygodniach po pustym raporcie.

FAQ: częste pytania o tworzenie parsera nieruchomości

Czy parsowanie Cian i innych portali jest legalne?

Zbieranie publicznie dostępnych informacji o ofertach do własnej analityki jest ogólnie dopuszczalne, ale warunki każdego portalu są opisane w jego regulaminie i trzeba go przeczytać. Kategorycznie nie wolno zbierać danych osobowych sprzedających, publikować skopiowanej bazy jako swojej i tworzyć obciążenia, które przeszkadza w działaniu serwisu. Jeśli planujesz komercyjne wykorzystanie danych, skonsultuj się z prawnikiem.

Dlaczego właśnie mobilne proxy, a nie serwerowe?

Adresy operatorów komórkowych są wspólne dla tysięcy realnych abonentów i stale się zmieniają. Portale traktują je łagodniej niż adresy centrów danych, z których realni kupujący prawie nie wchodzą. Do tego możliwość zmiany IP przez link daje kontrolowaną rotację bez kupowania setek adresów.

Jak często uruchamiać parser do historii cen?

Raz na dobę to optymalnie. Ceny nieruchomości zmieniają się rzadko, częściej niż raz dziennie nie ma sensu, a obciążenie rośnie. Jeśli chcesz łapać obniżki na bieżąco, uruchamiaj dwa razy dziennie rano i wieczorem, ale tylko po wąskim filtrze.

Ile ofert można zebrać dziennie przez jedno proxy?

Przy pauzach 4-9 sekund i planowej rotacji to około 500-700 żądań na godzinę bez problemów, czyli 8-12 tysięcy na dobę przy pracy całodobowej. Do monitoringu jednego miasta zwykle wystarcza 2-3 tysiące żądań dziennie, bo karty pobiera się tylko dla nowych ofert.

Co robić, jeśli selektory się zmieniły i nie mogę ich znaleźć?

Wróć do drugiego kroku i idź od ceny: prawy klik na cenie, Zbadaj, idź w górę drzewa do karty. Szukaj atrybutów ze słowem data i sensownymi nazwami: są stabilniejsze niż klasy z losowymi znakami. Sprawdź też zakładkę Network: być może dane zaczęły przychodzić osobnym żądaniem JSON i parsowanie stanie się nawet prostsze.

Czy można obejść się bez proxy w małym projekcie?

Do jednorazowego zebrania dwóch-trzech stron można. Do codziennego monitoringu z setkami żądań domowy adres szybko zacznie dostawać ograniczenia, a dane staną się niepełne. Historia cen z lukami traci wartość, dlatego do regularnej pracy proxy jest potrzebne.

Jak parsować wynajem, a nie sprzedaż?

Zmień parametr deal_type na rent i dodaj typ najmu w parametrach wyszukiwania: długoterminowy lub krótkoterminowy. Linki do ofert będą zawierać rent zamiast sale, więc zaktualizuj selektor linku w parse_cards. Reszta logiki, wraz z historią cen, działa bez zmian.

Czy trzeba przechowywać zdjęcia ofert?

Do analityki cenowej nie. Zdjęcia zajmują dużo miejsca i nie są potrzebne do obliczeń. Jeśli budujesz katalog do użytku wewnętrznego, zapisuj tylko linki do obrazów, a nie same pliki.

Jak rozpoznać, że oferta została sprzedana, a nie tylko zdjęta?

Portale nie podają przyczyny zdjęcia. Poszlaką jest to, że oferta zniknęła z wyników i nie pojawiła się ponownie w ciągu miesiąca. Oferty, które znikają i pojawiają się po kilku dniach z inną ceną, są raczej opublikowane ponownie. Wiąż je po adresie i powierzchni, jak opisano w rozdziale o błędach.

Co robić, jeśli dane są potrzebne z dziesięciu miast?

Zrób listę regionów w config.py i uruchamiaj zbieranie w pętli z osobnym kanałem proxy na każde dwa-trzy miasta. Przesuwaj uruchomienia w czasie, żeby nie zbierać wszystkiego naraz. Baza pozostaje wspólna, dodaj pole regionu do tabeli offers.

Zakończenie

Spójrzmy, co zrobiłeś. Przygotowałeś środowisko z Pythonem i bibliotekami, zrozumiałeś, jak zbudowane są wyniki portalu nieruchomości, i nauczyłeś się samodzielnie znajdować selektory. Napisałeś parser Cian, który zbiera wyniki i karty ofert. Podłączyłeś mobilne proxy z rotacją i ponownymi próbami, dzięki czemu zbieranie stało się odporne i przewidywalne. Zaprojektowałeś bazę z historią cen, skonfigurowałeś raporty i autostart według harmonogramu. To pełnoprawne narzędzie pracy, a nie przykład szkoleniowy.

Co robić dalej: daj parserowi pochodzić dwa tygodnie bez zmian. W tym czasie nazbiera się historia, a ty zobaczysz słabe miejsca: gdzie spada udział wypełnionych pól, które oferty się duplikują, gdzie raport wymaga nowych kolumn. Dopiero potem dodawaj funkcje. Następnie podłącz drugi portal, używając modułowej struktury z siódmego kroku: zdziwisz się, o ile szybciej pójdzie drugi raz.

Kierunki rozwoju: powiadomienia o obniżkach cen zamienią narzędzie w źródło transakcji. Analityka po dzielnicach i typach domów zrobi z ciebie eksperta rynku z liczbami w ręku. Wiązanie ponownie opublikowanych ofert pokaże realne rabaty, których nie widać na stronie. A staranne podejście do portalu, pauzy, rotacja adresów przez mobilne proxy i zbieranie tylko potrzebnych danych pozwolą narzędziu pracować miesiącami bez awarii.

Parsowanie nieruchomości to nie kwestia szybkości, a regularności i jakości danych. Położyłeś właściwy fundament. Powodzenia w zbieraniu i niech twoja baza rośnie z każdym rankiem.