Giriş: Bu rehber sonunda ne elde edeceksiniz

Emlak piyasası rakamlarla yaşar. Kimi piyasa değerinin altında daire arar, kimi yeni projelerde rakiplerini değerlendirir, kimi yatırımcılar için rapor hazırlar. Ortak noktaları bir: güncel ilan ve fiyat verisine ihtiyaçları var ve bunu elle toplamak imkânsız. İşte tam burada kendi CİAN kazıyıcınız devreye giriyor.

Bu rehberde sıfırdan üç şeyi yapabilen çalışan bir araç kuracaksınız. Birincisi: belirli bir filtreye göre sonuç sayfalarını gezmek ve fiyat, adres, metrekare ve bağlantı içeren ilan listesini toplamak. İkincisi: her ilanın detay sayfasına girip kat, yapım yılı ve bina tipi gibi ayrıntıları çekmek. Üçüncüsü ve en değerlisi: verileri bir veritabanına kaydetmek ve gün be gün fiyat geçmişi biriktirmek; böylece hangi ilanların ucuzladığını, hangilerinin satıştan kaldırıldığını ve belirli bir semtte piyasanın nasıl hareket ettiğini görürsünüz.

Sonuç şöyle görünüyor: elinizde Python betiklerinin olduğu bir klasör, bir SQLite veritabanı dosyası ve Excel ya da Google Sheets'te açabileceğiniz bir tablo var. Sabah betiği çalıştırırsınız, taze bir kesit alırsınız. Bir hafta sonra elinizde dinamik bir tablo olur.

Bu rehber kimin için

  • Pahalı raporlar satın almadan semt bazında fiyat takibi yapmak isteyen emlak ofislerindeki pazarlamacılar ve analistler için.
  • Niş alanlar arayan ve arz-talep dengesini rakamlarla anlamak isteyen arbitrajcılar ve işletme sahipleri için.
  • Canlı ve anlaşılır bir örnek üzerinden kazımayı öğrenmek isteyen yeni geliştiriciler için.
  • Fiyatı düşen ilanları başkalarından önce yakalamak isteyen yatırımcılar ve bireysel alıcılar için.

Önceden bilmeniz gerekenler

Programlama deneyimi şart değil. Her kod satırını açıklayacağız ve ne işe yaradığını anlatacağız. Program kurmayı, komut satırını açmayı ve metin kopyalamayı bilmeniz yeterli. Tarayıcıda geliştirici araçlarını en az bir kez açtıysanız işiniz çok daha kolay olacak. Açmadıysanız da nerede olduklarını göstereceğiz.

Tek şart: dikkat. Kazıma, sınıf adlarındaki ve adreslerdeki yazım hatalarına karşı hassastır. Fazladan bir harf yüzünden betik boş liste döndürür. Korkmayın: her adımda, her şeyin yolunda gittiğinden emin olacağınız bir kontrol noktası var.

Ne kadar zaman gerekir

Ortam hazırlığı yaklaşık 30 dakika sürer. İlk çalışan sonuç kazıyıcısını bir saatte yazarsınız. İlan kartları, proxy ve veritabanı bir buçuk-iki saat daha ister. Yani acele etmeden ilerlerseniz toplam üç-dört saat temiz süre. Fiyat geçmişi ikinci çalıştırmadan itibaren kendi kendine birikmeye başlar.

Ön hazırlık: araçlar ve ortam

Kodu yazmadan önce gerekli her şeyi toplayalım. Bu bölümü atlamayın: yeni başlayanların yarısının sorunu Python'un yanlış kurulmasından ya da eksik kütüphanelerden kaynaklanıyor.

Sistem gereksinimleri

  • Windows 10 veya 11, macOS ya da Linux bir bilgisayar. Son sekiz yılın herhangi bir dizüstü bilgisayarı yeterli.
  • En az 4 GB RAM. Tarayıcı otomasyonlu sürüm için 8 GB tercih edilir.
  • Python, kütüphaneler ve veritabanı için diskte yaklaşık 2 GB boş alan.
  • Kararlı bir internet bağlantısı.

Kurulması gerekenler

  1. Python 3.11 veya üzeri. Kurulum dosyasını python.org resmi sitesinden indirin. Windows'ta kurulum sırasında ilk ekranın altındaki Add Python to PATH kutusunu mutlaka işaretleyin. Bu olmadan terminalde python komutu çalışmaz. macOS'te Python çoğu zaman hazırdır ama güncel bir sürüm kurmak daha iyi.
  2. Kod editörü. Visual Studio Code'u öneriyoruz: ücretsiz, söz dizimini renklendirir ve hataları gösterir. Yerleşik uzantı mağazasından Python eklentisini kurun (sol panelde dört kareli simge).
  3. Chrome veya Edge tarayıcı. Sayfa yapısını incelemek için geliştirici araçlarına ihtiyacımız olacak.
  4. Python kütüphaneleri. Birazdan terminal üzerinden kuracağız.
  5. Mobil proxy erişimi. Beşinci adımda gerekecek. Sunucu adresi, port, kullanıcı adı, şifre ve IP değiştirme bağlantısına ihtiyacınız var. Bunların tümü satın alma sırasında servisin kontrol panelinde verilir. Henüz proxy yoksa ilk adımları proxysiz de tamamlayabilirsiniz.

Çalışma klasörü ve sanal ortam oluşturma

  1. Diskte cian_parser adında bir klasör oluşturun. Yolda Türkçe karakter ve boşluk kullanmaktan kaçının: bunlar bazen araçları bozar.
  2. Terminali açın. Windows'ta Win+R tuşlayıp cmd yazın ve Enter'a basın. macOS'te Spotlight üzerinden Terminal'i açın.
  3. cd komutu ve klasör yolu ile klasöre geçin, örneğin Windows'ta: cd C:\projects\cian_parser veya macOS'te cd ~/projects/cian_parser.
  4. python -m venv venv komutuyla sanal ortam oluşturun. Bu, proje kütüphanelerinin sistemdekilerle çakışmaması için izole bir Python kopyasıdır.
  5. Ortamı etkinleştirin. Windows'ta: venv\Scripts\activate. macOS ve Linux'ta: source venv/bin/activate. Terminal satırının başında (venv) yazısı görünecek.
  6. Kütüphaneleri tek komutla kurun: pip install requests beautifulsoup4 lxml pandas openpyxl. Kurulum bir-iki dakika sürer.

Kontrol: terminale python -c "import requests, bs4, pandas; print('ok')" yazın. Ekranda hata olmadan ok yazısı çıktıysa ortam hazır. ModuleNotFoundError görüyorsanız ortam etkin değil ya da kurulum yarıda kesildi. venv'i yeniden etkinleştirip pip install'u tekrarlayın.

Yedeklemeler

