الدروس التطبيقية

Python Beautiful Soup + CaptchaAI: التعامل مع الصفحات المحمية بـ CAPTCHA

هل تحتاج إلى استخراج بيانات من صفحة يحرسها اختبار CAPTCHA من دون تشغيل متصفح كامل؟ الإجابة نعم، ويكفي أن تجمع بين ثلاث أدوات خفيفة: مكتبة requests لجلب الصفحة، وBeautiful Soup لتحليل الـ HTML واستخراج معلمات CAPTCHA، وCaptchaAI لحل الاختبار وإعادة رمز صالح. هذا الثلاثي هو أخفّ وأسرع مسار لجمع بيانات الصفحات المحمية عندما يرسل الخادم الـ HTML جاهزاً.

يبقى شرط واحد يحدد صلاحية هذا الأسلوب: يجب أن يقدّم الموقع محتواه مباشرةً من الخادم (server-side rendered). أما المواقع التي تبني صفحاتها عبر JavaScript داخل المتصفح — مثل تطبيقات الصفحة الواحدة (SPA) — فيلزمها Selenium أو Playwright بدلاً من هذا الأسلوب.


التثبيت والمتطلبات

ثبّت المكتبات الثلاث المطلوبة عبر أمر واحد؛ نستخدم lxml كمحلّل سريع لـ Beautiful Soup:

pip install beautifulsoup4 requests lxml

يؤدي كل مكوّن دوراً واضحاً في هذا الثلاثي:

  • requests: جلب صفحات HTML عبر HTTP.
  • Beautiful Soup: تحليل الوسوم واستخراج القيم.
  • lxml: محرّك تحليل سريع أسفل Beautiful Soup.

سير العمل في خمس خطوات

الفكرة كلها تختصر في حلقة واضحة: تجلب الصفحة، تقرأ ما تحتاجه، تحلّ الاختبار، ثم تُعيد الإرسال:

  1. أحضِر صفحة HTML عبر requests
  2. حلّل الصفحة بـ Beautiful Soup لاستخراج معلمات CAPTCHA
  3. أرسل المعلمات إلى CaptchaAI لحلّها
  4. أرسل النموذج مع رمز CAPTCHA عبر requests
  5. حلّل صفحة النتائج بـ Beautiful Soup

استخراج مفتاح موقع reCAPTCHA من الـ HTML

مفتاح الموقع (sitekey) هو أول ما تحتاجه لتشغيل الحل. الدالة التالية تجرّب ثلاث طرق للعثور عليه بالترتيب: سمة data-sitekey على عنصر div.g-recaptcha، ثم أي عنصر يحمل السمة نفسها، وأخيراً استخراجه من رابط سكربت التحميل عند غياب السمة الظاهرة:

import requests
from bs4 import BeautifulSoup

def extract_recaptcha_sitekey(url):
    """Extract reCAPTCHA v2 sitekey from page HTML."""
    resp = requests.get(url, timeout=30)
    soup = BeautifulSoup(resp.text, "lxml")

    # Method 1: data-sitekey attribute on div
    recaptcha_div = soup.find("div", class_="g-recaptcha")
    if recaptcha_div and recaptcha_div.get("data-sitekey"):
        return recaptcha_div["data-sitekey"]

    # Method 2: data-sitekey on any element
    element = soup.find(attrs={"data-sitekey": True})
    if element:
        return element["data-sitekey"]

    # Method 3: from script src
    import re
    for script in soup.find_all("script", src=True):
        match = re.search(r"render=([A-Za-z0-9_-]{40})", script["src"])
        if match:
            return match.group(1)

    return None


sitekey = extract_recaptcha_sitekey("https://example.com/login")
print(f"Sitekey: {sitekey}")

نصيحة: عندما تفشل الطرق الثلاث في إيجاد المفتاح، فالسبب الأرجح حقنُه عبر JavaScript بعد تحميل الصفحة.


استخراج مفتاح Cloudflare Turnstile

المنطق نفسه ينطبق على Cloudflare Turnstile مع فارق مفيد نستغلّه للتمييز: مفاتيح Turnstile تبدأ عادةً بالبادئة 0x:

