Use Cases

Pemantauan Tarif Penerbangan dan Perjalanan dengan Penanganan CAPTCHA

Script pemantau tarif jarang gagal karena pola ekstraksi harganya salah. Ia berhenti karena halaman yang dikembalikan server bukan hasil pencarian, melainkan tantangan CAPTCHA. Solusinya bukan menambah retry, melainkan memperlakukan CAPTCHA sebagai bagian normal dari alur pengambilan halaman: deteksi, selesaikan, lanjutkan. Artikel ini menunjukkan cara mengenali reCAPTCHA v2/v3 dan Cloudflare Turnstile di halaman pencarian tiket, menyelesaikannya lewat API CaptchaAI, lalu melanjutkan parsing harga seperti biasa.

Konteksnya nyata untuk pasar Indonesia. Banyak pekerjaan monitoring harga di sini datang dari agensi kecil dan freelancer kontrak yang memantau puluhan rute domestik — CGK–DPS, CGK–SUB, SUB–UPG — untuk klien travel. Biaya per pengecekan menentukan margin, dan itulah alasan model harga berbasis thread lebih masuk akal daripada tarif per solve.

Contoh di artikel ini memakai domain contoh. Pantau hanya endpoint yang Anda miliki atau yang izin aksesnya sudah Anda kantongi, dan jauhi data pribadi — UU 27/2022 tentang Pelindungan Data Pribadi berlaku atas data yang Anda kumpulkan dan simpan.

Jenis CAPTCHA yang muncul di situs perjalanan

Petakan dulu apa yang Anda hadapi sebelum menulis kode. Kategori situs cukup andal memprediksi jenis tantangan dan frekuensinya.

Kategori situs Jenis CAPTCHA Tingkat kesulitan
Maskapai (situs resmi) reCAPTCHA v3, Cloudflare Sedang
OTA (Expedia, Booking.com) reCAPTCHA v2, Turnstile Sedang–Tinggi
Meta-search (Google Flights, Kayak) reCAPTCHA v3 Sedang
Maskapai berbiaya rendah CAPTCHA gambar, reCAPTCHA Rendah–Sedang
Agregator hotel Cloudflare Challenge Tinggi

Semua tipe di tabel ini termasuk yang ditangani CaptchaAI. Dua di antaranya menyumbang sebagian besar kasus di jalur perjalanan: reCAPTCHA v2 pada form pencarian OTA dan Cloudflare Turnstile pada halaman yang dilindungi Cloudflare. Untuk situs yang memakai GeeTest v3, polanya identik — yang berubah hanya nilai parameter method saat Anda mengirim task.

Satu fungsi fetch yang sadar CAPTCHA

Kesalahan arsitektur paling umum adalah menyebar logika CAPTCHA ke seluruh script. Bungkus semuanya dalam satu fungsi pengambil halaman. Alurnya tetap empat langkah yang sama seperti integrasi CaptchaAI lainnya: kirim task ke in.php → simpan task ID → polling res.php → pakai token pada request berikutnya.

Deteksi tipe dibaca dari HTML respons. Kehadiran recaptcha/api.js?render= menandakan v3, yang butuh parameter version dan action; data-sitekey tanpa penanda itu berarti v2. Penanda cf-turnstile menandakan Turnstile, dan tokennya dikirim balik lewat field cf-turnstile-response, bukan g-recaptcha-response.

Implementasi pemantau tarif

Kelas FareMonitor berikut menyatukan tiga tanggung jawab yang tetap terpisah rapi: pengambilan halaman yang sadar CAPTCHA, ekstraksi harga, dan pendeteksian penurunan harga terhadap riwayat.

import requests
import time
import re
import json
import os
from datetime import datetime, timedelta

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 FareMonitor:
    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"
        )
        self.history = []

    def fetch_with_captcha(self, url):
        """Fetch a travel page, solving CAPTCHAs if encountered."""
        resp = self.session.get(url)

        # reCAPTCHA v2/v3
        match = re.search(
            r'data-sitekey=["\']([A-Za-z0-9_-]+)["\']', resp.text
        )
        if match:
            site_key = match.group(1)

            # Detect v3 vs v2
            if "recaptcha/api.js?render=" in resp.text:
                token = solve_captcha({
                    "method": "userrecaptcha",
                    "googlekey": site_key,
                    "pageurl": url,
                    "version": "v3",
                    "action": "search",
                })
            else:
                token = solve_captcha({
                    "method": "userrecaptcha",
                    "googlekey": site_key,
                    "pageurl": url,
                })

            resp = self.session.post(url, data={
                "g-recaptcha-response": token,
            })

        # Cloudflare 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 check_fares(self, routes):
        """Check fares for a list of routes."""
        results = []
        for route in routes:
            try:
                html = self.fetch_with_captcha(route["url"])
                prices = self._extract_prices(html)

                result = {
                    "route": f"{route['origin']}-{route['destination']}",
                    "date": route["date"],
                    "prices": prices,
                    "min_price": min(prices) if prices else None,
                    "timestamp": datetime.utcnow().isoformat(),
                }
                results.append(result)
                self.history.append(result)

                if prices:
                    print(f"  {result['route']} ({route['date']}): "
                          f"${min(prices)}-${max(prices)}")
                else:
                    print(f"  {result['route']}: No prices found")

                time.sleep(3)  # Respectful delay

            except Exception as e:
                print(f"  {route.get('origin', '?')}-"
                      f"{route.get('destination', '?')}: ERROR - {e}")

        return results

    def _extract_prices(self, html):
        """Extract prices from travel page HTML."""
        prices = []
        # Common price patterns
        for match in re.finditer(
            r'\$\s*([\d,]+(?:\.\d{2})?)', html
        ):
            price = float(match.group(1).replace(",", ""))
            if 20 < price < 10000:  # Filter noise
                prices.append(price)
        return sorted(set(prices))

    def detect_price_drops(self, threshold_pct=5):
        """Detect significant price drops in history."""
        route_prices = {}
        for entry in self.history:
            key = f"{entry['route']}_{entry['date']}"
            if key not in route_prices:
                route_prices[key] = []
            if entry["min_price"]:
                route_prices[key].append(entry["min_price"])

        alerts = []
        for key, prices in route_prices.items():
            if len(prices) >= 2:
                prev = prices[-2]
                current = prices[-1]
                change_pct = ((current - prev) / prev) * 100
                if change_pct < -threshold_pct:
                    alerts.append({
                        "route": key,
                        "previous": prev,
                        "current": current,
                        "change": f"{change_pct:.1f}%",
                    })

        return alerts

    def export_report(self, filename="fare_report.json"):
        """Export fare history to JSON."""
        with open(filename, "w") as f:
            json.dump(self.history, f, indent=2)
        print(f"Exported {len(self.history)} fare checks to {filename}")


