Use Cases

Penanganan CAPTCHA untuk Ekstraksi Data GIS dan Pemetaan

Untuk mengambil batas bidang tanah, penetapan zonasi, atau nilai penilaian properti dari portal GIS pemerintah yang dikunci image CAPTCHA, alurnya sebenarnya pendek: unduh gambar CAPTCHA-nya, kirim ke endpoint OCR CaptchaAI, terima teks hasilnya, lalu kirim ulang form pencarian beserta jawaban itu. Setelah token teks masuk, sisanya — parsing parcel, iterasi ribuan koordinat — berjalan seperti scraping HTML biasa. Artikel ini merangkai pola lengkapnya dengan Python dan JavaScript, plus cara menjaga ekstraksi tetap stabil ketika volume naik.

Di mana CAPTCHA muncul di portal GIS

Sebelum menulis skrip, petakan dulu di titik mana portal memasang tantangan. Kebanyakan sistem GIS daerah dan assessor hanya memicu CAPTCHA pada aksi tertentu, bukan di setiap request — jadi Anda tidak perlu menyelesaikan CAPTCHA untuk setiap halaman.

Jenis portal Jenis CAPTCHA Kapan muncul
County GIS/assessor Image text CAPTCHA Query pencarian parcel
Portal geospasial negara bagian CAPTCHA kustom Request download data
Portal data USGS reCAPTCHA v2 Akses data massal
Peta zonasi kota Image CAPTCHA Pencarian properti berulang
Database lingkungan Math CAPTCHA Pembuatan laporan
Pencarian zona banjir Image text CAPTCHA Query alamat

Mayoritas kolom di atas adalah image/OCR CAPTCHA — persis tipe yang ditangani endpoint OCR CaptchaAI. Portal USGS yang memakai reCAPTCHA v2 adalah pengecualian; untuk itu Anda memakai method userrecaptcha, bukan alur base64 di bawah.

Alur penyelesaian image CAPTCHA: unduh, kirim, ambil teks

Semua kode di halaman ini memakai satu kerangka yang sama, dan pola inilah yang perlu Anda hafal: unduh gambar → kirim ke in.php → simpan task ID → polling res.php → pakai teks token. Untuk image CAPTCHA, gambar diubah menjadi base64 lalu dikirim dengan method=base64. CaptchaAI membalas dengan task ID, dan Anda melakukan polling ke endpoint hasil sampai status bernilai 1. Jeda 3 detik antar polling sudah aman untuk OCR image; jangan menembak res.php tanpa jeda karena hanya menghabiskan request.

Kunci keberhasilan di portal GIS adalah sesi: gambar CAPTCHA sering terikat cookie sesi, jadi unduh gambar dan kirim ulang jawaban harus dilakukan dalam satu Session yang sama. Begitu pula hidden form field — ambil semuanya sebelum submit, atau server menolak jawaban yang benar sekalipun.

Ekstraktor parcel dengan Python

Kelas berikut membungkus seluruh alur: pencarian parcel per ID, pencarian per alamat, dan ekstraksi batch dengan jeda antar request. Perhatikan bahwa deteksi CAPTCHA, penyelesaian, dan pengiriman ulang seluruhnya berjalan di dalam satu requests.Session.

import requests
import base64
import time
import re

