Hướng Dẫn Thực Hành

Ghi nhật ký có cấu trúc cho hoạt động CAPTCHA

Câu hỏi "sáng nay job scraping hỏng ở đâu" chỉ mất vài giây để trả lời nếu mỗi lần giải CAPTCHA để lại một dòng JSON có task_id, captcha_type, solve_time_mserror. Với log văn bản thuần, bạn phải grep mò giữa hàng nghìn dòng Error solving captcha giống hệt nhau.

Bài này dựng log có cấu trúc quanh vòng đời một task CaptchaAI: gửi task tới in.php → nhận ID task → polling res.php → dùng token. Mỗi bước phát ra một sự kiện có tên cố định, đủ trường để truy vấn bằng jq.


Vì sao log CAPTCHA phải ở dạng JSON

Văn bản thuần JSON có cấu trúc
Captcha solved in 12.3s {"event":"captcha_solved","task_id":"abc123","type":"recaptcha_v2","solve_time_ms":12300}
Phải viết parser mới bóc được số liệu Máy đọc được ngay, không cần parser
Chỉ tìm được bằng grep Lọc theo bất kỳ trường nào
Không nối được các bước với nhau task_id nối gửi task → polling → dùng token

Cột bên phải cho phép truy vấn: bạn trả lời được "domain nào có thời gian giải reCAPTCHA v2 cao nhất tuần qua" mà không cần viết parser riêng.

Một tình huống quen thuộc với đội scraping ở Việt Nam

Một team ở TP.HCM chạy job theo dõi giá trên vài sàn thương mại điện tử, mỗi đêm vài nghìn request qua gói ADVANCE ($90/tháng, 50 thread). Sáng ra tỷ lệ giải thành công tụt còn hơn một nửa. Với log văn bản, cả buổi sáng trôi qua trong việc đọc file. Với log JSON, một câu jq gom theo error cho thấy ngay ERROR_WRONG_GOOGLEKEY chiếm phần lớn: sàn đã đổi sitekey.

Thêm một lý do: Nghị định 13/2023/NĐ-CP về bảo vệ dữ liệu cá nhân buộc nhiều đội phải rà soát xem log có chứa dữ liệu nhạy cảm không — log có cấu trúc cho bạn kiểm soát chính xác trường nào được ghi.


Python: cấu hình structlog

structlog biến mọi lời gọi log thành một dòng JSON, kèm timestamp ISO và log level:

import structlog
import time

structlog.configure(
    processors=[
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.add_log_level,
        structlog.processors.JSONRenderer(),
    ],
    logger_factory=structlog.PrintLoggerFactory(),
)

log = structlog.get_logger()

log.bind() gắn ngữ cảnh cố định cho một task; mọi sự kiện sau đó đều mang theo.

Ghi log toàn bộ vòng đời một task

Hàm dưới đây gửi task, polling kết quả và phát sự kiện riêng cho từng kết cục: gửi lỗi, giải xong, thất bại, timeout.

import requests

API_KEY = "YOUR_API_KEY"

def solve_captcha(captcha_type, sitekey, page_url, proxy=None):
    solve_log = log.bind(
        captcha_type=captcha_type,
        site_url=page_url,
        sitekey=sitekey[:12] + "...",
    )

    # Submit
    start = time.time()
    solve_log.info("captcha_submit_start")

    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": page_url,
        "json": "1",
    }).json()

    if resp["status"] != 1:
        solve_log.error("captcha_submit_failed", error=resp["request"])
        return None

    task_id = resp["request"]
    submit_ms = int((time.time() - start) * 1000)
    solve_log = solve_log.bind(task_id=task_id)
    solve_log.info("captcha_submitted", submit_ms=submit_ms)

    # Poll
    for attempt in range(24):
        time.sleep(5)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY, "action": "get", "id": task_id, "json": "1"
        }).json()

        if result["status"] == 1:
            solve_ms = int((time.time() - start) * 1000)
            solve_log.info(
                "captcha_solved",
                solve_time_ms=solve_ms,
                poll_attempts=attempt + 1,
                token_length=len(result["request"]),
            )
            return result["request"]

        if result["request"] != "CAPCHA_NOT_READY":
            solve_log.error(
                "captcha_solve_failed",
                error=result["request"],
                poll_attempts=attempt + 1,
            )
            return None

    solve_log.warning("captcha_solve_timeout", poll_attempts=24)
    return None

