Emlak portallarından fiyat ve ilan verisi toplayan her iş akışı er ya da geç aynı duvara çarpar: reCAPTCHA veya Cloudflare doğrulaması. Gece çalışan toplama işiniz sabah boş bir CSV ile dönmüşse, sorun kodunuzda değil — sayfanın arkasına eklenen doğrulama katmanındadır. Bu rehber, CAPTCHA'yı iş akışınızı durduran bir engel olmaktan çıkarıp API ile çözülen tek bir adıma indirger. Aşağıda hangi portal türünün hangi doğrulamayı kullandığını, uçtan uca çalışan bir Python toplayıcıyı ve üretimde ayakta kalan çalıştırma pratiklerini bulacaksınız.
Emlak portallarında hangi CAPTCHA türleriyle karşılaşırsınız?
Gayrimenkul siteleri tek tip koruma kullanmaz. Aynı portalın arama sayfası görünmez bir reCAPTCHA v3 skoruyla çalışırken, ilan detay sayfası onay kutulu bir reCAPTCHA v2 gösterebilir. Toplayıcınızı yazmadan önce hedef portalların hangi katmanı kullandığını bilmek, doğru API method değerini seçmenizi sağlar. Türkiye pazarında yoğun kullanılan emlak ilan portalları da dahil olmak üzere en sık karşılaşılan eşleşmeler şöyle:
| Portal türü | Koruma | CAPTCHA türü |
|---|---|---|
| MLS toplayıcıları | Cloudflare doğrulama akışı | Tam doğrulama + proxy |
| Zillow tipi portallar | reCAPTCHA v3 | Görünmez, davranışsal |
| Emlakçı rehberleri | reCAPTCHA v2 | Onay kutusu veya görünmez |
| Emlak vergisi kayıtları | Resim CAPTCHA'sı | Metin tanıma (OCR) |
| Açık artırma siteleri | Cloudflare Turnstile | Widget doğrulaması |
| Ticari ilanlar | reCAPTCHA v2 Enterprise | Gelişmiş doğrulama |
Bu türlerin tamamı CaptchaAI tarafından desteklenir: reCAPTCHA v2/v3, v2 Enterprise, Cloudflare Turnstile ve Cloudflare doğrulama akışı ile resim/OCR CAPTCHA'ları. Farklı türler için ayrı entegrasyonlar yazmanız gerekmez; tek bir çözme fonksiyonu method parametresini değiştirerek hepsini karşılar.
Mülk verisi toplayıcı: tam Python örneği
Aşağıdaki toplayıcı, her sayfayı çeker; sayfada bir reCAPTCHA veya Turnstile bulursa sitekey ve pageurl değerlerini çıkarır, CaptchaAI'ye gönderir, dönen token'ı formla birlikte tekrar POST eder. solve_captcha fonksiyonu görevi in.php'ye gönderir, ardından sonucu res.php üzerinden periyodik olarak sorgular. CAPCHA_NOT_READY yanıtı geldiği sürece bekler; OK| ile başlayan yanıt geldiğinde token'ı döndürür. Bu, üretimde ihtiyacınız olan tüm mantığı tek dosyada toplar.
import requests
import time
import re
import json
import csv
import os
from datetime import datetime
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
def solve_captcha(params):
params["key"] = API_KEY
resp = requests.get("https://ocr.captchaai.com/in.php", params=params)
if not resp.text.startswith("OK|"):
raise Exception(f"Submit: {resp.text}")
task_id = resp.text.split("|")[1]
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get", "id": task_id,
})
if result.text == "CAPCHA_NOT_READY":
continue
if result.text.startswith("OK|"):
return result.text.split("|", 1)[1]
raise Exception(f"Solve: {result.text}")
raise TimeoutError()
class PropertyCollector:
def __init__(self):
self.session = requests.Session()
self.session.headers["User-Agent"] = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/120.0.0.0"
)
def fetch(self, url):
"""Fetch page with automatic CAPTCHA handling."""
resp = self.session.get(url)
# reCAPTCHA
match = re.search(
r'data-sitekey=["\']([A-Za-z0-9_-]+)["\']', resp.text
)
if match:
# Detect v3
is_v3 = "recaptcha/api.js?render=" in resp.text
params = {
"method": "userrecaptcha",
"googlekey": match.group(1),
"pageurl": url,
}
if is_v3:
params["version"] = "v3"
params["action"] = "search"
token = solve_captcha(params)
resp = self.session.post(url, data={
"g-recaptcha-response": token,
})
# Turnstile
if "cf-turnstile" in resp.text:
match = re.search(r'data-sitekey=["\']([^"\']+)', resp.text)
if match:
token = solve_captcha({
"method": "turnstile",
"sitekey": match.group(1),
"pageurl": url,
})
resp = self.session.post(url, data={
"cf-turnstile-response": token,
})
return resp.text
def collect_listings(self, urls):
"""Collect property listings from multiple pages."""
listings = []
for url in urls:
try:
html = self.fetch(url)
page_listings = self._parse_listings(html)
listings.extend(page_listings)
print(f" {len(page_listings)} listings from {url}")
time.sleep(3)
except Exception as e:
print(f" Error: {url} - {e}")
return listings
def _parse_listings(self, html):
"""Extract property data from HTML."""
listings = []
# Price extraction
prices = re.findall(r'\$\s*([\d,]+)', html)
# Address extraction
addresses = re.findall(
r'class="address"[^>]*>(.*?)</(?:div|span|p)', html
)
# Bed/Bath extraction
beds = re.findall(r'(\d+)\s*(?:bed|br|bedroom)', html, re.I)
baths = re.findall(r'(\d+)\s*(?:bath|ba|bathroom)', html, re.I)
# Sqft extraction
sqft = re.findall(r'([\d,]+)\s*(?:sq\s*ft|sqft)', html, re.I)
# Combine available data
count = max(len(prices), len(addresses), 1)
for i in range(min(count, 50)): # Cap at 50 per page
listing = {
"price": prices[i] if i < len(prices) else None,
"address": (
addresses[i].strip() if i < len(addresses) else None
),
"beds": beds[i] if i < len(beds) else None,
"baths": baths[i] if i < len(baths) else None,
"sqft": sqft[i] if i < len(sqft) else None,
"collected_at": datetime.utcnow().isoformat(),
}
if listing["price"] or listing["address"]:
listings.append(listing)
return listings
def export_csv(self, listings, filename):
if not listings:
print("No listings to export")
return
keys = ["price", "address", "beds", "baths", "sqft", "collected_at"]
with open(filename, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=keys)
writer.writeheader()
writer.writerows(listings)
print(f"Exported {len(listings)} listings to {filename}")
# Usage
collector = PropertyCollector()
search_urls = [
"https://example-realty.com/search?city=austin&type=sale&page=1",
"https://example-realty.com/search?city=austin&type=sale&page=2",
"https://example-realty.com/search?city=austin&type=sale&page=3",
]
listings = collector.collect_listings(search_urls)
collector.export_csv(listings, "austin_listings.csv")
Kod, reCAPTCHA v3'ü recaptcha/api.js?render= imzasına bakarak v2'den ayırır ve v3 için version ile action parametrelerini ekler. Türü elle belirlemek yerine bu otomatik tespite güvenmek, tek bir portal farklı sayfalarında farklı sürümler kullandığında sizi kırılgan koşullardan kurtarır.
Toplamaya değer veri alanları
Ham HTML'i çektikten sonra asıl değer, doğru alanları düzenli olarak çıkarmakta. Aşağıdaki tablo, çoğu piyasa analizi iş akışının dayandığı çekirdek alanları ve bunların ne işe yaradığını özetler:
| Alan | Kaynak | Kullanım örneği |
|---|---|---|
| İlan fiyatı | Mülk sayfası | Piyasa değerlemesi |
| Adres | Mülk sayfası | Coğrafi analiz |
| Yatak/Banyo/m² | Mülk ayrıntıları | Karşılaştırılabilir analiz |
| Piyasada geçen gün | İlan meta verisi | Piyasa hızı |
| Fiyat geçmişi | Fiyat değişikliği günlüğü | Trend analizi |
| Emlak vergisi | Vergi kayıtları | Yatırım analizi |
| Aidat / site giderleri | İlan ayrıntıları | Maliyet analizi |
Bu alanların hepsini her ilan taşımaz. Toplayıcıdaki _parse_listings fonksiyonu, eksik alanlar için None bırakarak çalışmayı sürdürür; böylece tek bir eksik regex eşleşmesi tüm sayfayı düşürmez.
Günlük toplama ve piyasa analizi akışı
Emlak verisi tek seferlik bir çekim değil, tekrarlanan bir zaman serisidir. Değer, aynı ilanları gün gün izleyip fiyat değişimini ve envanter hareketini görmekten gelir. Pratikte işe yarayan katmanlı bir ritim şöyle görünür:
Daily Collection
→ Property listings (500-1000 per market)
→ Price changes (delta from previous day)
→ New listings vs delisted
Weekly Analysis
→ Median price trends
→ Inventory levels
→ Days-on-market averages
→ Price-per-sqft by neighborhood
Monthly Report
→ Market heat map
→ Competitive pricing analysis
→ Investment opportunity scoring
Günlük katman ham veriyi toplar, haftalık katman eğilimleri çıkarır, aylık katman ise karar destek raporunu üretir. Bu ayrımı korumak, bir günün toplama işi başarısız olsa bile analiz hattınızın çökmesini önler.
Üretimde güvenilir çalıştırma pratikleri
Örnek kod çalışır durumda, ama gerçek bir üretim işine dönüştürmek için birkaç noktayı sağlamlaştırmanız gerekir. Sayfalandırmayı ?page=N veya &offset=N parametresini artırarak yönetin ve son sayfayı boş yanıt döndüğünde algılayın. İstekler arasına gecikme koyup eşzamanlılığı planınızın thread sayısıyla sınırlı tutun; CaptchaAI thread tabanlı faturalandırır, yani aynı anda kaç CAPTCHA çözebileceğinizi thread sayınız belirler. Küçük bir izleme işi için BASIC ($15/ay, 5 thread) başlangıç için yeterlidir; birden fazla pazarı paralel tarayan bir ekip ADVANCE ($90/ay, 50 thread) ile rahatça çalışır. Fiyatlar USD üzerinden sabit kaldığından, TL dalgalanmasından etkilenmeyen öngörülebilir bir aylık maliyet elde edersiniz.
MLS toplayıcılarındaki Cloudflare doğrulama akışı, proxy parametresi gerektirdiği için en fazla dikkat isteyen türdür — token'ı üreten oturumla, token'ı kullanan isteğin aynı çıkış IP'sinden gelmesi beklenir. Son olarak, kazıdığınız verinin kapsamına dikkat edin: satıcı veya emlakçı adı, telefon ve e-posta gibi kişisel veriler Türkiye'de KVKK kapsamına girer. Herkese açık ilan alanlarıyla (fiyat, konum, metrekare) sınırlı kalmak ve yetkili bir veri toplama iş akışı içinde çalışmak, hem yasal hem de teknik olarak en güvenli yoldur.
Sık sorulan sorular
Emlak verisi kazımak KVKK açısından güvenli mi?
Herkese açık ilan alanları (fiyat, konum, metrekare, oda sayısı) genellikle toplanabilir. Ancak satıcı veya emlakçıya ait ad, telefon ve e-posta gibi kişisel veriler KVKK kapsamına girer — bunları toplamaktan kaçının ve her zaman sitenin kullanım şartlarına uyun.
reCAPTCHA v3'ü tarayıcı çalıştırmadan çözebilir miyim?
Evet. Yukarıdaki toplayıcı headless tarayıcı bile açmadan çalışır: sayfadan sitekey ve pageurl değerlerini çıkarır, CaptchaAI'ye gönderir, dönen token'ı g-recaptcha-response alanıyla POST eder. Tüm iş requests ile sunucu tarafında biter.
Görünmez reCAPTCHA'yı sayfada nasıl tespit ederim?
Sayfa kaynağında recaptcha/api.js?render= imzasını arayın; bu, v3 (görünmez, skor tabanlı) bir reCAPTCHA olduğunu gösterir. İmza yoksa ve data-sitekey bir onay kutusuyla eşleşiyorsa, büyük olasılıkla klasik bir reCAPTCHA v2 ile karşı karşıyasınızdır.
Cloudflare doğrulama akışı neden proxy istiyor?
Cloudflare, doğrulamayı istekte bulunan IP'ye bağlar. Token'ı üreten oturum ile onu kullanan isteğin aynı çıkış IP'sinden gelmesi gerekir; bu yüzden çözme görevine bir proxy parametresi eklemeniz beklenir. reCAPTCHA ve Turnstile'da bu genellikle gerekmez.
Emlak kazıma için hangi CaptchaAI planı yeterli?
Tek pazarı günlük izleyen küçük bir iş için BASIC ($15/ay, 5 thread) yeterlidir. Birden çok şehri veya portalı paralel taramak istiyorsanız, daha yüksek eşzamanlılık için ADVANCE ($90/ay, 50 thread) daha uygun bir başlangıçtır. Her planda thread başına çözüm sayısı sınırsızdır.