def extract_turnstile_sitekey(url):
    """Extract Cloudflare Turnstile sitekey from page HTML."""
    resp = requests.get(url, timeout=30)
    soup = BeautifulSoup(resp.text, "lxml")

    # Method 1: Turnstile div
    turnstile_div = soup.find("div", class_="cf-turnstile")
    if turnstile_div and turnstile_div.get("data-sitekey"):
        return turnstile_div["data-sitekey"]

    # Method 2: Any element with Turnstile sitekey pattern
    element = soup.find(attrs={"data-sitekey": True})
    if element:
        sitekey = element["data-sitekey"]
        if sitekey.startswith("0x"):
            return sitekey

    # Method 3: In inline script
    import re
    for script in soup.find_all("script"):
        if script.string:
            match = re.search(r"sitekey\s*:\s*['\"]([0-9x][A-Za-z0-9_-]+)['\"]", script.string)
            if match:
                return match.group(1)

    return None

ينفعك هذا التمييز عند وجود أكثر من مفتاح في الصفحة:

  • البادئة 0x تفصل مفاتيح Turnstile عن مفاتيح reCAPTCHA.
  • ابحث داخل السكربتات المضمّنة حين لا يظهر عنصر cf-turnstile.

قراءة الحقول المخفية ورموز CSRF

لا يكفي مفتاح الموقع وحده. استخرج دائماً الحقول المخفية في النموذج، فهي كثيراً ما تحمل رموز CSRF ومعلمات أخرى يتوقعها الخادم؛ وإسقاطها هو السبب الأول لرفض النموذج بعد الإرسال:

def extract_form_data(soup, form_selector="form"):
    """Extract all form field names and values."""
    form = soup.select_one(form_selector)
    if not form:
        return {}

    data = {}
    # Hidden inputs (CSRF tokens, etc.)
    for inp in form.find_all("input", type="hidden"):
        name = inp.get("name")
        value = inp.get("value", "")
        if name:
            data[name] = value

    # Text inputs with default values
    for inp in form.find_all("input", type=["text", "email", "password"]):
        name = inp.get("name")
        value = inp.get("value", "")
        if name:
            data[name] = value

    return data

تذكّر: إغفال رمز CSRF واحد كافٍ لرفض النموذج بأكمله بعد الإرسال.


دورة استخراج كاملة لصفحة محمية بـ reCAPTCHA

هنا نجمع القطع السابقة في تدفّق واحد: جلب صفحة تسجيل الدخول، واستخراج مفتاح الموقع والحقول المخفية، وحلّ الاختبار بطريقة userrecaptcha، ثم إرسال النموذج ضمن جلسة واحدة. لاحظ أن الرمز المحلول يوضع في الحقل g-recaptcha-response كما يتوقعه الخادم:

import time
import requests
from bs4 import BeautifulSoup

API_KEY = "YOUR_API_KEY"


def solve_captcha(method, **params):
    """Solve CAPTCHA via CaptchaAI."""
    submit = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY, "method": method, "json": 1, **params,
    }, timeout=30).json()

    if submit.get("status") != 1:
        raise Exception(f"Submit error: {submit.get('request')}")

    task_id = submit["request"]
    for _ in range(30):
        time.sleep(5)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY, "action": "get", "id": task_id, "json": 1,
        }, timeout=30).json()
        if result.get("status") == 1:
            return result["request"]
    raise TimeoutError("Solve timed out")


def scrape_protected_page(url, credentials=None):
    """Scrape a reCAPTCHA-protected page — no browser needed."""
    session = requests.Session()
    session.headers.update({
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                      "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
    })

    # Step 1: Fetch the login page
    resp = session.get(url, timeout=30)
    soup = BeautifulSoup(resp.text, "lxml")

    # Step 2: Extract sitekey
    sitekey = None
    recaptcha_div = soup.find(attrs={"data-sitekey": True})
    if recaptcha_div:
        sitekey = recaptcha_div["data-sitekey"]
    if not sitekey:
        raise ValueError("No CAPTCHA sitekey found")
    print(f"Sitekey: {sitekey}")

    # Step 3: Extract form fields (CSRF tokens, etc.)
    form_data = extract_form_data(soup)
    print(f"Form fields: {list(form_data.keys())}")

    # Step 4: Add credentials
    if credentials:
        form_data.update(credentials)

    # Step 5: Solve CAPTCHA
    token = solve_captcha("userrecaptcha", googlekey=sitekey, pageurl=url)
    form_data["g-recaptcha-response"] = token

    # Step 6: Submit the form
    form = soup.find("form")
    action_url = form.get("action", url) if form else url
    if not action_url.startswith("http"):
        from urllib.parse import urljoin
        action_url = urljoin(url, action_url)

    method = (form.get("method", "POST") if form else "POST").upper()

    if method == "POST":
        result = session.post(action_url, data=form_data, timeout=30)
    else:
        result = session.get(action_url, params=form_data, timeout=30)

    # Step 7: Parse the result
    result_soup = BeautifulSoup(result.text, "lxml")
    return result_soup, session