Kết quả là ba dòng JSON chung task_id, đủ để dựng lại dòng thời gian:

{"event":"captcha_submit_start","captcha_type":"recaptcha_v2","site_url":"https://example.com","sitekey":"6Le-wvkSAAAA...","timestamp":"2025-07-15T10:30:00Z","level":"info"}
{"event":"captcha_submitted","task_id":"71845302","submit_ms":245,"timestamp":"2025-07-15T10:30:00Z","level":"info"}
{"event":"captcha_solved","task_id":"71845302","solve_time_ms":18230,"poll_attempts":4,"token_length":580,"timestamp":"2025-07-15T10:30:18Z","level":"info"}

Chú ý sitekey bị cắt còn 12 ký tự đầu, còn API key không xuất hiện trong log ở bất kỳ đâu — quy tắc không có ngoại lệ.


Node.js: cấu hình pino

pino là lựa chọn mặc định của hệ sinh thái Node cho log JSON. Cấu hình tối thiểu chỉ cần level và định dạng timestamp:

const pino = require('pino');

const log = pino({
  level: 'info',
  timestamp: pino.stdTimeFunctions.isoTime,
});

Ghi log vòng đời task bằng logger con

log.child() trong pino đóng vai trò như log.bind() bên Python.

const axios = require('axios');

const API_KEY = 'YOUR_API_KEY';

async function solveCaptcha(captchaType, sitekey, pageUrl) {
  const taskLog = log.child({
    captchaType,
    siteUrl: pageUrl,
    sitekey: sitekey.substring(0, 12) + '...',
  });

  const start = Date.now();
  taskLog.info('captcha_submit_start');

  const submit = await axios.post('https://ocr.captchaai.com/in.php', null, {
    params: {
      key: API_KEY, method: 'userrecaptcha',
      googlekey: sitekey, pageurl: pageUrl, json: 1,
    },
  });

  if (submit.data.status !== 1) {
    taskLog.error({ error: submit.data.request }, 'captcha_submit_failed');
    return null;
  }

  const taskId = submit.data.request;
  const boundLog = taskLog.child({ taskId });
  boundLog.info({ submitMs: Date.now() - start }, 'captcha_submitted');

  for (let attempt = 1; attempt <= 24; attempt++) {
    await new Promise(r => setTimeout(r, 5000));
    const poll = await axios.get('https://ocr.captchaai.com/res.php', {
      params: { key: API_KEY, action: 'get', id: taskId, json: 1 },
    });

    if (poll.data.status === 1) {
      boundLog.info({
        solveTimeMs: Date.now() - start,
        pollAttempts: attempt,
        tokenLength: poll.data.request.length,
      }, 'captcha_solved');
      return poll.data.request;
    }

    if (poll.data.request !== 'CAPCHA_NOT_READY') {
      boundLog.error({ error: poll.data.request, pollAttempts: attempt }, 'captcha_solve_failed');
      return null;
    }
  }

  boundLog.warn({ pollAttempts: 24 }, 'captcha_solve_timeout');
  return null;
}

Tên sự kiện giữ nguyên giữa Python và Node.js, nên một truy vấn duy nhất chạy được trên log của cả hai service.


Bảng trường log nên chuẩn hóa

Thống nhất tên trường ngay từ đầu — đổi tên sau vài tháng rất khó chịu.

Trường Kiểu Mô tả
event chuỗi Tên sự kiện: captcha_submitted, captcha_solved
task_id chuỗi ID task CaptchaAI, nối các sự kiện của cùng một lần giải
captcha_type chuỗi recaptcha_v2, turnstile, image
site_url chuỗi URL trang chứa CAPTCHA
solve_time_ms số nguyên Thời gian từ lúc gửi task đến khi có token
poll_attempts số nguyên Số lần gọi res.php trước khi có kết quả
error chuỗi Mã lỗi CaptchaAI trả về
token_length số nguyên Độ dài token, ghi thay cho token đầy đủ

