Параллельное решение задач CAPTCHA в Go

Автор: solvethree3 мин на чтение

Используйте один общий клиент Go SDK и пул воркеров фиксированного размера. Создавайте отдельный контекст для каждого задания и отправляйте каждое готовое решение сразу, а не накапливайте токены для последующей пакетной отправки.

На этой странице

Подготовьте клиент и целевой сайт

  • Используйте Go 1.22 или новее и официальный модуль solvethree.com/sdk версии v0.1.0. Подробности установки приведены в документации по интеграции с Go.
  • Задайте SOLVETHREE_API_KEY и SOLVETHREE_PROXY в окружении сервера. Не размещайте API-ключ в браузерном коде.
  • Замените accounts.example.com и login на имя целевого хоста и действие. В рабочем пространстве должен быть активный профиль этого сайта; передача имени хоста не создаёт профиль. См. настройку профиля сайта.

В примере запрашивается reCAPTCHA Enterprise — вариант по умолчанию. Передаётся один прокси, а поля Type и URL намеренно не задаются: запросы reCAPTCHA используют имя хоста сайта и действие, а не эти поля.

Ограничивайте объём параллельной работы, а не число клиентов

Клиент безопасен для конкурентного использования. В этом примере запускаются четыре воркера, которым через небуферизованный канал передаются восемь заданий. Когда все воркеры заняты, отправитель ждёт, а не запускает горутину для каждого задания. Четыре — это пример настройки приложения, а не ограничение сервиса 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 генерирует его для этого создания. При неудачном вызове ошибка записывается в журнал, а continue переводит только этот воркер к следующему заданию; обработка пакета не останавливается.

Вызывайте отмену внутри цикла, как показано в примере, чтобы каждое задание своевременно освобождало ресурсы своего контекста. Отмена прекращает локальное ожидание, но не отменяет удалённую работу.

Обработайте решение, прежде чем брать следующее задание

Пример выводит ID готовой задачи, но ничего не отправляет на целевой сайт. Замените комментарий запросом к целевому сайту, затем сообщите результат отправки для этого задания, прежде чем воркер возьмёт следующее. Не добавляйте токены в срез для использования после workers.Wait.

  • Отправляйте result.Solution.Token, а не result.ID.
  • Используйте SuccessfulProxy с исходной задачей и готовым результатом, чтобы определить успешный прокси. Отправляйте запрос через этот прокси с возвращёнными данными браузера UserAgent и SecChUA.
  • Выполняйте отправку в контексте задания, до cancel(). Тогда лимит в 180 секунд охватывает и решение, и отправку; если для отправки нужен отдельный лимит времени, используйте отдельный контекст.

Сообщайте результаты решения и отправки на целевой сайт отдельно. Готовая задача предоставляет пригодное к использованию решение, но только ответ целевого сайта показывает, принят ли запрос приложения.

Восстановите прерванное задание без перезапуска всего пакета

Для этого небольшого примера достаточно записать ошибку в журнал, но интеграция с поддержкой восстановления должна сохранять точные входные данные каждого задания и доступные ID запроса и задачи. Тайм-аут не доказывает, что создать задачу не удалось.

  • Извлеките *solvethree.Error, чтобы получить RequestID и TaskID. Если ID задачи известен, возобновите получение результата через GetTask с новым контекстом.
  • Если результат создания неизвестен и у вас есть ID запроса, повторно отправьте исходные входные данные с тем же RequestID. Тот же запрос возвращает существующую задачу; изменение входных данных при повторном использовании ID вызывает конфликт.
  • Для *TaskFailedError сохраните ErrorCode и получите завершившуюся с ошибкой задачу, если нужен текст её Error. Прекратите опрос этой задачи и устраните причину, а не отправляйте вслепую те же некорректные входные данные снова.

Различайте HTTP-ошибки и окончательные сбои задач. errors.As позволяет извлечь вложенную *APIError с StatusCode и RetryAfter. Solve уже повторяет опрос при временных ответах HTTP 429, 502, 503 или 504, соблюдая Retry-After в пределах срока действия контекста. Создание задачи никогда не повторяется автоматически.

Запустите первую задачу.

Начните с $5 на балансе рабочего пространства.

Создать рабочее пространство
Параллельное решение задач CAPTCHA в Go · solvethree