Script Python dùng requests quét được hàng nghìn trang mỗi giờ mà không cần mở trình duyệt — cho đến khi gặp reCAPTCHA hoặc Cloudflare Turnstile. Cách xử lý nhanh nhất không phải chuyển cả scraper sang Selenium, mà gắn API CaptchaAI ngay vào lớp gửi request hiện có: lấy sitekey, gửi task tới in.php, polling res.php để nhận token, rồi dùng token điền vào form. Bài này đi kèm code cho scraping nhiều trang, CAPTCHA hình ảnh và retry cho môi trường production.
Cần chuẩn bị gì trước khi bắt đầu
- Python 3.7+ — cần có sẵn pip.
- Thư viện
requests— cài bằngpip install requests. - Thư viện
beautifulsoup4— cài bằngpip install beautifulsoup4. - API key CaptchaAI — lấy tại captchaai.com.
Bốn bước xử lý CAPTCHA khi scraping
Mọi đoạn code trong bài đều đi theo đúng một luồng, chỉ khác nhau ở loại CAPTCHA và cách trích tham số:
- Trích sitekey (hoặc pageurl) từ HTML của trang đích.
- Gửi task tới
in.phpkèm sitekey và pageurl. - Polling
res.phpcho đến khi nhận được token. - Điền token vào đúng field (
g-recaptcha-response,cf-turnstile-response…) rồi submit lại form.
Điền token vào field sai tên là nguyên nhân phổ biến nhất khiến scraper "giải xong vẫn bị chặn" — quan trọng hơn cả tốc độ giải.
Class CaptchaSolver dùng lại cho nhiều dự án Python
Viết một class solver duy nhất rồi tái sử dụng ở mọi script scraping thay vì lặp lại logic gửi/polling mỗi lần:
import requests
import time
class CaptchaSolver:
def __init__(self, api_key):
self.api_key = api_key
self.base = "https://ocr.captchaai.com"
def _submit(self, params):
params["key"] = self.api_key
resp = requests.get(f"{self.base}/in.php", params=params)
if not resp.text.startswith("OK|"):
raise Exception(f"Submit error: {resp.text}")
return resp.text.split("|")[1]
def _poll(self, task_id, timeout=300):
deadline = time.time() + timeout
while time.time() < deadline:
time.sleep(5)
resp = requests.get(f"{self.base}/res.php", params={
"key": self.api_key,
"action": "get",
"id": task_id
})
if resp.text == "CAPCHA_NOT_READY":
continue
if resp.text.startswith("OK|"):
return resp.text.split("|")[1]
raise Exception(f"Solve error: {resp.text}")
raise TimeoutError("Solve timed out")
def solve_recaptcha_v2(self, site_key, page_url):
task_id = self._submit({
"method": "userrecaptcha",
"googlekey": site_key,
"pageurl": page_url
})
return self._poll(task_id)
def solve_recaptcha_v3(self, site_key, page_url, action="verify"):
task_id = self._submit({
"method": "userrecaptcha",
"googlekey": site_key,
"pageurl": page_url,
"version": "v3",
"action": action
})
return self._poll(task_id)
def solve_turnstile(self, site_key, page_url):
task_id = self._submit({
"method": "turnstile",
"sitekey": site_key,
"pageurl": page_url
})
return self._poll(task_id)
def solve_image(self, image_base64):
task_id = self._submit({
"method": "base64",
"body": image_base64
})
return self._poll(task_id)
Các phương thức đã dựng sẵn trong class
solve_recaptcha_v2— dùng cho reCAPTCHA v2 checkbox/invisible.solve_recaptcha_v3— cần truyền thêmactionkhớp với hành động thật trên trang.solve_turnstile— dùng cho Cloudflare Turnstile.solve_image— nhận ảnh CAPTCHA đã base64-hoá, trả về text.
Quét biểu mẫu có reCAPTCHA từng bước
Ví dụ dưới đây tải trang, trích sitekey từ HTML, giải token rồi gửi lại chính form đó trong cùng một session.
Giữ nguyên requests.Session() xuyên suốt — nhiều trang kiểm tra cookie phiên trước khi chấp nhận token, tách session ra là nguyên nhân phổ biến khiến submit thất bại dù token hợp lệ.
from bs4 import BeautifulSoup
import requests
solver = CaptchaSolver("YOUR_API_KEY")
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
})
# Step 1: Load the page
url = "https://example.com/search"
page = session.get(url)
soup = BeautifulSoup(page.text, "html.parser")
# Step 2: Extract the site key
recaptcha_div = soup.find("div", class_="g-recaptcha")
site_key = recaptcha_div["data-sitekey"]
# Step 3: Solve the CAPTCHA
token = solver.solve_recaptcha_v2(site_key, url)
# Step 4: Submit the form with the token
form_data = {
"q": "search term",
"g-recaptcha-response": token
}
result = session.post(url, data=form_data)
# Step 5: Parse the results
result_soup = BeautifulSoup(result.text, "html.parser")
items = result_soup.find_all("div", class_="result-item")
for item in items:
print(item.text.strip())
Quét nhiều trang bị chặn CAPTCHA
Vì sao phải giải lại cho từng trang
Token CaptchaAI gắn với một pageurl và một lần submit cụ thể, không dùng lại được cho URL trang tiếp theo — vòng lặp dưới đây giải lại token ở đầu mỗi trang thay vì chỉ giải một lần rồi tái sử dụng:
def scrape_all_pages(base_url, site_key, max_pages=10):
solver = CaptchaSolver("YOUR_API_KEY")
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
})
all_results = []
for page_num in range(1, max_pages + 1):
page_url = f"{base_url}?page={page_num}"
# Solve CAPTCHA for each page if needed
token = solver.solve_recaptcha_v2(site_key, page_url)
resp = session.get(page_url, params={
"g-recaptcha-response": token,
"page": page_num
})
soup = BeautifulSoup(resp.text, "html.parser")
items = soup.find_all("div", class_="item")
if not items:
break
all_results.extend([item.text.strip() for item in items])
print(f"Page {page_num}: {len(items)} items")
time.sleep(2) # Polite delay
return all_results
Giải CAPTCHA hình ảnh (OCR)
Một số trang vẫn dùng CAPTCHA văn bản dạng ảnh thay vì reCAPTCHA. Quy trình khác ở chỗ base64-hoá ảnh trước khi gửi, nhưng vẫn theo đúng bốn bước gửi → polling → token:
import base64
def scrape_with_image_captcha(url):
solver = CaptchaSolver("YOUR_API_KEY")
session = requests.Session()
page = session.get(url)
soup = BeautifulSoup(page.text, "html.parser")
# Find the CAPTCHA image
captcha_img = soup.find("img", {"id": "captcha-image"})
captcha_url = captcha_img["src"]
# Download and encode the image
img_resp = session.get(captcha_url)
img_base64 = base64.b64encode(img_resp.content).decode()
# Solve
captcha_text = solver.solve_image(img_base64)
# Submit
form_data = {
"captcha": captcha_text,
"username": "user"
}
result = session.post(url, data=form_data)
return result.text
Thêm retry cho scraper chạy production
Mạng chập chờn hoặc token hết hạn giữa lúc gửi là chuyện bình thường khi scraper chạy liên tục 24/7 — bọc lần gọi solve trong logic thử lại thay vì để cả job chết vì một request lỗi:
def solve_with_retry(solver, site_key, page_url, max_retries=3):
for attempt in range(max_retries):
try:
return solver.solve_recaptcha_v2(site_key, page_url)
except Exception as e:
if attempt == max_retries - 1:
raise
print(f"Attempt {attempt + 1} failed: {e}. Retrying...")
time.sleep(2)
Với job chạy production nhiều giờ liên tục, log số lần retry theo từng site — tần suất retry tăng đột biến thường báo hiệu trang đã đổi sitekey hoặc thêm lớp chặn mới.
Ví dụ thực tế: theo dõi giá trên sàn thương mại điện tử
Một tình huống quen thuộc với các đội scraping engineer ở TP.HCM và Hà Nội là theo dõi giá trên Shopee, Lazada hoặc Tiki để phục vụ nghiên cứu thị trường hoặc đối chiếu với catalogue của chính doanh nghiệp. Các trang này thường đặt reCAPTCHA v3 hoặc Turnstile sau form tìm kiếm hoặc trang sản phẩm khi vượt một ngưỡng request nhất định. Class CaptchaSolver áp dụng nguyên vẹn: gọi solve_recaptcha_v3 hoặc solve_turnstile trước khi gửi request tìm kiếm, giữ requests.Session() để không mất cookie, và giãn time.sleep() giữa các lần gửi.
Cần quét song song nhiều danh mục thay vì lặp tuần tự? Chuyển phần gửi task sang bất đồng bộ — xem tích hợp aiohttp với CaptchaAI.
Nếu quy trình có chạm tới dữ liệu cá nhân (tên người bán, số điện thoại trong đánh giá…), ghi log rõ nguồn và áp dụng nguyên tắc tối thiểu hoá dữ liệu theo Nghị định 13/2023/NĐ-CP trước khi lưu trữ lâu dài — đây không phải tư vấn pháp lý, chỉ là thói quen nên có.
Lỗi thường gặp khi tích hợp và cách xử lý
| Vấn đề | Nguyên nhân | Cách xử lý |
|---|---|---|
ERROR_WRONG_USER_KEY |
API key không hợp lệ | Kiểm tra lại key trong bảng điều khiển |
ERROR_ZERO_BALANCE |
Tài khoản hết số dư | Nạp thêm số dư vào tài khoản |
| Gửi form xong lại quay về trang CAPTCHA | Token đã hết hạn hoặc sai tên field | Dùng token ngay sau khi nhận; kiểm tra lại tên field của form |
ConnectionError |
Sự cố mạng | Thêm logic thử lại với backoff tăng dần |
| Kết quả trống sau khi gửi | Trang yêu cầu cookie/session | Dùng requests.Session() để giữ cookie xuyên suốt |
CAPTCHA xuất hiện đột biến hoặc request bị chặn thẳng tay thường là dấu hiệu rate limit, không phải lỗi ở solver — xem xoay proxy khi scraping có CAPTCHA trước khi tăng số luồng gửi.
Câu hỏi thường gặp
Có cần Selenium để scraping CAPTCHA bằng Python không?
Không phải lúc nào cũng cần. Nếu form hoạt động được với HTTP POST tiêu chuẩn, requests + CaptchaAI nhanh và nhẹ hơn Selenium. Chỉ cần Selenium khi trang bắt buộc render JavaScript trước khi form hoặc CAPTCHA xuất hiện trong DOM.
CaptchaAI hỗ trợ những loại CAPTCHA nào khi scraping bằng Python?
CaptchaAI giải được reCAPTCHA v2/v3, Cloudflare Turnstile và Challenge, GeeTest v3, cùng CAPTCHA hình ảnh/OCR như trong bài. hCaptcha và FunCaptcha (Arkose Labs) hiện chưa được hỗ trợ.
Giải CAPTCHA bằng Python với CaptchaAI tốn bao nhiêu?
CaptchaAI tính phí theo thread chạy song song, không theo từng lần giải. Gói BASIC ($15/tháng, 5 thread) là mức khởi điểm phổ biến; tăng thread khi cần scraping nhiều trang cùng lúc.
reCAPTCHA v3 trả về score thấp thì scraper phải làm gì?
Score thấp nghĩa là trang đánh giá phiên hiện tại rủi ro cao, không phải lỗi của token. Gọi solve_recaptcha_v3 với action khớp đúng hành động thật trên trang (search, login…), giữ User-Agent và cookie nhất quán trong toàn session.
Scraping trang có CAPTCHA có hợp pháp không?
Tuỳ điều khoản sử dụng và loại dữ liệu thu thập. An toàn nhất là giới hạn ở dữ liệu công khai, tuân thủ robots.txt, và áp dụng nguyên tắc tối thiểu hoá dữ liệu nếu có chạm đến thông tin cá nhân. Đây là hướng dẫn kỹ thuật, không phải tư vấn pháp lý.
Hướng dẫn liên quan
- Xử lý CAPTCHA bằng Selenium và Python
- Scraping CAPTCHA bằng Node.js
- Scraping ổn định mà không bị chặn