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:
- Gửi task tới
in.phpkèm sitekey và pageurl. - Nhận task ID từ response.
- Polling
res.phptheo chu kỳ tới khi có token. - 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
- Tích hợp Crawlee với CaptchaAI để scraping hiện đại
- Xây dựng framework scraping tùy chỉnh với CaptchaAI
Thêm bước giải CAPTCHA vào scraper Go của bạn — lấy API key CaptchaAI.