Trường Hợp Sử Dụng

Xử lý CAPTCHA để theo dõi trạng thái chuyến bay

Cổng thông tin tình trạng chuyến bay thường chặn bot sau vài lần gọi liên tiếp — không phải vì dữ liệu nhạy cảm, mà vì request lặp lại trông giống crawler hơn hành khách đang F5 trang. Một đội automation tại công ty du lịch outsource ở TP.HCM từng gặp đúng vấn đề này khi giám sát song song nhiều chuyến nội địa và quốc tế cho khách hàng B2B: sau khoảng 10 request trong 5 phút, Cloudflare bắt đầu chèn Turnstile vào mọi response. Bài viết chỉ ra CAPTCHA xuất hiện ở đâu, cách xử lý bằng CaptchaAI, và cách chọn tần suất kiểm tra hợp lý.

CAPTCHA xuất hiện ở đâu khi theo dõi chuyến bay

Loại cổng thông tin CAPTCHA thường gặp Yếu tố kích hoạt
Trang tình trạng chuyến bay của hãng Cloudflare Turnstile Request lặp lại từ cùng một IP
Bảng giờ đến/đi tại sân bay Cloudflare Challenge Hệ thống phát hiện bot
Công cụ tìm kiếm chuyến bay reCAPTCHA v2/v3 Gửi form tìm kiếm
Tra cứu tình trạng đặt chỗ reCAPTCHA v2 Trước khi hiển thị hành trình
Trang giới hạn tần suất API CAPTCHA tùy chỉnh Sau khi vượt rate limit

Chọn tần suất kiểm tra để giảm tỷ lệ gặp CAPTCHA

Tần suất kiểm tra quyết định phần lớn tỷ lệ gặp CAPTCHA:

Tần suất kiểm tra Tỷ lệ gặp CAPTCHA điển hình Khuyến nghị
Mỗi 1 phút Cao (50–80%) Quá dày — nới khoảng cách ra
Mỗi 5 phút Trung bình (10–30%) Chấp nhận được với các chuyến bay quan trọng
Mỗi 15 phút Thấp (5–10%) Cân bằng tốt cho giám sát thường xuyên
Mỗi 30 phút Rất thấp (<5%) Phù hợp để theo dõi dài hạn
Mỗi giờ Tối thiểu (<1%) CAPTCHA gần như không xuất hiện

Xây dựng bộ giám sát: kiến trúc xử lý CAPTCHA trong Python

Cấu trúc dưới đây tách phát hiện CAPTCHA, gửi task giải và parse dữ liệu thành ba phần:

import requests
import time
from datetime import datetime

class FlightMonitor:
    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 check_flight(self, airline_url, flight_number):
        """Check flight status, handling CAPTCHAs if encountered."""
        response = self.session.get(
            f"{airline_url}/flight-status/{flight_number}"
        )

        if self._is_captcha_page(response):
            response = self._solve_and_retry(response, airline_url)

        return self._parse_flight_data(response.text)

    def _is_captcha_page(self, response):
        return (
            response.status_code == 403 or
            "cf-turnstile" in response.text or
            "g-recaptcha" in response.text
        )

    def _solve_and_retry(self, response, url):
        import re

        # Detect CAPTCHA type
        if "cf-turnstile" in response.text:
            match = re.search(r'data-sitekey="(0x[^"]+)"', response.text)
            token = self._solve_turnstile(match.group(1), url)
            field = "cf-turnstile-response"
        else:
            match = re.search(r'data-sitekey="([^"]+)"', response.text)
            token = self._solve_recaptcha(match.group(1), url)
            field = "g-recaptcha-response"

        return self.session.post(url, data={field: token})

    def _solve_turnstile(self, site_key, page_url):
        resp = requests.post("https://ocr.captchaai.com/in.php", data={
            "key": self.api_key,
            "method": "turnstile",
            "sitekey": site_key,
            "pageurl": page_url,
            "json": 1
        })
        task_id = resp.json()["request"]
        return self._poll_result(task_id)

    def _solve_recaptcha(self, site_key, page_url):
        resp = requests.post("https://ocr.captchaai.com/in.php", data={
            "key": self.api_key,
            "method": "userrecaptcha",
            "googlekey": site_key,
            "pageurl": page_url,
            "json": 1
        })
        task_id = resp.json()["request"]
        return self._poll_result(task_id)

    def _poll_result(self, task_id):
        for _ in range(60):
            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 _parse_flight_data(self, html):
        # Parse flight status from 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 {
            "status": text_or_none(soup.select_one(".flight-status")),
            "departure": text_or_none(soup.select_one(".departure-time")),
            "arrival": text_or_none(soup.select_one(".arrival-time")),
            "gate": text_or_none(soup.select_one(".gate-info")),
            "checked_at": datetime.now().isoformat()
        }

FlightMonitor tự phát hiện Turnstile hay reCAPTCHA qua data-sitekey, gửi task tới CaptchaAI qua in.php, rồi polling res.php đến khi có token.

Chạy vòng lặp giám sát định kỳ

def monitor_flight(monitor, airline_url, flight_number, 
                   interval_seconds=300, max_checks=48):
    """Monitor a flight every N seconds, handling CAPTCHAs as needed."""
    history = []

    for check_num in range(max_checks):
        try:
            status = monitor.check_flight(airline_url, flight_number)
            history.append(status)

            # Alert on changes
            if len(history) > 1 and status["status"] != history[-2]["status"]:
                print(f"Status changed: {history[-2]['status']} → {status['status']}")

            print(f"Check {check_num + 1}: {status['status']} "
                  f"(Gate: {status.get('gate', 'Coming soon')})")

        except Exception as e:
            print(f"Check {check_num + 1} failed: {e}")

        time.sleep(interval_seconds)

    return history

