Web Unlocker API 튜토리얼: Python 및 Node.js, 차단된 사이트 vs 차단되지 않은 사이트

웹 언락커 API를 위한 작동하는 Python 및 Node.js 코드: 일반 클라이언트를 차단하는 사이트와 그렇지 않은 사이트에 대한 동일한 요청, 언락커가 보고하는 내용을 읽는 방법, 차단이 감지될 때만 언락커에 비용을 지불하는 도우미.

웹 언락커 API는 브라우저 TLS 지문 아래에서 주거 IP로부터 HTTP 요청을 재생하며, 사이트가 챌린지로 응답할 때 새로운 IP로 재시도하고 실제 브라우저로 전환합니다. 이 웹 언락커 API 튜토리얼은 차단된 사이트와 그렇지 않은 사이트에서 Python 및 Node.js로 이를 보여줍니다.

아래에 인용된 모든 결과는 2026년 9월 29일에 측정되었습니다: 프랑크푸르트의 데이터센터 서버에서 일반 curl로 가져온 동일한 URL, 미국 출구가 있는 주거 프록시를 통한 일반 curl, 그리고 미국 출구가 있는 QuantumProxies.io Web Unlocker를 통해 가져온 것입니다. 사이트가 언락커를 필요로 하지 않는 경우, 우리는 그렇게 말합니다. 왜냐하면 가장 저렴한 요청은 그것 없이 작동하는 요청이기 때문입니다.

시작하기 전에 필요한 것

두 비밀을 환경 변수에 보관하십시오. 예제는 QP_UNLOCK_USER, QP_UNLOCK_PASSWORD 및 QP_API_KEY를 읽습니다. 프록시 비밀번호에 @ 또는 :와 같은 문자가 포함된 경우, 프록시 URL에 넣기 전에 urllib.parse.quote로 URL 인코딩하십시오.

1단계: 일반 Python requests로 차단 확인

수정할 내용을 알기 위해 실패부터 시작하십시오. 대상은 Indeed의 구인 검색입니다. 프랑크푸르트 데이터센터 서버에서 403과 'Security Check - Indeed.com'이라는 제목의 페이지로 응답했습니다. 일반 주거 프록시를 통해 여전히 403으로 응답했으며, 이번에는 'Enable JavaScript and cookies to continue'라는 본문이 포함되어 있었습니다. 깨끗한 홈 IP로는 충분하지 않았습니다.

import re
import requests

URL = "https://www.indeed.com/jobs?q=data+engineer&l=Austin%2C+TX"
HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
        "(KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36"
    ),
    "Accept-Language": "en-US,en;q=0.9",
}


def title_of(html: str) -> str:
    m = re.search(r"<title[^>]*>(.*?)</title>", html, re.I | re.S)
    return m.group(1).strip() if m else ""


r = requests.get(URL, headers=HEADERS, timeout=30)
print(r.status_code, "|", title_of(r.text))
# Our datacenter run on 29 Sep 2026 saw: 403 | Security Check - Indeed.com

우리의 측정은 Chrome User-Agent를 사용한 curl을 사용했으며, requests가 더 나은 성능을 보일 이유는 없습니다. User-Agent 문자열은 하나의 헤더입니다. 안티봇 레이어는 TLS 핸드셰이크와 HTTP/2 설정도 읽으며, 설정한 User-Agent와 상관없이 여전히 Python을 나타냅니다. 이것이 Python requests에서 헤더만 교체해도 봇 탐지를 통과하지 못하는 이유입니다. 핸드셰이크 측면은 TLS 지문 인식 및 깨끗한 IP가 여전히 차단되는 이유 가이드에 포함되어 있습니다. 403은 서버가 요청을 이해하고 거부했음을 의미합니다 (RFC 9110, 섹션 15.5.4). 이는 URL이나 매개변수가 잘못되었음을 의미하지 않습니다.

2단계: 포워드 프록시를 통한 동일한 호출

