Scarnox · Blog

captcha解決を高速化:パイプラインの遅延を減らす方法

速いcaptchaソルバーとは、適切なタイミングで、並列に、無駄なリクエストをせずに呼び出せるソルバーのことです。Scarnoxの解決呼び出しは、トークンの準備ができると戻るので、その所要時間はcaptchaの種類、対象サイト、そのときの負荷によって変わります。このガイドは、固定の時間を約束しません。自分のコードで制御できる部分、それを計測する方法、そして多くの解決を同時に行う方法を示します。そうすれば、実際に使う時間を有効に使えます。

解決時間を決めるもの

重要なのは3つで、そのうち自分の手で変えられるのは1つだけです。

手をかけるべきは3つ目です。それは計測して変えられる部分だからです。

最適化の前に計測する

呼び出しにタイマーを付けて、結果をログに残してください。知りたいのは、解決時間と、その前後にコードが費やす時間です。

import time
import requests

API_KEY = "scarnox_YOURAPIKEY"
URL = "https://api.scarnox.com/api/tasks/create"

def timed_solve(sitekey: str, page_url: str) -> dict:
    started = time.perf_counter()
    r = requests.post(
        URL,
        headers={"Authorization": f"Bearer {API_KEY}"},
        json={
            "captcha_type": "PopularCaptchaTaskProxyless",
            "site_key": sitekey,
            "site_url": page_url,
        },
        timeout=185,
    )
    elapsed = time.perf_counter() - started
    data = r.json()
    if data.get("errorId") != 0:
        return {"ok": False, "error": data.get("errorCode"), "seconds": elapsed}
    return {"ok": True, "token": data["solution"]["token"], "seconds": elapsed}

result = timed_solve("10000000-ffff-ffff-ffff-000000000001", "https://example.com/signup")
print(result["ok"], round(result["seconds"], 2), "s")

計測したいページに対して、ジョブを実行する時間帯に実行してください。数値を残しておきましょう。変更が本当に役立ったかどうかが分かります。

並列に解決する

解決はそれぞれ独立しています。10個のサイトキーを扱うなら、1つずつ待つと、他の処理に使えたはずの時間が無駄になります。各解決はほとんどの時間を待ち時間に費やすHTTPリクエストが1回なので、ほとんどのスクリプトにはスレッドプールで十分です。

from concurrent.futures import ThreadPoolExecutor

jobs = [
    ("10000000-ffff-ffff-ffff-000000000001", "https://example.com/signup"),
    ("10000000-ffff-ffff-ffff-000000000001", "https://example.com/register"),
    ("10000000-ffff-ffff-ffff-000000000001", "https://example.com/contact"),
]

def run(job):
    sitekey, page_url = job
    return timed_solve(sitekey, page_url)

# 少なめに始める。エラーが少ない間だけワーカー数を増やす。
with ThreadPoolExecutor(max_workers=5) as pool:
    results = list(pool.map(run, jobs))

for r in results:
    status = "ok" if r["ok"] else r["error"]
    print(f"{r['seconds']:.2f} s  {status}")

ワーカー数を増やしたときに、レート制限や処理能力の問題に見えるエラーが出たら、減らしてください。並列化が役立つのは、サーバーがそれに追いつけるときだけです。

早すぎる解決をしない

最速のコードは、まだ使わないトークンを待たないコードです。トークンを事前にまとめて取得して保存しておくのは、よくある間違いです。トークンは発行から約2分で期限切れになるため、保存したトークンは必要になったときには古くなっていることが多いです。フォームを投稿する直前に解決してください。詳しい理由はcaptchaトークンの期限切れ:原因と対処方法で説明しています。

残りの経路を短く保つ

期限のないものは再利用する

一部の作業は、複数の解決の間で安全に共有できます。次のものはホットパスから外してください。

これらの変更はコストがほとんどかからず、すべての呼び出しに現れるオーバーヘッドを取り除きます。

平均ではなく末尾を計測する

解決時間の平均は、バッチを遅らせる遅い呼び出しを隠してしまいます。解決ごとの時間を記録して、中央値と95パーセンタイルを見てください。最も遅いジョブの時間で終わるパイプラインは、末尾に制限されています。そのため、リトライの遅い呼び出しの扱いを少し変えるだけで、中央値をいじるよりも効果があることがあります。

import statistics

def summarise(seconds: list[float]) -> str:
    ordered = sorted(seconds)
    p95 = ordered[int(0.95 * (len(ordered) - 1))]
    return f"median {statistics.median(ordered):.2f} s, p95 {p95:.2f} s, n={len(ordered)}"

print(summarise([r["seconds"] for r in results]))

変更の前後で集計を比べてください。p95がほとんど動かないなら、時間はコードの別の場所で使われています。

速さだけでなく結果を確認する

速く失敗しても、それは失敗です。エラーを返した解決の数を数え、エラー率の上昇は速度を落とすべきサインとして扱ってください。失敗した解決は課金されませんが、それにかけた時間は、パイプラインが失った時間です。完全なリトライのパターンはcaptcha解決のエラー処理にあります。

次のステップ