# Usage
monitor = FlightMonitor("YOUR_API_KEY")
monitor_flight(monitor, "https://airline.example.com", "AA1234")

Với interval_seconds=300max_checks=48, kịch bản này giám sát một chuyến liên tục trong 4 giờ — mức "trung bình" ở bảng phía trên.

Theo dõi nhiều hãng hàng không cùng lúc bằng JavaScript

class FlightTracker {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.flights = new Map();
  }

  async addFlight(airline, flightNumber, checkUrl) {
    this.flights.set(flightNumber, {
      airline,
      url: checkUrl,
      history: [],
      lastCheck: null
    });
  }

  async checkAll() {
    const results = [];

    for (const [flightNum, flight] of this.flights) {
      try {
        const status = await this.checkFlight(flight.url, flightNum);
        flight.history.push(status);
        flight.lastCheck = new Date();
        results.push({ flight: flightNum, ...status });
      } catch (error) {
        results.push({ flight: flightNum, error: error.message });
      }
    }

    return results;
  }

  async checkFlight(url, flightNumber) {
    const response = await fetch(`${url}/status/${flightNumber}`);
    const html = await response.text();

    // Check for CAPTCHA
    if (html.includes('cf-turnstile') || response.status === 403) {
      return this.solveAndRetry(url, flightNumber, html);
    }

    return this.parseStatus(html);
  }

  async solveAndRetry(url, flightNumber, html) {
    const siteKeyMatch = html.match(/data-sitekey="(0x[^"]+)"/);
    if (!siteKeyMatch) throw new Error('No sitekey found');

    const token = await this.solveTurnstile(siteKeyMatch[1], url);

    const response = await fetch(`${url}/status/${flightNumber}`, {
      method: 'POST',
      body: new URLSearchParams({ 'cf-turnstile-response': token })
    });

    return this.parseStatus(await response.text());
  }
}

checkAll() duyệt tuần tự qua Map. Cần chạy song song thật sự thì bọc checkFlight trong Promise.all — nhưng bắn nhiều request cùng lúc dễ kích hoạt CAPTCHA hơn.

Giảm số lần gặp CAPTCHA bằng tối ưu phiên

Kỹ thuật Hiệu quả
Giữ cookie phiên giữa các lần kiểm tra Cloudflare qa_session_cookie có hiệu lực 15–30 phút
Dùng User-Agent nhất quán Đổi UA liên tục khiến Cloudflare tạo thử thách mới
Giữ nguồn IP ổn định qua các lần gọi Cùng nguồn request làm giảm nghi ngờ
Giãn đều các lần gọi, tránh dồn cụm Request dồn dập trong thời gian ngắn dễ kích hoạt rate limit

Khắc phục các lỗi thường gặp

Vấn đề Nguyên nhân Cách xử lý
CAPTCHA xuất hiện ở mọi lần kiểm tra Phiên không được giữ lại giữa các lần gọi Tái sử dụng requests.Session() xuyên suốt
Cloudflare chặn (lỗi 1020) Gọi quá nhiều request trong thời gian ngắn Tăng khoảng cách giữa các lần kiểm tra
Dữ liệu chuyến bay bị cũ sau khi giải CAPTCHA Token hết hạn trong lúc chờ giải Giải CAPTCHA ngay trước khi dùng token (just-in-time), không lưu để dùng sau
Dữ liệu khác với những gì trình duyệt hiển thị Trang cần render JavaScript Dùng browser automation cho các trang nặng JS

Câu hỏi thường gặp

CaptchaAI có xử lý được Cloudflare Turnstile trên trang tình trạng chuyến bay không?

Có. CaptchaAI hỗ trợ Cloudflare Turnstile, Cloudflare Challenge và reCAPTCHA v2/v3 — ba loại phổ biến nhất trên cổng hàng không, với tỷ lệ giải thành công cao và thời gian giải chỉ vài giây.

Kiểm tra tình trạng chuyến bay bao lâu một lần thì an toàn?

Khoảng 5–15 phút là hợp lý cho hầu hết trường hợp. Kiểm tra dày hơn đẩy tỷ lệ gặp CAPTCHA lên 50–80% và tăng nguy cơ bị chặn IP — yếu tố giới hạn thực sự là rate limit của cổng, không phải bước giải CAPTCHA.

Dữ liệu chuyến bay có bị cũ sau khi giải CAPTCHA không?

Có thể, nếu bạn giải CAPTCHA quá sớm rồi mới dùng token. Token có thời hạn ngắn — hãy giải ngay trước khi gửi request lấy dữ liệu.

API di động của hãng hàng không có cần xử lý CAPTCHA không?

Thường thì không — app di động hay dùng API key hoặc OAuth thay vì CAPTCHA. Nhưng endpoint web mà app gọi ngầm bên dưới vẫn có thể được Cloudflare bảo vệ, nên vẫn có thể gặp CAPTCHA khi crawl qua đường này.

Bài viết liên quan

Bước tiếp theo

Xây dựng hệ thống giám sát chuyến bay đáng tin cậy — lấy khóa API CaptchaAI và tự động xử lý CAPTCHA của hãng hàng không.

Os comentários estão desativados para este artigo.