# Define routes to monitor
routes = [
    {
        "origin": "JFK",
        "destination": "LAX",
        "date": "2025-03-15",
        "url": "https://example-airline.com/flights?from=JFK&to=LAX&date=2025-03-15",
    },
    {
        "origin": "SFO",
        "destination": "ORD",
        "date": "2025-03-20",
        "url": "https://example-airline.com/flights?from=SFO&to=ORD&date=2025-03-20",
    },
]

monitor = FareMonitor()
results = monitor.check_fares(routes)
monitor.export_report()

Perhatikan time.sleep(3) di antara rute. Jeda itu bukan formalitas: pengecekan yang terlalu rapat memicu lebih banyak tantangan, sehingga menaikkan biaya dan waktu total. Untuk halaman yang harganya baru muncul setelah JavaScript berjalan, ganti bagian session.get dengan Selenium atau Playwright dan biarkan sisa alurnya tetap sama.

Menjadwalkan pengecekan

Cron sudah lebih dari cukup untuk sebagian besar kebutuhan. Jalankan dari server yang dekat dengan target — misalnya region AWS ap-southeast-1 (Singapura) atau ap-southeast-3 (Jakarta) — agar latensi pengambilan halaman tetap rendah.

# Check fares every 4 hours
0 */4 * * * cd /opt/fare-monitor && python fare_monitor.py

Sebarkan jadwal antar-rute; jangan menembakkan 50 rute sekaligus pada menit yang sama. Antrean yang merata membuat jumlah thread yang Anda butuhkan jauh lebih kecil.

Perkiraan biaya dan pemilihan paket

Angka berikut adalah perkiraan beban kerja, bukan tagihan pasti — hasilnya bergantung pada situs target, frekuensi, dan seberapa sering tantangan muncul.

Skala pemantauan Rute Cek/hari CAPTCHA/hari Paket yang wajar
Pribadi 5 6/rute ~30 BASIC ($15/bulan, 5 thread)
Agensi kecil 50 4/rute ~200 STANDARD ($30/bulan, 15 thread)
Tim data 500 6/rute ~3000 ADVANCE ($90/bulan, 50 thread)

Ini bagian yang paling sering disalahpahami: CaptchaAI menagih per thread yang berjalan bersamaan, bukan per solve, dan setiap paket memberi solve tanpa batas selama bulan berjalan. Jadi pertanyaannya bukan "berapa banyak CAPTCHA per hari", melainkan "berapa banyak pengecekan yang berjalan bersamaan pada puncak jadwal". Agensi dengan 200 CAPTCHA per hari yang tersebar rata sepanjang hari hampir tidak pernah membutuhkan lebih dari belasan thread. Harga selalu dalam USD; daftar paket lengkap ada di halaman pricing CaptchaAI.

Pertanyaan umum

Berapa thread yang saya butuhkan untuk memantau 50 rute?

Hitung dari puncak konkurensi, bukan total harian. Kalau 50 rute dicek empat kali sehari dan Anda menyebarnya dalam jendela sepuluh menit, belasan thread sudah memadai — STANDARD ($30/bulan, 15 thread) jadi titik awal yang wajar. Naikkan paket hanya kalau antrean mulai menumpuk.

Situs target saya memakai hCaptcha — apa yang harus saya lakukan?

Belum. hCaptcha dan FunCaptcha tidak didukung saat ini, jadi untuk target yang memakai keduanya Anda memerlukan layanan lain. GeeTest v4 juga belum tersedia — statusnya segera hadir — sementara GeeTest v3 sudah didukung penuh.

Bagaimana kalau harga baru muncul setelah JavaScript dijalankan?

Ganti tahap pengambilan halaman dengan browser headless (Selenium atau Playwright), lalu tetap kirim sitekey ke CaptchaAI dan suntikkan token hasilnya ke halaman. Logika CAPTCHA tidak berubah, hanya cara halaman diambil.

Bisakah script yang sama dipakai untuk harga hotel?

Bisa. Alur deteksi dan penyelesaian CAPTCHA identik; yang perlu disesuaikan hanya pola ekstraksi harga dan parameter pencarian, karena halaman hotel memuat rentang tanggal dan jumlah tamu.

Kenapa jumlah CAPTCHA melonjak padahal jadwal tidak saya ubah?

Biasanya karena pengecekan menumpuk di menit yang sama. Cron 0 */4 * * * untuk semua rute berarti puluhan request serempak dari satu IP, dan situs merespons dengan lebih banyak tantangan. Sebarkan jadwal per rute ke menit yang berbeda sebelum menambah thread.

Panduan terkait

Komentar dinonaktifkan untuk artikel ini.