Tutorials

Python Beautiful Soup + CaptchaAI: Umgang mit CAPTCHA-geschützten Seiten

Liefert eine Seite ihr HTML serverseitig aus, brauchen Sie zum Scrapen keinen echten Browser. requests holt die Seite, Beautiful Soup liest die CAPTCHA-Parameter direkt aus dem Markup, und CaptchaAI löst die Abfrage. Dieser Dreiklang ergibt einen schlanken, schnellen Stack für CAPTCHA-geschützte Seiten – ganz ohne Headless-Browser.

Der Ansatz hat allerdings eine klare Grenze – entscheidend ist, wie die Seite ihr HTML ausliefert:

  • Serverseitig gerendert: requests holt fertiges Markup, Beautiful Soup liest die Parameter direkt aus – der schnelle Weg.
  • Per JavaScript nachgeladen: Inhalt oder CAPTCHA entstehen erst im Browser – hier führt an Selenium oder Playwright kein Weg vorbei.

Voraussetzungen

pip install beautifulsoup4 requests lxml

So läuft der Scraping-Workflow ab

  1. Rufen Sie den HTML-Code der Seite mit requests ab
  2. Analysieren Sie mit Beautiful Soup, um CAPTCHA-Parameter zu extrahieren
  3. Senden Sie Parameter zur Lösung an CaptchaAI
  4. Senden Sie das Formular mit dem CAPTCHA-Token über requests
  5. Analysieren Sie die Ergebnisseite mit Beautiful Soup

Jeder Schritt bleibt ein einfacher HTTP-Aufruf – ohne Browser und ohne Rendering.


Beautiful Soup oder Browser-Automatisierung – wann was?

Bevor Sie loslegen, lohnt die Grundsatzentscheidung: Für serverseitig geliefertes Markup ist der requests-Weg schneller und schlanker, für dynamische Seiten führt kein Weg am Browser vorbei.

Szenario BS4 + requests Selenium/Playwright
Serverseitig gerendertes HTML Ja Übertrieben
Per JavaScript gerenderter Inhalt Nein Ja
Komplexes mehrstufiges Formular Eventuell Bevorzugt
Scraping mit hohem Volumen Ja (schneller) Langsamer
Seiten mit JS-Fingerprinting Nein Ja
Einfacher Login + Scraping Ja Nicht nötig

Faustregel: Steckt das Markup schon in der ersten HTTP-Antwort, ist requests + Beautiful Soup die schnellere Wahl. Erst wenn JavaScript den Inhalt nachlädt, lohnt der Browser.


Sitekeys und Formularfelder aus dem HTML auslesen

Bevor CaptchaAI lösen kann, muss Beautiful Soup die richtigen Parameter aus dem Markup ziehen: den Sitekey des jeweiligen CAPTCHA-Typs und alle versteckten Formularfelder, die der Server beim Absenden erwartet.

reCAPTCHA-Sitekey auslesen

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}")

Die drei Methoden greifen der Reihe nach und decken so die gängigen Einbauvarianten ab:

  • data-sitekey am g-recaptcha-Div – der Standardfall.
  • data-sitekey an beliebigen Elementen – für abweichende Einbindungen.
  • render-Parameter im Script-src – typisch für reCAPTCHA v3.

Cloudflare-Turnstile-Sitekey auslesen

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

Cloudflare-Turnstile-Sitekeys beginnen fast immer mit 0x – daran erkennen Sie sie zuverlässig.

Versteckte Formularfelder und CSRF-Tokens auslesen

Lesen Sie immer die versteckten Formularfelder aus – sie enthalten oft CSRF-Tokens und weitere Parameter, die der Server beim Absenden erwartet. Fehlen sie, weist der Server das Formular kommentarlos zurück:

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

Kompletter Ablauf: reCAPTCHA-geschützte Seite scrapen

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)

Entscheidend ist hier requests.Session(): Das Objekt hält Cookies über alle Anfragen hinweg, sodass die nach dem Login gesetzte Sitzung erhalten bleibt. Der Token wandert einfach als Feld g-recaptcha-response in die Payload.

Hinweis: Das gelöste Token ist kurzlebig und läuft nach rund 120 Sekunden ab. Lösen Sie es unmittelbar vor dem Absenden des Formulars – nicht auf Vorrat.


Suchergebnisse hinter einem CAPTCHA abgreifen

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

DSGVO-Hinweis: Sobald Sie Ergebnisseiten mit personenbezogenen Daten abgreifen – dazu zählen bereits IP-Adressen –, gelten Sorgfaltspflichten. Prüfen Sie vorab Ihre Rechtsgrundlage und die AGB der Zielseite und erheben Sie nur Daten, die Sie verarbeiten dürfen.