Bu projede asıl değer kod değil, biriken fiyat geçmişli veritabanıdır. Onu geri getiremezsiniz: geçmiş fiyatlar başka hiçbir yerde görünmez. Bu yüzden ilk günden kendinizle şu sözleşmeyi yapın: veritabanı dosyası en az haftada bir buluta ya da harici diske kopyalanacak. İleride betiğe otomatik kopyalama da ekleyeceğiz.

Temel kavramlar: başlamadan önce anlaşılması gerekenler

İleride karşımıza çıkacak terimleri açıklayalım. Kazımayı zaten biliyorsanız bölümü gözden geçirin ama hukuki kısma dikkat edin.

Anahtar terimler sade bir dille

  • Kazıma (scraping): bir programın web sitesinin sayfasını otomatik olarak alıp içinden gerekli verileri çıkarması. Gözünüzle yaptığınız şeyin bin kat hızlısını betik yapar.
  • HTML: her web sayfasının yazıldığı işaretleme dili. CİAN'da dairenin fiyatı belirli niteliklere sahip bir HTML etiketinin içinde durur; bizim işimiz o etiketi bulmak.
  • Seçici (selector): HTML içindeki bir elemanın adresi. Örneğin data-mark niteliği MainPrice olan bir span. Kazıyıcı bu seçiciye bakarak fiyatın nereden alınacağını anlar.
  • HTTP isteği: siteye sunucuya yapılan başvuru. Tarayıcı sayfayı açtığında bunu yapar. requests kütüphanesi de aynı işi koddan yapar.
  • İstek başlıkları (headers): tarayıcının istek ile birlikte gönderdiği servis bilgileri: tarayıcı türü, dil, veri formatları. Sunucu bunlara bakarak ne döneceğine karar verir.
  • Proxy: isteklerinizin üzerinden geçtiği aracı sunucu. Mobil proxyler cep telefonu operatörlerinin IP adreslerini kullanır ve komutla adres değiştirmenizi sağlar.
  • Sayfalama (pagination): sonuçların sayfalara bölünmesi. Tüm ilanları toplamak için kazıyıcının ilk sayfadan son sayfaya kadar gitmesi gerekir.
  • SQLite: tek dosyada çalışan hafif bir veritabanı. Sunucu kurulumu gerektirmez, Python'a gömülüdür. Fiyat geçmişi için ideal.

Emlak platformlarında sonuç sayfaları nasıl çalışır

CİAN (Циан), Domclick, Yandex Nedvizhimost ve diğer platformalar benzer bir mantıkla çalışır. Şehir, işlem türü, oda sayısı, fiyat aralığı gibi filtreleri olan bir arama sayfası vardır. Her filtre adres çubuğunda bir parametreye dönüşür. Örneğin değeri sale olan deal_type parametresi satışı, 1'e eşit room1 ise bir odalı daireleri ekler. Bu parametreleri anlamak size güçlü bir araç verir: siteye tıklamak yerine istediğiniz adresi doğrudan oluşturursunuz.

Sonuç listesinde her ilan bir kartla temsil edilir: başlık, fiyat, adres, birkaç fotoğraf, detay sayfasına bağlantı. Detay sayfası tüm özellikleri içerir ve genellikle tüm verileri, sitenin arayüzü çizmek için kullandığı gizli bir JSON bloğunda tekrarlar. Bu bloğu ayrıştırmak HTML'den çok daha kolaydır.

Hukuki ve etik çerçeve

Dikkat: yalnızca ilanlar hakkındaki kamuya açık bilgileri toplayın: fiyat, metrekare, adres, bina özellikleri. Satıcıların ve emlakçıların telefonlarını, isimlerini ve diğer kişisel verilerini toplamayın ve saklamayın: bu, kişisel verilerin korunması kanunu kapsamındadır ve ihlali gerçek sorumluluk doğurur. Çalışmaya başlamadan önce platformun kullanıcı sözleşmesini okuyun ve verileri yeniden satmak ya da sitenin kopyasını oluşturmak için değil kendi analiziniz için kullanın. Makul istek sıklığına uyun: kazıyıcınız servisin çalışmasını engelleyecek yük oluşturmamalı.

Bu yaklaşım sadece yasal değil aynı zamanda pratiktir. Aralıklı ve adres rotasyonlu düzgün bir kazıyıcı aylarca çalışır; saldırgan olan ise bir saat içinde geçici kısıtlamalar alır.

Adım 1: Amacı ve veri yapısını belirleyin

Bu aşamanın amacı: tam olarak neyi topladığımızı ve nasıl saklanacağını net biçimde tanımlamak. Bu adım olmadan her şeyi çeken bir kazıyıcı yazar, sonra bir hafta veri çöplüğünü düzenlemekle uğraşırsınız.

  1. İş sorusunu formüle edin. Örnekler: son bir ayda Saint Petersburg'da hangi bir odalı daireler yüzde 5'ten fazla ucuzladı; belirli bir semtteki yeni projelerde metrekare kaç lira; belirli bir fiyatın altındaki ilanlar ne kadar hızlı satılıyor.
  2. Sonuç filtresini belirleyin. Bu rehberde örnek olarak şunu alacağız: satış, ikinci el, bir ve iki odalı daireler, Moskova, 15 milyon ruble'ye kadar. Siz kendi parametrelerinizi koyacaksınız.
  3. Alan listesini oluşturun. Her ilan için şunlar gerekli: benzersiz ilan kimliği, bağlantı, başlık, fiyat, adres, toplam metrekare, kat ve kat sayısı, bina tipi, yapım yılı, ilk görülme tarihi, son kontrol tarihi. Fiyat geçmişi için: ilan kimliği, tarih, fiyat.
  4. Kod editörünü açın ve proje klasöründe config.py dosyasını oluşturun. En sık değiştireceğimiz parametreleri yazın:
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'

Dikkat edin: örnek kodda satır sonları yeni satır karakteriyle gösteriliyor; editörde her değişkeni yeni satıra yazmanız yeterli. 1'e eşit region parametresi Moskova'ya, 2 Saint Petersburg'a karşılık gelir. Diğer bölgelerin kodlarını sitede filtreyi uygulayıp adres çubuğuna bakarak bulabilirsiniz.

İpucu: MAX_PAGES değerine 2-3 ile başlayın. Her sonuç sayfasında yaklaşık 28 ilan olur, hata ayıklama için bu yeter. Tam toplamayı tüm alanların doğru çekildiğinden emin olduktan sonra başlatın.

Kontrol: elinizde config.py dosyası var ve not defterinde ya da kafanızda 12 alanlık bir liste ile tek bir somut iş sorusu var. Soru "Rusya'nın tamamındaki tüm verileri istiyorum" gibi tuhaf geliyorsa geri dönün ve daraltın: ülke genelinde tam toplama yüz binlerce ilan ve tamamen farklı bir altyapı demektir.

Olası sorunlar

İstediğiniz filtrenin hangi parametreye karşılık geldiğini anlayamıyorsunuz. Çözüm: siteyi açın, filtreyi elle ayarlayın, adres çubuğundaki adresi kopyalayıp ve işaretlerine göre parçalara ayırın. Her anahtar=değer çifti bir parametredir.

