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

Xử lý CAPTCHA để giám sát trang web đấu giá

Đội mua hộ và các nhóm theo dõi giá tại Việt Nam thường chạy script quét eBay, Yahoo! Auction Nhật Bản hoặc các sàn đấu giá nội địa để bắt kịp giá thầu theo từng phút. Vấn đề không nằm ở crawler — mà ở chỗ tần suất truy vấn tăng lên là các sàn bật reCAPTCHA v2 hoặc Cloudflare Turnstile chặn traffic bất thường. Quy trình dưới đây giữ pipeline chạy liên tục thay vì dừng lại mỗi khi gặp CAPTCHA.

CAPTCHA xuất hiện ở đâu khi theo dõi đấu giá

Trên hầu hết các sàn, CAPTCHA gắn với một hành vi cụ thể chứ không xuất hiện ngẫu nhiên:

Hành động Loại CAPTCHA Mẫu kích hoạt
Search/browse danh sách reCAPTCHA v2 Tìm kiếm tuần tự nhanh
Xem chi tiết danh sách reCAPTCHA v2 Lượng truy cập lớn từ cùng một IP
Kiểm tra lịch sử giá thầu reCAPTCHA v2 Tải trang chi tiết lặp lại
Duyệt danh mục Cloudflare Turnstile Tốc độ điều hướng giống bot
Trang thông báo giá reCAPTCHA v2 Làm mới quá thường xuyên

Giảm tần suất gặp CAPTCHA trước khi cần giải nó

Cách rẻ nhất để giữ pipeline nhanh là tránh kích hoạt CAPTCHA ngay từ đầu:

Kỹ thuật Tác động
Tái sử dụng cookie phiên Duy trì trạng thái đã xác thực
Đa dạng nguồn yêu cầu Phân phối request trên nhiều IP
Ngẫu nhiên hóa khoảng thời gian request Tránh mẫu phát hiện định kỳ
Dùng tài khoản đã xác thực Ngưỡng kích hoạt CAPTCHA thấp hơn

Chọn tần suất kiểm tra theo mục tiêu theo dõi

Poll càng dày, CAPTCHA càng dày:

Tần suất kiểm tra Trường hợp dùng Tỷ lệ CAPTCHA dự kiến
Mỗi 30 giây Đấu giá phút chót Cao — cần đa dạng nguồn request
Mỗi 5 phút Theo dõi đấu giá đang diễn ra Trung bình
Mỗi 15 phút Giám sát watchlist Thấp
Mỗi giờ Nghiên cứu giá dài hạn Tối thiểu

Code mẫu: tự động giải CAPTCHA khi theo dõi đấu giá (Python)

Class AuctionMonitor gói cả ba việc — tìm listing, lấy chi tiết item, theo dõi giá thầu theo chu kỳ — mỗi bước tự giải CAPTCHA khi cần.

import requests
import time
import re
from datetime import datetime