포워드 프록시는 기존 코드에 대한 가장 작은 변경 사항입니다: 프록시로 전달하고 다른 것은 이동하지 않습니다. 타겟팅은 사용자 이름에 포함됩니다. -country-us는 출구 국가를 선택하고, -tier-mobile은 모바일 출구로 전환하며, -profile-firefox (또는 chrome, safari, safariios, edge, brave, mobile)는 재시도가 회전하는 대신 하나의 TLS 지문을 고정합니다.

import os
import requests
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

user = os.environ["QP_UNLOCK_USER"] + "-country-us"   # e.g. unlock-XXXXXXXX-country-us
password = os.environ["QP_UNLOCK_PASSWORD"]
PROXY = f"http://{user}:{password}@unlock.quantumproxies.io:9000"

r = requests.get(
    URL,
    headers=HEADERS,
    proxies={"http": PROXY, "https": PROXY},
    verify=False,   # or verify="qp-unlocker-ca.pem", see below
    timeout=120,
)
print(r.status_code, "|", title_of(r.text))
print("attempts:", r.headers.get("x-qp-unlocker-attempts"),
      "rendered:", r.headers.get("x-qp-unlocker-rendered"))

9월 29일에 이 요청은 첫 번째 TLS 시도에서 브라우저를 시작하지 않고 2.6초 만에 'Now Hiring: 2,000 Data Engineer Jobs in Austin, TX | Indeed'라는 제목의 실제 결과 페이지를 200으로 반환했습니다.

왜 verify=False인가요? HTTPS 요청을 다시 작성하려면 언락커가 클라이언트의 TLS를 종료하고 대상 호스트에 대해 자체 인증서를 제시해야 합니다. 두 가지 깨끗한 옵션이 있습니다. 위와 같이 이 세션에 대해서만 확인을 끄십시오. 이는 curl -k가 하는 것입니다. 또는 언락커 CA를 한 번 다운로드하여 명시적으로 신뢰하여 확인이 계속되도록 하십시오.

curl -H "Authorization: Bearer $QP_API_KEY" \
  -o qp-unlocker-ca.pem https://app.quantumproxies.io/api/v1/scraper/unlock/ca

그런 다음 verify="qp-unlocker-ca.pem"을 False 대신 전달하십시오. requests 고급 사용법 문서는 proxies 인수와 사용자 정의 CA 번들을 모두 다룹니다. 둘 다 원하지 않는 경우, 아래의 REST 엔드포인트는 인증서가 전혀 필요하지 않습니다.

3단계: 프록시 프로토콜이 없는 REST 엔드포인트

REST 엔드포인트는 일반 HTTPS 호출 뒤에서 동일한 엔진을 실행합니다. 수행할 요청을 설명(url, method, headers, body)하고 원본의 응답을 JSON으로 받습니다. 유일한 필수 필드는 url입니다.

import os
import requests

API_KEY = os.environ["QP_API_KEY"]

resp = requests.post(
    "https://app.quantumproxies.io/api/v1/scraper/unlock",
    headers={"Authorization": f"Bearer {API_KEY}"},
    json={
        "url": URL,
        "country": "us",
        # "tier": "mobile",        # mobile exits, billed on the mobile balance
        # "failOnBlock": True,     # HTTP 502 instead of 200 when every tier was blocked
    },
    timeout=130,
)
resp.raise_for_status()           # 402 = no unlocker GB left, 400 = bad input
data = resp.json()["payload"]

print(data["status"], "blocked:", data["blocked"])
print("attempts:", data["attempts"], "rendered:", data["rendered"],
      "escalated:", data["escalated"], "clearance:", data.get("clearance"))
if data["blocked"]:
    print("block class:", data.get("blockClass"), "vendor:", data.get("vendor"))
html = data.get("body") or ""     # text form; bodyBase64 is always present too

응답 봉투는 { type, message, payload }입니다. payload 내부에는 status (원본의 상태 코드), headers, body 및 bodyBase64, finalUrl, 지문 profile, attempts, rendered, escalated, blocked 및 요청을 중단한 경우 blockClass 및 vendor가 포함됩니다. usage 객체는 호출에 대해 차감된 바이트를 보고합니다.

