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:
- Trích
site key. - Gửi task, nhận
task ID. - Polling lấy
token. - Đí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_typetừ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
- Giải callback reCAPTCHA v2 bằng API
- Xử lý reCAPTCHA v2 và Turnstile trên cùng một site
- Cơ chế callback của reCAPTCHA v2
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.