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

Xử lý CAPTCHA để so sánh giá dược phẩm

Muốn crawl giá thuốc để dựng công cụ so sánh giá? Vấn đề đầu tiên không phải parse HTML — mà là CAPTCHA chặn ngay ở form tìm kiếm. Hầu hết cổng dược phẩm, cơ sở dữ liệu giá thuốc và danh mục bảo hiểm ở Mỹ bọc trang định giá bằng reCAPTCHA v2, Turnstile hoặc CAPTCHA hình ảnh.

Luồng xử lý chung gồm bốn bước:

  1. Trích site key.
  2. Gửi task, nhận task ID.
  3. Polling lấy token.
  4. Đính token, gửi lại request.

Đây cũng là bài toán quen thuộc với team outsource Việt Nam nhận dự án crawl giá thuốc cho khách hàng Mỹ: dữ liệu rải trên hàng chục domain, mỗi domain một kiểu CAPTCHA — gọi chung một API như CaptchaAI thay vì viết riêng từng solver.

CAPTCHA xuất hiện ở đâu trên cổng hiệu thuốc

Loại trang CAPTCHA phổ biến Điều kiện kích hoạt
Form tra cứu thuốc reCAPTCHA v2 Mỗi lần gửi truy vấn tìm kiếm
Trang kết quả giá Cloudflare Turnstile Khi hệ thống nghi ngờ truy cập tự động
Định vị nhà thuốc reCAPTCHA v2 Truy vấn theo vị trí (zip code)
Tra cứu mã giảm giá CAPTCHA hình ảnh Trước khi hiển thị mã coupon
Danh mục bảo hiểm reCAPTCHA v2 Cổng đăng nhập / tìm kiếm

Kiểm soát tần suất request để tránh bị chặn

Cổng dược phẩm theo dõi mẫu truy cập khá chặt:

Chiến lược Cách thực hiện
Giãn cách request 5–10 giây giữa các lần tìm kiếm
Xoay session Tạo session mới sau mỗi 20–30 truy vấn
Đổi mẫu tìm kiếm Không tra cùng một loại thuốc liên tục
Đa dạng nguồn request IP thuộc datacenter dễ kích hoạt CAPTCHA hơn IP thông thường

Các lỗi thường gặp và cách xử lý

Vấn đề Nguyên nhân Cách xử lý
CAPTCHA xuất hiện ở mọi lần tìm kiếm Session không giữ cookie Dùng requests.Session() và duy trì cookie xuyên suốt
Giá không tải sau khi giải CAPTCHA Thiếu token CSRF Trích xuất và gửi kèm các trường ẩn của form
Site chặn sau nhiều lần tìm kiếm Giới hạn tần suất (rate limit) Thêm độ trễ giữa request
Giá khác so với trình duyệt Thiếu cookie hoặc header session Khớp chính xác header với trình duyệt thật

Luồng xử lý cơ bản: giải reCAPTCHA v2 rồi gửi request tìm kiếm

Đoạn code dưới đây: trích data-sitekey, gửi qua in.php, polling res.php tới khi có token, đính vào request POST.

import requests
import time

session = requests.Session()
session.headers.update({
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
})

def solve_recaptcha(site_key, page_url):
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": "YOUR_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": "YOUR_API_KEY",
            "action": "get",
            "id": task_id,
            "json": 1
        })
        data = result.json()
        if data["status"] == 1:
            return data["request"]
    raise TimeoutError("Solve timed out")

def search_drug_prices(drug_name, zipcode):
    search_url = "https://pharmacy.example.com/search"

    # Load the search page to get the site key
    page = session.get(search_url)

    # Extract reCAPTCHA site key
    import re
    match = re.search(r'data-sitekey="([^"]+)"', page.text)
    site_key = match.group(1)

    # Solve the CAPTCHA
    token = solve_recaptcha(site_key, search_url)

    # Submit the search with token
    results = session.post(search_url, data={
        "drug": drug_name,
        "zip": zipcode,
        "g-recaptcha-response": token
    })

    return parse_prices(results.text)

Gộp kết quả từ nhiều nhà thuốc trong một lần chạy

Cách làm:

  • Khai báo captcha_type từng nguồn.
  • Điều phối tự chọn cách giải.
  • Lỗi một nguồn không hỏng cả lần chạy.
PHARMACY_SOURCES = [
    {
        "name": "PharmacyA",
        "url": "https://pharmacya.example.com/pricing",
        "captcha_type": "recaptcha_v2"
    },
    {
        "name": "PharmacyB",
        "url": "https://pharmacyb.example.com/drugs",
        "captcha_type": "turnstile"
    },
    {
        "name": "PharmacyC",
        "url": "https://pharmacyc.example.com/search",
        "captcha_type": "image"
    }
]

def compare_drug_price(drug_name, zipcode):
    results = []

    for source in PHARMACY_SOURCES:
        try:
            prices = fetch_prices(
                source["url"],
                source["captcha_type"],
                drug_name,
                zipcode
            )
            results.append({
                "source": source["name"],
                "prices": prices,
                "status": "success"
            })
        except Exception as e:
            results.append({
                "source": source["name"],
                "error": str(e),
                "status": "failed"
            })

    return sorted(results, key=lambda r: r.get("prices", {}).get("lowest", float("inf")))

