Use Cases

Scraping Data Real Estat dengan Penanganan CAPTCHA

Data listing properti berubah nyaris setiap hari — harga baru, unit yang laku, listing yang baru naik — dan justru karena data itu bernilai tinggi bagi tim price-monitoring, agensi riset pasar, dan developer yang memantau kompetitor, hampir semua portal properti besar menaruh lapisan CAPTCHA di jalur pengambilan datanya. CaptchaAI menjaga pipeline pengumpulan listing, histori harga, dan indikator pasar Anda tetap berjalan tanpa intervensi manual setiap kali reCAPTCHA, Turnstile, atau Cloudflare Challenge muncul di tengah crawl.

Ragam Proteksi CAPTCHA di Portal Properti

Setiap segmen properti cenderung memakai kombinasi proteksi yang berbeda, tergantung seberapa besar risiko scraping bagi pemilik platform:

Jenis Platform Proteksi Jenis CAPTCHA
Agregator MLS Cloudflare Challenge Tantangan penuh + validasi proxy
Portal tipe Zillow reCAPTCHA v3 Invisible, berbasis perilaku
Direktori agen properti reCAPTCHA v2 Checkbox atau invisible
Arsip pajak properti CAPTCHA gambar Pengenalan teks (OCR)
Situs lelang properti Cloudflare Turnstile Tantangan widget
Listing komersial reCAPTCHA v2 Enterprise Verifikasi tingkat lanjut

Sebagian portal lelang regional juga masih memakai GeeTest v3 di form pendaftaran peserta lelang. CaptchaAI menyelesaikan keenam jenis di atas lewat endpoint API yang sama, jadi Anda tidak perlu logic solver terpisah untuk tiap platform.

Membangun Collector Otomatis untuk Listing Properti

Pola paling stabil untuk scraper properti adalah deteksi-lalu-solve: cek dulu jenis CAPTCHA yang muncul di response, baru kirim task ke CaptchaAI. Contoh Python berikut mendeteksi reCAPTCHA (v2 maupun v3) dan Turnstile dalam satu fungsi fetch(), lalu mem-parsing harga, alamat, kamar tidur/kamar mandi, dan luas bangunan dari HTML yang berhasil diambil:

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

Fungsi solve_captcha() di atas memakai pola submit-lalu-polling standar CaptchaAI: kirim task ke in.php, simpan task_id, lalu polling res.php setiap 5 detik sampai statusnya OK. Method userrecaptcha menangani reCAPTCHA v2 maupun v3 — bedanya cukup di parameter version dan action untuk v3. Kelas PropertyCollector membatasi hasil parsing ke 50 listing per halaman dan menyisipkan jeda 3 detik antar-request supaya pola trafik tetap wajar, bukan burst yang mudah dicurigai sebagai bot.

Kapan Anda Butuh Paket Thread Lebih Besar untuk Scraping Properti

CaptchaAI menagih per thread — jumlah CAPTCHA yang sedang diproses bersamaan — bukan per solve, sehingga biaya scraping properti gampang diprediksi. Ini kabar baik untuk tim yang sensitif terhadap budget, seperti freelancer riset pasar atau agensi data kecil yang selama ini terbiasa dengan tarif per-solve yang naik-turun. Setiap paket menyertakan solve tanpa batas per thread selama satu bulan berjalan, jadi lonjakan volume listing di musim ramai tidak otomatis menaikkan tagihan.

Pemetaan volume yang realistis untuk tim data properti:

  • Kolektor solo yang crawl 2-3 portal setiap pagi umumnya cukup dengan BASIC ($15/bulan, 5 thread).
  • Agensi kecil yang menjalankan beberapa crawler paralel untuk beberapa kota biasanya naik ke STANDARD ($30/bulan, 15 thread) atau ADVANCE ($90/bulan, 50 thread).
  • Tim enterprise yang memantau ribuan listing lintas negara setiap hari umumnya berada di PREMIUM ($170/bulan, 100 thread) ke atas.

Poin Data Properti yang Layak Dikumpulkan