설계해야 할 한 가지 동작: 기본적으로 모든 시도 후에도 차단된 상태로 남아 있는 요청은 여전히 HTTP 200으로 API에서 반환되며, blocked: true와 body에 차단 페이지가 검사용으로 포함됩니다. 이는 레이블이 붙어 있으며 위장되지 않았습니다. 파이프라인이 모든 200을 성공으로 처리하는 경우, failOnBlock: true를 설정하면 동일한 진단을 포함한 502를 대신 받습니다. 알면 좋은 다른 필드: POST의 경우 method 및 body, 브라우저에서 시작하기 위한 render: "html", waitForSelector, returnCookies.

하나의 Web Unlocker 요청 흐름: 서로 다른 출구 IP와 브라우저 지문에서 최대 세 번의 TLS 시도, JavaScript 챌린지가 나타나면 헤드리스 브라우저, 그런 다음 페이지 또는 레이블이 붙은 차단
언락커를 통한 하나의 GET: TLS 시도 먼저, JavaScript 챌린지가 나타날 때만 실제 브라우저, 그리고 어느 쪽이든 레이블이 붙은 결과.

Web Unlocker가 차단된 요청을 처리하는 방법 보기

4단계: x-qp 헤더 읽기 및 보고된 차단 처리

포워드 프록시를 통해 언락커는 응답 자체에 대해 보고합니다. 모든 호출에서 기록할 가치가 있는 헤더는 다음과 같습니다:

헤더알려주는 것
x-qp-unlocker-attempts요청이 걸린 시도 횟수, 재시도 포함
x-qp-unlocker-rendered / x-qp-unlocker-escalated헤드리스 브라우저가 페이지를 가져왔을 때 / 요청이 TLS 계층에서 상승했을 때 1로 설정
x-qp-unlocker-profile반환된 시도가 사용한 브라우저 지문
x-qp-clearance이 호출에서 브라우저 클리어런스 캐시에서 무슨 일이 있었는지: minted, hit 또는 miss
x-qp-session / x-qp-country실제로 사용된 출구 세션 및 국가
x-qp-unlocker-blocked모든 시도 후에도 페이지가 여전히 차단된 경우에만 존재
x-qp-block-class / x-qp-vendor어떤 종류의 벽이었는지, 인식된 경우 어떤 안티봇 벤더인지

여전히 차단된 페이지는 조용한 200으로 도착하지 않습니다. 원본이 자체 오류(403, 429, 503)로 거부한 경우, 해당 상태는 변경되지 않고 x-qp-unlocker-blocked가 설정됩니다. 원본이 2xx로 챌린지 페이지를 제공한 경우, 프록시는 이를 502로 변환하고 원본의 코드를 x-qp-origin-status에 넣습니다. 따라서 하나의 확인으로 두 가지를 모두 커버합니다:

class Blocked(Exception):
    pass


s = requests.Session()
s.proxies = {"http": PROXY, "https": PROXY}
s.verify = False  # or "qp-unlocker-ca.pem"

NEXT_STEP = {
    "js_challenge":  "a browser could not clear it; retry later in a pinned session",
    "captcha":       "interactive captcha; decide in your own code",
    "access_denied": "the site denied access; retry with a fresh session or test tier-mobile",
    "rate_limited":  "the site is rate limiting; slow down and retry later",
    "fingerprint":   "the client fingerprint was refused; retry with another profile",
    "geo":           "the content depends on location; set the country explicitly",
    "timeout":       "the target was too slow; raise x-qp-timeout or retry",
}


def unlock_get(url: str, session_id: str | None = None) -> requests.Response:
    headers = {"x-qp-session": session_id} if session_id else {}
    r = s.get(url, headers=headers, timeout=120)
    if "x-qp-unlocker-blocked" in r.headers:
        cls = r.headers.get("x-qp-block-class", "unknown")
        raise Blocked(
            f"{url}: HTTP {r.status_code}, class={cls}, "
            f"vendor={r.headers.get('x-qp-vendor')}, next: {NEXT_STEP.get(cls, 'inspect the body')}"
        )
    return r

5단계: Node.js 버전 (JavaScript의 web unlocker API)