class GISDataExtractor:
    def __init__(self, api_key):
        self.api_key = api_key
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
        })

    def lookup_parcel(self, portal_url, parcel_id):
        """Look up parcel data by ID, solving CAPTCHAs as needed."""
        response = self.session.get(
            f"{portal_url}/parcel", params={"id": parcel_id}
        )

        if self._has_image_captcha(response.text):
            captcha_url = self._extract_captcha_url(response.text, portal_url)
            captcha_text = self._solve_captcha(captcha_url)

            # Re-submit with solved CAPTCHA
            response = self.session.post(f"{portal_url}/parcel", data={
                "id": parcel_id,
                "captcha": captcha_text,
                **self._extract_hidden_fields(response.text)
            })

        return self._parse_parcel_data(response.text)

    def search_by_address(self, portal_url, address):
        """Search GIS records by street address."""
        response = self.session.get(
            f"{portal_url}/search", params={"address": address}
        )

        if self._has_image_captcha(response.text):
            captcha_url = self._extract_captcha_url(response.text, portal_url)
            captcha_text = self._solve_captcha(captcha_url)

            response = self.session.post(f"{portal_url}/search", data={
                "address": address,
                "captcha": captcha_text,
                **self._extract_hidden_fields(response.text)
            })

        return self._parse_search_results(response.text)

    def bulk_extract(self, portal_url, parcel_ids, delay=3):
        """Extract data for multiple parcels with rate limiting."""
        results = {}

        for parcel_id in parcel_ids:
            try:
                results[parcel_id] = self.lookup_parcel(portal_url, parcel_id)
            except Exception as e:
                results[parcel_id] = {"error": str(e)}
            time.sleep(delay)

        return results

    def _has_image_captcha(self, html):
        return bool(re.search(
            r'captcha|verification.?image|security.?code',
            html, re.IGNORECASE
        ))

    def _extract_captcha_url(self, html, base_url):
        from bs4 import BeautifulSoup
        from urllib.parse import urljoin
        soup = BeautifulSoup(html, "html.parser")

        img = (
            soup.find("img", attrs={"src": lambda s: s and "captcha" in s.lower()}) or
            soup.find("img", {"id": re.compile(r"captcha", re.I)}) or
            soup.find("img", {"class": re.compile(r"captcha", re.I)})
        )

        if img and img.get("src"):
            return urljoin(base_url, img["src"])
        raise ValueError("CAPTCHA image not found")

    def _solve_captcha(self, captcha_url):
        """Download and solve image CAPTCHA."""
        img_response = self.session.get(captcha_url)
        img_base64 = base64.b64encode(img_response.content).decode("utf-8")

        resp = requests.post("https://ocr.captchaai.com/in.php", data={
            "key": self.api_key,
            "method": "base64",
            "body": img_base64,
            "json": 1
        })
        task_id = resp.json()["request"]

        for _ in range(30):
            time.sleep(3)
            result = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": self.api_key,
                "action": "get",
                "id": task_id,
                "json": 1
            })
            data = result.json()
            if data["status"] == 1:
                return data["request"]

        raise TimeoutError("CAPTCHA solve timed out")

    def _extract_hidden_fields(self, html):
        from bs4 import BeautifulSoup
        soup = BeautifulSoup(html, "html.parser")
        fields = {}
        for inp in soup.select("input[type='hidden']"):
            name = inp.get("name")
            if name:
                fields[name] = inp.get("value", "")
        return fields

    def _parse_parcel_data(self, html):
        from bs4 import BeautifulSoup
        soup = BeautifulSoup(html, "html.parser")

        def text_or_none(node):
            return node.text.strip() if node and node.text else None

        return {
            "parcel_id": text_or_none(soup.select_one(".parcel-id, #parcelId")),
            "owner": text_or_none(soup.select_one(".owner, .owner-name")),
            "address": text_or_none(soup.select_one(".address, .situs")),
            "zoning": text_or_none(soup.select_one(".zoning, .zone-code")),
            "acreage": text_or_none(soup.select_one(".acreage, .area")),
            "assessed_value": text_or_none(soup.select_one(".assessed, .value")),
            "land_use": text_or_none(soup.select_one(".land-use, .use-code"))
        }

    def _parse_search_results(self, html):
        from bs4 import BeautifulSoup
        soup = BeautifulSoup(html, "html.parser")

        def text_or_none(node):
            return node.text.strip() if node and node.text else None

        results = []
        for row in soup.select(".result-row, tr.parcel"):
            results.append({
                "parcel_id": text_or_none(row.select_one(".parcel-id")),
                "address": text_or_none(row.select_one(".address")),
                "owner": text_or_none(row.select_one(".owner"))
            })
        return results


# Usage
extractor = GISDataExtractor("YOUR_API_KEY")

# Single parcel lookup
parcel = extractor.lookup_parcel(
    "https://gis.county.example.gov",
    "12-34-567-890"
)
print(f"Owner: {parcel['owner']}, Zoning: {parcel['zoning']}")

# Bulk extraction
parcels = extractor.bulk_extract(
    "https://gis.county.example.gov",
    ["12-34-567-890", "12-34-567-891", "12-34-567-892"]
)

Ekstraksi berbasis koordinat dengan JavaScript

Ketika portal mengekspos endpoint identify per titik lat/lng, Anda dapat menyapu satu wilayah dengan grid koordinat. Versi JavaScript ini memakai pola yang sama, tetapi menambahkan iterasi grid dan jeda antar titik agar tidak membanjiri target.