# Usage
result_soup, session = scrape_protected_page(
    "https://example.com/login",
    credentials={"username": "user@example.com", "password": "pass123"},
)

# Now use the authenticated session to scrape protected content
dashboard = session.get("https://example.com/dashboard", timeout=30)
dashboard_soup = BeautifulSoup(dashboard.text, "lxml")
print(dashboard_soup.title.string)

مثال من الواقع: مراقبة أسعار متجر إقليمي

تخيّل فريق تسعير في متجر إلكتروني بمنطقة الخليج يريد متابعة أسعار منافسيه يومياً. كثير من هذه المتاجر تضع reCAPTCHA v2 على صفحة البحث أو تسجيل الدخول لمنع الجمع الآلي. يطبّق الفريق النمط أعلاه من دون أسطول من المتصفحات، على ثلاث ركائز:

  • جدولة ليلية تجمع الأسعار المعروضة علناً.
  • جلسة واحدة تحفظ ملفات تعريف الارتباط عبر الطلبات.
  • حفظ النتائج في جدول للمقارنة صباحاً.

استخراج نتائج بحث خلف حاجز CAPTCHA

عند استخراج نتائج محرك بحث محمي، لا يظهر اختبار CAPTCHA دائماً؛ لذا نفحص وجوده أولاً، ونحلّه فقط عند ظهوره، ثم نعيد إرسال الاستعلام ونقرأ النتائج:

def scrape_search_results(search_url, query):
    """Scrape search results from a CAPTCHA-protected search engine."""
    session = requests.Session()
    session.headers["User-Agent"] = (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36"
    )

    # Fetch search page
    resp = session.get(search_url, params={"q": query}, timeout=30)

    # Check if CAPTCHA is present
    soup = BeautifulSoup(resp.text, "lxml")
    sitekey_el = soup.find(attrs={"data-sitekey": True})

    if sitekey_el:
        # Solve CAPTCHA
        sitekey = sitekey_el["data-sitekey"]
        token = solve_captcha("userrecaptcha", googlekey=sitekey, pageurl=resp.url)

        # Resubmit with token
        form_data = extract_form_data(soup)
        form_data["g-recaptcha-response"] = token
        form_data["q"] = query
        resp = session.post(resp.url, data=form_data, timeout=30)
        soup = BeautifulSoup(resp.text, "lxml")

    # Extract results
    results = []
    for item in soup.select(".result, .search-result, .g"):
        title_el = item.select_one("h3, .title")
        link_el = item.select_one("a")
        snippet_el = item.select_one(".snippet, .description, .st")

        if title_el and link_el:
            results.append({
                "title": title_el.get_text(strip=True),
                "url": link_el.get("href", ""),
                "snippet": snippet_el.get_text(strip=True) if snippet_el else "",
            })

    return results

ملاحظة: افحص وجود data-sitekey أولاً؛ كثير من صفحات البحث لا تعرض الاختبار إلا بعد عدة طلبات.


التعامل مع كابتشا الصور (image CAPTCHA)

بعض المواقع تعرض كابتشا صورة بدل reCAPTCHA. نحمّل الصورة ونرسلها إلى CaptchaAI للحصول على النص، ثم نضعه في حقل الإدخال المناسب:

import base64
from urllib.parse import urljoin

def solve_image_captcha_bs4(url, captcha_img_selector="img.captcha"):
    """Extract, solve, and submit an image CAPTCHA."""
    session = requests.Session()
    resp = session.get(url, timeout=30)
    soup = BeautifulSoup(resp.text, "lxml")

    # Find CAPTCHA image
    img = soup.select_one(captcha_img_selector)
    if not img:
        raise ValueError("CAPTCHA image not found")

    # Download the image
    img_url = img.get("src", "")
    if img_url.startswith("data:image"):
        # Base64 inline image
        img_base64 = img_url.split(",", 1)[1]
    else:
        # URL — download it
        img_url = urljoin(url, img_url)
        img_resp = session.get(img_url, timeout=30)
        img_base64 = base64.b64encode(img_resp.content).decode()

    # Solve
    answer = solve_captcha("base64", body=img_base64)
    print(f"CAPTCHA answer: {answer}")

    # Submit form
    form_data = extract_form_data(soup)
    # Find the captcha input field name
    captcha_input = soup.select_one("input[name*='captcha'], input[name*='code']")
    if captcha_input:
        form_data[captcha_input["name"]] = answer

    form = soup.find("form")
    action = urljoin(url, form.get("action", "")) if form else url
    result = session.post(action, data=form_data, timeout=30)

    return BeautifulSoup(result.text, "lxml"), session