Node에서는 fetch가 dispatcher를 사용합니다. 포워드 프록시를 위해 undici의 ProxyAgent를 사용하고, 두 버전이 일치하도록 동일한 패키지에서 fetch를 가져옵니다. ProxyAgent 문서는 여기에서 사용된 옵션을 나열합니다: Proxy-Authorization 값에 대한 token과 대상에 대한 TLS 설정에 대한 requestTls.

// unlock.mjs  (npm i undici)
import { readFileSync } from "node:fs";
import { fetch, ProxyAgent } from "undici";

const user = process.env.QP_UNLOCK_USER + "-country-us";
const pass = process.env.QP_UNLOCK_PASSWORD;

const dispatcher = new ProxyAgent({
  uri: "http://unlock.quantumproxies.io:9000",
  token: "Basic " + Buffer.from(user + ":" + pass).toString("base64"),
  requestTls: { rejectUnauthorized: false },
  // verified instead: requestTls: { ca: readFileSync("qp-unlocker-ca.pem") },
});

const url = "https://www.indeed.com/jobs?q=data+engineer&l=Austin%2C+TX";
const res = await fetch(url, { dispatcher, headers: { "x-qp-session": "indeed-001" } });

if (res.headers.get("x-qp-unlocker-blocked")) {
  console.error("blocked:", res.status, res.headers.get("x-qp-block-class"), res.headers.get("x-qp-vendor"));
} else {
  const html = await res.text();
  console.log(res.status, res.headers.get("x-qp-unlocker-attempts"), html.length);
}

REST 엔드포인트는 Node 18+에 내장된 fetch 외에는 필요하지 않습니다:

const resp = await fetch("https://app.quantumproxies.io/api/v1/scraper/unlock", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + process.env.QP_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ url: "https://www.indeed.com/jobs?q=data+engineer&l=Austin%2C+TX", country: "us" }),
});
const { payload } = await resp.json();
console.log(payload.status, payload.blocked, payload.blockClass ?? "-", payload.attempts);

axios에서는 프록시 뒤의 HTTPS 대상을 위한 일반 패턴이 https-proxy-agent에서 에이전트를 httpsAgent로 전달하고 proxy: false를 설정하거나 위의 REST 엔드포인트를 호출하여 프록시 프로토콜을 완전히 건너뛰는 것입니다.

6단계: 언락커가 필요하지 않을 때

언락커는 이동하는 모든 바이트에 대해 GB당 청구되며, 재시도 및 브라우저 페이지 로드도 포함됩니다. 일반 요청이 잘 가져오는 페이지를 보내는 것은 잔액을 낭비하는 것입니다. 우리의 실행에서, 다음 사이트는 아무런 도움 없이 잘 돌아왔습니다:

사이트일반, 데이터센터 서버일반, 주거 프록시Web Unlocker
docs.python.org (urllib.request 페이지)200200200, 1.6 s
books.toscrape.com200200200, 1.9 s
news.ycombinator.com200200200, 4.1 s
en.wikipedia.org (웹 스크래핑)200200200, 1.9 s
github.com (scrapy 저장소)200200200, 2.0 s
bbc.com (기술)302 리다이렉트302 리다이렉트200, 1.4 s

BBC 행은 차단이 아닙니다. 우리의 테스트 클라이언트는 리다이렉트를 따르지 않았으며, 302는 사이트가 다른 URL로 보내는 것입니다. requests는 기본적으로 리다이렉트를 따르며 페이지에 도착합니다.

따라서 올바른 패턴은 먼저 일반적으로 시도하고, 차단이 감지되면 언락커를 통해 요청을 보내는 것입니다. 감지는 주의가 필요합니다. 우리의 자체 테스트 하네스에서 마커 스캔은 Wikipedia와 GitHub 페이지를 차단된 것으로 표시했는데, 그들의 본문에 'captcha'라는 단어가 포함되어 있기 때문입니다. 그러나 둘 다 200으로 실제 페이지를 반환했습니다. 상태 코드, Cloudflare의 cf-mitigated 헤더 및 페이지 제목을 확인하십시오, 전체 본문이 아닙니다:

from urllib.parse import urlsplit

BLOCK_STATUSES = {403, 429, 503}
BLOCK_TITLES = (
    "just a moment",                        # Cloudflare challenge
    "attention required",                   # Cloudflare block page
    "access denied",                        # Akamai and others
    "security check",
    "access to this page has been denied",  # PerimeterX
)
needs_unlocker: set[str] = set()            # domains that blocked plain requests


