在 Go 中并发求解 CAPTCHA 任务

作者:solvethree阅读需 1 分钟

使用一个共享的 Go SDK 客户端和固定大小的工作池。为每个作业创建独立的上下文,并在每个解决结果就绪后立即提交,不要收集令牌留待后续批量提交。

本页内容

准备客户端和目标站点

  • 使用 Go 1.22 或更高版本,以及版本为 v0.1.0 的官方 solvethree.com/sdk 模块。安装详情请参阅 Go 集成文档。
  • 在服务器环境中设置 SOLVETHREE_API_KEY 和 SOLVETHREE_PROXY。不要将 API 密钥放入浏览器端代码。
  • 将 accounts.example.com 和 login 替换为目标主机名和操作。工作区中需要有该站点的有效配置;提交主机名不会创建配置。请参阅站点配置设置。

此示例请求默认的 reCAPTCHA Enterprise。它提供一个代理,并特意不设置 Type 和 URL:reCAPTCHA 请求使用站点主机名和操作,而不是这些字段。

限制并发工作量,而不是客户端数量

该客户端可安全地并发使用。此示例启动四个工作协程,并通过无缓冲通道向它们分配八个作业。当所有工作协程都忙碌时,生产者会等待,而不是为每个作业启动一个 goroutine。四个仅是示例应用的设置,并非 solvethree 的服务限制。

Go · 使用四个工作进程并即时处理每项作业
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 会为此次创建生成一个 ID。调用失败时会记录日志,continue 仅让该工作协程转到下一个作业,不会停止整个批次。

如示例所示,在循环内部执行取消操作,以便每个作业及时释放上下文资源。取消只会停止本地等待,不会取消远程工作。

先处理解决结果,再领取下一个作业

此示例会打印已就绪任务的 ID,不会向目标站点提交任何内容。请将注释替换为目标请求,并在工作协程领取下一个作业之前,报告该作业的提交结果。不要将令牌追加到切片中,留到 workers.Wait 之后再使用。

  • 提交 result.Solution.Token,而不是 result.ID。
  • 将原始任务和已就绪结果传给 SuccessfulProxy,以确定成功使用的代理。通过该代理提交,并使用返回的 UserAgent 和 SecChUA 浏览器身份信息。
  • 在作业上下文内、调用 cancel() 之前完成提交。这样,180 秒的截止时间就同时涵盖求解和提交;如果需要单独的时间预算,请使用独立的提交上下文。

分别报告求解结果和目标提交结果。已就绪的任务会提供可用的解决结果,但只有目标站点的响应才能说明它是否接受了应用请求。

恢复中断的作业,无需重启整个批次

对于这个小示例,记录错误就足够了,但支持恢复的集成应保留每个作业的确切任务输入,以及可获取的请求 ID 和任务 ID。超时并不能证明任务创建失败。

  • 提取 *solvethree.Error 以获取 RequestID 和 TaskID。如果已知任务 ID,请使用新的上下文,通过 GetTask 恢复结果获取。
  • 如果无法确定创建是否成功,且你持有请求 ID,请使用相同的 RequestID 重放原始输入。相同的请求会返回现有任务;复用 ID 时更改输入会导致冲突。
  • 对于 *TaskFailedError,记录 ErrorCode;如果需要其 Error 文本,请获取失败的任务。停止轮询该任务并解决原因,不要盲目再次提交相同的无效输入。

区分 HTTP 错误与任务的终止性失败。errors.As 可以提取嵌套的 *APIError,其中包含 StatusCode 和 RetryAfter。对于 HTTP 429、502、503 或 504 这类临时轮询响应,Solve 已会自动重试,并在上下文截止时间内遵循 Retry-After。任务创建绝不会自动重试。

运行您的第一个任务。

使用 $5 工作区额度开始体验。

创建工作区
在 Go 中并发求解 CAPTCHA 任务 · solvethree