Use Cases

CAPTCHA İşleme ile Gayrimenkul Verilerini Kazıma

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.

İlgili kılavuzlar

Bu makale için yorumlar devre dışı bırakılmıştır.