Adım 2: Sonuç sayfasının yapısını inceleyin

Bu aşamanın amacı: fiyat, başlık, adres ve bağlantıyı alacağımız HTML elemanlarını bulmak. Bu en araştırmacı adım ve yeni başlayanların en çok kaybolduğu yer; bu yüzden çok yavaş ilerliyoruz.

  1. Tarayıcıyı açın ve filtrelerinizle CİAN sonuç sayfasına gidin. Daire listesini gördüğünüzden emin olun.
  2. Herhangi bir dairenin fiyatının üzerine gelin, sağ tıklayın ve İncele (Edge'de Denetle) seçeneğini seçin. Geliştirici araçları paneli açılır ve fiyatı olan eleman vurgulanır.
  3. Vurgulanan satıra bakın. Bu rehberin yazıldığı tarihte data-mark niteliği MainPrice olan bir span etiketi. Bu niteliği not edin: fiyat için seçici olacak.
  4. Üst etiketlere tıklayarak eleman ağacında yukarı çıkın ve tüm ilan kartını kapsayan etiketi bulun. Genellikle data-name niteliği CardComponent olan bir article'dır. Panelde üzerine geldiğinizde sayfada fotoğraflı tüm kart vurgulanır.
  5. Kartın içinde başlığı (data-mark niteliği OfferTitle olan span), adresi (bir araya geldiklerinde adresi oluşturan data-name niteliği GeoLabel olan birkaç a bağlantısı) ve ilan bağlantısını (href'i cian.ru/sale/flat/numara biçiminde olan a etiketi) bulun. Dört seçicinin hepsini not edin.
  6. Sayfanın altındaki sayfalama bloğunu bulun. Sonuçları en alta kaydırın, ikinci sayfa numarasına sağ tıklayın ve adresinin nasıl göründüğüne bakın. p parametresinin 2 olduğunu göreceksiniz. Yani sayfalar arasında geçiş için bu parametreyi değiştirmek yeterli.

Dikkat: data-mark ve data-name niteliklerinin adları platformlarda tasarım güncellendiğinde periyodik olarak değişir. Bu metindeki seçicileri körü körüne kopyalamayın: geliştirici araçları panelinde gerçek sayfayla mutlaka karşılaştırın. Bir seçiciyi kendi başınıza bulabilmek hazır listelerden daha değerlidir.

Gizli JSON var mı diye kontrol edin

Birçok platform sonuç verilerini hazır halde bir script etiketinin içinde tutar. Bu HTML'den daha kullanışlıdır: adresi parçalardan birleştirmek gerekmez.

  1. Geliştirici araçları panelinde Ctrl+F (macOS'te Cmd+F) basın ve offers veya initialState yazın.
  2. Arama, süslü parantezli bir sözlüğe benzeyen büyük metin içeren bir script etiketi bulduysa, veriler JSON'da var demektir. Bu bloğun başındaki değişken adını not edin.
  3. Hiçbir şey bulunmadıysa sorun değil: bir sonraki adımdaki HTML yaklaşımı her durumda çalışır.

İpucu: geliştirici araçları panelinde Network (Ağ) sekmesini açın, sayfayı yenileyin ve istekleri Fetch/XHR türüne göre filtreleyin. Bazen site sonuçları ayrı bir JSON isteği olarak yükler. offers alanı olan böyle bir istek görürseniz ayrıştırması en kolay yöntem bu olur: HTML olmadan temiz veri elde edersiniz.

Kontrol: kart, fiyat, başlık, adres ve bağlantı için seçicileriniz kayıtlı, sayfalama parametresinin adını biliyorsunuz. Panelde her seçiciye tıkladığınızda sayfada ilgili elemanın vurgulandığını görüyorsunuz.

Olası sorunlar

Geliştirici araçları panelinde HTML görünüyor ama fiyat yok. Nedeni: site verilerin bir kısmını yüklemeden sonra betikle çiziyor. Çözüm: Network sekmesinde fiyatın ayrı bir istek olarak gelip gelmediğine bakın ya da ileri seviye bölümündeki tarayıcı otomasyonunu kullanın.

Adım 3: Sonuç sayfası için ilk CİAN kazıyıcısını yazın

Bu aşamanın amacı: sonuç sayfasını indiren, ilan listesini çıkaran ve konsola yazdıran bir betik elde etmek. Bu adımdan sonra üstüne özellik ekleyeceğimiz çalışan bir iskeletiniz olur.

  1. Proje klasöründe parser.py dosyasını oluşturun.
  2. Dosyanın başında kütüphaneleri ve ayarları içe aktarın:
import time
import random
import requests
from bs4 import BeautifulSoup
from config import BASE_URL, SEARCH_PARAMS, MAX_PAGES, PAUSE_MIN, PAUSE_MAX
  1. İstek başlıklarını yazın. Sunucu bunlarda Rusça yerel ayarlı normal bir tarayıcı görmeli, aksi hâlde sayfanın istemediğiniz sürümünü alabilirsiniz:
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. Sayfa indirme fonksiyonunu yazın. Sayfa numarasını alır, parametrelere ekler ve HTML döndürür. Yanıt kodunu mutlaka kontrol ediyoruz: 200 başarı demek, gerisi durup incelemek için işaret:
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. Ayrıştırma fonksiyonunu yazın. Tüm kartları bulur ve her biri için alanları çeker. if yapısına dikkat: eleman yoksa hata vermeyip None kaydediyoruz:
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. Ana döngüyü kurun. Sayfaları gezer, istekler arasında rastgele duraklama yapar ve sonuçları ortak listede toplar. Rastgele duraklama önemli: eşit aralıklar yapay görünür ve ani yük oluşturur:
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. Dosyayı kaydedin ve terminalde python parser.py komutuyla çalıştırın. venv ortamının etkin olduğundan emin olun.

Önemli noktaları açalım. Session, istekler arasında cookies'i saklar, böylece site tek bir ziyaretçinin tutarlı davranışını görür, bir düzine kopuk başvuru değil. select_one ilk uyan elemanı ya da None döndürür, bu yüzden get_text çağrısından önce sonucu her zaman kontrol ederiz. İlan kimliğini bağlantıdan alıyoruz: adresin son parçası, 312456789 gibi bir sayı. Fiyat geçmişi için anahtar bu olacak.

İpucu: hata ayıklama aşamasında ilk sayfanın HTML'ini open('page1.html', 'w', encoding='utf-8').write(html) komutuyla bir dosyaya kaydedin. Böylece siteye fazla istek göndermeden ayrıştırma fonksiyonunu yerel kopya üzerinde geliştirebilirsiniz.

Kontrol: konsolda "Страница 1 объектов: 28", "Страница 2 объектов: 28" gibi satırlar ve altta gerçek fiyat ve adres içeren beş sözlük görüyorsunuz. Fiyatlar boşluksuz ve ruble işareti olmayan tam sayı olmalı. Sayı yerine boşsa ikinci adımdaki seçicilere geri dönün.

Olası sorunlar

Betik ilk sayfada "объектов: 0" yazdırıyor. Nedenleri: kart seçicisi değişti ya da sunucu sahte bir sayfa döndürdü. Kaydedilen page1.html dosyasını tarayıcıda açıp ne aldığınıza bakın. Robot olmadığınızı doğrulamanızı isteyen bir sayfaysa duraklamaları artırın ve proxy ile beşinci adıma geçin. Normal sonuç sayfasıysa seçicileri karşılaştırın.

Fiyat dönüştürmede ValueError hatası. Nedeni: fiyat metninde rakam yok, örneğin "Fiyat sorulur" yazıyor. Çözüm: dönüştürmeyi try içine alın ve bu durumlar için None kaydedin.

Adım 4: İlan kartlarını toplayın

Bu aşamanın amacı: kazıyıcıya her ilanın sayfasına girip ayrıntılı özellikleri çıkarmayı öğretmek: metrekare, kat, bina tipi, yapım yılı. Bu alanlar metrekare fiyatını hesaplamak ve benzer daireleri karşılaştırmak için gerekli.

  1. Tarayıcıda sonuç listesinden herhangi bir ilan sayfasını açın. Sayfa kaynak kodunu görmek için Ctrl+U basın.
  2. Ctrl+F basın ve totalArea yazın. Bu rehberin yazıldığı tarihte kart verileri, frontend-offer-card gibi bir adı olan anahtarla frontend yapılandırması nesnesinin içindeki script etiketinde bulunuyor. totalArea, floorNumber, floorsCount, buildYear, materialType ve diğer alanları göreceksiniz.
  3. totalArea araması sonuç vermezse özellikleri HTML'de arayın: genellikle "Toplam alan" ve "38,5 m²" gibi ad-değer çiftlerinin olduğu bir bloktur. Bu bloğun seçicisini not edin.
  4. parser.py dosyasına karttan JSON çıkarma fonksiyonunu ekleyin. İlgili script'i bulur, süslü parantezlerle nesneyi çıkarır ve json modülüyle ayrıştırırız:
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. Burada tam JSON ayrıştırması yerine bilinçli olarak düzenli ifadeler kullanıyoruz. Nedeni basit: sayfadaki betik sadece JSON değil kod da içerir ve saf nesneyi ayırmak zor olabilir. Düzenli ifadeler anahtarı ve ardındaki ilk sayıyı arar; sayısal alanlar için bu yeterince güvenilirdir.
  2. Sonuç listesindeki ilanları alıp her birini kart verileriyle zenginleştiren fonksiyonu ekleyin. Duraklamalar burada daha da önemli çünkü istek sayısı 28 kat artıyor:
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. if __name__ bloğunda collect_listing'den sonra enrich_offers(data, requests.Session()) çağrısını ekleyin ve uzun beklememek için MAX_PAGES değerini 1 yaparak betiği yeniden çalıştırın.

Ne kadar sürer: ortalama 6 saniyelik duraklamayla 28 kart yaklaşık 3 dakika. Kartlarla birlikte 5 sonuç sayfasının tam toplanması yaklaşık 15 dakika. Bu normal. Gayrimenkul kazıyıcısı hızlı değil, kararlı olmalı.

İpucu: her çalıştırmada kartları yeniden kazımayın. Dairenin özellikleri değişmez: metrekare ve yapım yılını bir kez toplamak yeterli. Sadece fiyat değişir, o da sonuç listesinde var. Altıncı adımda kartın yalnızca yeni ilanlar için istenmesini sağlayacağız. Bu, istek sayısını onlarca kat azaltır.

Kontrol: sözlük çıktılarında alanlar, kat, kat sayısı ve yapım yılı anahtarları makul değerlerle göründü: metrekare 15 ile 200 arası, kat kat sayısından büyük değil, yıl 1900 ile 2026 arası. Bazı ilanlarda alanların eksik olması normal: satıcıların hepsi yapım yılını doldurmaz.

Olası sorunlar

Düzenli ifade toplam yerine oda alanını buluyor. Nedeni: JSON'da livingArea veya kitchenArea gibi benzer anahtarlar var. Çözüm: şablona anahtarın önüne tırnak ya da iki nokta ekleyerek başka kelimenin parçasıyla eşleşmemesini sağlayın.

Adım 5: Mobil proxy bağlayın ve toplamayı dayanıklı hâle getirin

Bu aşamanın amacı: istekleri IP rotasyonlu mobil proxy üzerinden dağıtmak, yeniden deneme ve doğru yanıt işleme eklemek. Bu adımdan sonra kazıyıcı tek adresten aşırı yük oluşturmadan düzenli ve uzun süre çalışabilir.

Gayrimenkul kazıyıcısı neden mobil proxy'ye ihtiyaç duyar

Her büyük platform tek bir IP adresinden gelen istek sıklığını sınırlar. Bu, aşırı yüklenmeye karşı bir korumadır ve her otomasyonda devreye girer. Ev adresiniz birkaç yüz istekten sonra 429 yanıtları ya da doğrulama sayfaları almaya başlar. Mobil proxyler sorunu farklı çözer: cep operatörünün havuzundan bir IP alırsınız ve komutla ya da zamanlayıcıyla adres değişir. İstekleriniz adresler arasında dağılır, her birindeki yük düşük kalır ve kazıyıcı düzenli çalışır. Düzenli fiyat izleme için bu esastır: size tek seferlik veri değil, aylarca süren günlük kesitler gerekir.

Kurulum

  1. Mobil proxy servisinizin kontrol panelini açın ve satın aldığınız proxy'yi bulun. Dört değeri kopyalayın: host, port, kullanıcı adı, şifre. Ayrıca IP değiştirme bağlantısını da kopyalayın: genellikle anahtar içeren bir adres olur ve bu adrese gidildiğinde proxy yeni adres alır.
  2. config.py dosyasına proxy parametrelerini ekleyin. Şifreleri bir yere yüklediğiniz koda asla yazmayın: ayrı bir dosyada ya da ortam değişkenlerinde tutun:
PROXY_HOST = 'ваш_хост'
PROXY_PORT = 'ваш_порт'
PROXY_USER = 'ваш_логин'
PROXY_PASS = 'ваш_пароль'
ROTATE_URL = 'ссылка_для_смены_ip'
ROTATE_EVERY = 25
  1. parser.py dosyasına proxy'li oturum oluşturan fonksiyonu ekleyin. requests kütüphanesi http ve https için adresleri içeren bir sözlük alır:
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. Proxy'nin çalıştığını kontrol edin. Oturum üzerinden IP belirleme servisini çağıran ve yanıtı yazdıran geçici bir check_proxy.py dosyası oluşturun:
from parser import make_session
s = make_session()
print(s.get('https://api.ipify.org', timeout=20).text)
  1. Çalıştırın. Ev adresinizden farklı bir IP adresi görmelisiniz. rotate_ip çağırın ve kontrolü yeniden çalıştırın: adres değişmeli.
  2. Şimdi yeniden denemeli istek fonksiyonunu ekleyin. Üç durumu ele alır: başarılı yanıt, 429 veya 403 yanıtı (beklemek ve adres değiştirmek gerek), ağ hatası (tekrarla):
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. fetch_page ve enrich_offers içindeki session.get çağrılarını safe_get ile değiştirin. enrich_offers'a bir sayaç ekleyin: her ROTATE_EVERY istekte rotate_ip çağırın. Bu planlı rotasyondur ve adresin aşırı istek biriktirmesini engeller.

Dikkat: 429 yanıtı ya da doğrulama sayfası aldıysanız sık tekrarlarla aşmaya çalışmayın. Bu sadece mevcut adres için durumu kötüleştirir. Doğru tepki: durmak, duraklamaları artırmak, IP'yi değiştirmek ve sakin bir tempoyla devam etmek. Sitenin limitlerine saygı duyan kazıyıcı daha uzun yaşar ve daha çok veri toplar.

İpucu: bir kazıma iş parçacığı için bir proxy kanalı kullanın. Tek adres üzerinden on iş parçacığı başlatma cazip gelir ama bu doğrudan kısıtlamaya giden yoldur. Hız gerekiyorsa birkaç kanal satın alın ve farklı bölgeleri ya da filtreleri bunlara dağıtın.

Kontrol: check_proxy.py cep operatörünün adresini gösteriyor, rotasyondan sonra adres değişiyor. Kazıyıcı kartlarla birlikte iki sonuç sayfasını tek bir 429 olmadan geçiyor. Logda her 25 kartta bir "Смена IP: 200" satırları görünüyor.

Olası sorunlar

ProxyError ya da 407 hatası. Nedeni: yanlış kullanıcı adı ya da şifre, ya da yanlış port. Çözüm: kontrol panelindeki verileri kontrol edin, SOCKS değil HTTP proxy portunu kullandığınızdan emin olun. SOCKS5'iniz varsa pysocks kütüphanesini kurun ve proxy_url'de http yerine socks5h önekini kullanın.

Rotasyondan sonra adres değişmiyor. Nedeni: operatör aynı adresi verdi ya da rotasyon henüz uygulanmadı. Çözüm: rotate_ip'den sonraki duraklamayı 10 saniyeye çıkarın ve tarifenizde IP değiştirme sıklığının sınırlı olup olmadığını kontrol edin.

Adım 6: Verileri kaydedin ve fiyat geçmişini kurun

Bu aşamanın amacı: kazıyıcıyı konsola yazmaktan SQLite veritabanına yazmaya geçirmek; öyle ki her çalıştırma eskiyi üzerine yazmak yerine fiyat geçmişine yeni bir nokta eklesin. Tüm projenin kalbi burada.

Tabloları tasarlıyoruz

İki tabloya ihtiyacımız var. İlki, offers, ilanı saklar: özellikler ve tarihlerle birlikte her ilan için bir satır. İkincisi, prices, tarihli fiyatı saklar: her ilan için birkaç satır. Ayrım, her fiyat kaydında metrekare ve adresi tekrarlamamak için gerekli.

  1. storage.py dosyasını oluşturun ve tablo oluşturmayı yazın:
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. Zaten bilinen offer_id kümesini döndüren fonksiyonu ekleyin. Bu, kartları yalnızca yeni ilanlar için istemek için gerekli:
def known_ids(conn):
rows = conn.execute('SELECT offer_id FROM offers').fetchall()
return set(r[0] for r in rows)
  1. Kaydetme fonksiyonunu yazın. Yeni ilan için offers tablosuna satır ekliyoruz. Her ilan için last_seen güncellenir ve bugünün fiyatı kaydedilir. prices'teki INSERT OR REPLACE şu anlama gelir: bugünkü fiyat zaten kaydedildiyse güncelle, yoksa ekle:
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. Kaldırılan ilanları işaretleyen fonksiyonu ekleyin. Bir ilan üç günden fazla sonuç listesinde görünmediyse pasif sayıyoruz. Bu size satış hızı hakkında veri verir:
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. parser.py ana bloğunu, sonuç listesini toplayacak, yeni ilanları belirleyecek, yalnızca yenileri zenginleştirecek ve hepsini kaydedecek şekilde yeniden yazın:
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. collect_listing'i kendi oturumunu oluşturmak yerine session parametresini alacak şekilde değiştirmeyi unutmayın. Betiği çalıştırın. Proje klasöründe realty.db dosyası belirecek.

Fiyat geçmişine bakıyoruz

Fiyat değişikliklerini Excel'e aktaran report.py dosyasını oluşturun. Aşağıdaki sorgu, son fiyatı ilk fiyatından farklı olan ilanları bulur:

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))

İlk çalıştırmadan sonra change_pct sütunu sıfır olacak: henüz geçmiş yok. İkinci günden itibaren ilk değişiklikler görünür. İki hafta sonra tabloyu görürsünüz: kaç ilan fiyat indirdi, ortalama ne kadar, hangi semtler daha hızlı hareket ediyor.

İpucu: parser.py'nin sonuna veritabanı kopyalama ekleyin: shutil.copy(DB_PATH, 'backup_' + date.today().isoformat() + '.db'). Tek satır kod haftalarca biriken veriyi korur. Ayda bir eski kopyaları silip son beşi bırakın.

Kontrol: realty.db dosyası var, report.xlsx Excel'de açılıyor, içinde adres, metrekare, fiyatlar ve metrekare fiyatı sütunları var. parser.py'yi birkaç dakika sonra ikinci kez çalıştırın: "Новых объектов" satırında 0 ya da küçük bir sayı görünmeli ve kartlar yeniden istenmemeli. Bu, tekilleştirmenin çalıştığını doğrular.

Olası sorunlar

"database is locked" hatası. Nedeni: veritabanı başka bir programda, örneğin bir SQLite görüntüleyicide açık, ya da betiğin iki kopyası aynı anda çalışıyor. Çözüm: gereksiz programları kapatın ve betiği kendi kendine paralel çalıştırmayın.

report.xlsx'te tüm ilanlar aynı tarihi gösteriyor. Nedeni: betik yalnızca bir gün çalıştı. Bu beklenen bir durum, sonraki çalıştırmaları bekleyin.

Adım 7: Çalıştırmayı otomatikleştirin ve diğer platformlara genişletin

Bu aşamanın amacı: kazıyıcının her sabah kendiliğinden çalışmasını sağlamak ve kodu diğer emlak platformlarına bağlanmaya hazırlamak. Fiyat geçmişi ancak düzenli olduğunda değerlidir, bu yüzden otomasyon şart.

Çalıştırma zamanlaması

  1. Windows'ta Başlat menüsünden Görev Zamanlayıcısı'nı açın. Basit görev oluştur'a tıklayın. Bir ad girin, örneğin "Gayrimenkul kazıyıcı".
  2. Tetikleyici olarak Günlük'ü seçin, saat belirleyin, örneğin 07:30. Sabahın erken saatleri uygundur: sitelerdeki yük minimumdur ve iş günü başında taze verileriniz olur.
  3. Eylemde Program başlat'ı seçin. Program alanına venv klasörü içindeki python.exe'nin tam yolunu yazın, örneğin C:\projects\cian_parser\venv\Scripts\python.exe. Argümanlar alanına parser.py yazın. Çalışma klasörü alanına proje klasörünü girin.
  4. Görevi kaydedin ve test için sağ panelde Çalıştır'a basın. Proje klasöründeki veritabanı güncellenmeli.
  5. macOS ve Linux'ta cron kullanın. Terminale crontab -e yazın ve şu satırı ekleyin: 30 7 * * * cd /путь/к/cian_parser && ./venv/bin/python parser.py >> run.log 2>&1. Tüm çalıştırmaların logu run.log dosyasında birikir.

Diğer platformlara hazırlık

Domclick, Yandex Nedvizhimost, Metr kvadratnıy ve bölgesel portallar benzer çalışır ama seçicileri ve filtre parametreleri farklıdır. Kazıyıcıyı her biri için yeniden yazmamak için farklı olan kısımları ayrı modüllere ayırın.

  1. Proje içinde sites adında bir klasör oluşturun. İçinde cian.py dosyasını oluşturun ve fetch_page ile parse_cards fonksiyonlarını arama parametreleriyle birlikte buraya taşıyın. Aynı arayüzü bırakın: parse_cards fonksiyonu HTML alır ve aynı anahtarlara sahip sözlüklerden oluşan bir liste döndürür: offer_id, url, title, price, address.
  2. Yeni platform için örneğin domclick.py dosyasını oluşturun ve ikinci adımdaki araştırmayı tekrarlayın: sonuç listesini açın, kartı, fiyatı, bağlantıyı ve sayfalama parametresini bulun. Kendi fetch_page ve parse_cards sürümlerinizi yazın.
  3. offer_id'ye platform öneki ekleyin, örneğin cian_312456789 ve domclick_98765. Aksi hâlde farklı sitelerdeki kimlikler çakışıp geçmişi karıştırabilir.
  4. parser.py'de sites modüllerini içe aktarın ve toplamayı döngüyle her biri için çalıştırın. Veritabanındaki tablolar ortaktır: tüm kaynaklar için tek şema, source sütunu ilanın nereden geldiğini söyler.

Domclick ve Yandex Nedvizhimost hakkında önemli bir not: bu platformlar Network sekmesinde görünen JSON formatlı iç API'leri aktif olarak kullanır. Sonuç listeleri genelde kazımak için HTML'den daha uygundur ama yanıt yapıları daha sık değişir. Seçicileri ve alanları ayda bir kontrol edin.

İpucu: birden fazla platformda listelenen aynı ilan için fiyatlar farklı olabilir. Bu çiftleri karşılaştırmak ilginç analiz sağlar ve bir yerde fiyatı düşürüp başka yerde güncellemeyi unutan satıcıları bulmanıza yardımcı olur. İlanları adres, metrekare ve kat üzerinden eşleştirebilirsiniz.

Kontrol: zamanlayıcıdaki görev elle hata vermeden çalıştı, run.log'da ya da zamanlayıcı geçmişinde başarılı kaydı görünüyor. Klasör yapısında en az bir modül içeren sites klasörü var, kazıyıcı proje kökünden çalışıyor ve eskisi gibi işliyor.

Olası sorunlar

Zamanlayıcı görevin tamamlandığını söylüyor ama veritabanı güncellenmemiş. Nedeni: betik başka bir çalışma klasöründen başladı ve başka bir yerde yeni boş bir veritabanı oluşturdu. Çözüm: görevdeki Çalışma klasörü alanını doldurun ya da config.py'de veritabanına mutlak yol kullanın.

Sonucu kontrol edin: hazır kazıyıcı için kontrol listesi

Listeyi gözden geçirin ve her maddeyi işaretleyin. Hepsi tamamsa elinizde fiyat geçmişli tam işlevsel bir CİAN kazıyıcısı var.

  • parser.py betiği etkin ortamdan komutla içe aktarma hatası olmadan çalışıyor.
  • Sonuç listesi birden fazla sayfadan toplanıyor, sayfadaki ilan sayısı tarayıcıda gördüğünüzle uyuşuyor.
  • Fiyatlar tam sayı olarak kaydediliyor, adresler okunaklı, bağlantılar açılıyor.
  • Kartlar yalnızca yeni ilanlar için isteniyor, logda tekrarlı çalıştırmalarda azalan "Новых объектов" satırı görünüyor.
  • İstekler mobil proxy üzerinden geçiyor, check_proxy.py operatörün adresini gösteriyor, rotasyon onu değiştiriyor.
  • 429 durumunda kazıyıcı düşmek yerine duraklayıp IP değiştiriyor.
  • realty.db dosyası büyüyor, prices tablosu her gün yeni satır alıyor.
  • report.xlsx oluşturuluyor ve açılıyor, birkaç gün sonra içinde sıfır olmayan fiyat değişimleri görünüyor.
  • Otomatik başlatma kuruldu, log her çalıştırmayı kaydediyor.
  • Veritabanının yedek kopyası otomatik oluşturuluyor.

Baştan sona nasıl test edilir

  1. realty.db dosyasını silin ya da yeniden adlandırın, temiz sayfa açılsın.
  2. MAX_PAGES değerini 2 yapın ve parser.py'yi çalıştırın. Süreyi ölçün: kartlar dahil yaklaşık 6-8 dakika sürmeli.
  3. report.py'yi çalıştırın ve raporda yaklaşık 56 satır olduğundan emin olun.
  4. realty.db dosyasını herhangi bir SQLite görüntüleyiciyle açın ya da Python'da SELECT COUNT(*) FROM prices sorgusunu çalıştırın. Sayı, ilan sayısıyla eşleşmeli.
  5. parser.py'yi yeniden çalıştırın. Süre bir dakikaya düşmeli çünkü kartlar istenmiyor. Tarih aynı olduğu için prices satır sayısı değişmemeli.
  6. Veritabanında bir ilanın fiyatını elle başka bir sayıyla değiştirin, kayıt tarihini düne çevirin ve kazıyıcıyı çalıştırın. report.xlsx'te bu ilan fiyat değişimini göstermeli. Böylece gerçek değişiklikleri beklemeden geçmiş mantığının çalıştığından emin olursunuz.

Başarı göstergeleri

Metrekare alanı doldurulmuş ilanların oranı yüzde 90'ın üzerinde. 200 durumlu istek oranı yüzde 97'nin üzerinde. Bir çalıştırma boyunca işlenmemiş tek istisna yok. Tam çalıştırma süresi öngörülebilir ve çalıştırmadan çalıştırmaya artmıyor. Göstergeler düşükse tipik hatalar bölümüne dönün.

Tipik hatalar ve çözümleri

Emlak ilanı kazıyıcısı yazan neredeyse herkesin karşılaştığı sorunları topladık. Biçim: sorun, neden, çözüm.

Kazıyıcı dün çalışıyordu ama şimdi 0 ilan döndürüyor

Nedeni: platform tasarımı güncelledi ve data-mark ya da data-name niteliklerini değiştirdi. Çözüm: sonuç listesini tarayıcıda açın, ikinci adımı tekrarlayın ve seçicileri güncelleyin. Seçicileri modülün başında tek yerde tutma alışkanlığı edinin, düzeltme bir dakika sürsün. Kazıyıcıya bir kontrol ekleyin: ilk sayfada 0 ilan varsa mesajla kendinize bildirim gönderin.

Fiyatlar bin kat hatalı kaydediliyor

Nedeni: bazı ilanlarda fiyat binlerle ya da "aylık" ibaresiyle belirtilmiş, veya metne metrekare fiyatı karışmış. Çözüm: yan taraftaki metrekare fiyatı değil MainPrice'ı aldığınızdan emin olun. Makullük kontrolü ekleyin: Moskova'da bir dairenin satış fiyatının bir milyon ruble'den düşük olması neredeyse kesin bir ayrıştırma hatasıdır, bu durumları loglayın.

429 yanıtı daha üçüncü sayfada geliyor

Nedeni: duraklamalar çok kısa ya da proxy henüz bağlanmadı, tüm istekler tek ev IP'sinden gidiyor. Çözüm: PAUSE_MIN ve PAUSE_MAX değerlerini 6 ve 12'ye çıkarın, mobil proxy bağlayın, her 20-25 istekte planlı rotasyonu etkinleştirin. Betiğin birkaç kopyasını aynı anda çalıştırmadığınızdan emin olun.

Windows konsoluna yazarken UnicodeEncodeError hatası

Nedeni: standart Windows konsolu Kiril alfabesini her zaman doğru görüntülemez. Çözüm: terminalde çalıştırmadan önce chcp 65001 komutunu uygulayın ya da betiğin başına sys.stdout.reconfigure(encoding='utf-8') satırını ekleyin. Logları konsol yerine dosyaya da yazabilirsiniz.

Adres eksik ya da yinelenen parçalarla geliyor

Nedeni: karttaki adres birkaç GeoLabel bağlantısından oluşuyor, bazıları şehir ve bölgeyi tekrarlıyor. Çözüm: sırayı koruyarak yinelenenleri kaldırın ya da adresi tek satırda sunan ilan kartından alın. Semt bazlı analiz için adresten bilinen semt listesine göre district adında ayrı bir alan ekleyin.

Kazıyıcı elle çalışıyor ama zamanlayıcıda çalışmıyor

Nedeni: zamanlayıcı kütüphanelerin kurulu olmadığı başka bir Python yorumlayıcısı ya da başka bir çalışma klasörü kullanıyor. Çözüm: venv içindeki python.exe'ye mutlak yol verin ve çalışma klasörü alanını doldurun. Hataları görmek için çıktıyı log dosyasına yönlendirin.

Veritabanı bir ayda gigabaytlara ulaştı

Nedeni: sayfaların tam HTML'ini ya da tüm JSON alanlarını veritabanına kaydediyorsunuz. Çözüm: yalnızca gerekli alanları saklayın. Ham sayfaları yeniden ayrıştırmak için tutmak istiyorsanız, SQLite yerine diskte sıkıştırılmış dosyalarda saklayın. Üç ayda bir veritabanını sıkıştırmak için VACUUM komutunu çalıştırın.

Aynı ilanlar farklı kimliklerle yineleniyor

Nedeni: satıcı ilanı kaldırıp yeni numarayla yeniden yayınladı. Çözüm: adres, metrekare ve kattan oluşan ek bir eşleştirme anahtarı ekleyin. Aynı anahtara ama farklı offer_id'ye sahip ilanları ayrı bir tabloda ilişkilendirip fiyat geçmişini bağlantı üzerinden hesaplayabilirsiniz. Bu artık ileri analizdir ama yeniden yayınlamanın arkasına gizlenen gerçek fiyat düşüşlerini gösteren tam da bu yaklaşımdır.

İleri seviye için ek olanaklar

Temel kazıyıcı hazır. Daha fazlasını istiyorsanız en yüksek getiriyi sağlayan yönler şunlar.

Playwright ile tarayıcı otomasyonu

Bazı sayfalar verileri yüklemeden sonra betiklerle çeker ve requests boş bir iskelet alır. Bu durumlarda Playwright kullanın: gerçek bir tarayıcıyı yönetir. pip install playwright ve playwright install chromium komutlarıyla kurun. Proxy, tarayıcı başlatılırken server, username, password sözlüğünü alan proxy parametresiyle verilir. Kartların görünmesini page.wait_for_selector ile bekleyin ve page.content()'i zaten yazdığınız parse_cards fonksiyonuna geçirin. Tarayıcının on kat daha fazla kaynak tükettiğini unutmayın, bu yüzden onu noktasal olarak yalnızca sorunlu sayfalar için kullanın.

Birden fazla proxy kanalıyla paralel toplama

Birkaç bölgeyi toplamak istiyorsanız her bölge için ayrı bir mobil proxy satın alın ve her kanal için ayrı bir süreç başlatın. Tek kanal içinde çok iş parçacığı kullanmayın: yük dağıtımının anlamı kaybolur. Basit yöntem: bölge parametresi betiğe komut satırı argümanı olarak verilir, zamanlayıcı da farklı argümanlarla ve küçük zaman kaydırmasıyla birkaç görev başlatır.

Fiyat düşüşü bildirimleri

Kazıyıcının sonuna her ilan için bugünkü fiyatı öncekiyle karşılaştırma ekleyin. Düşüş belirlenen eşiğin, örneğin yüzde 3'ün üzerindeyse adres, eski ve yeni fiyat, bağlantı içeren bir mesaj oluşturun ve bir mesajlaşma botu aracılığıyla kendinize gönderin. Bu, kazıyıcıyı bir analiz aracından eylem aracına dönüştürür: avantajlı ilanları değişiklikten bir saat sonra öğrenirsiniz.

Analitik ve görselleştirme

pandas ile verileri semtlere göre gruplayıp medyan metrekare fiyatını, fiyat düşüşü olan ilanların oranını, ortalama satış süresini (pasif ilanlar için first_seen ile last_seen farkını) hesaplayabilirsiniz. matplotlib üç satır kodla aylık dinamik grafiği çizer. Raporu Google Sheets'e yükleyin ve programlama bilmeyen iş arkadaşlarınız canlı bir gösterge paneline sahip olsun.

PostgreSQL'de saklama

İlan sayısı yüz bini geçtiğinde SQLite analitik sorgularda yavaşlamaya başlar. PostgreSQL'e geçiş kolaydır: tablo şeması aynı, yalnızca bağlantı dizesi ve kütüphane değişir (sqlite3 yerine psycopg2). Bunu yalnızca gerçekten gerektiğinde yapın: tek şehir için SQLite yıllarca yeter.

Kazıyıcı sağlık takibi

Ayrı bir tabloya çalıştırma başlangıç saati, bitiş saati, toplanan ilan sayısı, hata sayısı ve IP rotasyonu sayısını kaydedin. İlan sayısı aniden düştüyse ya da hatalar yüzde 5'i geçtiyse bildirim gönderin. Böyle bir izleme, tasarım değişikliğini iki hafta sonra boş rapordan değil, olduğu gün fark etmenizi sağlar.

SSS: emlak kazıyıcı yazma hakkında sık sorulan sorular

CİAN ve diğer platformları kazımak yasal mı?

İlanlar hakkında kamuya açık bilgileri kişisel analiz için toplamak genel olarak kabul edilebilir, ancak her platformun koşulları kullanıcı sözleşmesinde yazılıdır ve okunması gerekir. Satıcıların kişisel verilerini toplamak, kopyalanmış veritabanını kendi veriniz gibi yayınlamak ve servisin çalışmasını engelleyecek yük oluşturmak kesinlikle yasaktır. Verileri ticari amaçla kullanmayı planlıyorsanız bir avukata danışın.

Neden sunucu proxy değil de mobil proxy?

Mobil operatörlerin adresleri binlerce gerçek abone tarafından paylaşılır ve sürekli değişir. Platformlar bu adreslere, gerçek alıcıların neredeyse hiç girmediği veri merkezi adreslerinden daha hoşgörülü yaklaşır. Üstelik IP'yi bağlantıyla değiştirebilmek, yüzlerce adres satın almadan yönetilebilir rotasyon sağlar.

Fiyat geçmişi için kazıyıcı ne sıklıkla çalıştırılmalı?

Günde bir kez idealdir. Emlak fiyatları seyrek değişir, günde birden fazla toplamanın anlamı yoktur, yük artar. Düşüşleri hızlı yakalamak istiyorsanız sabah ve akşam iki kez çalıştırın ama yalnızca dar bir filtreyle.

Tek proxy ile günde kaç ilan toplanabilir?

4-9 saniye duraklama ve planlı rotasyonla saatte yaklaşık 500-700 istek sorunsuz geçer, yani 24 saat çalışmayla günde 8-12 bin. Tek şehir izleme için günde 2-3 bin istek genelde yeter çünkü kartlar yalnızca yeni ilanlar için istenir.

Seçiciler değişti ve bulamıyorum, ne yapmalıyım?

İkinci adıma dönün ve fiyattan gidin: fiyata sağ tıklayın, İncele'yi seçin, kartı bulana kadar ağaçta yukarı çıkın. data ve anlamlı isimli nitelikleri arayın: rastgele karakterli sınıflardan daha kararlıdır. Network sekmesini de kontrol edin: belki veriler artık ayrı bir JSON isteğiyle geliyordur ve ayrıştırması daha da kolaylaşır.

Küçük bir proje için proxysiz olur mu?

İki-üç sayfalık tek seferlik toplama için olur. Yüzlerce istekli günlük izlemede ev adresi kısa sürede kısıtlamalar almaya başlar ve veriler eksik kalır. Eksikliklerle dolu fiyat geçmişi değerini yitirir, bu yüzden düzenli çalışma için proxy şart.

Satış yerine kiralama nasıl kazınır?

deal_type parametresini rent olarak değiştirin ve arama parametrelerine kiralama türünü ekleyin: uzun dönem ya da günlük. İlan bağlantılarında sale yerine rent olacağı için parse_cards'taki bağlantı seçicisini güncelleyin. Fiyat geçmişi dahil diğer tüm mantık değişmeden çalışır.

İlan fotoğraflarını saklamak gerekli mi?

Fiyat analizi için gerekli değil. Fotoğraflar çok yer kaplar ve hesaplamalar için lazım değildir. Dahili kullanım için katalog oluşturuyorsanız yalnızca görsel bağlantılarını saklayın, dosyaların kendisini değil.

İlanın satıldığını mı yoksa sadece kaldırıldığını mı nasıl anlarım?

Platformlar kaldırma nedenini bildirmez. Dolaylı işaret: ilan sonuç listesinden kayboldu ve bir ay içinde yeniden görünmedi. Birkaç gün sonra başka bir fiyatla kaybolup yeniden görünen ilanlar büyük olasılıkla yeniden yayınlanmıştır. Hatalar bölümünde anlatıldığı gibi bunları adres ve metrekare üzerinden ilişkilendirin.

On şehir için veri gerekiyorsa ne yapmalıyım?

config.py'de bölge listesi oluşturun ve toplamayı her iki-üç şehir için ayrı proxy kanalıyla döngü içinde çalıştırın. Tümünü aynı anda toplamamak için başlatmaları zaman içinde kaydırın. Veritabanı ortak kalır, offers tablosuna bölge alanı ekleyin.

Sonuç

Ne yaptığınıza bakalım. Python ve kütüphanelerle ortam hazırladınız, emlak platformunun sonuç sayfasının nasıl çalıştığını anladınız ve seçicileri kendi başınıza bulmayı öğrendiniz. Sonuç listesini ve ilan kartlarını toplayan bir CİAN kazıyıcısı yazdınız. Rotasyonlu ve yeniden denemeli mobil proxy bağladınız, böylece toplama kararlı ve öngörülebilir hâle geldi. Fiyat geçmişli bir veritabanı tasarladınız, raporları ve zamanlanmış otomatik başlatmayı kurdunuz. Bu artık eğitim örneği değil, tam işlevsel bir araçtır.

Sonraki adım: kazıyıcıyı hiç değiştirmeden iki hafta çalıştırın. Bu sürede geçmiş birikir ve zayıf noktaları görürsünüz: hangi alanların doldurulma oranı düşük, hangi ilanlar yineleniyor, rapor hangi yeni sütunları istiyor. Ancak bundan sonra yeni özellik ekleyin. Ardından yedinci adımdaki modüler yapıyı kullanarak ikinci platformu bağlayın: ikinci seferde ne kadar hızlı gittiğine şaşıracaksınız.

Gelişim yönü: fiyat düşüşü bildirimleri aracı işlem kaynağına dönüştürür. Semt ve bina tipi bazlı analiz sizi elinde rakamlar olan bir piyasa uzmanı yapar. Yeniden yayınlanan ilanları ilişkilendirmek sitede görünmeyen gerçek indirimleri gösterir. Platforma saygılı davranmak, duraklamalar, mobil proxylerle adres rotasyonu ve yalnızca gereken veriyi toplamak ise aracın aylarca sorunsuz çalışmasını sağlar.

Emlak kazıma hızla değil, düzenlilik ve veri kalitesiyle ilgilidir. Doğru temeli attınız. Toplamalarınızda başarılar ve veritabanınız her sabah büyümeye devam etsin.