class GISExtractor {
  constructor(apiKey) {
    this.apiKey = apiKey;
  }

  async extractByCoordinates(portalUrl, lat, lng) {
    const url = `${portalUrl}/identify?lat=${lat}&lng=${lng}`;
    const response = await fetch(url);
    const html = await response.text();

    if (this.hasCaptcha(html)) {
      return this.solveAndExtract(portalUrl, html, { lat, lng });
    }

    return this.parseGISData(html);
  }

  async extractRegion(portalUrl, bounds, gridSize = 0.01) {
    const results = [];
    const { north, south, east, west } = bounds;

    for (let lat = south; lat <= north; lat += gridSize) {
      for (let lng = west; lng <= east; lng += gridSize) {
        try {
          const data = await this.extractByCoordinates(portalUrl, lat, lng);
          if (data.parcelId) results.push(data);
        } catch (error) {
          console.error(`Failed at ${lat},${lng}: ${error.message}`);
        }
        // Rate limit
        await new Promise(r => setTimeout(r, 2000));
      }
    }

    return results;
  }

  hasCaptcha(html) {
    return /captcha|verification.?image|security.?code/i.test(html);
  }

  async solveAndExtract(portalUrl, html, params) {
    const imgMatch = html.match(/src="([^"]*captcha[^"]*)"/i);
    if (!imgMatch) throw new Error('CAPTCHA image not found');

    const imgUrl = new URL(imgMatch[1], portalUrl).href;
    const imgResp = await fetch(imgUrl);
    const buffer = await imgResp.arrayBuffer();
    const base64 = Buffer.from(buffer).toString('base64');

    const submitResp = await fetch('https://ocr.captchaai.com/in.php', {
      method: 'POST',
      body: new URLSearchParams({
        key: this.apiKey,
        method: 'base64',
        body: base64,
        json: '1'
      })
    });
    const { request: taskId } = await submitResp.json();

    for (let i = 0; i < 30; i++) {
      await new Promise(r => setTimeout(r, 3000));
      const result = await fetch(
        `https://ocr.captchaai.com/res.php?key=${this.apiKey}&action=get&id=${taskId}&json=1`
      );
      const data = await result.json();
      if (data.status === 1) {
        const response = await fetch(portalUrl, {
          method: 'POST',
          body: new URLSearchParams({
            ...params,
            captcha: data.request
          })
        });
        return this.parseGISData(await response.text());
      }
    }
    throw new Error('CAPTCHA solve timed out');
  }

  parseGISData(html) {
    return {
      parcelId: html.match(/parcel.?id[^>]*>([^<]+)/i)?.[1]?.trim(),
      zoning: html.match(/zon(?:e|ing)[^>]*>([^<]+)/i)?.[1]?.trim(),
      acreage: html.match(/acreage|area[^>]*>([^<]+)/i)?.[1]?.trim(),
      landUse: html.match(/land.?use[^>]*>([^<]+)/i)?.[1]?.trim()
    };
  }
}

// Usage
const gis = new GISExtractor('YOUR_API_KEY');

// Single coordinate lookup
const data = await gis.extractByCoordinates(
  'https://gis.county.example.gov',
  34.0522, -118.2437
);

// Extract entire region
const region = await gis.extractRegion('https://gis.county.example.gov', {
  north: 34.10, south: 34.00, east: -118.20, west: -118.30
});

Parameter OCR yang menentukan akurasi

Image CAPTCHA di portal GIS sangat bervariasi: ada yang murni angka, ada yang math, ada yang huruf besar dengan panjang tetap. Menetapkan parameter yang tepat pada request in.php menaikkan tingkat keberhasilan jauh lebih efektif daripada sekadar coba ulang.

Parameter Nilai Kapan dipakai
method base64 Image CAPTCHA standar
numeric 1 CAPTCHA khusus angka
min_len 4 Saat panjang karakter diketahui
max_len 6 Saat panjang karakter diketahui
language 0 Karakter Latin/Inggris
textinstructions Kustom Math CAPTCHA atau kode berformat khusus

Checklist sebelum ekstraksi batch besar

