Scarnox · Blog

Playwright와 Puppeteer에서 PopularCaptcha 풀기 (Node.js)

PopularCaptcha가 나타나는 순간 브라우저 자동화는 멈춥니다. 스크립트로 이미지 격자를 클릭하는 것은 느리고 깨지기 쉽습니다. 풀이 API에서 토큰을 받아 페이지에 넣는 것은 요청 한 번이면 되고, 스크립트의 나머지는 평소처럼 진행됩니다.

이 가이드는 Node.js의 Playwright와 Puppeteer를 다룹니다. 두 곳의 흐름은 같으며, 예제는 Scarnox가 자체 네트워크에서 실행하는 프록시리스 풀이를 사용합니다. Scarnox가 오늘 푸는 유형은 PopularCaptcha입니다. 다른 captcha 유형은 출시 예정이므로, 아래 코드는 PopularCaptcha 전용입니다.

흐름

  1. 페이지에서 sitekey를 읽습니다.
  2. 블로킹 POST 요청 한 번으로 Scarnox에 토큰을 요청합니다.
  3. 토큰을 응답 필드에 넣거나 사이트의 콜백에 넘긴 뒤 폼을 제출합니다.

키 설정

계정을 만들고 무료 체험 크레딧으로 카드 없이 시작하세요. 키는 테스트 코드가 아니라 환경 변수에 보관하세요.

export SCARNOX_KEY="scarnox_YOURAPIKEY"

Node 18 이상에는 fetch가 포함되어 있으므로, API 호출에 추가 패키지가 필요하지 않습니다.

Playwright

import { chromium } from "playwright";

const API_KEY = process.env.SCARNOX_KEY;

async function solve(sitekey, url) {
  const r = await fetch("https://api.scarnox.com/api/tasks/create", {
    method: "POST",
    headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ captcha_type: "PopularCaptchaTaskProxyless", site_key: sitekey, site_url: url }),
  });
  const data = await r.json();
  if (data.errorId !== 0) throw new Error(`${data.errorCode}: ${data.errorDescription}`);
  return data.solution.token;
}

const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto("https://example.com/signup");

const sitekey = await page.getAttribute("[data-sitekey]", "data-sitekey");
const token = await solve(sitekey, page.url());

// 사이트가 읽을 수 있는 두 응답 필드에 모두 토큰을 넣습니다.
await page.evaluate((t) => {
  for (const name of ["h-captcha-response", "g-recaptcha-response"]) {
    document.querySelectorAll(`[name="${name}"]`).forEach((el) => { el.value = t; });
  }
}, token);

await page.fill("#email", "you@example.com");
await page.click("button[type=submit]");

site_url이 page.url()에서 온다는 점에 주목하세요. 위젯이 있는 페이지이며, 풀이기가 필요로 하는 값이 바로 그것입니다. 풀이는 페이지가 로드된 뒤에 실행되므로, sitekey는 이미 DOM에 있습니다.

Puppeteer

Puppeteer는 셀렉터 API만 다릅니다. 위의 solve 함수를 재사용하고, 페이지 호출만 바꾸세요.

const sitekey = await page.$eval("[data-sitekey]", (el) => el.dataset.sitekey);
const token = await solve(sitekey, page.url());

await page.evaluate((t) => {
  document.querySelectorAll('[name="h-captcha-response"],[name="g-recaptcha-response"]')
    .forEach((el) => { el.value = t; });
}, token);

사이트가 콜백을 사용할 때

일부 사이트는 숨겨진 필드를 읽지 않고, 위젯의 JavaScript 콜백을 기다립니다. 위젯의 data-callback 속성에 이름이 적힌 함수를 찾아 토큰과 함께 호출하세요.

await page.evaluate((t) => {
  const name = document.querySelector("[data-sitekey]")?.dataset.callback;
  if (name && typeof window[name] === "function") window[name](t);
}, token);

사이트가 둘 다 사용한다면 필드를 설정하고 콜백도 호출하세요. 둘 다 해도 해가 없습니다.

보이지 않는 위젯

보이지 않는 위젯에는 체크박스가 없습니다. 사이트가 자체 코드에서 시작하며, 보통 방문자가 제출 버튼을 누를 때 시작합니다. 같은 요청을 보내되, 선택 필드인 is_invisible을 true로 설정하세요.

body: JSON.stringify({
  captcha_type: "PopularCaptchaTaskProxyless",
  site_key: sitekey,
  site_url: url,
  is_invisible: true,
}),

보이지 않는 captcha 가이드가 이런 위젯을 알아보는 방법과 토큰을 돌려주는 방법을 설명합니다.

타이밍 팁

내 흐름 테스트하기

더 큰 대상에 적용하기 전에, 예제를 직접 소유한 폼에서 먼저 실행하세요. 세 가지를 확인하세요. 토큰이 받아들여지는지, 폼이 제출되는지, 다음 페이지가 나타나는지. 흐름이 끝까지 작동하면, 실행마다 실제 풀이 한 번으로 CI에서 녹색을 유지하세요.

무료로 체험하기

계정을 만들고 무료 체험 크레딧으로 카드 없이 시작하세요. 전체 흐름을 연결하고 확인하기에 충분합니다. 대량으로 풀 때의 비용은 현재 요금을 확인하세요.

거부된 토큰 디버깅하기

사이트가 토큰을 거부하면, 다른 것을 바꾸기 전에 요청 한 번과 페이지 로드 한 번으로 흐름을 재현하세요. 다음을 순서대로 확인하세요.

실패한 순간의 페이지 HTML을 await page.content()로 저장하세요. 그것을 성공한 실행과 비교하면 차이가 한눈에 보이는 경우가 많습니다. sitekey가 다르거나, 필드가 빠졌거나, 위젯이 다른 모드로 렌더링되었을 수 있습니다.

다음 단계