def looks_blocked(r: requests.Response) -> bool:
    if r.status_code in BLOCK_STATUSES:
        return True
    if r.headers.get("cf-mitigated") == "challenge":
        return True
    return any(t in title_of(r.text).lower() for t in BLOCK_TITLES)


def fetch(url: str, session_id: str | None = None) -> tuple[requests.Response, str]:
    host = urlsplit(url).hostname or ""
    if host not in needs_unlocker:
        try:
            r = requests.get(url, headers=HEADERS, timeout=20)
            if not looks_blocked(r):
                return r, "plain"
        except requests.RequestException:
            pass  # a reset or a timeout counts as a block too
        needs_unlocker.add(host)
    return unlock_get(url, session_id), "unlocker"   # from step 4; raises Blocked


for u in ("https://docs.python.org/3/library/urllib.request.html", URL):
    resp, route = fetch(u)
    print(route, resp.status_code, title_of(resp.text)[:60])

except 분기는 중요합니다. Best Buy의 노트북 카테고리는 우리의 일반 클라이언트에게 HTTP 응답을 전혀 주지 않았습니다: 데이터센터 서버와 주거 프록시 모두에서 연결이 실패했습니다. 언락커를 통해 동일한 URL은 첫 번째 시도에서 6.3초 만에 200을 반환했습니다. 상태 코드만 확인하는 스크래퍼는 절대 그 하나를 에스컬레이트하지 않을 것입니다.

측정된 결과: 언락커가 필요한 네 사이트

2026년 9월 29일에 측정되었습니다. 일반 요청은 프랑크푸르트 데이터센터 서버에서 Chrome User-Agent로 한 번, 미국 출구가 있는 주거 프록시를 통해 한 번 curl로 수행되었습니다. Web Unlocker 요청은 미국 출구가 있는 주거 계층을 사용했습니다. 시간은 단일 요청에 대한 벽 시계 시간입니다.

사이트일반, 데이터센터 서버일반, 주거 프록시Web Unlocker
Indeed (구인 검색)403, 'Security Check' 페이지403, 'Enable JavaScript and cookies to continue'200, 1 시도, 2.6 s
Glassdoor (회사 리뷰)403, 'Security' 페이지와 캡차403, 'Enable JavaScript and cookies to continue'200, 2 시도 + 브라우저, 17.2 s
Walmart (검색)307 리다이렉트, 결과 페이지 없음307 리다이렉트, 결과 페이지 없음200, 1 시도, 4.8 s
Best Buy (노트북 카테고리)연결 실패, 응답 없음연결 실패, 응답 없음200, 1 시도, 6.3 s

요약: Indeed, Walmart 및 Best Buy는 브라우저 없이 첫 번째 TLS 시도에서 통과했습니다. Glassdoor는 JavaScript 챌린지에 응답하기 위해 전체 경로, 두 번의 TLS 시도 및 헤드리스 브라우저가 필요했기 때문에 17.2초가 걸렸습니다. Walmart의 경우 307이 어디로 향했는지 기록하지 않았습니다. 우리가 말할 수 있는 것은 일반 클라이언트가 결과 페이지를 받지 못했으며 언락커는 받았다는 것입니다.

챌린지 보호 사이트에 대한 반복 요청은 세션을 고정할 때 더 빨라집니다. 동일한 x-qp-session 값을 보내거나 프록시 사용자 이름에 동일한 -session-<id>를 유지하면 브라우저가 획득한 클리어런스 쿠키가 재사용됩니다. 2026년 9월 29일 Glassdoor 테스트에서, 사용자 이름에 세션이 고정되고 두 개의 별도 연결이 있는 경우, 첫 번째 요청은 브라우저를 통해 14.0초가 걸렸고 두 번째 요청은 1.4초가 걸렸으며, x-qp-clearance: hit가 있었습니다. 우리의 Indeed 구인 공고 스크래핑 가이드는 페이지를 얻은 후 무엇을 해야 하는지 다룹니다.