Một hàm xử lý ba loại CAPTCHA cùng lúc

Hàm dưới đây gộp ba nhánh xử lý:

  • Rẽ nhánh theo captcha_type.
  • Trích site key/ảnh phù hợp.
  • Đổi tên field khi submit.
def fetch_prices(url, captcha_type, drug_name, zipcode):
    page = session.get(url)

    if captcha_type == "recaptcha_v2":
        import re
        match = re.search(r'data-sitekey="([^"]+)"', page.text)
        token = solve_recaptcha(match.group(1), url)
        field_name = "g-recaptcha-response"

    elif captcha_type == "turnstile":
        import re
        match = re.search(r'data-sitekey="(0x[^"]+)"', page.text)
        token = solve_turnstile(match.group(1), url)
        field_name = "cf-turnstile-response"

    elif captcha_type == "image":
        img_data = extract_captcha_image(page.text)
        token = solve_image_captcha(img_data)
        field_name = "captcha"

    return session.post(url, data={
        "drug": drug_name,
        "zip": zipcode,
        field_name: token
    })

Giữ session để giảm số lần gặp CAPTCHA

Một session giữ cookie xuyên suốt sẽ không bị thử thách CAPTCHA ở mọi request — chỉ khi portal chủ động yêu cầu.

class PharmacySession:
    def __init__(self, base_url):
        self.session = requests.Session()
        self.base_url = base_url
        self.searches_since_captcha = 0

    def search(self, drug_name, zipcode):
        result = self.session.post(f"{self.base_url}/search", data={
            "drug": drug_name,
            "zip": zipcode
        })

        if self.is_captcha_page(result.text):
            token = self.solve_page_captcha(result.text)
            result = self.session.post(f"{self.base_url}/search", data={
                "drug": drug_name,
                "zip": zipcode,
                "g-recaptcha-response": token
            })
            self.searches_since_captcha = 0
        else:
            self.searches_since_captcha += 1

        return result

    def is_captcha_page(self, html):
        return "g-recaptcha" in html or "cf-turnstile" in html

    def solve_page_captcha(self, html):
        import re
        match = re.search(r'data-sitekey="([^"]+)"', html)
        return solve_recaptcha(match.group(1), self.base_url)

Phiên bản Node.js cho crawler chạy song song

Cùng logic, giải song song bằng Promise.all rồi sắp xếp kết quả theo giá:

async function comparePharmacyPrices(drugName, zipCode, sources) {
  const results = await Promise.all(
    sources.map(async (source) => {
      try {
        const price = await fetchDrugPrice(source, drugName, zipCode);
        return { source: source.name, price, status: 'success' };
      } catch (error) {
        return { source: source.name, error: error.message, status: 'failed' };
      }
    })
  );

  return results
    .filter(r => r.status === 'success')
    .sort((a, b) => a.price - b.price);
}

async function fetchDrugPrice(source, drugName, zipCode) {
  // Solve CAPTCHA for this source
  const token = await solveCaptcha(source.siteKey, source.url);

  const response = await fetch(source.url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      drug: drugName,
      zip: zipCode,
      'g-recaptcha-response': token
    })
  });

  return parsePrice(await response.text());
}

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

CaptchaAI có giải được CAPTCHA trên cổng bảo hiểm y tế và nhà thuốc Mỹ không?

Có, với reCAPTCHA v2, Turnstile và CAPTCHA hình ảnh/OCR — các loại phổ biến trên nhóm site này. CaptchaAI chưa hỗ trợ hCaptcha và FunCaptcha; cổng nào dùng hai loại đó cần phương án riêng.

Chi phí giải CAPTCHA khi crawl giá thuốc từ 10–20 nguồn mỗi ngày là bao nhiêu?

CaptchaAI tính phí theo thread, không theo lần giải. Gói BASIC ($15/tháng, 5 thread) đủ cho vài nguồn thử nghiệm; mở rộng 10–20 nguồn thì STANDARD ($30/tháng, 15 thread) hoặc ADVANCE ($90/tháng, 50 thread) phù hợp hơn.

reCAPTCHA v2 và Cloudflare Turnstile trên cổng dược phẩm khác nhau ở đâu?

reCAPTCHA v2 thường cố định trên form tìm kiếm — mọi truy vấn phải giải. Turnstile chỉ bật khi nghi ngờ truy cập tự động. Khác biệt trong code chỉ ở tên field (g-recaptcha-response so với cf-turnstile-response).

Có thể giải CAPTCHA song song cho nhiều cổng hiệu thuốc cùng lúc không?

Có — số lượng song song phụ thuộc số thread trong gói, không giới hạn theo domain. Giới hạn thực tế thường đến từ rate limit của từng cổng, không phải CaptchaAI.

Bài viết liên quan

Bước tiếp theo

Bắt đầu crawl giá thuốc từ nhiều nguồn ngay hôm nay — lấy API key CaptchaAI và cắm vào pipeline giải CAPTCHA cho tất cả cổng hiệu thuốc bạn cần theo dõi.

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