Tích Hợp

Colly + CaptchaAI: Quét dựa trên Go bằng cách giải CAPTCHA

Colly là framework scraping viết bằng Go được nhiều đội outsource ở Việt Nam ưa dùng vì tốc độ cao, tiêu thụ RAM thấp và cấu trúc collector gọn nhẹ. Vấn đề là Colly không tự giải CAPTCHA — gặp reCAPTCHA v2 hoặc Cloudflare Turnstile, request coi như dừng lại. Bài này ghép CaptchaAI vào một collector Colly theo bốn bước:

  1. Gửi task tới in.php kèm sitekey và pageurl.
  2. Nhận task ID từ response.
  3. Polling res.php theo chu kỳ tới khi có token.
  4. Gắn token vào đúng field form rồi submit — cộng thêm rate limit và hỗ trợ Turnstile để chạy ổn định ở quy mô lớn hơn.

Bước 1: Viết CaptchaAI client bằng Go

Phần lõi là một client HTTP gọi hai endpoint chuẩn của CaptchaAI: in.php để gửi task và res.php để polling kết quả. Đoạn code dưới đây gửi task userrecaptcha kèm googlekey (sitekey) và pageurl, đợi 15 giây rồi polling mỗi 5 giây, tối đa 24 lần (khoảng 2 phút) trước khi trả lỗi timeout. Token trả về khớp đúng field g-recaptcha-response mà form reCAPTCHA v2 yêu cầu khi submit.

package captchaai

import (
    "encoding/json"
    "errors"
    "fmt"
    "net/http"
    "net/url"
    "strings"
    "time"
)

type Client struct {
    APIKey     string
    HTTPClient *http.Client
}

type apiResponse struct {
    Status  int    `json:"status"`
    Request string `json:"request"`
}

func NewClient(apiKey string) *Client {
    return &Client{
        APIKey: apiKey,
        HTTPClient: &http.Client{Timeout: 30 * time.Second},
    }
}

func (c *Client) SolveRecaptchaV2(sitekey, pageurl string) (string, error) {
    // Submit task
    data := url.Values{
        "key":       {c.APIKey},
        "method":    {"userrecaptcha"},
        "googlekey": {sitekey},
        "pageurl":   {pageurl},
        "json":      {"1"},
    }

    resp, err := c.HTTPClient.PostForm("https://ocr.captchaai.com/in.php", data)
    if err != nil {
        return "", fmt.Errorf("submit error: %w", err)
    }
    defer resp.Body.Close()

    var result apiResponse
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return "", fmt.Errorf("decode error: %w", err)
    }

    if result.Status != 1 {
        return "", fmt.Errorf("submit failed: %s", result.Request)
    }

    taskID := result.Request

    // Poll for result
    time.Sleep(15 * time.Second)

    for i := 0; i < 24; i++ {
        pollURL := fmt.Sprintf(
            "https://ocr.captchaai.com/res.php?key=%s&action=get&id=%s&json=1",
            c.APIKey, taskID,
        )
        resp, err := c.HTTPClient.Get(pollURL)
        if err != nil {
            time.Sleep(5 * time.Second)
            continue
        }

        var pollResult apiResponse
        json.NewDecoder(resp.Body).Decode(&pollResult)
        resp.Body.Close()

        if pollResult.Status == 1 {
            return pollResult.Request, nil
        }
        if pollResult.Request != "CAPCHA_NOT_READY" {
            return "", fmt.Errorf("solve error: %s", pollResult.Request)
        }

        time.Sleep(5 * time.Second)
    }

    return "", errors.New("solve timeout")
}

Client trả lỗi rõ ràng ở từng bước — submit thất bại, decode JSON lỗi, hay solve timeout — để phần gọi ở collector Colly log lại và tự quyết định có retry hay không, thay vì đoán nguyên nhân.


Bước 2: Gắn client vào collector Colly

OnHTML("[data-sitekey]") bắt mọi trang có sitekey CAPTCHA. Khi phát hiện, code gọi SolveRecaptchaV2, lấy token rồi post lại form với field g-recaptcha-response — đúng tên field mà backend reCAPTCHA v2 kiểm tra. colly.AllowedDomains giới hạn collector chỉ chạy trên domain đã khai báo, phù hợp khi test trên staging hoặc trên domain bạn sở hữu.

