يمنحك بروتوكول Chrome DevTools (CDP) تحكمًا مباشرًا في Chrome على مستوى البروتوكول - بدون WebDriver، ولا توجد طبقات تجريد إضافية. بالنسبة لأتمتة اختبار CAPTCHA في بيئات الاختبار المُملوكة، يعني هذا تحكمًا دقيقًا في طلبات الشبكة وتنفيذ الصفحة وسلوك المتصفح ضمن اختبار موثّق ومأذون.
لماذا CDP على WebDriver؟
| ميزة | برنامج تشغيل الويب | CDP |
|---|---|---|
| اعتراض الشبكة | يتطلب سلك السيلينيوم | مدمج (Fetch.requestPaused) |
| حقن JavaScript | executeScript |
Runtime.evaluate |
| الأداء العام | متوسطة عالية | منخفض |
| دعم البروتوكول | سلك HTTP + JSON | WebSocket (في الوقت الحقيقي) |
اتصال CDP المباشر (Node.js)
اتصل بمتصفح Chrome
# Launch Chrome with remote debugging
chrome --remote-debugging-port=9222 --no-first-run --no-default-browser-check
const WebSocket = require("ws");
const http = require("http");
class CDPClient {
constructor() {
this.ws = null;
this.id = 0;
this.callbacks = new Map();
this.eventHandlers = new Map();
}
async connect(port = 9222) {
// Get WebSocket URL from Chrome
const targets = await this.httpGet(
`http://127.0.0.1:${port}/json/list`
);
const target = targets.find((t) => t.type === "page");
return new Promise((resolve, reject) => {
this.ws = new WebSocket(target.webSocketDebuggerUrl);
this.ws.on("open", () => resolve());
this.ws.on("error", reject);
this.ws.on("message", (data) => this.handleMessage(JSON.parse(data)));
});
}
httpGet(url) {
return new Promise((resolve, reject) => {
http.get(url, (res) => {
let body = "";
res.on("data", (c) => (body += c));
res.on("end", () => resolve(JSON.parse(body)));
}).on("error", reject);
});
}
handleMessage(msg) {
if (msg.id && this.callbacks.has(msg.id)) {
this.callbacks.get(msg.id)(msg);
this.callbacks.delete(msg.id);
}
if (msg.method && this.eventHandlers.has(msg.method)) {
for (const handler of this.eventHandlers.get(msg.method)) {
handler(msg.params);
}
}
}
send(method, params = {}) {
return new Promise((resolve) => {
const id = ++this.id;
this.callbacks.set(id, resolve);
this.ws.send(JSON.stringify({ id, method, params }));
});
}
on(method, handler) {
if (!this.eventHandlers.has(method)) {
this.eventHandlers.set(method, []);
}
this.eventHandlers.get(method).push(handler);
}
}
تكامل CaptchaAI + CDP
const https = require("https");
class CDPCaptchaSolver {
constructor(apiKey) {
this.apiKey = apiKey;
this.cdp = new CDPClient();
this.API = "https://ocr.captchaai.com";
}
async init(port = 9222) {
await this.cdp.connect(port);
// Enable required domains
await this.cdp.send("Page.enable");
await this.cdp.send("Runtime.enable");
await this.cdp.send("Network.enable");
await this.cdp.send("DOM.enable");
}
async navigate(url) {
const result = await this.cdp.send("Page.navigate", { url });
await this.waitForLoad();
return result;
}
async waitForLoad() {
return new Promise((resolve) => {
this.cdp.on("Page.loadEventFired", () => resolve());
});
}
async detectSitekey() {
const result = await this.cdp.send("Runtime.evaluate", {
expression: `
(() => {
// reCAPTCHA
const recaptcha = document.querySelector('[data-sitekey]');
if (recaptcha) {
return {
type: 'recaptcha_v2',
sitekey: recaptcha.getAttribute('data-sitekey'),
};
}
// Turnstile
const turnstile = document.querySelector('.cf-turnstile[data-sitekey]');
if (turnstile) {
return {
type: 'turnstile',
sitekey: turnstile.getAttribute('data-sitekey'),
};
}
// reCAPTCHA v3 (script-based)
const scripts = document.querySelectorAll('script[src*="recaptcha"]');
for (const s of scripts) {
const match = s.src.match(/render=([\\w-]+)/);
if (match && match[1] !== 'explicit') {
return { type: 'recaptcha_v3', sitekey: match[1] };
}
}
return null;
})()
`,
returnByValue: true,
});
return result.result?.value || null;
}
async solveCaptcha(siteUrl, sitekey, type = "recaptcha_v2") {
const submitData = {
key: this.apiKey,
pageurl: siteUrl,
json: "1",
};
if (type === "turnstile") {
submitData.method = "turnstile";
submitData.sitekey = sitekey;
} else {
submitData.method = "userrecaptcha";
submitData.googlekey = sitekey;
}
const submitResp = await this.httpPost(
`${this.API}/in.php`,
submitData
);
if (submitResp.status !== 1) {
throw new Error(`Submit: ${submitResp.request}`);
}
const taskId = submitResp.request;
for (let i = 0; i < 60; i++) {
await this.sleep(5000);
const params = new URLSearchParams({
key: this.apiKey,
action: "get",
id: taskId,
json: "1",
});
const result = await this.httpGet(
`${this.API}/res.php?${params}`
);
if (result.request === "CAPCHA_NOT_READY") continue;
if (result.status !== 1) throw new Error(`Solve: ${result.request}`);
return result.request;
}
throw new Error("Timeout");
}
async injectToken(token, type = "recaptcha_v2") {
if (type === "turnstile") {
await this.cdp.send("Runtime.evaluate", {
expression: `
const input = document.querySelector('input[name="cf-turnstile-response"]');
if (input) {
input.value = '${token}';
input.dispatchEvent(new Event('change', { bubbles: true }));
}
`,
});
} else {
await this.cdp.send("Runtime.evaluate", {
expression: `
// Set response textarea
const textarea = document.querySelector('#g-recaptcha-response');
if (textarea) {
textarea.style.display = 'block';
textarea.value = '${token}';
}
// Set all hidden fields
document.querySelectorAll('[name="g-recaptcha-response"]')
.forEach(el => { el.value = '${token}'; });
// Trigger callback
if (typeof ___grecaptcha_cfg !== 'undefined') {
const clients = ___grecaptcha_cfg.clients;
for (const key in clients) {
const client = clients[key];
for (const prop in client) {
const val = client[prop];
if (val && typeof val === 'object') {
for (const p in val) {
if (typeof val[p]?.callback === 'function') {
val[p].callback('${token}');
}
}
}
}
}
}
`,
});
}
}
// Full workflow
async solveOnPage(url) {
await this.navigate(url);
await this.sleep(2000);
const captcha = await this.detectSitekey();
if (!captcha) {
console.log("No CAPTCHA detected");
return null;
}
console.log(`Detected: ${captcha.type} (${captcha.sitekey})`);
const token = await this.solveCaptcha(url, captcha.sitekey, captcha.type);
await this.injectToken(token, captcha.type);
console.log("Token injected");
return token;
}
// HTTP helpers
httpPost(url, data) {
return new Promise((resolve, reject) => {
const params = new URLSearchParams(data).toString();
const u = new URL(url);
const req = https.request({
hostname: u.hostname, path: u.pathname,
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
}, (res) => {
let body = "";
res.on("data", (c) => (body += c));
res.on("end", () => resolve(JSON.parse(body)));
});
req.on("error", reject);
req.write(params);
req.end();
});
}
httpGet(url) {
return new Promise((resolve, reject) => {
https.get(url, (res) => {
let body = "";
res.on("data", (c) => (body += c));
res.on("end", () => resolve(JSON.parse(body)));
}).on("error", reject);
});
}
sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
}
اعتراض الشبكة مع CDP
async function interceptCaptchaRequests(solver) {
// Enable Fetch domain for request interception
await solver.cdp.send("Fetch.enable", {
patterns: [
{ urlPattern: "*recaptcha*", requestStage: "Request" },
{ urlPattern: "*turnstile*", requestStage: "Request" },
{ urlPattern: "*challenges.cloudflare.com*", requestStage: "Request" },
],
});
solver.cdp.on("Fetch.requestPaused", async (params) => {
const { requestId, request } = params;
console.log(`Intercepted: ${request.method} ${request.url}`);
// Log CAPTCHA-related requests for debugging
if (request.url.includes("userverify") || request.url.includes("reload")) {
console.log("CAPTCHA verification request detected");
}
// Continue the request
await solver.cdp.send("Fetch.continueRequest", { requestId });
});
}
تكامل بايثون CDP
import asyncio
import aiohttp
import json
class CDPCaptchaSolver:
CAPTCHAAI_URL = "https://ocr.captchaai.com"
def __init__(self, api_key, cdp_port=9222):
self.api_key = api_key
self.cdp_port = cdp_port
self.ws = None
self.msg_id = 0
async def connect(self):
async with aiohttp.ClientSession() as session:
async with session.get(
f"http://127.0.0.1:{self.cdp_port}/json/list"
) as resp:
targets = await resp.json()
target = next(t for t in targets if t["type"] == "page")
self.ws = await asyncio.get_event_loop().create_connection(
lambda: CDPProtocol(self),
target["webSocketDebuggerUrl"],
)
async def send(self, method, params=None):
self.msg_id += 1
msg = {"id": self.msg_id, "method": method, "params": params or {}}
self.ws.send(json.dumps(msg))
# Wait for response (simplified)
return await self._wait_response(self.msg_id)
async def solve_and_inject(self, url):
await self.send("Page.navigate", {"url": url})
await asyncio.sleep(3)
# Detect sitekey via Runtime.evaluate
result = await self.send("Runtime.evaluate", {
"expression": "document.querySelector('[data-sitekey]')?.getAttribute('data-sitekey')",
"returnByValue": True,
})
sitekey = result.get("result", {}).get("value")
if not sitekey:
return None
# Solve via CaptchaAI
token = await self._solve_recaptcha(url, sitekey)
# Inject
await self.send("Runtime.evaluate", {
"expression": f"""
document.querySelector('#g-recaptcha-response').value = '{token}';
document.querySelectorAll('[name="g-recaptcha-response"]')
.forEach(el => {{ el.value = '{token}'; }});
""",
})
return token
async def _solve_recaptcha(self, site_url, sitekey):
import requests
resp = requests.post(f"{self.CAPTCHAAI_URL}/in.php", data={
"key": self.api_key, "method": "userrecaptcha",
"googlekey": sitekey, "pageurl": site_url, "json": 1,
})
task_id = resp.json()["request"]
for _ in range(60):
await asyncio.sleep(5)
resp = requests.get(f"{self.CAPTCHAAI_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:
return data["request"]
raise TimeoutError("CAPTCHA solve timeout")
مثال الاستخدام
// Full workflow
async function main() {
const solver = new CDPCaptchaSolver("YOUR_API_KEY");
await solver.init(9222);
// Enable network monitoring
await interceptCaptchaRequests(solver);
// Solve CAPTCHA on target page
const token = await solver.solveOnPage("https://example.com/login");
if (token) {
// Submit form
await solver.cdp.send("Runtime.evaluate", {
expression: `document.querySelector('form').submit()`,
});
}
}
main().catch(console.error);
استكشاف الأخطاء وإصلاحها
| المشكلة | السبب | الإجراء |
|---|---|---|
| لا يمكن الاتصال بمتصفح Chrome | لم يتم تمكين تصحيح الأخطاء عن بعد | ابدأ باستخدام --remote-debugging-port=9222 |
| يتم إغلاق WebSocket | تعطل Chrome أو تم إغلاق علامة التبويب | إضافة منطق إعادة الاتصال |
فشل Runtime.evaluate |
لم يتم تحميل الصفحة | انتظر Page.loadEventFired |
| فشل حقن الرمز المميز في المرحلة الأولى | CAPTCHA داخل Shadow DOM | استخدم DOM.describeNode لاجتياز جذور الظل |
| فشل حقن الرمز المميز | يحتوي Shadow DOM على اختبار CAPTCHA | استخدم DOM.describeNode لاجتياز جذور الظل |
الأسئلة الشائعة
هل استخدام CDP أصعب من WebDriver؟
يمنح CDP وصولًا أدق إلى أحداث الصفحة والشبكة، لكنه يتطلب فهمًا أوثق لدورة حياة التبويب. استخدمه عندما تحتاج إلى تشخيص تفصيلي داخل بيئة مملوكة.
هل يمكنني استخدام CDP مع Puppeteer؟
نعم - يستخدم Puppeteer CDP تحت الغطاء. يمكنك الوصول إلى جلسة CDP مباشرة عبر page._client() أو page.createCDPSession().
هل يعمل CDP مع فايرفوكس؟
يتمتع Firefox بدعم CDP جزئي. للحصول على CDP الكامل، استخدم المتصفحات المستندة إلى Chromium.
هل يغيّر CDP قرارات الحماية على الموقع؟
لا. CDP مجرد واجهة تحكم وتشخيص. إذا كان الموقع يفرض تحديات معينة، فاختبرها داخل بيئة تملكها أو بيئة staging مفوضة، ولا تتعامل مع CDP على أنه وسيلة لتغيير سياسات الحماية.
أدلة ذات صلة
- البدء السريع مع CaptchaAI: حلّ أول كابتشا في 5 دقائق
- CaptchaAI Webhook Security: التحقق من صحة تواقيع رد الاتصال
الحصول على التحكم على مستوى البروتوكول في أتمتة اختبار CAPTCHA —احصل على مفتاح CaptchaAI الخاص بكوالتكامل مع بروتوكول Chrome DevTools.