يتعامل الكود مع حالتَي مصدر الصورة تلقائياً:

  • الصور المضمّنة بترميز Base64 تُقرأ مباشرة من سمة src.
  • الصور المرتبطة برابط تُنزَّل عبر الجلسة نفسها قبل إرسالها.

فئة (class) جاهزة للإنتاج

عند تكرار العمل عبر عدة صفحات، اجمع المنطق في فئة واحدة تعيد استخدام الجلسة نفسها وتحلّ CAPTCHA تلقائياً عند ظهوره. الفئة التالية توفّر دالتَي get وlogin:

import time
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin


class ProtectedScraper:
    """Scrape CAPTCHA-protected pages without a browser."""

    def __init__(self, api_key):
        self.api_key = api_key
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                          "AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
            "Accept-Language": "en-US,en;q=0.9",
        })

    def get(self, url):
        """Fetch and parse a page, solving CAPTCHAs automatically."""
        resp = self.session.get(url, timeout=30)
        soup = BeautifulSoup(resp.text, "lxml")

        # Check for CAPTCHA
        sitekey_el = soup.find(attrs={"data-sitekey": True})
        if sitekey_el:
            soup = self._handle_captcha(soup, resp.url, sitekey_el)

        return soup

    def login(self, url, credentials):
        """Log in through a CAPTCHA-protected form."""
        resp = self.session.get(url, timeout=30)
        soup = BeautifulSoup(resp.text, "lxml")

        form_data = self._extract_form(soup)
        form_data.update(credentials)

        sitekey_el = soup.find(attrs={"data-sitekey": True})
        if sitekey_el:
            token = self._solve(sitekey_el["data-sitekey"], url)
            form_data["g-recaptcha-response"] = token

        form = soup.find("form")
        action = urljoin(url, form.get("action", "")) if form else url

        result = self.session.post(action, data=form_data, timeout=30)
        return BeautifulSoup(result.text, "lxml")

    def _handle_captcha(self, soup, url, sitekey_el):
        token = self._solve(sitekey_el["data-sitekey"], url)
        form_data = self._extract_form(soup)
        form_data["g-recaptcha-response"] = token

        form = soup.find("form")
        action = urljoin(url, form.get("action", "")) if form else url
        resp = self.session.post(action, data=form_data, timeout=30)
        return BeautifulSoup(resp.text, "lxml")

    def _extract_form(self, soup):
        data = {}
        for inp in soup.select("form input[type='hidden']"):
            if inp.get("name"):
                data[inp["name"]] = inp.get("value", "")
        return data

    def _solve(self, sitekey, url):
        submit = requests.post("https://ocr.captchaai.com/in.php", data={
            "key": self.api_key, "method": "userrecaptcha",
            "googlekey": sitekey, "pageurl": url, "json": 1,
        }, timeout=30).json()

        if submit.get("status") != 1:
            raise Exception(f"Error: {submit.get('request')}")

        task_id = submit["request"]
        for _ in range(30):
            time.sleep(5)
            result = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": self.api_key, "action": "get", "id": task_id, "json": 1,
            }, timeout=30).json()
            if result.get("status") == 1:
                return result["request"]
        raise TimeoutError("Solve timed out")


# Usage
scraper = ProtectedScraper("YOUR_API_KEY")

# Login and scrape
scraper.login("https://example.com/login", {
    "email": "user@example.com",
    "password": "pass123",
})

# Now scrape authenticated pages
soup = scraper.get("https://example.com/dashboard")
for row in soup.select("table tr"):
    cells = [td.get_text(strip=True) for td in row.select("td")]
    print(cells)

عملياً: أعد استخدام كائن ProtectedScraper واحد عبر كل الصفحات لتوفير إقلاع الجلسة في كل طلب.


Beautiful Soup أم متصفح مؤتمت؟ متى تختار كلاً منهما