Một đội QA hoặc data team ở TP.HCM giám sát giá trên catalogue Shopee/Tiki của chính công ty mình thường ghép pattern này vào một cron job Go: CaptchaAI xử lý phần CAPTCHA, Colly xử lý phần parse HTML bảng giá. Khi lưu log dữ liệu thu thập, nên tuân thủ Nghị định 13/2023/NĐ-CP về bảo vệ dữ liệu cá nhân — tối thiểu hoá trường dữ liệu lưu và giới hạn quyền truy cập log.

package main

import (
    "fmt"
    "log"
    "os"
    "strings"

    "github.com/gocolly/colly/v2"
)

func main() {
    apiKey := os.Getenv("CAPTCHAAI_API_KEY")
    solver := captchaai.NewClient(apiKey)

    c := colly.NewCollector(
        colly.AllowedDomains("example.com"),
        colly.MaxDepth(2),
    )

    // Detect CAPTCHA pages
    c.OnHTML("[data-sitekey]", func(e *colly.HTMLElement) {
        sitekey := e.Attr("data-sitekey")
        pageURL := e.Request.URL.String()

        log.Printf("CAPTCHA detected on %s, solving...", pageURL)

        token, err := solver.SolveRecaptchaV2(sitekey, pageURL)
        if err != nil {
            log.Printf("Solve failed: %v", err)
            return
        }

        log.Printf("CAPTCHA solved, token length: %d", len(token))

        // Post form with token
        err = c.Post(pageURL, map[string]string{
            "g-recaptcha-response": token,
        })
        if err != nil {
            log.Printf("Form submit failed: %v", err)
        }
    })

    // Extract data
    c.OnHTML("table tr", func(e *colly.HTMLElement) {
        cols := []string{}
        e.ForEach("td", func(_ int, td *colly.HTMLElement) {
            cols = append(cols, strings.TrimSpace(td.Text))
        })
        if len(cols) > 0 {
            fmt.Printf("Row: %s\n", strings.Join(cols, " | "))
        }
    })

    c.OnError(func(r *colly.Response, err error) {
        log.Printf("Error %s: %v", r.Request.URL, err)
    })

    c.Visit("https://example.com/data")
}

OnError ghi log riêng URL và lỗi gốc, tách biệt với lỗi solve CAPTCHA — hữu ích khi debug để biết lỗi đến từ mạng hay từ CAPTCHA.


Bước 3: Giới hạn tốc độ request

Giải CAPTCHA tốn thread trả phí, nên tránh gửi request dồn dập khiến trang target trả về CAPTCHA nhiều hơn cần thiết. colly.LimitRule dưới đây giới hạn 1 request mỗi 3 giây cho mỗi domain, cộng thêm độ trễ ngẫu nhiên tối đa 2 giây để tránh pattern request đều tăm tắp — pattern dễ bị hệ thống chống bot để ý.

package main

import (
    "time"

    "github.com/gocolly/colly/v2"
)

func main() {
    c := colly.NewCollector()

    // Rate limit: 1 request per 3 seconds per domain
    c.Limit(&colly.LimitRule{
        DomainGlob:  "*",
        Parallelism: 1,
        Delay:       3 * time.Second,
        RandomDelay: 2 * time.Second,
    })

    // ... CAPTCHA handling as above ...

    c.Visit("https://example.com")
}

Với nhịp độ này, gói BASIC ($15/tháng, 5 thread) thường đủ cho một job scraping vài nghìn trang mỗi ngày. Khi cần nhiều collector chạy song song, chuyển lên STANDARD ($30/tháng, 15 thread) hoặc ADVANCE ($90/tháng, 50 thread) tăng throughput mà không phải đổi kiến trúc code — CaptchaAI tính phí theo thread đang chạy, không theo số lần giải.


Bước 4: Thêm hỗ trợ Cloudflare Turnstile

Một số trang chuyển từ reCAPTCHA sang Cloudflare Turnstile. Hàm SolveTurnstile dùng method turnstile thay vì userrecaptcha, tham số đổi từ googlekey sang sitekey, và vòng polling ngắn hơn — đợi 5 giây rồi polling mỗi 3 giây, tối đa 20 lần — vì Turnstile thường giải nhanh hơn reCAPTCHA v2.

