Use Cases

CAPTCHA İşleme ile Seyahat Ücreti Takibi

Ücret takip script'iniz genellikle koda takılmaz; doğrulama ekranına takılır. İlk birkaç sorgu sorunsuz döner, ardından aynı rota için gelen yanıt fiyat listesi yerine bir reCAPTCHA ya da Cloudflare Turnstile widget'ı içerir ve o günün verisi eksik kalır. Çözüm, isteği daha seyrek atmak değil; doğrulamayı iş akışının normal bir adımı olarak ele almaktır. CaptchaAI API'si bu adımı otomatikleştirir: sayfada sitekey bulunduğunda görevi gönderirsiniz, dönen token'ı forma yerleştirirsiniz ve toplama kaldığı yerden devam eder.

Bu rehber tek bir Python sınıfıyla çalışan bir ücret monitörü kurar: sayfayı getirir, karşısına çıkan CAPTCHA türünü ayırt eder, çözer, fiyatları çıkarır ve düşüşleri raporlar.

Ücret takibi neden CAPTCHA'ya çarpar?

Havayolu ve OTA sayfaları, aynı rota-tarih kombinasyonunu kısa aralıklarla sorgulayan trafiği ayırt etmekte iyidir. Fiyat sorgusu onlar için pahalı bir işlemdir — her sorgu envanter sistemine gider — bu yüzden otomatik görünen oturumlara doğrulama ekranı gösterirler. Buradaki ayrım önemlidir: sizi kalıcı olarak kapı dışında bırakmak yerine, insan olduğunuzu bir kez daha doğrulamanızı isterler. Doğrulamayı çözebilen bir iş akışı, bu yüzden tıkanmış bir iş akışından yapısal olarak farklıdır.

Türkiye'den çalışan ekipler için pratik bir ayrıntı: rota fiyatları çoğu zaman Europe/Istanbul saatiyle gece yarısından sonra toplu güncellenir, bu yüzden cron planınızı UTC üzerinden kurarken saat farkını hesaba katın. Bir başka nokta da veri tarafında: topladığınız şey fiyat ve müsaitlik verisidir; akışa yolcu adı, e-posta veya rezervasyon kaydı girdiği anda konu KVKK kapsamına geçer ve bu rehberin sınırlarının dışına çıkar.

Hangi sitede hangi doğrulama çıkar?

Aşağıdaki dağılım, seyahat sayfalarında en sık karşılaşılan doğrulama türlerini özetler. Son sütun çözüm süresini değil, entegrasyonun karmaşıklığını anlatır.

Site kategorisi CAPTCHA türü Entegrasyon zorluğu
Havayolları (doğrudan satış) reCAPTCHA v3, Cloudflare Orta
OTA'lar (Expedia, Booking) reCAPTCHA v2, Turnstile Orta-Yüksek
Meta arama (Google Flights, Kayak) reCAPTCHA v3 Orta
Ekonomik havayolları Resim CAPTCHA, reCAPTCHA Düşük-Orta
Otel toplayıcıları Cloudflare doğrulama akışı Yüksek

Tablodaki türlerin tamamı CaptchaAI tarafından destekleniyor: reCAPTCHA v2 ve v3, Cloudflare Turnstile ve Cloudflare doğrulama akışı, resim/OCR ve GeeTest v3. hCaptcha ve FunCaptcha (Arkose Labs) desteklenmiyor; bir seyahat sitesi bu ikisinden birini kullanıyorsa o kaynağı izleme listenizden çıkarın. GeeTest v4 desteği ise çok yakında.

Monitör sınıfını kurun

Aşağıdaki FareMonitor sınıfı üç işi tek yerde toplar: oturum yönetimi, doğrulama çözümü ve fiyat çıkarımı. solve_captcha fonksiyonu görevi in.php uç noktasına gönderir, ardından sonucu res.php üzerinden periyodik olarak sorgular; CAPCHA_NOT_READY yanıtı geldiği sürece beklemeye devam eder.

fetch_with_captcha metodunun ayırt etme mantığına dikkat edin: sayfa kaynağında recaptcha/api.js?render= varsa istek v3 olarak, yoksa v2 olarak gönderilir. Turnstile ayrı bir kontrolle yakalanır ve token cf-turnstile-response alanına yazılır — reCAPTCHA'nın g-recaptcha-response alanıyla karıştırılmamalıdır, iki alan birbirinin yerini tutmaz.

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

Kodda üç ayar üretimde fark yaratır. Birincisi rotalar arasındaki üç saniyelik bekleme: sunucuya saygılı bir ritim doğrulama sıklığını da düşürür. İkincisi _extract_prices içindeki 20–10000 aralığı; bu filtre olmadan sayfadaki kampanya rozetleri ve puan bilgileri fiyat sanılır. Üçüncüsü detect_price_drops içindeki yüzde eşiği: %5 kişisel takip için makul, ajans tarafında %2–3 daha kullanışlıdır.

Çalıştırmayı planlayın

