Web Unlocker API チュートリアル: Python と Node.js、ブロックされた場合とブロックされていない場合

Web Unlocker API の動作する Python と Node.js コード: プレーンなクライアントをブロックするサイトとしないサイトに対する同じリクエスト、アンロッカーが報告する内容の読み方、ブロックが検出されたときにのみアンロッカーの料金を支払うヘルパー。

Web Unlocker API は、ブラウザの TLS フィンガープリントの下で住宅用 IP から HTTP リクエストを再生し、新しい IP で再試行し、サイトがチャレンジで応答した場合は実際のブラウザに切り替えます。この Web Unlocker 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 ではありませんでした。requests がそれよりも良くなる理由はありません。User-Agent 文字列は 1 つのヘッダーです。アンチボットレイヤーは 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 を終了し、ターゲットホストのために独自の証明書を提示する必要があります。クリーンなオプションは 2 つあります。上記のように、このセッションのみの検証をオフにすることです。これは curl -k が行うことです。または、アンロッカー CA を一度ダウンロードして明示的に信頼し、検証をオンに保つことです:

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

その後、False の代わりに verify="qp-unlocker-ca.pem" を渡します。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 とブラウザフィンガープリントで最大 3 回の TLS 試行、JavaScript チャレンジが表示された場合はヘッドレスブラウザ、その後ページまたはラベル付きブロック
アンロッカーを通じた 1 回の 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 に入れます。したがって、1 つのチェックで両方をカバーします:

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 バージョン(web unlocker API in JavaScript)

Node では、fetch は dispatcher を取ります。フォワードプロキシには undici からの ProxyAgent を使用し、同じパッケージから fetch をインポートして、2 つのバージョンを一致させます。ProxyAgent ドキュメント は、ここで使用されるオプションをリストしています: token は Proxy-Authorization 値、requestTls はターゲットへの TLS 設定です。

// 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 秒
books.toscrape.com200200200、1.9 秒
news.ycombinator.com200200200、4.1 秒
en.wikipedia.org(Web scraping)200200200、1.9 秒
github.com(scrapy リポジトリ)200200200、2.0 秒
bbc.com(Technology)302 リダイレクト302 リダイレクト200、1.4 秒

BBC の行はブロックではありません。我々のテストクライアントはリダイレクトを追跡しませんでしたが、302 はサイトが別の URL に送信していることを示しています。requests はデフォルトでリダイレクトを追跡し、ページに着地します。

したがって、正しいパターンは、最初にプレーンを試し、ブロックが検出された場合にのみアンロッカーを使用することです。検出には注意が必要です。我々自身のテストハーネスのマーカースキャンは、Wikipedia と GitHub のページをブロックされたとしてフラグを立てましたが、両方とも 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 を返しました。ステータスコードのみをチェックするスクレイパーは、それをエスカレートすることはありません。

測定された結果: アンロッカーを必要とした 4 つのサイト

2026年9月29日に測定されました。プレーンリクエストは、フランクフルトのデータセンターサーバーからの Chrome User-Agent を使用した curl で、米国の出口を持つ住宅用プロキシを通じて 1 回行われました。Web Unlocker リクエストは、米国の出口を持つ住宅用ティアを使用しました。時間は単一のリクエストのウォールクロックです。

サイトプレーン、データセンターサーバープレーン、住宅用プロキシWeb Unlocker
Indeed(求人検索)403、'Security Check' ページ403、'Enable JavaScript and cookies to continue'200、1 回の試行、2.6 秒
Glassdoor(会社レビュー)403、キャプチャ付きの 'Security' ページ403、'Enable JavaScript and cookies to continue'200、2 回の試行 + ブラウザ、17.2 秒
Walmart(検索)307 リダイレクト、結果ページなし307 リダイレクト、結果ページなし200、1 回の試行、4.8 秒
Best Buy(ラップトップカテゴリ)接続失敗、応答なし接続失敗、応答なし200、1 回の試行、6.3 秒

要するに: Indeed、Walmart、Best Buy はブラウザなしで最初の TLS 試行で通過しました。Glassdoor は、JavaScript チャレンジに応答するためにフルパス、2 回の TLS 試行、そしてヘッドレスブラウザを必要としました。そのため、17.2 秒かかりました。Walmart については、307 がどこに向かっていたかを記録していません。言えることは、どちらのプレーンクライアントも結果ページを受け取らず、アンロッカーが受け取ったということです。

チャレンジ保護されたサイトへの繰り返しリクエストは、セッションを固定すると速くなります。同じ x-qp-session 値を送信するか、プロキシユーザー名に同じ -session-<id> を保持すると、ブラウザによって得られたクリアランスクッキーが再利用されます。2026年9月29日の Glassdoor テストでは、ユーザー名にセッションを固定し、2 つの別々の接続で、最初のリクエストはブラウザを通じて 14.0 秒かかり、2 回目は 1.4 秒で、x-qp-clearance: hit でした。我々の Indeed の求人投稿をスクレイプするためのガイド では、ページを取得した後に何をすべきかをカバーしています。

スクレイパーのための意思決定チャート: まずプレーンリクエストを試し、ステータス、cf-mitigated ヘッダー、またはページタイトルがブロックを示した場合にのみリクエストを web unlocker に送信する
まずプレーン、ブロックが検出された場合にアンロッカー。アンロッカーは移動するすべてのバイトに対して請求されるため、最も安価なリクエストはそれなしで動作するものです。

ブラウザの外でのみ発生する 403 からここに来た場合、curl が 403 を受け取る理由とブラウザが動作する理由 の説明が診断を深く掘り下げます。どの層が実際に必要かをサイトごとに見るには、web unlocker 対 住宅用プロキシ、テスト済み を参照してください。

よくある質問

無料の web unlocker API はありますか?

オープンソースの部分は無料です: requests、curl_cffi はブラウザのような TLS、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 エンドポイントでは、rotation を sticky に設定して sessionId を渡します。スティッキーセッションは 3 分から 1,440 分の間続くことができます。

全体のパターンは 1 文に収まります: プレーンにフェッチし、ステータス、ヘッダー、タイトルでブロックを検出し、アンロッカーを通じてそれらの URL のみを送信し、各ページがどれだけのコストをかけたかを知るために x-qp ヘッダーをログに記録します。住宅用ティアから始め、住宅用範囲を拒否するターゲットにはモバイルティアを保持します。

住宅用出口で Web Unlocker を試してみてください