Bidang Sumber Kegunaan
Harga listing Halaman properti Valuasi pasar
Alamat Halaman properti Analisis geografis
Kamar tidur/mandi/luas bangunan Detail properti Perbandingan antar-unit
Lama waktu di pasar Metadata listing Kecepatan penjualan
Riwayat harga Log perubahan harga Analisis tren
Pajak properti Arsip pajak Analisis investasi
Biaya HOA/iuran lingkungan Detail listing Analisis biaya

Tujuh field ini konsisten dipakai hampir semua tim yang membangun dashboard pasar properti internal — cukup untuk model valuasi sederhana tanpa perlu scraping seluruh halaman.

Alur Analisis Pasar dari Data Listing yang Terkumpul

Setelah listing masuk ke penyimpanan Anda, alur analisisnya biasanya berjenjang tiga tingkat: harian untuk delta harga, mingguan untuk tren median, bulanan untuk skor peluang investasi.

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

Struktur berjenjang ini memisahkan proses collection — yang butuh solve CAPTCHA di setiap run — dari proses analisis, yang murni bekerja di atas data yang sudah tersimpan. Kegagalan solve di satu run harian jadi tidak merusak laporan mingguan atau bulanan yang sudah jadi.

Kepatuhan Data saat Scraping Properti di Indonesia

UU Pelindungan Data Pribadi (UU PDP 27/2022) dan UU ITE membuat satu aturan praktis relevan untuk tim data properti: kumpulkan data listing publik yang memang ditujukan untuk pencarian, dan hindari mengumpulkan data pribadi penjual, pembeli, atau agen di luar kontak bisnis yang memang dipublikasikan. Artikel ini bukan nasihat hukum — jika model bisnis Anda menyimpan data kontak dalam skala besar, konsultasikan dengan tim legal internal sebelum produksi.

Pertanyaan Umum seputar Scraping Data Properti

Bagaimana alur CaptchaAI menangani reCAPTCHA v3 yang invisible di portal properti?

CaptchaAI membaca sitekey dan parameter action dari halaman, mengirim task dengan method userrecaptcha beserta version: v3, lalu mengembalikan token lewat polling res.php. Token itu langsung Anda kirim balik sebagai g-recaptcha-response — tidak ada interaksi visual karena reCAPTCHA v3 memang tidak menampilkan widget.

Data listing yang memang dipublikasikan untuk pencarian publik umumnya boleh dikumpulkan. Yang perlu dihindari adalah data pribadi penjual/pembeli di luar kontak bisnis yang dipublikasikan, dan pelanggaran terhadap ketentuan layanan situs sumber — lihat catatan kepatuhan di atas.

Paket CaptchaAI mana yang cocok untuk crawl properti skala besar?

Tergantung jumlah proses paralel, bukan jumlah listing. Tim dengan puluhan worker crawler berjalan bersamaan biasanya perlu ADVANCE ($90/bulan, 50 thread) atau PREMIUM ($170/bulan, 100 thread); tim enterprise lintas negara bisa naik ke CORPORATE ($240/bulan, 150 thread) atau ENTERPRISE ($300/bulan, 200 thread).

Apakah CaptchaAI bisa menyelesaikan CAPTCHA gambar di arsip pajak properti yang lama?

Bisa. Arsip pajak properti pemerintah daerah sering memakai CAPTCHA gambar OCR klasik tanpa JavaScript modern. CaptchaAI menyelesaikannya lewat method post yang sama dipakai untuk CAPTCHA teks biasa, dan biasanya lebih cepat dibanding reCAPTCHA atau Turnstile.

Apakah CaptchaAI bisa menangani hCaptcha untuk portal properti alternatif?

Belum. hCaptcha tidak didukung CaptchaAI saat ini. Sebagian besar portal properti besar memakai reCAPTCHA atau Cloudflare, tapi jika target Anda memang memakai hCaptcha, Anda memerlukan layanan lain untuk jenis itu.

Baca Juga

Komentar dinonaktifkan untuk artikel ini.