Bild-CAPTCHAs auslesen und lösen

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

Der Code übergibt die Grafik per Methode base64 an CaptchaAI und deckt dabei beide Einbauvarianten ab:

  • Inline-Base64: direkt aus dem src-Attribut extrahiert.
  • Separate URL: nachgeladen und anschließend Base64-kodiert.

Scraper-Klasse für den Produktivbetrieb

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)

Diese Klasse bündelt Login, CAPTCHA-Erkennung und Lösung in einem wiederverwendbaren Objekt. Für den Produktivbetrieb im deutschsprachigen Raum bewährt sich ein schlankes Setup:

  • Hosting: Ein einzelner Worker auf einem VPS bei Hetzner oder netcup genügt für den Einstieg.
  • Threads: Ein CaptchaAI-Thread reicht für serielle Läufe; höheres Volumen skaliert über mehr parallele Threads.
  • Abrechnung: CaptchaAI rechnet pro Thread ab – nicht pro Lösung –, sodass die Kosten planbar bleiben.

Typische Fehler und ihre Lösung

Die meisten Probleme lassen sich auf drei Ursachen zurückführen: fehlende Formularfelder, unglaubwürdige Header oder eine verlorene Sitzung. Diese Tabelle ordnet Symptom, Ursache und Lösung zu:

Symptom Ursache Lösung
Die Sitekey-Extraktion gibt None zurück CAPTCHA wird per JavaScript geladen Auf Selenium/Playwright wechseln
Nach dem Absenden erscheint erneut die Anmeldeseite CSRF-Token fehlt Alle versteckten Felder mit extract_form_data() auslesen
403 nach dem Formular-POST Bot-Erkennung an den Headern Realistische User-Agent- und Referer-Header setzen
Token wird abgelehnt Falscher pageurl-Parameter Exakt die URL verwenden, die im Browser steht
Cookies gehen zwischen Anfragen verloren Kein requests.Session() im Einsatz Immer ein Session-Objekt verwenden

Häufige Fragen

Kurz beantwortet – die Fragen, die beim Scrapen CAPTCHA-geschützter Seiten mit Beautiful Soup am häufigsten auftauchen.

Welche CAPTCHA-Typen kann ich mit BeautifulSoup und CaptchaAI verarbeiten?

reCAPTCHA v2 und v3, Cloudflare Turnstile und Cloudflare Challenge, GeeTest v3 sowie Bild- und Text-CAPTCHAs – alle über denselben in.php/res.php-Ablauf. hCaptcha und FunCaptcha werden nicht unterstützt; CaptchaFox, Friendly Captcha und Lemin befinden sich in der Beta. Beautiful Soup liest dabei nur die Parameter aus, gelöst wird serverseitig.

Was kostet das Lösen von CAPTCHAs bei hohem Scraping-Volumen?

CaptchaAI rechnet pro Thread ab, nicht pro Lösung – ein Thread ist eine gleichzeitig laufende Abfrage. Der Einstieg BASIC kostet 15 $/Monat mit 5 Threads und unbegrenzten Lösungen; ADVANCE liegt bei 90 $/Monat mit 50 Threads. Ihren Durchsatz bestimmt allein die Zahl paralleler Threads, nicht die Menge gelöster CAPTCHAs.

Warum bekomme ich nach dem Absenden des Formulars einen 403?

Meist fehlen glaubwürdige Header. Setzen Sie einen realistischen User-Agent und – wo passend – einen Referer, und senden Sie alle versteckten Felder inklusive CSRF-Token mit. Prüfen Sie außerdem, ob pageurl exakt der geschützten Seite entspricht.

Muss ich beim Scraping die DSGVO beachten?

Ja, sobald personenbezogene Daten im Spiel sind – und dazu zählen bereits IP-Adressen. Klären Sie vor dem Projekt Ihre Rechtsgrundlage sowie die AGB der Zielseite und erheben Sie nur Daten, die Sie verarbeiten dürfen. Das betrifft Ihre Datenverarbeitung; CaptchaAI übernimmt ausschließlich das Lösen der CAPTCHA-Abfrage.


Fazit

Für serverseitig gerenderte, CAPTCHA-geschützte Seiten ist Python Beautiful Soup + CaptchaAI ein schlanker, schneller Scraping-Stack. Sitekeys mit Beautiful Soup auslesen, per API lösen und über requests.Session() absenden – ohne Browser. Lädt eine Seite ihre Inhalte per JavaScript nach, greifen Sie zu Selenium oder Playwright.

Weiterführende Artikel

Kommentare sind für diesen Artikel deaktiviert.