Crawlee에서 CapSolver로 reCAPTCHA와 Turnstile 우회하는 방법

발행: (2025년 12월 24일 오후 05:47 GMT+9)
11 min read
원문: Dev.to

Source: Dev.to

위에 제공된 소스 링크 외에 번역할 텍스트가 포함되어 있지 않습니다. 번역을 원하는 본문을 제공해 주시면 한국어로 번역해 드리겠습니다.

TL;DR

현대적인 웹 스크래핑을 Crawlee로 수행할 때, 공격적인 CAPTCHA 챌린지 때문에 작업이 중단되는 경우가 많습니다. CapSolver를 통합하면 reCAPTCHA, Turnstile 및 기타 안티‑봇 메커니즘을 프로그래밍 방식으로 우회할 수 있어 스크래핑 워크플로를 안정적이고 완전 자동화된 상태로 유지할 수 있습니다.

Crawlee와 같은 라이브러리를 사용해 견고한 웹 크롤러를 개발할 때는 CAPTCHA(Completely Automated Public Turing test to tell Computers and Humans Apart)를 마주치는 것이 불가피합니다. Google의 reCAPTCHA와 Cloudflare의 Turnstile과 같은 공격적인 봇 방지 서비스는 자동 접근을 차단하도록 설계되어 있어, 가장 정교한 Playwright나 Puppeteer 크롤러조차도 작업이 중단될 수 있습니다.

이 가이드는 CapSolverCrawlee와 통합하여 이러한 일반적인 CAPTCHA 유형을 자동으로 감지하고 우회하는 실용적이고 코드 중심의 접근 방식을 제공합니다. 해결 토큰을 페이지 컨텍스트에 직접 주입함으로써, 마치 사람이 챌린지를 완료한 것처럼 크롤러가 계속 진행할 수 있게 합니다.

Crawlee는 Node.js용 강력한 오픈‑소스 웹 스크래핑 및 브라우저 자동화 라이브러리입니다. 인간 행동을 모방하고 기본적인 봇 탐지를 회피할 수 있는 신뢰성 높은 프로덕션‑레디 크롤러를 만들도록 설계되었습니다.

기능

기능설명
통합 API빠른 HTTP 기반 크롤링(Cheerio)과 전체 브라우저 자동화(Playwright/Puppeteer)를 모두 지원하는 단일 인터페이스.
안티봇 스텔스자동 브라우저 지문 생성 및 세션 관리를 위한 내장 기능으로 인간처럼 보이게 합니다.
스마트 큐폭넓은 탐색 또는 깊이 우선 크롤링을 위한 지속적인 요청 큐 관리.
프록시 회전IP 회전 및 차단 회피를 위한 프록시 제공업체와의 원활한 통합.

Crawlee의 강점은 복잡한 탐색을 처리할 수 있는 능력에 있지만, 강력한 CAPTCHA 장벽에 부딪히면 외부 서비스가 필요합니다.

CapSolver는 AI를 사용하여 다양한 챌린지를 빠르고 정확하게 해결하는 선도적인 CAPTCHA 우회 서비스입니다. 간단한 REST API를 제공하여 Crawlee와 같은 자동화 워크플로에 통합하기에 이상적입니다.

CapSolver는 다양한 챌린지를 지원합니다

  • reCAPTCHA v2 (체크박스 및 인비저블)
  • reCAPTCHA v3 (점수 기반)
  • Cloudflare Turnstile
  • AWS WAF

3️⃣ 핵심 통합: CapSolver 서비스 설정

1️⃣ 필요한 패키지 설치

npm install crawlee playwright axios
# or
yarn add crawlee playwright axios

2️⃣ 서비스 클래스 생성 (capsolver-service.ts)

// capsolver-service.ts
import axios from 'axios';

const CAPSOLVER_API_KEY = 'YOUR_CAPSOLVER_API_KEY';

interface TaskResult {
    status: string;
    solution?: {
        gRecaptchaResponse?: string;
        token?: string;
    };
    errorDescription?: string;
}