func (c *Client) SolveTurnstile(sitekey, pageurl string) (string, error) {
    data := url.Values{
        "key":       {c.APIKey},
        "method":    {"turnstile"},
        "sitekey":   {sitekey},
        "pageurl":   {pageurl},
        "json":      {"1"},
    }

    resp, err := c.HTTPClient.PostForm("https://ocr.captchaai.com/in.php", data)
    if err != nil {
        return "", fmt.Errorf("submit error: %w", err)
    }
    defer resp.Body.Close()

    var result apiResponse
    json.NewDecoder(resp.Body).Decode(&result)

    if result.Status != 1 {
        return "", fmt.Errorf("submit failed: %s", result.Request)
    }

    // Poll (same as reCAPTCHA)
    time.Sleep(5 * time.Second)
    for i := 0; i < 20; i++ {
        pollURL := fmt.Sprintf(
            "https://ocr.captchaai.com/res.php?key=%s&action=get&id=%s&json=1",
            c.APIKey, result.Request,
        )
        resp, err := c.HTTPClient.Get(pollURL)
        if err != nil {
            time.Sleep(3 * time.Second)
            continue
        }

        var pr apiResponse
        json.NewDecoder(resp.Body).Decode(&pr)
        resp.Body.Close()

        if pr.Status == 1 {
            return pr.Request, nil
        }
        if pr.Request != "CAPCHA_NOT_READY" {
            return "", fmt.Errorf("error: %s", pr.Request)
        }
        time.Sleep(3 * time.Second)
    }

    return "", errors.New("timeout")
}

Gắn hàm này vào collector theo đúng cách đã làm ở Bước 2: bắt selector đặc trưng của Turnstile trên trang, gọi SolveTurnstile, rồi post token vào field cf-turnstile-response.

Tóm tắt hai loại CAPTCHA đã dùng trong bài:

Loại CAPTCHA method Token field
reCAPTCHA v2 userrecaptcha g-recaptcha-response
Cloudflare Turnstile turnstile cf-turnstile-response

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

CaptchaAI hỗ trợ những loại CAPTCHA nào khi dùng với Colly?

Ví dụ trong bài dùng reCAPTCHA v2 và Cloudflare Turnstile — cả hai đều được CaptchaAI hỗ trợ đầy đủ (GA). Với cùng client Go này, bạn có thể đổi tham số method để giải thêm GeeTest v3, Cloudflare Challenge hoặc CAPTCHA dạng ảnh. CaptchaAI không hỗ trợ hCaptcha và FunCaptcha, còn GeeTest v4 hiện mới ở dạng sắp ra mắt.

Giải CAPTCHA có làm chậm tốc độ scraping bằng Colly không?

Có một khoảng chờ nhất định — reCAPTCHA v2 trong ví dụ trên polling tới 2 phút, còn Turnstile thường giải nhanh hơn nhiều. Cách giảm ảnh hưởng: chạy nhiều thread CaptchaAI song song với nhiều collector Colly, để việc polling một token không chặn toàn bộ job.

Colly có cần trình duyệt headless để giải CAPTCHA không?

Không. Client trong bài chỉ gọi HTTP API của CaptchaAI, không cần render JavaScript. Chỉ khi bản thân trang mục tiêu cần JavaScript để hiển thị nội dung, bạn mới cần ghép thêm chromedp hoặc rod: Colly xử lý phần tĩnh, trình duyệt headless xử lý phần động, còn CaptchaAI vẫn chỉ lo giải token.

API CaptchaAI có cần SDK riêng cho Go không?

Không cần. CaptchaAI chỉ là hai endpoint HTTP chuẩn (in.php, res.php) nên gói net/http có sẵn của Go là đủ, đúng như client mẫu ở Bước 1.

Nên chọn gói CaptchaAI nào cho một dự án scraping bằng Colly?

Với job vài nghìn trang mỗi ngày, BASIC ($15/tháng, 5 thread) thường đủ. Khi chạy nhiều collector song song hoặc cần polling nhanh cho nhiều loại CAPTCHA cùng lúc, chuyển lên STANDARD ($30/tháng, 15 thread) hoặc ADVANCE ($90/tháng, 50 thread) sẽ hợp lý hơn.


Hướng dẫn liên quan


Thêm bước giải CAPTCHA vào scraper Go của bạn — lấy API key CaptchaAI.

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