class AuctionMonitor:
    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 search_listings(self, auction_url, query, category=None):
        """Search auction listings, solving CAPTCHAs when triggered."""
        params = {"q": query}
        if category:
            params["category"] = category

        response = self.session.get(
            f"{auction_url}/search", params=params
        )

        if self._has_captcha(response.text):
            site_key = self._extract_site_key(response.text)
            token = self._solve_recaptcha(site_key, f"{auction_url}/search")
            response = self.session.post(
                f"{auction_url}/search",
                data={**params, "g-recaptcha-response": token}
            )

        return self._parse_listings(response.text)

    def monitor_listing(self, auction_url, listing_id):
        """Get current bid and listing details."""
        url = f"{auction_url}/item/{listing_id}"
        response = self.session.get(url)

        if self._has_captcha(response.text):
            site_key = self._extract_site_key(response.text)
            token = self._solve_recaptcha(site_key, url)
            response = self.session.post(url, data={
                "g-recaptcha-response": token
            })

        return self._parse_listing_detail(response.text)

    def track_bids(self, auction_url, listing_ids, interval=60):
        """Track bid changes across multiple listings."""
        history = {lid: [] for lid in listing_ids}

        while True:
            for listing_id in listing_ids:
                try:
                    detail = self.monitor_listing(auction_url, listing_id)
                    previous = history[listing_id]

                    if previous and detail["current_bid"] != previous[-1]["current_bid"]:
                        print(f"Bid change on {listing_id}: "
                              f"${previous[-1]['current_bid']} → ${detail['current_bid']}")

                    history[listing_id].append(detail)
                except Exception as e:
                    print(f"Error checking {listing_id}: {e}")

            time.sleep(interval)

    def _has_captcha(self, html):
        return "g-recaptcha" in html or "recaptcha" in html.lower()

    def _extract_site_key(self, html):
        match = re.search(r'data-sitekey="([^"]+)"', html)
        if match:
            return match.group(1)
        match = re.search(r"sitekey['\"]?\s*[:=]\s*['\"]([^'\"]+)", html)
        if match:
            return match.group(1)
        raise ValueError("Could not find reCAPTCHA site key")

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

        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("reCAPTCHA solve timed out")

    def _parse_listings(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

        def attr_or_none(node, attr):
            return node.get(attr) if node else None

        listings = []
        for item in soup.select(".listing-item, .auction-item"):
            listings.append({
                "title": text_or_none(item.select_one(".title")),
                "current_bid": text_or_none(item.select_one(".price, .bid")),
                "time_left": text_or_none(item.select_one(".time-left")),
                "url": attr_or_none(item.select_one("a"), "href")
            })
        return listings

    def _parse_listing_detail(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 {
            "title": text_or_none(soup.select_one("h1, .item-title")),
            "current_bid": text_or_none(soup.select_one(".current-bid, .price")),
            "bid_count": text_or_none(soup.select_one(".bid-count")),
            "time_left": text_or_none(soup.select_one(".time-remaining")),
            "checked_at": datetime.now().isoformat()
        }

# Usage
monitor = AuctionMonitor("YOUR_API_KEY")
listings = monitor.search_listings(
    "https://auctions.example.com",
    "vintage electronics",
    category="collectibles"
)

Cảnh báo giá tự động bằng JavaScript

Bản JavaScript này khác: đăng ký watchlist kèm ngưỡng giá, checkAll() tự bắn cảnh báo khi giá thầu tiến gần mức đặt.

class AuctionTracker {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.watchList = new Map();
  }

  addWatch(listingId, url, maxPrice) {
    this.watchList.set(listingId, { url, maxPrice, history: [] });
  }

  async checkAll() {
    const alerts = [];

    for (const [id, watch] of this.watchList) {
      try {
        const detail = await this.fetchListing(watch.url);
        watch.history.push(detail);

        const price = parseFloat(detail.currentBid.replace(/[^0-9.]/g, ''));
        if (price >= watch.maxPrice * 0.9) {
          alerts.push({
            listing: id,
            price,
            threshold: watch.maxPrice,
            message: `Price approaching limit: $${price} / $${watch.maxPrice}`
          });
        }
      } catch (error) {
        alerts.push({ listing: id, error: error.message });
      }
    }

    return alerts;
  }

  async fetchListing(url) {
    const response = await fetch(url);
    const html = await response.text();

    if (html.includes('g-recaptcha')) {
      return this.solveAndFetch(url, html);
    }

    return this.parseDetail(html);
  }

  async solveAndFetch(url, html) {
    const siteKeyMatch = html.match(/data-sitekey="([^"]+)"/);
    if (!siteKeyMatch) throw new Error('No reCAPTCHA site key found');

    const submitResp = await fetch('https://ocr.captchaai.com/in.php', {
      method: 'POST',
      body: new URLSearchParams({
        key: this.apiKey,
        method: 'userrecaptcha',
        googlekey: siteKeyMatch[1],
        pageurl: url,
        json: '1'
      })
    });

    const { request: taskId } = await submitResp.json();

    for (let i = 0; i < 60; 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) {
        // Resubmit with token
        const response = await fetch(url, {
          method: 'POST',
          body: new URLSearchParams({ 'g-recaptcha-response': data.request })
        });
        return this.parseDetail(await response.text());
      }
    }

    throw new Error('reCAPTCHA solve timed out');
  }

  parseDetail(html) {
    // Parse auction listing details from HTML
    return {
      currentBid: html.match(/current.?bid[^>]*>([^<]+)/i)?.[1]?.trim(),
      bidCount: html.match(/(\d+)\s*bids?/i)?.[1],
      timeLeft: html.match(/time.?(?:left|remaining)[^>]*>([^<]+)/i)?.[1]?.trim(),
      checkedAt: new Date().toISOString()
    };
  }
}

// Usage
const tracker = new AuctionTracker('YOUR_API_KEY');
tracker.addWatch('item-123', 'https://auctions.example.com/item/123', 500);
tracker.addWatch('item-456', 'https://auctions.example.com/item/456', 200);
const alerts = await tracker.checkAll();

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 request Không duy trì phiên Tái sử dụng requests.Session()
Token reCAPTCHA bị từ chối Token hết hạn (sống 2 phút) Giải ngay trước khi submit
Trang danh sách trả về 0 kết quả CAPTCHA chưa giải lọc âm thầm kết quả Kiểm tra phần tử CAPTCHA ẩn trước khi parse
IP bị chặn sau nhiều CAPTCHA Vượt rate limit của site Đa dạng nguồn request, tăng khoảng poll

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

Nên chọn gói CaptchaAI nào để theo dõi hàng chục phiên đấu giá cùng lúc?

Tùy số request đồng thời, không phải tổng số listing. Vài item lẻ tẻ thì BASIC ($15/tháng, 5 thread) đủ dùng. Watchlist lớn hoặc nhiều worker song song thì cân nhắc STANDARD ($30/tháng, 15 thread) hoặc ADVANCE ($90/tháng, 50 thread) — thread càng nhiều, request càng ít phải xếp hàng chờ giải.

CaptchaAI giải CAPTCHA trên eBay, Yahoo! Auction hay các sàn đấu giá quốc tế khác được không?

Được, miễn site dùng loại CAPTCHA CaptchaAI hỗ trợ — phổ biến nhất là reCAPTCHA v2 và Cloudflare Turnstile. Quy trình y hệt ví dụ trong bài: trích sitekey, gửi task tới in.php, polling res.php, dùng token. Tự kiểm tra loại CAPTCHA thực tế từng site trước khi tích hợp.

Theo dõi đấu giá liên tục có bị site phát hiện là bot không?

Việc bị phát hiện phụ thuộc vào mẫu request, không phải việc giải CAPTCHA. Dùng khoảng poll không cố định tuyệt đối, luân phiên user agent, tránh thu thập dồn dập vào giờ thấp điểm.

Có theo dõi được phút chót của một phiên đấu giá theo thời gian thực không?

Được, nhưng nên chuẩn bị phiên đã xác thực trước. reCAPTCHA v2 thường được CaptchaAI giải trong dưới 60 giây — đủ nhanh cho CAPTCHA phát sinh ở giây cuối, dù tránh CAPTCHA trước vẫn đáng tin cậy hơn.

Bài viết liên quan

Bước tiếp theo

Lấy API key CaptchaAI và ghép vào pipeline giám sát đấu giá hiện có — hai class mẫu ở trên là điểm bắt đầu.

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