class CapSolverService {
    private apiKey: string;
    private baseUrl = 'https://api.capsolver.com';

    constructor(apiKey: string = CAPSOLVER_API_KEY) {
        this.apiKey = apiKey;
    }

    /** 1️⃣ Create a new CAPTCHA task and return the task ID */
    async createTask(taskData: object): Promise {
        const response = await axios.post(`${this.baseUrl}/createTask`, {
            clientKey: this.apiKey,
            task: taskData,
        });

        if (response.data.errorId !== 0) {
            throw new Error(`CapSolver error: ${response.data.errorDescription}`);
        }

        return response.data.taskId;
    }

    /** 2️⃣ Poll the API until the task is ready or fails */
    async getTaskResult(taskId: string, maxAttempts = 60): Promise {
        for (let i = 0; i  {
            return new Promise((resolve) => setTimeout(resolve, ms));
        }

        /** 3️⃣ Bypass reCAPTCHA v2 */
        async bypassReCaptchaV2(websiteUrl: string, websiteKey: string): Promise {
            const taskId = await this.createTask({
                type: 'ReCaptchaV2TaskProxyLess',
                websiteURL: websiteUrl,
                websiteKey,
            });

            const result = await this.getTaskResult(taskId);
            return result.solution?.gRecaptchaResponse ?? '';
        }

        /** 4️⃣ Bypass Cloudflare Turnstile */
        async bypassTurnstile(websiteUrl: string, websiteKey: string): Promise {
            const taskId = await this.createTask({
                type: 'AntiTurnstileTaskProxyLess',
                websiteURL: websiteUrl,
                websiteKey,
            });

            const result = await this.getTaskResult(taskId);
            return result.solution?.token ?? '';
        }

        /** 5️⃣ Bypass reCAPTCHA v3 */
        async bypassReCaptchaV3(
            websiteUrl: string,
            websiteKey: string,
            pageAction = 'submit'
        ): Promise {
            const taskId = await this.createTask({
                type: 'ReCaptchaV3TaskProxyLess',
                websiteURL: websiteUrl,
                websiteKey,
                pageAction,
            });

            const result = await this.getTaskResult(taskId);
            return result.solution?.gRecaptchaResponse ?? '';
        }
    }

export const capSolver = new CapSolverService();

핵심 로직 개요

  1. 페이지에서 CAPTCHA 요소를 감지합니다.
  2. data-sitekey와 페이지 URL을 추출합니다.
  3. 적절한 capSolver.bypass… 메서드를 호출하여 토큰을 얻습니다.
  4. 반환된 토큰을 숨겨진 폼 필드에 주입합니다.
  5. 스크래핑 과정을 계속하기 위해 폼을 제출합니다.

참고:

  • reCAPTCHA v2는 일반적으로 체크박스로 표시됩니다. 토큰은 숨겨진 “에 주입되어야 합니다.
  • Cloudflare Turnstile는 다른 숨겨진 입력 필드(cf-turnstile-response)를 사용합니다.

reCAPTCHA v2 예제

import { PlaywrightCrawler, Dataset } from 'crawlee';
import { capSolver } from './capsolver-service';

const crawler = new PlaywrightCrawler({
    async requestHandler({ page, request, log }) {
        log.info(`Processing ${request.url}`);

        // 1️⃣ Detect reCAPTCHA v2 element
        const hasRecaptcha = await page.$('.g-recaptcha');

        if (hasRecaptcha) {
            log.info('reCAPTCHA v2 detected, initiating bypass...');

            // 2️⃣ Extract the site key
            const siteKey = await page.$eval(
                '.g-recaptcha',
                el => el.getAttribute('data-sitekey')
            );

            if (siteKey) {
                // 3️⃣ Get the bypass token from CapSolver
                const token = await capSolver.bypassReCaptchaV2(request.url, siteKey);

                // 4️⃣ Inject the token into the hidden textarea
                await page.$eval(
                    '#g-recaptcha-response',
                    (el: HTMLTextAreaElement, t: string) => {
                        // Optional: make it visible for debugging
                        el.style.display = 'block';
                        el.value = t;
                    },
                    token
                );

                // 5️⃣ Submit the form
                await page.click('button[type="submit"]');
                await page.waitForLoadState('networkidle');

                log.info('reCAPTCHA v2 successfully bypassed!');
            }
        }

        // Continue with data extraction…
        const title = await page.title();
        await Dataset.pushData({ title, url: request.url });
    },
});

await crawler.run(['https://example.com/protected-page']);

Cloudflare Turnstile 예제

import { PlaywrightCrawler, Dataset } from 'crawlee';
import { capSolver } from './capsolver-service';

const crawler = new PlaywrightCrawler({
    async requestHandler({ page, request, log }) {
        log.info(`Processing ${request.url}`);

        // 1️⃣ Detect Turnstile widget
        const hasTurnstile = await page.$('.cf-turnstile');

        if (hasTurnstile) {
            log.info('Cloudflare Turnstile detected, initiating bypass...');

            // 2️⃣ Extract the site key
            const siteKey = await page.$eval(
                '.cf-turnstile',
                el => el.getAttribute('data-sitekey')
            );

            if (siteKey) {
                // 3️⃣ Get the bypass token
                const token = await capSolver.bypassTurnstile(request.url, siteKey);

                // 4️⃣ Inject token into the hidden input
                await page.$eval(
                    'input[name="cf-turnstile-response"]',
                    (el: HTMLInputElement, t: string) => {
                        el.value = t;
                    },
                    token
                );

                // 5️⃣ Submit the form
                await page.click('button[type="submit"]');
                await page.waitForLoadState('networkidle');

                log.info('Turnstile successfully bypassed!');
            }
        }

        // Continue with data extraction…
        const title = await page.title();
        await Dataset.pushData({ title, url: request.url });
    },
});

await crawler.run(['https://example.com/turnstile-protected']);

동적 CAPTCHA 감지 및 우회 (프로덕션‑급)

// ------------------------------------------------
// Types & Helper
// ------------------------------------------------
interface CaptchaInfo {
    type: 'recaptcha-v2' | 'recaptcha-v3' | 'turnstile' | 'none';
    siteKey: string | null;
}

// ------------------------------------------------
// Detection
// ------------------------------------------------
async function detectCaptcha(page: any): Promise {
    // reCAPTCHA v2
    const recaptchaV2 = await page.$('.g-recaptcha');
    if (recaptchaV2) {
        const siteKey = await page.$eval(
            '.g-recaptcha',
            (el: Element) => el.getAttribute('data-sitekey')
        );
        return { type: 'recaptcha-v2', siteKey };
    }

    // Turnstile
    const turnstile = await page.$('.cf-turnstile');
    if (turnstile) {
        const siteKey = await page.$eval(
            '.cf-turnstile',
            (el: Element) => el.getAttribute('data-sitekey')
        );
        return { type: 'turnstile', siteKey };
    }

    // reCAPTCHA v3 (identified by script tag)
    const recaptchaV3Script = await page.$('script[src*="recaptcha/api.js?render="]');
    if (recaptchaV3Script) {
        const scriptSrc = await recaptchaV3Script.getAttribute('src') ?? '';
        const match = scriptSrc.match(/render=([^&]+)/);
        const siteKey = match ? match[1] : null;
        return { type: 'recaptcha-v3', siteKey };
    }

    // No known CAPTCHA found
    return { type: 'none', siteKey: null };
}

// ------------------------------------------------
// Bypass & Injection
// ------------------------------------------------
async function bypassAndInject(
    page: any,
    url: string,
    captchaInfo: CaptchaInfo
): Promise {
    if (!captchaInfo.siteKey || captchaInfo.type === 'none') return;

    let token: string;

    switch (captchaInfo.type) {
        case 'recaptcha-v2':
            token = await capSolver.bypassReCaptchaV2(url, captchaInfo.siteKey);
            await page.$eval(
                '#g-recaptcha-response',
                (el: HTMLTextAreaElement, t: string) => {
                    el.style.display = 'block';
                    el.value = t;
                },
                token
            );
            break;

        case 'recaptcha-v3':
            token = await capSolver.bypassReCaptchaV3(url, captchaInfo.siteKey);
            await page.$eval(
                'input[name="g-recaptcha-response"]',
                (el: HTMLInputElement, t: string) => {
                    el.value = t;
                },
                token
            );
            break;

        case 'turnstile':
            token = await capSolver.bypassTurnstile(url, captchaInfo.siteKey);
            await page.$eval(
                'input[name="cf-turnstile-response"]',
                (el: HTMLInputElement, t: string) => {
                    el.value = t;
                },
                token
            );
            break;
    }

    // Submit the form (generic selector – adjust if needed)
    await page.click('button[type="submit"]');
    await page.waitForLoadState('networkidle');
}

크롤러에서 헬퍼 사용하기

import { PlaywrightCrawler, Dataset } from 'crawlee';
import { capSolver } from './capsolver-service';

const crawler = new PlaywrightCrawler({
    async requestHandler({ page, request, log }) {
        log.info(`Processing ${request.url}`);

        // Detect any supported CAPTCHA
        const captchaInfo = await detectCaptcha(page);
        if (captchaInfo.type !== 'none') {
            log.info(`${captchaInfo.type} detected, bypassing...`);
            await bypassAndInject(page, request.url, captchaInfo);
            log.info(`${captchaInfo.type} successfully bypassed!`);
        }

        // Continue wi

일반 데이터 추출

        const title = await page.title();
        await Dataset.pushData({ title, url: request.url });
    },
});

await crawler.run(['https://example.com/mixed-protected']);

Production‑Ready 크롤러를 위한 핵심 포인트

  • 오류 처리try/catch 로 우회 호출을 감싸고 재시도를 구현합니다.
  • 세션 관리 – 인증된 세션을 재사용하여 반복적인 챌린지를 방지합니다.
  • 동적 감지detectCaptcha 헬퍼를 사용하면 최소한의 코드 변경으로 새로운 CAPTCHA 유형을 지원할 수 있습니다.
  • 로그 및 모니터링 – 감지된 유형, 성공/실패 등을 상세히 기록하여 디버깅을 용이하게 합니다.

이러한 패턴을 적용하면 reCAPTCHA v2, reCAPTCHA v3, Cloudflare Turnstile을 견고하게 처리하고, 새로운 챌린지가 등장할 때도 손쉽게 확장할 수 있습니다.

CapSolver와 Crawlee를 이용한 CAPTCHA 우회

아래는 CapSolverCrawlee‑ 기반 스크레이퍼(Playwright 사용)와 통합하는 방법에 대한 간결한 가이드입니다. 코드 스니펫은 바로 복사‑붙여넣기 할 수 있도록 준비되어 있으며, 설명 텍스트는 원래 정보를 유지하면서 가독성을 높이기 위해 재구성되었습니다.

1. Turnstile 토큰 주입

// ... inside your request handler
case 'turnstile':
    // Get the token from CapSolver
    const token = await capSolver.bypassTurnstile(url, captchaInfo.siteKey);

    // Inject the token into the hidden input field
    await page.$eval(
        'input[name="cf-turnstile-response"]',
        (el: HTMLInputElement, t: string) => { el.value = t; },
        token
    );
    break;
}

// Submit the form after the token has been injected
const submitBtn = await page.$('button[type="submit"], input[type="submit"]');
if (submitBtn) {
    await submitBtn.click
Back to Blog

관련 글

더 보기 »

정적 코드 리뷰만 의존할 때의 비용

정적 코드 리뷰란 무엇인가? 정적 코드 리뷰는 코드를 실행하지 않고 소스 코드를 분석하는 과정이다. 목표는 소스 코드를 검사하여 문제를 식별하는 것이다.