Sebelum menjalankan sweep ribuan parcel atau satu wilayah penuh, lewati dulu daftar singkat ini agar batch tidak berhenti di tengah jalan:

  • Verifikasi viewport peta, filter wilayah, dan kontrol pagination sebelum memulai pengumpulan besar.
  • Simpan payload koordinat yang dinormalisasi beserta respons target mentah, agar bug ekstraksi tetap bisa di-debug belakangan.
  • Jeda batch saat kepadatan CAPTCHA melonjak; jangan biarkan coba ulang menyembunyikan perubahan perilaku di sisi target.

Biaya dan skala: model thread CaptchaAI

Untuk ekstraksi GIS, biaya biasanya jadi pertimbangan utama — apalagi bagi tim data properti atau agensi price-monitoring di Jakarta yang menagih klien per proyek. CaptchaAI menagih per thread bersamaan, bukan per solve, dan setiap plan memberi solve tak terbatas dalam bulan tagihan. Satu thread adalah satu CAPTCHA yang sedang berjalan; begitu selesai, thread langsung menerima antrean berikutnya.

Untuk pencarian parcel sesekali, paket BASIC ($15/bulan, 5 threads) sudah cukup. Untuk menyapu ribuan parcel atau satu county secara paralel, paket ADVANCE ($90/bulan, 50 threads) memberi konkurensi jauh lebih tinggi tanpa biaya tambahan per gambar — model biaya tetap ini yang membuatnya mudah dianggarkan dibanding harga per-solve. Daftar tier lengkap tersedia di halaman pricing captchaai.com.

Jika Anda men-deploy worker di cloud, tempatkan dekat target: region seperti AWS ap-southeast-1 (Singapura) atau ap-southeast-3 (Jakarta) memangkas latency bolak-balik, sehingga jeda polling 3 detik terasa lebih ringan pada batch besar. Terakhir, catatan kepatuhan: UU Pelindungan Data Pribadi (UU 27/2022) dan UU ITE mewajibkan Anda hanya memproses data yang memang boleh diproses — ekstrak data GIS publik, hindari data pribadi. Ini bukan nasihat hukum.

Mengatasi masalah umum

Masalah Penyebab Perbaikan
Gambar CAPTCHA termuat rusak Cookie sesi belum ada Muat halaman pencarian lebih dulu dalam sesi yang sama
Teks hasil ditolak Sensitif huruf besar/kecil Tambahkan parameter case_sensitive=1
Portal memberi CAPTCHA berbeda CAPTCHA terikat sesi Unduh dan selesaikan dalam satu sesi
Data parcel kosong setelah CAPTCHA Hidden form field terlewat Ambil semua hidden input sebelum submit

Pertanyaan umum

Berapa biaya menyelesaikan image CAPTCHA untuk ekstraksi GIS skala besar?

Biaya mengikuti jumlah thread, bukan jumlah solve — setiap plan memberi solve tak terbatas per thread. BASIC ($15/bulan, 5 threads) memadai untuk pencarian sesekali; untuk ribuan parcel, ADVANCE ($90/bulan, 50 threads) memberi paralelisme lebih besar tanpa biaya per gambar. Tidak ada biaya per-solve dan tidak ada surcharge per jenis CAPTCHA.

Apakah CaptchaAI mendukung reCAPTCHA v2 yang dipakai portal USGS?

Ya. reCAPTCHA v2 didukung penuh, tetapi alurnya berbeda dari image CAPTCHA — Anda memakai method userrecaptcha dengan sitekey halaman, bukan endpoint OCR base64. Lihat cara solve reCAPTCHA v2 via API untuk polanya.

Apakah saya boleh mengekstrak data dari portal GIS pemerintah?

Data GIS publik seperti batas parcel dan zonasi umumnya terbuka, tetapi tetap hormati ToS portal dan batas laju request-nya. Di Indonesia, UU 27/2022 dan UU ITE mengharuskan Anda hanya memproses data yang boleh diproses — hindari data pribadi. Artikel ini bukan nasihat hukum.

Bagaimana menangani format CAPTCHA yang berbeda tiap daerah?

Setiap daerah bisa memakai implementasi CAPTCHA yang berlainan. Manfaatkan parameter textinstructions untuk mendeskripsikan format spesifik — misalnya "5 huruf besar" atau "selesaikan persamaan matematika" — sehingga OCR tahu bentuk keluaran yang diharapkan.

Langkah selanjutnya

Ambil API key CaptchaAI Anda dan mulai selesaikan image CAPTCHA portal GIS secara otomatis, langsung dari skrip Python atau JavaScript.

Komentar dinonaktifkan untuk artikel ini.