Monitörü elle tetiklemek yerine cron ile dört saatte bir çalıştırın. Havayolları fiyatları toplu güncellediği için bundan daha sık sorgulamak veri kalitesini artırmaz, yalnızca doğrulama sayısını yükseltir.

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

Uzun süreli çalıştırmalarda iki alışkanlık işe yarar: her koşunun çıktısını tarih damgalı bir JSON dosyasına yazın (kod bunu export_report ile yapıyor) ve zaman aşımıyla biten koşuları ayrı bir kayda alın. Zaman aşımlarının belirli bir rotada kümelenmesi, o sayfanın doğrulama katmanının değiştiğine dair en erken sinyaldir.

Maliyet: thread hesabı nasıl yapılır?

CaptchaAI çözüm başına değil, eşzamanlı thread başına ücretlendirir ve her planda thread başına çözüm sınırsızdır. Bu, ücret takibi gibi düzenli ve öngörülebilir yüklerde aylık maliyeti sabitler — kur dalgalanmasının olduğu bir piyasada USD cinsinden sabit bir aylık kalem, bütçe planlamasını belirgin biçimde kolaylaştırır.

İzleme seviyesi Rota Kontrol/gün CAPTCHA/gün Uygun plan
Kişisel 5 6/rota ~30 BASIC ($15/ay, 5 thread)
Küçük ajans 50 4/rota ~200 STANDARD ($30/ay, 15 thread)
Kurumsal 500 6/rota ~3.000 ADVANCE ($90/ay, 50 thread)

Tablodaki plan eşlemesi eşzamanlılığa göredir, günlük toplam hacme göre değil: 3.000 çözüm 50 thread ile rahatça dağıtılır, çünkü her thread biten çözümün ardından hemen bir sonrakini alır. Darboğaz yaşadığınızı düşünüyorsanız önce eşzamanlı istek sayınızı ölçün, sonra plan yükseltmeyi değerlendirin. Plan seçimini rota sayısına göre değil, tepe saatteki eşzamanlılığa göre yapın.

Üretimde tipik sorunlar

  • Token reddedildi. Token'ın ömrü kısadır; çözümden sonra 60 saniye içinde gönderin. Çözüm ile form gönderimi arasına ağır bir işlem koymayın.
  • Fiyat listesi boş dönüyor. Sayfa büyük olasılıkla JavaScript ile render ediliyordur. HTML'i Selenium veya Playwright ile alın, doğrulamayı yine CaptchaAI ile çözün.
  • Aynı rota sürekli doğrulama gösteriyor. Kontrol sıklığını düşürün ve User-Agent başlığınızın gerçekçi olduğundan emin olun; aralığı 4 saatten 6 saate çekmek çoğu durumda yeterli olur.
  • CAPCHA_NOT_READY döngüsü uzuyor. Bu normaldir; yoğun saatlerde çözüm süresi uzayabilir. Sorgulama aralığını 5 saniyenin altına indirmeyin, yalnızca gereksiz istek üretirsiniz.

Bu dört başlık, ilk hafta içinde karşınıza çıkacakların neredeyse tamamını kapsıyor; geri kalanı sayfa yapısına özgü ayrıntılardır.

Sık sorulan sorular

Ücret takibi için hangi planla başlamalıyım?

Beş-on rotayı günde birkaç kez kontrol ediyorsanız BASIC ($15/ay, 5 thread) yeterlidir. Rota sayınız elliyi aştığında STANDARD ($30/ay, 15 thread) daha rahat çalışır; belirleyici olan toplam çözüm sayısı değil, aynı anda kaç istek çalıştırdığınızdır.

Token'ı aldım ama sayfa hâlâ doğrulama istiyor, nerede hata yapıyorum?

Genellikle iki nedenden biri: token'ı yanlış alana yazıyorsunuzdur (Turnstile için cf-turnstile-response, reCAPTCHA için g-recaptcha-response) ya da token'ın süresi dolmuştur. Çözümden sonra formu hemen gönderin.

CaptchaAI hCaptcha kullanan seyahat sitelerinde çalışır mı?

Hayır. hCaptcha ve FunCaptcha (Arkose Labs) şu anda desteklenmiyor. Bu türleri kullanan kaynakları izleme listenizden çıkarmak, başarısız koşuları sonradan ayıklamaktan daha verimlidir.

Otel fiyatlarını da aynı kodla izleyebilir miyim?

Evet, akış değişmez. Yalnızca _extract_prices içindeki desenleri otel sayfalarının biçimine göre ayarlayın; gecelik fiyat ile toplam tutarı ayırt eden bir kontrol eklemek raporun doğruluğunu gözle görülür biçimde artırır.

Topladığım fiyat verisini saklarken KVKK'ya dikkat etmem gerekir mi?

Yalnızca fiyat ve müsaitlik verisi topluyorsanız kişisel veri işlemiyorsunuz demektir. Akışa yolcu bilgisi, e-posta veya rezervasyon kaydı girdiği anda KVKK yükümlülükleri devreye girer — bu durumda saklama süresini ve erişim yetkilerini önceden tanımlayın.

İlgili rehberler

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