Giữ nguyên tiếng Anh cho khóa JSON. Nếu đẩy log của cả hai service vào cùng một index, chuẩn hóa snake_casecamelCase về một dạng.


Truy vấn và cảnh báo

Lọc nhanh bằng jq

Khi log còn ở file trên máy, jq đủ trả lời hầu hết câu hỏi.

# With jq
cat captcha.log | jq 'select(.level == "error" and .event == "captcha_solve_failed")'

Đổi biểu thức là nhóm được theo error hoặc tính trung bình solve_time_ms.

Cảnh báo khi tỷ lệ lỗi tăng

Đừng cảnh báo trên từng lần giải hỏng — lỗi lẻ tẻ là bình thường. Theo dõi tỷ lệ lỗi trên một cửa sổ trượt và chỉ báo động khi vượt ngưỡng.

# Count errors vs successes in a rolling window
from collections import deque

class ErrorRateMonitor:
    def __init__(self, window_size=100, threshold=0.2):
        self.results = deque(maxlen=window_size)
        self.threshold = threshold

    def record(self, success):
        self.results.append(success)
        if len(self.results) >= 50:
            error_rate = 1 - sum(self.results) / len(self.results)
            if error_rate > self.threshold:
                log.warning(
                    "captcha_error_rate_high",
                    error_rate=round(error_rate, 3),
                    window=len(self.results),
                )

Ngưỡng 20% trên cửa sổ 100 lần giải là điểm khởi đầu hợp lý.


Xử lý các vấn đề thường gặp

Vấn đề Nguyên nhân Cách xử lý
Log phình quá nhanh Ghi cả mỗi lần polling Chỉ ghi sự kiện gửi task, giải xong và thất bại
Không nối được các sự kiện Chưa gắn task_id Gắn task_id ngay khi in.php trả về, bằng log.bind() hoặc log.child()
Không lọc được theo trường Vẫn là log văn bản thuần Chuyển sang JSON với structlog hoặc pino
Dữ liệu nhạy cảm lọt vào log Ghi nguyên API key Không bao giờ ghi API key; cắt bớt sitekey

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

Nên đặt log level nào cho từng sự kiện CAPTCHA?

info cho captcha_submittedcaptcha_solved, error cho lỗi có mã trả về, warning cho timeout. CAPCHA_NOT_READY trong lúc polling thì không ghi — đó là trạng thái bình thường.

Log có cấu trúc có làm chậm vòng lặp polling không?

Không đáng kể. structlogpino tuần tự hóa nhanh hơn nhiều so với thời gian chờ một request HTTP, còn thời gian giải CAPTCHA tính bằng giây.

Có nên ghi giá trị token vào log không?

Không. Chỉ ghi token_length. Token thường dài trên 500 ký tự, làm phình log mà không giúp gì khi debug.

Log này dùng được cho những loại CAPTCHA nào?

Cấu trúc sự kiện dùng chung cho mọi loại CaptchaAI hỗ trợ: reCAPTCHA v2/v3 (kể cả Enterprise), Cloudflare Turnstile, Cloudflare Challenge, GeeTest v3, CAPTCHA ảnh/OCR, grid-image, BLS CAPTCHA, cùng CaptchaFox, Friendly Captcha và Lemin (cả ba đang ở giai đoạn beta). Chỉ cần đổi trường captcha_type và tham số method.

Cần gói nào để số liệu log có ý nghĩa thống kê?

Gói BASIC ($15/tháng, 5 thread) đủ để dựng và kiểm thử pipeline log; production chạy song song nhiều crawler thường bắt đầu từ ADVANCE ($90/tháng, 50 thread). Giá tính theo thread chứ không theo lần giải, nên bật log chi tiết không làm tăng chi phí.


Bắt đầu với CaptchaAI

Lấy API key tại captchaai.com, dán vào YOUR_API_KEY và chạy hàm ở trên — dòng JSON đầu tiên xuất hiện ngay ở lần giải đầu.


Bài viết liên quan

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