القاعدة العملية بسيطة: إن كان الـ HTML يصل جاهزاً من الخادم فـ requests + Beautiful Soup أسرع وأخفّ؛ وإن كان المحتوى يُبنى بـ JavaScript فلا بديل عن المتصفح المؤتمت:

السيناريو استخدم requests + Beautiful Soup استخدم Selenium/Playwright
HTML مُقدَّم من الخادم نعم مبالغة
المحتوى المُقدَّم بواسطة JavaScript لا نعم
نموذج متعدد الخطوات ممكن مفضّل
استخراج بحجم كبير نعم (أسرع) أبطأ
مواقع ببصمة JavaScript لا نعم
تسجيل دخول بسيط + استخراج نعم غير ضروري

حل المشكلات الشائعة

معظم الأعطال ترجع إلى حقل مفقود أو رأس ناقص أو غياب جلسة ثابتة. يلخّص الجدول التالي الأعراض الأكثر شيوعاً وحلولها المباشرة:

العَرَض السبب الحل
استخراج مفتاح الموقع يُعيد None تحميل CAPTCHA عبر JavaScript انتقل إلى Selenium/Playwright
إرسال النموذج يعيدك إلى صفحة تسجيل الدخول رمز CSRF مفقود استخرج كل المدخلات المخفية عبر extract_form_data()
خطأ 403 بعد POST كشف الروبوت اعتماداً على الرؤوس أضف رؤوس User-Agent وReferer واقعية
رفض الرمز معلمة pageurl خاطئة استخدم عنوان URL الدقيق كما يظهر في المتصفح
فقدان ملفات تعريف الارتباط بين الطلبات عدم استخدام requests.Session() استخدم كائن جلسة دائماً

الأسئلة المتداولة

هل يعمل هذا الأسلوب مع Cloudflare Turnstile وليس reCAPTCHA فقط؟

نعم. المنطق نفسه ينطبق: استخرج data-sitekey من عنصر cf-turnstile، ثم أرسله إلى CaptchaAI باستخدام طريقة turnstile، وضع الرمز الناتج في الحقل cf-turnstile-response قبل إرسال النموذج. الفارق الوحيد عن reCAPTCHA هو اسم الطريقة واسم حقل الرمز.

أيّ باقة من CaptchaAI تناسب الاستخراج بحجم كبير؟

تسعير CaptchaAI قائم على عدد الـ Threads المتزامنة لا على عدد عمليات الحل، مع حلول غير محدودة لكل Thread طوال الشهر. تبدأ باقة BASIC من 15 دولاراً شهرياً بـ 5 Threads، بينما توفّر باقة ADVANCE بسعر 90 دولاراً نحو 50 Thread لأحمال أثقل. اختر عدد الـ Threads بما يوازي عدد الطلبات المتوازية في سكربت الأتمتة لديك.

ماذا أفعل إذا أعاد استخراج مفتاح الموقع القيمة None؟

غالباً يعني ذلك أن اختبار CAPTCHA يُحمَّل عبر JavaScript بعد وصول الـ HTML الأول، فلا يجده Beautiful Soup في المصدر الخام. تحقّق من مصدر الصفحة عبر curl أو خاصية "عرض المصدر"؛ فإن لم يظهر data-sitekey في الـ HTML، فالحل هو الانتقال إلى Selenium أو Playwright لعرض الصفحة أولاً ثم قراءتها.

هل requests مع Beautiful Soup موثوق للاستخراج المتكرر؟

نعم، وهو أسرع بمراحل من أتمتة المتصفح لأنه يتخطى إقلاع المتصفح وتنفيذ JavaScript والعرض. وللحفاظ على الاستقرار: استخدم دائماً requests.Session() لتثبيت ملفات تعريف الارتباط، وأضف رؤوس User-Agent واقعية، وطبّق إعادة المحاولة مع مهلة انتهاء عند فشل الحل.


الخلاصة

يمنحك Beautiful Soup مع CaptchaAI أخفّ مسار وأسرعه لاستخراج بيانات الصفحات المحمية بـ CAPTCHA التي تُقدّم HTML مباشرةً: استخرج مفتاح الموقع بـ Beautiful Soup، وحلّه عبر الـ API، وأرسل النماذج ضمن requests.Session() واحدة تحفظ الحالة. احتفظ بهذا النمط قالباً جاهزاً، وبدّل بين reCAPTCHA وTurnstile بتغيير الطريقة وحقل الرمز فقط.

مقالات ذات صلة

أدلة ذات صلة

التعليقات غير مفعّلة لهذا المقال.