Kalau skrip Selenium Anda menebak jenis CAPTCHA dari DOM, cepat atau lambat tebakan itu meleset. Widget dimuat di dalam iframe lintas-origin, sitekey baru dikirim setelah satu panggilan AJAX, dan halaman terlihat bersih padahal challenge sudah aktif. Selenium Wire menutup celah itu: setiap request yang dikeluarkan browser tersedia sebagai objek Python, jadi jenis CAPTCHA dan sitekey bisa dibaca langsung dari lalu lintas jaringan, lalu dikirim ke CaptchaAI untuk diselesaikan.
Seluruh contoh di bawah memakai satu kelas Python yang sama dan menutup empat hal:
- Deteksi reCAPTCHA v2, reCAPTCHA v3, dan Cloudflare Turnstile dari
driver.requests - Interceptor untuk menambah header, memblokir skrip pelacakan, dan mengubah isi respons
- Rotasi proxy per request serta ekspor HAR sebagai bahan debugging
- Perhitungan thread agar biaya bulanan bisa diprediksi sejak awal
Kenapa deteksi lewat jaringan lebih andal daripada parsing DOM
Selenium standar hanya melihat hasil akhir render. Pada iframe lintas-origin, document.querySelector('[data-sitekey]') mengembalikan null walaupun challenge-nya jelas ada. Iframe itu tetap harus meminta recaptcha/api2/anchor?k=... lewat jaringan — dan di URL itulah sitekey terbaca apa adanya.
Selenium Wire membungkus WebDriver dengan proxy lokal dan menyediakan setiap pasangan request-respons sebagai objek Python:
| Kemampuan | Selenium standar | Selenium Wire |
|---|---|---|
| Membaca request | Tidak | Ya |
| Mengubah header | Terbatas | Kontrol penuh |
| Mencegat respons | Tidak | Ya |
| Dukungan proxy | Dasar | Termasuk proxy dengan autentikasi |
| Log jaringan | Hanya lewat DevTools | Objek Python |
| Ekspor HAR | Tidak | Ya |
Instalasi dan hal yang perlu disiapkan
Satu perintah pip cukup; webdriver-manager menyamakan versi ChromeDriver dengan Chrome di runner.
pip install seleniumwire selenium webdriver-manager requests
Selenium Wire memasang sertifikatnya sendiri agar bisa membaca lalu lintas HTTPS. Di CI, pastikan port lokal tidak diblokir kebijakan jaringan.
Kelas integrasi: deteksi, solve, lalu inject token
Kelas berikut dipakai ulang di sisa artikel, mengikuti empat langkah standar integrasi CaptchaAI:
- Kirim task ke
in.phpberisimethod, sitekey, danpageurl - Simpan task ID dari respons pengiriman
- Polling
res.phpsampai statusnya bukanCAPCHA_NOT_READY - Pakai token yang dikembalikan pada form target
from seleniumwire import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
import requests
import time
import json
class SeleniumWireCaptchaSolver:
BASE_URL = "https://ocr.captchaai.com"
def __init__(self, api_key, proxy=None):
self.api_key = api_key
self.proxy = proxy
self.driver = None
def create_driver(self, headless=True):
options = webdriver.ChromeOptions()
if headless:
options.add_argument("--headless=new")
options.add_argument("--no-sandbox")
options.add_argument("--window-size=1920,1080")
wire_options = {}
if self.proxy:
wire_options["proxy"] = {
"http": self.proxy,
"https": self.proxy,
}
self.driver = webdriver.Chrome(
service=Service(ChromeDriverManager().install()),
options=options,
seleniumwire_options=wire_options,
)
return self.driver
def detect_captcha_from_requests(self):
"""Detect CAPTCHA type from intercepted network requests."""
for request in self.driver.requests:
url = request.url
if "recaptcha/api2/anchor" in url or "recaptcha/enterprise/anchor" in url:
# Extract sitekey from reCAPTCHA iframe URL
import re
match = re.search(r"k=([A-Za-z0-9_-]+)", url)
if match:
return {
"type": "recaptcha_v2",
"sitekey": match.group(1),
"page_url": self.driver.current_url,
}
if "recaptcha/api2/reload" in url or "recaptcha/enterprise/reload" in url:
return {
"type": "recaptcha_v3",
"sitekey": self._extract_v3_sitekey(),
"page_url": self.driver.current_url,
}
if "challenges.cloudflare.com" in url:
sitekey = self._extract_turnstile_sitekey()
if sitekey:
return {
"type": "turnstile",
"sitekey": sitekey,
"page_url": self.driver.current_url,
}
return None
def _extract_v3_sitekey(self):
for request in self.driver.requests:
if "recaptcha" in request.url and "render=" in request.url:
import re
match = re.search(r"render=([A-Za-z0-9_-]+)", request.url)
if match:
return match.group(1)
return self.driver.execute_script(
"return document.querySelector('[data-sitekey]')?.getAttribute('data-sitekey')"
)
def _extract_turnstile_sitekey(self):
return self.driver.execute_script(
"return document.querySelector('.cf-turnstile[data-sitekey]')?.getAttribute('data-sitekey')"
)
def solve_captcha(self, captcha_info):
"""Solve detected CAPTCHA via CaptchaAI API."""
params = {"key": self.api_key, "json": 1}
if captcha_info["type"] == "recaptcha_v2":
params.update({
"method": "userrecaptcha",
"googlekey": captcha_info["sitekey"],
"pageurl": captcha_info["page_url"],
})
elif captcha_info["type"] == "recaptcha_v3":
params.update({
"method": "userrecaptcha",
"googlekey": captcha_info["sitekey"],
"pageurl": captcha_info["page_url"],
"version": "v3",
"action": "verify",
})
elif captcha_info["type"] == "turnstile":
params.update({
"method": "turnstile",
"key": captcha_info["sitekey"],
"pageurl": captcha_info["page_url"],
})
# Submit
resp = requests.post(f"{self.BASE_URL}/in.php", data=params)
data = resp.json()
if data["status"] != 1:
raise Exception(f"Submit: {data['request']}")
task_id = data["request"]
# Poll
for _ in range(60):
time.sleep(5)
resp = requests.get(f"{self.BASE_URL}/res.php", params={
"key": self.api_key,
"action": "get",
"id": task_id,
"json": 1,
})
data = resp.json()
if data["request"] == "CAPCHA_NOT_READY":
continue
if data["status"] != 1:
raise Exception(f"Solve: {data['request']}")
return data["request"]
raise Exception("Timeout")
def inject_token(self, captcha_type, token):
"""Inject solved token into the page."""
if captcha_type in ("recaptcha_v2", "recaptcha_v3"):
self.driver.execute_script(f"""
document.querySelector('#g-recaptcha-response').value = '{token}';
// Try hidden textareas in iframes
document.querySelectorAll('[name="g-recaptcha-response"]').forEach(
el => el.value = '{token}'
);
""")
elif captcha_type == "turnstile":
self.driver.execute_script(f"""
const input = document.querySelector('[name="cf-turnstile-response"]');
if (input) input.value = '{token}';
""")
def close(self):
if self.driver:
self.driver.quit()
Perhatikan pemetaan method-nya:
- reCAPTCHA v2 dan v3 →
userrecaptcha; v3 menambahkanversiondanaction - Cloudflare Turnstile →
turnstile - GeeTest v3 →
geetest
Token reCAPTCHA masuk ke g-recaptcha-response, token Turnstile ke cf-turnstile-response; tertukar sedikit saja, form menolak tanpa pesan yang jelas.
Alur kerja lengkap pada halaman login staging
Skenario umumnya: agensi price-monitoring di Jakarta menjalankan regression test malam hari terhadap halaman login staging miliknya sendiri, dengan runner di AWS ap-southeast-3. Karena challenge dilayani dari edge Cloudflare, latency ikut menentukan berapa lama jeda perlu ditahan sebelum request CAPTCHA tercatat.
def automate_login():
solver = SeleniumWireCaptchaSolver(
api_key="YOUR_API_KEY",
proxy="http://user:pass@proxy.example.com:8080"
)
try:
driver = solver.create_driver(headless=True)
# Navigate and wait for CAPTCHA resources to load
driver.get("https://staging.example.com/qa-login")
time.sleep(3)
# Fill form
driver.find_element("id", "email").send_keys("user@example.com")
driver.find_element("id", "password").send_keys("password123")
# Detect CAPTCHA from network requests
captcha = solver.detect_captcha_from_requests()
if captcha:
print(f"Found {captcha['type']} (sitekey: {captcha['sitekey'][:20]}...)")
token = solver.solve_captcha(captcha)
solver.inject_token(captcha["type"], token)
print(f"Token injected: {token[:50]}...")
# Submit
driver.find_element("id", "submit").click()
time.sleep(3)
print(f"Current URL: {driver.current_url}")
finally:
solver.close()
automate_login()
Kalau detect_captcha_from_requests() mengembalikan None, biasanya halaman selesai dimuat sebelum iframe CAPTCHA mengeluarkan request pertamanya.
Mencegat dan memodifikasi request
Tiga interceptor berikut menutupi kebutuhan harian.
Menambahkan header sendiri
request_interceptor dipanggil sebelum setiap request dikirim, jadi header bisa ditambah atau ditimpa dari satu tempat.
def add_headers_interceptor(request):
request.headers["X-Custom-Header"] = "value"
request.headers["Accept-Language"] = "en-US,en;q=0.9"
driver = solver.create_driver()
driver.request_interceptor = add_headers_interceptor
Memblokir skrip pelacakan
Halaman berat menghabiskan detik pertama untuk analytics dan iklan; menggugurkannya memangkas waktu muat sekaligus merapikan driver.requests.
def block_trackers(request):
blocked = ["analytics", "tracking", "ads", "facebook.net", "doubleclick"]
if any(b in request.url for b in blocked):
request.abort()
driver.request_interceptor = block_trackers
Mengubah isi respons
response_interceptor menerima request beserta responsnya, sehingga body bisa diubah sebelum sampai ke halaman — berguna untuk menguji cabang error di aplikasi Anda sendiri.
def modify_response(request, response):
if "captcha-config" in request.url:
# Modify CAPTCHA configuration response
import json
body = json.loads(response.body.decode("utf-8"))
body["difficulty"] = "easy"
response.body = json.dumps(body).encode("utf-8")
driver.response_interceptor = modify_response
Menarik parameter CAPTCHA dari panggilan AJAX
Sebagian situs tidak menaruh sitekey di HTML; nilainya datang lewat endpoint konfigurasi. Fungsi berikut mengawasi jaringan sampai ada body JSON yang memuat sitekey.
def extract_captcha_from_api(driver, timeout=10):
"""Watch network for CAPTCHA configuration API calls."""
start = time.time()
while time.time() - start < timeout:
for request in driver.requests:
if request.response and "captcha" in request.url.lower():
try:
data = json.loads(request.response.body.decode())
if "sitekey" in str(data) or "siteKey" in str(data):
return data
except (json.JSONDecodeError, UnicodeDecodeError):
pass
time.sleep(0.5)
return None
Rotasi proxy per request
Konfigurasi proxy ditulis langsung di seleniumwire_options, termasuk proxy yang memerlukan autentikasi — di Selenium standar hal ini butuh ekstensi tambahan.
from seleniumwire import webdriver
proxies = [
"http://user:pass@proxy1.example.com:8080",
"http://user:pass@proxy2.example.com:8080",
"http://user:pass@proxy3.example.com:8080",
]
current_proxy = [0]
def rotate_proxy(request):
proxy = proxies[current_proxy[0] % len(proxies)]
request.headers["Proxy-Authorization"] = None # Reset
current_proxy[0] += 1
wire_options = {
"proxy": {
"http": proxies[0],
"https": proxies[0],
}
}
driver = webdriver.Chrome(seleniumwire_options=wire_options)
Pakai hanya egress jaringan yang Anda miliki atau sewa resmi. Untuk uji regional, satu egress di Singapura (ap-southeast-1) dan satu di Jakarta cukup untuk melihat beda perilaku challenge antar-region.
Catatan kepatuhan: UU Pelindungan Data Pribadi (UU 27/2022) membuat prinsip "ambil hanya data yang boleh Anda proses" bukan sekadar anjuran etika.
Membaca lalu lintas CAPTCHA apa adanya
Ketika integrasi tiba-tiba berhenti, periksa request CAPTCHA mana yang terjadi dan status apa yang dikembalikan.
def analyze_captcha_traffic(driver):
"""Analyze all CAPTCHA-related network traffic."""
captcha_requests = []
for request in driver.requests:
if any(k in request.url for k in ["recaptcha", "turnstile", "hcaptcha", "geetest"]):
captcha_requests.append({
"url": request.url,
"method": request.method,
"status": request.response.status_code if request.response else None,
"content_type": request.response.headers.get("Content-Type") if request.response else None,
"size": len(request.response.body) if request.response and request.response.body else 0,
})
print(f"\nCAPTCHA Network Traffic ({len(captcha_requests)} requests):")
for req in captcha_requests:
print(f" [{req['status']}] {req['method']} {req['url'][:80]}...")
return captcha_requests
Ekspor HAR sebagai bahan debugging
HAR berguna saat masalahnya sulit diulang: simpan satu file per kegagalan, lalu buka di DevTools atau kirim ke rekan setim.
from seleniumwire.utils import decode
def export_har(driver, filename="captcha_traffic.har"):
"""Export HAR file for debugging CAPTCHA issues."""
import json
har = {
"log": {
"version": "1.2",
"entries": []
}
}
for request in driver.requests:
if not request.response:
continue
entry = {
"request": {
"method": request.method,
"url": request.url,
"headers": [{"name": k, "value": v} for k, v in request.headers.items()],
},
"response": {
"status": request.response.status_code,
"headers": [{"name": k, "value": v} for k, v in request.response.headers.items()],
},
}
har["log"]["entries"].append(entry)
with open(filename, "w") as f:
json.dump(har, f, indent=2)
print(f"HAR exported: {filename} ({len(har['log']['entries'])} entries)")
Masalah umum dan cara mengatasinya
| Gejala | Penyebab umum | Yang perlu dilakukan |
|---|---|---|
seleniumwire gagal di-import |
Paket belum terpasang | pip install seleniumwire |
driver.requests kosong |
Halaman selesai dimuat sebelum request CAPTCHA keluar | Tambahkan time.sleep(3) setelah navigasi |
| Kesalahan SSL saat memakai proxy | Sertifikat proxy tidak dipercaya | Setel seleniumwire_options['verify_ssl'] = False |
| Pemakaian memori naik terus | Riwayat request menumpuk pada proses yang berjalan lama | Panggil del driver.requests secara berkala |
| Halaman terasa lambat | Seluruh lalu lintas melewati proxy lokal | Batasi dengan exclude_hosts di seleniumwire_options |
Deteksi mengembalikan None |
Widget dimuat di dalam iframe | Telusuri driver.requests untuk URL iframe-nya |
Urutan diagnosis yang paling cepat menemukan penyebabnya:
- Cetak jumlah
driver.requeststepat sebelum deteksi - Saring URL yang memuat
recaptchaataucloudflare - Ekspor HAR-nya kalau kegagalan hanya muncul sesekali
Menghitung thread untuk pipeline Selenium Wire
Biaya CaptchaAI dihitung per thread yang berjalan bersamaan, bukan per solve. Jadi angka yang perlu diperkirakan cuma satu: berapa browser yang menunggu token pada saat bersamaan.
- Regression run malam hari dengan 3–5 browser paralel: BASIC ($15/bulan, 5 threads) sudah memadai
- Suite QA yang jalan di beberapa cabang sekaligus, sekitar 10–15 browser: STANDARD ($30/bulan, 15 threads)
- Crawler produksi dengan puluhan worker: ADVANCE ($90/bulan, 50 threads)
Karena tagihannya tetap berapa pun jumlah solve-nya, biaya bulanan bisa dikunci sejak awal — penting untuk kontrak beranggaran tetap yang jamak di komunitas otomasi Indonesia.
Pertanyaan yang sering muncul
Kenapa driver.requests kosong padahal CAPTCHA jelas muncul di layar?
Hampir selalu soal urutan waktu: daftar baru terisi setelah browser mengirim request, sedangkan detect_captcha_from_requests() umumnya dipanggil tepat setelah driver.get(). Beri jeda beberapa detik atau tunggu container widget muncul, baru periksa daftarnya.
Berapa thread yang dibutuhkan kalau saya menjalankan 10 browser Selenium Wire sekaligus?
Satu thread setara satu CAPTCHA yang sedang diselesaikan, jadi patokannya jumlah browser yang menunggu token bersamaan, bukan total solve per hari. Sepuluh browser paralel nyaman di STANDARD ($30/bulan, 15 threads); kalau hanya tiga sampai lima yang benar-benar bersamaan, BASIC ($15/bulan, 5 threads) sudah cukup.
Apakah proxy dengan autentikasi bisa dipakai bersama Selenium Wire?
Bisa, dan itu salah satu alasan utama orang memilihnya: kredensial cukup ditulis di URL pada seleniumwire_options, tanpa ekstensi tambahan. Untuk host yang tidak perlu lewat proxy, pakai exclude_hosts agar halaman tidak melambat.
CAPTCHA di traffic ternyata hCaptcha — apa yang bisa dilakukan?
hCaptcha tidak didukung saat ini, begitu pula FunCaptcha (Arkose Labs); GeeTest v4 masih berstatus segera hadir. Lewat API Anda bisa menyelesaikan reCAPTCHA v2 dan v3 termasuk Enterprise, Cloudflare Turnstile dan Challenge, GeeTest v3, CAPTCHA gambar dan grid, plus CaptchaFox (beta), Friendly Captcha (beta), dan Lemin (beta).
Mulai dari satu request yang tertangkap
Ambil API key di captchaai.com, jalankan kelas di atas terhadap halaman staging Anda sendiri, lalu lihat CAPTCHA apa yang benar-benar muncul di driver.requests. Tiga panduan lanjutannya: