GoでCAPTCHAタスクを並行して解く

著者:solvethree読了時間:1分

共有の Go SDK クライアントを1つと、固定数のワーカープールを使用します。ジョブごとに専用のコンテキストを用意し、トークンを後のバッチ用にため込まず、各ソリューションが準備でき次第すぐに送信します。

このページの内容

クライアントと送信先を準備する

  • Go 1.22 以降と、公式の solvethree.com/sdk モジュールのバージョン v0.1.0 を使用します。インストールの詳細は Go 連携ドキュメントを参照してください。
  • サーバー環境で SOLVETHREE_API_KEY と SOLVETHREE_PROXY を設定します。API キーをブラウザー側のコードに含めないでください。
  • accounts.example.com と login を、対象のホスト名とアクションに置き換えます。ワークスペースには、そのサイトの有効なプロファイルが必要です。ホスト名を送信してもプロファイルは作成されません。サイトプロファイルの設定を参照してください。

この例では、デフォルトの reCAPTCHA Enterprise をリクエストします。プロキシを1つ指定し、Type と URL は意図的に未設定にしています。reCAPTCHA リクエストでは、これらのフィールドではなく、サイトのホスト名とアクションを使用します。

クライアント数ではなく、同時実行する処理数を制限する

クライアントは並行処理で安全に使用できます。この例では4つのワーカーを起動し、バッファなしのチャネル経由で8つのジョブを渡します。すべてのワーカーが処理中の場合、プロデューサーはジョブごとに goroutine を起動せずに待機します。4という数はアプリケーション設定の一例であり、solvethree サービスの制限ではありません。

Go · 4つのワーカーとジョブごとの即時処理
package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "sync"
    "time"
    solvethree "solvethree.com/sdk"
)

func main() {
    client, err := solvethree.NewClient(os.Getenv("SOLVETHREE_API_KEY"), nil)
    if err != nil { log.Fatal(err) }
    jobs := make(chan int)
    var workers sync.WaitGroup
    for worker := 0; worker < 4; worker++ {
        workers.Add(1)
        go func() {
            defer workers.Done()
            for job := range jobs {
                ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second)
                input := solvethree.TaskRequest{Site: "accounts.example.com", Action: "login", Proxy: os.Getenv("SOLVETHREE_PROXY")}
                result, err := client.Solve(ctx, input, solvethree.SolveOptions{})
                if err != nil { cancel(); log.Printf("Job %d: %v", job, err); continue }
                fmt.Println("Ready task:", result.ID)
                // Submit this solution now, with its proxy and browser identity.
                cancel()
            }
        }()
    }
    for job := 0; job < 8; job++ { jobs <- job }
    close(jobs)
    workers.Wait()
}

各ワーカーは、ジョブごとに個別の180秒のコンテキストを作成します。Solve はタスクを一度だけ作成し、その結果を待ちます。この例では RequestID を省略しているため、SDK が作成時に生成します。呼び出しが失敗するとログに記録され、continue によってそのワーカーだけが次のジョブに進みます。バッチ全体は停止しません。

例のようにループ内でキャンセルし、各ジョブがコンテキストのリソースを速やかに解放するようにしてください。キャンセルで停止するのはローカルの待機だけで、リモートの処理はキャンセルされません。

次のジョブを受け取る前にソリューションを処理する

この例は準備完了のタスク ID を出力するだけで、送信先には何も送信しません。コメントを実際の送信先へのリクエストに置き換え、ワーカーが次のジョブを受け取る前に、そのジョブの送信結果を報告してください。workers.Wait の後で使うためにトークンをスライスへ追加しないでください。

  • result.ID ではなく、result.Solution.Token を送信してください。
  • 元のタスクと準備完了の結果を SuccessfulProxy に渡して、成功したプロキシを特定します。返された UserAgent と SecChUA のブラウザー識別情報を使い、そのプロキシ経由で送信してください。
  • 送信はジョブのコンテキスト内で、cancel() の前に行ってください。これにより180秒の期限が解決と送信の両方に適用されます。別の制限時間が必要な場合は、送信用に別のコンテキストを使用してください。

解決結果と送信先への送信結果は分けて報告してください。準備完了のタスクからは使用可能なソリューションが得られますが、アプリケーションのリクエストが受け入れられたかどうかは、送信先のレスポンスでしか確認できません。

バッチを再起動せずに、中断したジョブを復旧する

この小規模な例ではエラーのログ記録だけで十分ですが、復旧可能な連携にするには、各ジョブのタスク入力を正確に保持し、取得できたリクエスト ID とタスク ID も保存する必要があります。タイムアウトしたからといって、タスクの作成が失敗したとは限りません。

  • *solvethree.Error を取り出して RequestID と TaskID を取得します。タスク ID がわかっている場合は、新しいコンテキストで GetTask を使って取得を再開してください。
  • 作成されたかどうかが不明で、リクエスト ID がある場合は、同じ RequestID と元の入力で再送します。同じリクエストであれば既存のタスクが返されます。同じ ID を再利用しながら入力を変更すると競合します。
  • *TaskFailedError の場合は ErrorCode を記録し、Error のテキストが必要であれば失敗したタスクを取得します。そのタスクのポーリングを停止し、同じ無効な入力を無条件に再送するのではなく、原因に対処してください。

HTTP エラーとタスクの最終的な失敗は区別してください。errors.As を使うと、StatusCode と RetryAfter を持つ、ネストされた *APIError を取り出せます。Solve は、HTTP 429、502、503、504 の一時的なポーリングレスポンスを、コンテキストの期限内で Retry-After に従って再試行します。タスクの作成は自動では再試行されません。

最初のタスクを実行しましょう。

$5のワークスペースクレジットで始められます。

ワークスペースを作成
GoでCAPTCHAタスクを並行して解く · solvethree