스크래퍼를 위한 결정 차트: 먼저 일반 요청을 시도하고, 상태, cf-mitigated 헤더 또는 페이지 제목이 차단을 나타낼 때만 웹 언락커를 통해 요청을 보냅니다.
먼저 일반적으로, 차단이 감지되면 언락커를 사용합니다. 언락커는 이동하는 모든 바이트에 대해 청구되므로 가장 저렴한 요청은 그것 없이 작동하는 요청입니다.

브라우저 외부에서만 발생하는 403에서 여기로 도착한 경우, curl이 403을 받고 브라우저가 작동하는 이유에 대한 설명이 진단을 더 깊이 파고듭니다. 실제로 필요한 계층을 사이트별로 살펴보려면 웹 언락커 vs 주거 프록시, 테스트됨을 참조하십시오.

자주 묻는 질문

무료 웹 언락커 API가 있나요?

오픈 소스 구성 요소는 무료입니다: requests, 브라우저와 유사한 TLS를 위한 curl_cffi, 실제 브라우저를 위한 Playwright. 잘 실행하는 것이 작업입니다. 왜냐하면 깨끗한 출구 IP와 JavaScript 챌린지를 위한 브라우저도 필요하기 때문입니다. QuantumProxies.io에서는 계정을 생성하는 것이 무료이며, 언락커 트래픽은 대시보드에서 충전하는 선불 잔액에서 GB당 청구됩니다.

Python으로 Glassdoor 또는 Zillow를 스크래핑할 수 있나요?

2026년 9월 29일 테스트에서, Glassdoor 리뷰 페이지에 대한 일반 요청은 데이터센터 서버와 주거 프록시 모두에서 403을 받았고, 언락커는 브라우저 에스컬레이션 후 페이지를 반환했습니다. Zillow는 데이터센터 서버를 차단했지만 일반 주거 프록시를 통해 페이지를 제공했습니다. 데이터를 수집하기 전에 각 사이트의 약관을 확인하십시오.

웹 스크래핑이 감지될 수 있나요?

예. 사이트는 IP의 네트워크 유형과 기록, TLS 핸드셰이크, HTTP/2 설정, 헤더 순서 및 챌린지 페이지에서 JavaScript가 브라우저에 대해 보고하는 내용을 평가합니다. 일반 Python 요청은 여러 번에 걸쳐 실패합니다. 이것이 User-Agent만 변경해도 거의 도움이 되지 않는 이유이며, 언락커가 IP, 지문 및 필요할 때 전체 클라이언트를 변경하는 이유입니다.

포워드 프록시를 사용하려면 인증서를 설치해야 하나요?

클래식 포워드 프록시를 통한 HTTPS의 경우에만, 언락커가 요청을 다시 작성하기 위해 TLS를 종료하기 때문입니다. 해당 세션에 대해 확인을 건너뛰거나(Python에서 verify=False, curl에서 -k) API 키로 /api/v1/scraper/unlock/ca에서 CA를 다운로드하여 신뢰하십시오. REST 엔드포인트는 인증서가 전혀 필요하지 않습니다.

여러 요청에 동일한 IP를 유지하려면 어떻게 해야 하나요?

포워드 프록시를 통해 각 요청에 동일한 x-qp-session 헤더 값을 보내거나 프록시 사용자 이름에 동일한 -session-<id>를 넣으십시오. 해당 세션을 공유하는 호출은 동일한 출구에서 나가며, 브라우저가 획득한 클리어런스 쿠키가 재사용됩니다. REST 엔드포인트에서는 회전을 고정으로 설정하여 sessionId를 전달하십시오. 고정 세션은 3분에서 1,440분까지 지속될 수 있습니다.

전체 패턴은 한 문장으로 맞춥니다: 일반적으로 가져오고, 상태, 헤더 및 제목으로 차단을 감지하고, 언락커를 통해서만 해당 URL을 보내고, 각 페이지가 비용이 얼마나 들었는지 알 수 있도록 x-qp 헤더를 기록합니다. 주거 계층으로 시작하고, 주거 범위도 거부하는 대상을 위해 모바일 계층을 유지하십시오.

주거 출구에서 Web Unlocker를 시도해 보세요