CAPSOLVER
Blog
Integración de Katana con CapSolver: Resolución automatizada de CAPTCHA para el rastreo de la web

Integración de Katana con CapSolver: Resolución automatizada de CAPTCHA para rastreo de web

Logo of CapSolver

Adélia Cruz

Neural Network Developer

12-Jan-2026

Cómo resolver CAPTCHA con Katana usando CapSolver

El raspado de web es una técnica esencial para investigadores de seguridad, testers de penetración y analistas de datos. Sin embargo, los sitios web modernos emplean cada vez más CAPTCHAs para protegerse contra el acceso automatizado. Esta guía muestra cómo integrar Katana, el potente framework de raspado web de ProjectDiscovery, con CapSolver, un servicio líder para resolver CAPTCHAs, para crear una solución de raspado robusta que resuelva automáticamente los desafíos de CAPTCHA.

Lo que aprenderás

  • Configurar Katana en modo navegador sin cabeza
  • Integrar la API de Capsolver para resolver CAPTCHAs automáticamente
  • Manejar reCAPTCHA v2 y Cloudflare Turnstile
  • Ejemplos de código completos y ejecutables para cada tipo de CAPTCHA
  • Mejores prácticas para un raspado eficiente y responsable

¿Qué es Katana?

Katana es un framework de raspado web de próxima generación desarrollado por ProjectDiscovery. Está diseñado para velocidad y flexibilidad, siendo ideal para reconocimiento de seguridad y pipelines de automatización.

Características clave

  • Dos modos de raspado: Raspado basado en HTTP estándar y automatización de navegador sin cabeza
  • Soporte para JavaScript: Analizar y raspar contenido renderizado con JavaScript
  • Configuración flexible: Encabezados personalizados, cookies, relleno de formularios y control de alcance
  • Múltiples formatos de salida: Texto plano, JSON o JSONL

Instalación

bash Copy
# Requiere Go 1.24+
CGO_ENABLED=1 go install github.com/projectdiscovery/katana/cmd/katana@latest

Uso básico

bash Copy
katana -u https://example.com -headless

¿Qué es Capsolver?

CapSolver es un servicio de resolución de CAPTCHA impulsado por inteligencia artificial que ofrece soluciones rápidas y confiables para diversos tipos de CAPTCHA.

Tipos de CAPTCHA admitidos

  • reCAPTCHA: v2 y versiones Enterprise
  • Cloudflare: Turnstile y Challenge
  • AWS WAF: Bypass de protección WAF
  • Y más

Flujo de trabajo de la API

CapSolver utiliza un modelo de API basado en tareas:

  1. Crear tarea: Enviar parámetros de CAPTCHA (tipo, siteKey, URL)
  2. Obtener ID de tarea: Recibir un identificador único de tarea
  3. Consultar resultado: Verificar el estado de la tarea hasta que la solución esté lista
  4. Recibir token: Obtener el token de CAPTCHA resuelto

Requisitos previos

Antes de comenzar, asegúrate de tener:

  1. Go 1.24+ instalado
  2. Clave de API de Capsolver - Regístrate aquí
  3. Navegador Chrome (para el modo sin cabeza)

Establece tu clave de API como variable de entorno:

bash Copy
export CAPSOLVER_API_KEY="TU_CLAVE_DE_API"

Arquitectura de integración

Copy
┌─────────────────────────┐
│   Aplicación Go       │
│   (navegador go-rod)  │
└───────────┬─────────────┘
            │
            ▼
┌─────────────────────────┐
│   Sitio web objetivo  │
│   (con CAPTCHA)       │
└───────────┬─────────────┘
            │
    CAPTCHA detectado
            │
            ▼
┌─────────────────────────┐
│   Extraer parámetros  │
│   (siteKey, URL, tipo)│
└───────────┬─────────────┘
            │
            ▼
┌─────────────────────────┐
│   API de Capsolver    │
│   createTask()        │
└───────────┬─────────────┘
            │
            ▼
┌─────────────────────────┐
│   Consultar resultado   │
│   getTaskResult()       │
└───────────┬─────────────┘
            │
            ▼
┌─────────────────────────┐
│   Inyectar token      │
│   en la página        │
└───────────┬─────────────┘
            │
            ▼
┌─────────────────────────┐
│   Continuar con el raspado     │
└─────────────────────────┘

Resolviendo reCAPTCHA v2 con CapSolver

reCAPTCHA v2 es el tipo de CAPTCHA más común, mostrando un checkbox de "No soy un robot" o desafíos de imágenes. Aquí tienes un script completo y ejecutable para resolver reCAPTCHA v2:

go Copy
// Resolutor de reCAPTCHA v2 - Ejemplo completo
// Uso: go run main.go
// Requiere: variable de entorno CAPSOLVER_API_KEY

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/go-rod/rod"
	"github.com/go-rod/rod/lib/launcher"
)

// Configuración
var (
	CAPSOLVER_API_KEY = os.Getenv("CAPSOLVER_API_KEY")
	CAPSOLVER_API     = "https://api.capsolver.com"
)

// Estructuras de respuesta de la API
type CreateTaskResponse struct {
	ErrorID          int    `json:"errorId"`
	ErrorCode        string `json:"errorCode"`
	ErrorDescription string `json:"errorDescription"`
	TaskID           string `json:"taskId"`
}

type GetTaskResultResponse struct {
	ErrorID          int    `json:"errorId"`
	ErrorCode        string `json:"errorCode"`
	ErrorDescription string `json:"errorDescription"`
	Status           string `json:"status"`
	Solution         struct {
		GRecaptchaResponse string `json:"gRecaptchaResponse"`
	} `json:"solution"`
}

type BalanceResponse struct {
	ErrorID int     `json:"errorId"`
	Balance float64 `json:"balance"`
}

// CapsolverClient maneja la comunicación con la API
type CapsolverClient struct {
	APIKey string
	Client *http.Client
}

// NewCapsolverClient crea un nuevo cliente de Capsolver
func NewCapsolverClient(apiKey string) *CapsolverClient {
	return &CapsolverClient{
		APIKey: apiKey,
		Client: &http.Client{Timeout: 120 * time.Second},
	}
}

// GetBalance recupera el saldo de la cuenta
func (c *CapsolverClient) GetBalance() (float64, error) {
	payload := map[string]string{"clientKey": c.APIKey}
	jsonData, _ := json.Marshal(payload)

	resp, err := c.Client.Post(CAPSOLVER_API+"/getBalance", "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		return 0, err
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	var result BalanceResponse
	json.Unmarshal(body, &result)

	if result.ErrorID != 0 {
		return 0, fmt.Errorf("falló la verificación del saldo")
	}

	return result.Balance, nil
}

// SolveRecaptchaV2 resuelve un desafío de reCAPTCHA v2
func (c *CapsolverClient) SolveRecaptchaV2(websiteURL, siteKey string) (string, error) {
	log.Printf("Creando tarea de reCAPTCHA v2 para %s", websiteURL)

	// Crear tarea
	task := map[string]interface{}{
		"type":       "ReCaptchaV2TaskProxyLess",
		"websiteURL": websiteURL,
		"websiteKey": siteKey,
	}

	payload := map[string]interface{}{
		"clientKey": c.APIKey,
		"task":      task,
	}

	jsonData, _ := json.Marshal(payload)
	resp, err := c.Client.Post(CAPSOLVER_API+"/createTask", "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		return "", fmt.Errorf("no se pudo crear la tarea: %w", err)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	var createResult CreateTaskResponse
	json.Unmarshal(body, &createResult)

	if createResult.ErrorID != 0 {
		return "", fmt.Errorf("error de API: %s - %s", createResult.ErrorCode, createResult.ErrorDescription)
	}

	log.Printf("Tarea creada: %s", createResult.TaskID)

	// Consultar resultado
	for i := 0; i < 120; i++ {
		result, err := c.getTaskResult(createResult.TaskID)
		if err != nil {
			return "", err
		}

		if result.Status == "ready" {
			log.Printf("CAPTCHA resuelto con éxito!")
			return result.Solution.GRecaptchaResponse, nil
		}

		if result.Status == "failed" {
			return "", fmt.Errorf("tarea fallida: %s", result.ErrorDescription)
		}

		if i%10 == 0 {
			log.Printf("Esperando solución... (%ds)", i)
		}
		time.Sleep(1 * time.Second)
	}

	return "", fmt.Errorf("tiempo de espera agotado para la solución")
}

func (c *CapsolverClient) getTaskResult(taskID string) (*GetTaskResultResponse, error) {
	payload := map[string]string{
		"clientKey": c.APIKey,
		"taskId":    taskID,
	}

	jsonData, _ := json.Marshal(payload)
	resp, err := c.Client.Post(CAPSOLVER_API+"/getTaskResult", "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	var result GetTaskResultResponse
	json.Unmarshal(body, &result)

	return &result, nil
}

// extractSiteKey extrae la clave del sitio de reCAPTCHA del HTML de la página
func extractSiteKey(html string) string {
	// Buscar atributo data-sitekey
	patterns := []string{
		`data-sitekey="`,
		`data-sitekey='`,
		`"sitekey":"`,
		`'sitekey':'`,
	}

	for _, pattern := range patterns {
		if idx := strings.Index(html, pattern); idx != -1 {
			start := idx + len(pattern)
			end := start
			for end < len(html) && html[end] != '"' && html[end] != '\'' {
				end++
			}
			if end > start {
				return html[start:end]
			}
		}
	}
	return ""
}

// injectRecaptchaToken inyecta el token resuelto en la página
func injectRecaptchaToken(page *rod.Page, token string) error {
	js := fmt.Sprintf(`
		(function() {
			// Establecer el campo de respuesta
			var responseField = document.getElementById('g-recaptcha-response');
			if (responseField) {
				responseField.style.display = 'block';
				responseField.value = '%s';
			}

			// También establecer cualquier textarea oculto
			var textareas = document.querySelectorAll('textarea[name="g-recaptcha-response"]');
			for (var i = 0; i < textareas.length; i++) {
				textareas[i].value = '%s';
			}

			// Activar el callback si existe
			if (typeof ___grecaptcha_cfg !== 'undefined') {
				var clients = ___grecaptcha_cfg.clients;
				for (var key in clients) {
					var client = clients[key];
					if (client) {
						// Intentar encontrar y llamar al callback
						try {
							var callback = client.callback ||
								(client.Q && client.Q.callback) ||
								(client.S && client.S.callback);
							if (typeof callback === 'function') {
								callback('%s');
							}
						} catch(e) {}
					}
				}
			}

			return true;
		})();
	`, token, token, token)

	_, err := page.Eval(js)
	return err
}

func main() {
	// Verificar clave de API
	if CAPSOLVER_API_KEY == "" {
		log.Fatal("La variable de entorno CAPSOLVER_API_KEY es requerida")
	}

	// URL objetivo - página de demostración de reCAPTCHA de Google
	targetURL := "https://www.google.com/recaptcha/api2/demo"

	log.Println("==============================================")
	log.Println("Katana + Capsolver - Demo de reCAPTCHA v2")
	log.Println("==============================================")

	// Inicializar cliente de Capsolver
	client := NewCapsolverClient(CAPSOLVER_API_KEY)

	// Verificar saldo
	balance, err := client.GetBalance()
	if err != nil {
		log.Printf("Advertencia: No se pudo verificar el saldo: %v", err)
	} else {
		log.Printf("Saldo de Capsolver: $%.2f", balance)
	}

	// Iniciar navegador
	log.Println("Iniciando navegador...")
	path, _ := launcher.LookPath()
	u := launcher.New().Bin(path).Headless(true).MustLaunch()
	browser := rod.New().ControlURL(u).MustConnect()
	defer browser.MustClose()

	// Navegar a la URL objetivo
	log.Printf("Navegando a: %s", targetURL)
	page := browser.MustPage(targetURL)
	page.MustWaitLoad()
	time.Sleep(2 * time.Second)

	// Obtener HTML de la página y extraer clave del sitio
	html := page.MustHTML()

	// Verificar si hay reCAPTCHA
	if !strings.Contains(html, "g-recaptcha") && !strings.Contains(html, "grecaptcha") {
		log.Fatal("No se encontró reCAPTCHA en la página")
	}

	log.Println("reCAPTCHA detectado!")

	// Extraer clave del sitio
	siteKey := extractSiteKey(html)
	if siteKey == "" {
		log.Fatal("No se pudo extraer la clave del sitio")
	}
	log.Printf("Clave del sitio: %s", siteKey)

	// Resolver CAPTCHA
	log.Println("Resolviendo CAPTCHA con Capsolver...")
	token, err := client.SolveRecaptchaV2(targetURL, siteKey)
	if err != nil {
		log.Fatalf("Falló al resolver el CAPTCHA: %v", err)
	}

	log.Printf("Token recibido: %s...", token[:50])

	// Inyectar token
	log.Println("Inyectando token en la página...")
	err = injectRecaptchaToken(page, token)
	if err != nil {
		log.Fatalf("Falló al inyectar el token: %v", err)
	}

	// Enviar formulario
	log.Println("Enviando formulario...")
	submitBtn := page.MustElement("#recaptcha-demo-submit")
	submitBtn.MustClick()

	// Esperar resultado
	time.Sleep(3 * time.Second)

	// Verificar resultado
	newHTML := page.MustHTML()
	if strings.Contains(newHTML, "Verificación Exitosa") || strings.Contains(newHTML, "success") {
		log.Println("==============================================")
		log.Println("¡ÉXITO! reCAPTCHA resuelto y verificado!")
		log.Println("==============================================")
	} else {
		log.Println("Formulario enviado - verifique la página para el resultado")
	}

	// Obtener título de la página
	title := page.MustEval(`document.title`).String()
	log.Printf("Título de la página final: %s", title)
}

Configuración y ejecución

bash Copy
# Crear proyecto
mkdir katana-recaptcha-v2
cd katana-recaptcha-v2
go mod init katana-recaptcha-v2

# Instalar dependencias
go get github.com/go-rod/rod@latest

# Establecer clave de API
export CAPSOLVER_API_KEY="TU_CLAVE_DE_API"

# Ejecutar
go run main.go

Resolviendo Cloudflare Turnstile con CapSolver

Cloudflare Turnstile es una alternativa de CAPTCHA enfocada en la privacidad. Aquí tienes un script completo:

go Copy
// Resolutor de Cloudflare Turnstile - Ejemplo completo
// Uso: go run main.go
// Requiere: variable de entorno CAPSOLVER_API_KEY

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"regexp"
	"strings"
	"time"

	"github.com/go-rod/rod"
	"github.com/go-rod/rod/lib/launcher"
)

// Configuración
var (
	CAPSOLVER_API_KEY = os.Getenv("CAPSOLVER_API_KEY")
	CAPSOLVER_API     = "https://api.capsolver.com"
)

// Estructuras de respuesta de la API
type CreateTaskResponse struct {
	ErrorID          int    `json:"errorId"`
	ErrorCode        string `json:"errorCode"`
	ErrorDescription string `json:"errorDescription"`
	TaskID           string `json:"taskId"`
}

type GetTaskResultResponse struct {
	ErrorID          int    `json:"errorId"`
	ErrorCode        string `json:"errorCode"`
	ErrorDescription string `json:"errorDescription"`
	Status           string `json:"status"`
	Solution         struct {
		Token string `json:"token"`
	} `json:"solution"`
}

type BalanceResponse struct {
	ErrorID int     `json:"errorId"`
	Balance float64 `json:"balance"`
}

// CapsolverClient maneja la comunicación con la API
type CapsolverClient struct {
	APIKey string
	Client *http.Client
}

// NewCapsolverClient crea un nuevo cliente de Capsolver
func NewCapsolverClient(apiKey string) *CapsolverClient {
	return &CapsolverClient{
		APIKey: apiKey,
		Client: &http.Client{Timeout: 120 * time.Second},
	}
}

// GetBalance recupera el saldo de la cuenta
func (c *CapsolverClient) GetBalance() (float64, error) {
	payload := map[string]string{"clientKey": c.APIKey}
	jsonData, _ := json.Marshal(payload)

	resp, err := c.Client.Post(CAPSOLVER_API+"/getBalance", "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		return 0, err
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	var result BalanceResponse
	json.Unmarshal(body, &result)

	return result.Balance, nil
}

// SolveTurnstile resuelve un desafío de Cloudflare Turnstile
func (c *CapsolverClient) SolveTurnstile(websiteURL, siteKey string) (string, error) {
	log.Printf("Creando tarea de Turnstile para %s", websiteURL)

	// Crear tarea
	task := map[string]interface{}{
		"type":       "AntiTurnstileTaskProxyLess",
		"websiteURL": websiteURL,

"websiteKey": siteKey,
}

Copy
payload := map[string]interface{}{
	"clientKey": c.APIKey,
	"task":      task,
}

jsonData, _ := json.Marshal(payload)
resp, err := c.Client.Post(CAPSOLVER_API+"/createTask", "application/json", bytes.NewBuffer(jsonData))
if err != nil {
	return "", fmt.Errorf("falló la creación de la tarea: %w", err)
}
defer resp.Body.Close()

body, _ := io.ReadAll(resp.Body)
var createResult CreateTaskResponse
json.Unmarshal(body, &createResult)

if createResult.ErrorID != 0 {
	return "", fmt.Errorf("error de API: %s - %s", createResult.ErrorCode, createResult.ErrorDescription)
}

log.Printf("Tarea creada: %s", createResult.TaskID)

// Esperar resultado
for i := 0; i < 120; i++ {
	result, err := c.getTaskResult(createResult.TaskID)
	if err != nil {
		return "", err
	}

	if result.Status == "ready" {
		log.Printf("Turnstile resuelto correctamente!")
		return result.Solution.Token, nil
	}

	if result.Status == "failed" {
		return "", fmt.Errorf("tarea fallida: %s", result.ErrorDescription)
	}

	if i%10 == 0 {
		log.Printf("Esperando solución... (%ds)", i)
	}
	time.Sleep(1 * time.Second)
}

return "", fmt.Errorf("tiempo agotado esperando solución")

}

func (c *CapsolverClient) getTaskResult(taskID string) (*GetTaskResultResponse, error) {
payload := map[string]string{
"clientKey": c.APIKey,
"taskId": taskID,
}

Copy
jsonData, _ := json.Marshal(payload)
resp, err := c.Client.Post(CAPSOLVER_API+"/getTaskResult", "application/json", bytes.NewBuffer(jsonData))
if err != nil {
	return nil, err
}
defer resp.Body.Close()

body, _ := io.ReadAll(resp.Body)
var result GetTaskResultResponse
json.Unmarshal(body, &result)

return &result, nil

}

// extractTurnstileSiteKey extrae la clave de sitio de Turnstile del HTML de la página
func extractTurnstileSiteKey(html string) string {
// Patrón 1: atributo data-sitekey en div cf-turnstile
patterns := []string{
cf-turnstile[^>]*data-sitekey=['"]([^'"]+)['"],
data-sitekey=['"]([^'"]+)['"][^>]*class=['"][^'"]*cf-turnstile,
turnstile\.render\s*\([^,]+,\s*\{[^}]*sitekey['":\s]+['"]([^'"]+)['"],
sitekey['":\s]+['"]([0-9a-zA-Z_-]+)['"],
}

Copy
for _, pattern := range patterns {
	re := regexp.MustCompile(pattern)
	matches := re.FindStringSubmatch(html)
	if len(matches) > 1 {
		return matches[1]
	}
}

return ""

}

// injectTurnstileToken inyecta el token resuelto en la página
func injectTurnstileToken(page *rod.Page, token string) error {
js := fmt.Sprintf(`
(function() {
// Establecer el campo cf-turnstile-response
var responseField = document.querySelector('[name="cf-turnstile-response"]');
if (responseField) {
responseField.value = '%s';
}

Copy
		// También intentar por ID
		var byId = document.getElementById('cf-turnstile-response');
		if (byId) {
			byId.value = '%s';
		}

		// Crear campo oculto si es necesario
		if (!responseField && !byId) {
			var input = document.createElement('input');
			input.type = 'hidden';
			input.name = 'cf-turnstile-response';
			input.value = '%s';
			var form = document.querySelector('form');
			if (form) {
				form.appendChild(input);
			}
		}

		// Intentar activar el callback
		if (window.turnstile && window.turnstileCallback) {
			window.turnstileCallback('%s');
		}

		return true;
	})();
`, token, token, token, token)

_, err := page.Eval(js)
return err

}

func main() {
// Verificar clave de API
if CAPSOLVER_API_KEY == "" {
log.Fatal("La variable de entorno CAPSOLVER_API_KEY es requerida")
}

Copy
// URL objetivo - Reemplazar con un sitio que use Cloudflare Turnstile
targetURL := "https://example.com"

log.Println("==============================================")
log.Println("Katana + Capsolver - Demo de Turnstile")
log.Println("==============================================")

// Inicializar cliente de Capsolver
client := NewCapsolverClient(CAPSOLVER_API_KEY)

// Verificar saldo
balance, err := client.GetBalance()
if err != nil {
	log.Printf("Advertencia: No se pudo verificar el saldo: %v", err)
} else {
	log.Printf("Saldo de Capsolver: $%.2f", balance)
}

// Iniciar navegador
log.Println("Iniciando navegador...")
path, _ := launcher.LookPath()
u := launcher.New().Bin(path).Headless(true).MustLaunch()
browser := rod.New().ControlURL(u).MustConnect()
defer browser.MustClose()

// Navegar a objetivo
log.Printf("Navegando a: %s", targetURL)
page := browser.MustPage(targetURL)
page.MustWaitLoad()
time.Sleep(2 * time.Second)

// Obtener HTML de la página
html := page.MustHTML()

// Verificar Turnstile
if !strings.Contains(html, "cf-turnstile") && !strings.Contains(html, "turnstile") {
	log.Println("No se encontró Turnstile en la página")
	log.Println("Sugerencia: Reemplazar targetURL con un sitio que use Cloudflare Turnstile")
	return
}

log.Println("Se detectó Cloudflare Turnstile!")

// Extraer clave de sitio
siteKey := extractTurnstileSiteKey(html)
if siteKey == "" {
	log.Fatal("No se pudo extraer la clave de sitio")
}
log.Printf("Clave de sitio: %s", siteKey)

// Resolver Turnstile
log.Println("Resolviendo Turnstile con Capsolver...")
token, err := client.SolveTurnstile(targetURL, siteKey)
if err != nil {
	log.Fatalf("Falló al resolver Turnstile: %v", err)
}

log.Printf("Token recibido: %s...", token[:min(50, len(token))])

// Inyectar token
log.Println("Inyectando token en la página...")
err = injectTurnstileToken(page, token)
if err != nil {
	log.Fatalf("Falló al inyectar token: %v", err)
}

log.Println("==============================================")
log.Println("¡ÉXITO! Token de Turnstile inyectado!")
log.Println("==============================================")

// Obtener título de la página
title := page.MustEval(`document.title`).String()
log.Printf("Título de la página: %s", title)

}

func min(a, b int) int {
if a < b {
return a
}
return b
}

Copy
### Puntos clave de Turnstile

1. **Tipo de tarea**: Usar AntiTurnstileTaskProxyLess
2. **Campo de respuesta**: Turnstile usa cf-turnstile-response en lugar de g-recaptcha-response
3. **Resolución más rápida**: Turnstile suele resolverse más rápido que reCAPTCHA (1-10 segundos)
4. **Campo de token**: La solución está en solution.token en lugar de solution.gRecaptchaResponse

---

## Crawler Universal de CAPTCHA

Aquí está un crawler completo y modular que maneja todos los tipos de CAPTCHA automáticamente:

```go
// Crawler de CAPTCHA Universal - Ejemplo completo
// Detecta y resuelve automáticamente reCAPTCHA v2 y Turnstile
// Uso: go run main.go -url "https://example.com"
// Requiere: variable de entorno CAPSOLVER_API_KEY

package main

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"regexp"
	"strings"
	"time"

	"github.com/go-rod/rod"
	"github.com/go-rod/rod/lib/launcher"
)

// ============================================
// Configuración
// ============================================

var (
	CAPSOLVER_API_KEY = os.Getenv("CAPSOLVER_API_KEY")
	CAPSOLVER_API     = "https://api.capsolver.com"
)

// CaptchaType representa diferentes tipos de CAPTCHA
type CaptchaType string

const (
	RecaptchaV2 CaptchaType = "recaptcha_v2"
	Turnstile   CaptchaType = "turnstile"
	Unknown     CaptchaType = "unknown"
)

// CaptchaInfo contiene parámetros extraídos de CAPTCHA
type CaptchaInfo struct {
	Type    CaptchaType
	SiteKey string
}

// ============================================
// Tipos de API
// ============================================

type CreateTaskResponse struct {
	ErrorID          int    `json:"errorId"`
	ErrorCode        string `json:"errorCode"`
	ErrorDescription string `json:"errorDescription"`
	TaskID           string `json:"taskId"`
}

type GetTaskResultResponse struct {
	ErrorID          int    `json:"errorId"`
	ErrorCode        string `json:"errorCode"`
	ErrorDescription string `json:"errorDescription"`
	Status           string `json:"status"`
	Solution         struct {
		GRecaptchaResponse string `json:"gRecaptchaResponse"`
		Token              string `json:"token"`
	} `json:"solution"`
}

type BalanceResponse struct {
	ErrorID int     `json:"errorId"`
	Balance float64 `json:"balance"`
}

// ============================================
// Cliente de Capsolver
// ============================================

type CapsolverClient struct {
	APIKey string
	Client *http.Client
}

func NewCapsolverClient(apiKey string) *CapsolverClient {
	return &CapsolverClient{
		APIKey: apiKey,
		Client: &http.Client{Timeout: 120 * time.Second},
	}
}

func (c *CapsolverClient) GetBalance() (float64, error) {
	payload := map[string]string{"clientKey": c.APIKey}
	jsonData, _ := json.Marshal(payload)

	resp, err := c.Client.Post(CAPSOLVER_API+"/getBalance", "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		return 0, err
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	var result BalanceResponse
	json.Unmarshal(body, &result)

	return result.Balance, nil
}

func (c *CapsolverClient) Solve(info *CaptchaInfo, websiteURL string) (string, error) {
	switch info.Type {
	case RecaptchaV2:
		return c.solveRecaptchaV2(websiteURL, info.SiteKey)
	case Turnstile:
		return c.solveTurnstile(websiteURL, info.SiteKey)
	default:
		return "", fmt.Errorf("tipo de CAPTCHA no soportado: %s", info.Type)
	}
}

func (c *CapsolverClient) solveRecaptchaV2(websiteURL, siteKey string) (string, error) {
	task := map[string]interface{}{
		"type":       "ReCaptchaV2TaskProxyLess",
		"websiteURL": websiteURL,
		"websiteKey": siteKey,
	}
	return c.solveTask(task, "recaptcha")
}

func (c *CapsolverClient) solveTurnstile(websiteURL, siteKey string) (string, error) {
	task := map[string]interface{}{
		"type":       "AntiTurnstileTaskProxyLess",
		"websiteURL": websiteURL,
		"websiteKey": siteKey,
	}
	return c.solveTask(task, "turnstile")
}

func (c *CapsolverClient) solveTask(task map[string]interface{}, tokenType string) (string, error) {
	// Crear tarea
	payload := map[string]interface{}{
		"clientKey": c.APIKey,
		"task":      task,
	}

	jsonData, _ := json.Marshal(payload)
	resp, err := c.Client.Post(CAPSOLVER_API+"/createTask", "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		return "", fmt.Errorf("falló la creación de la tarea: %w", err)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	var createResult CreateTaskResponse
	json.Unmarshal(body, &createResult)

	if createResult.ErrorID != 0 {
		return "", fmt.Errorf("error de API: %s - %s", createResult.ErrorCode, createResult.ErrorDescription)
	}

	log.Printf("Tarea creada: %s", createResult.TaskID)

	// Esperar resultado
	for i := 0; i < 120; i++ {
		getPayload := map[string]string{
			"clientKey": c.APIKey,
			"taskId":    createResult.TaskID,
		}

		jsonData, _ := json.Marshal(getPayload)
		resp, err := c.Client.Post(CAPSOLVER_API+"/getTaskResult", "application/json", bytes.NewBuffer(jsonData))
		if err != nil {
			return "", err
		}

		body, _ := io.ReadAll(resp.Body)
		resp.Body.Close()

		var result GetTaskResultResponse
		json.Unmarshal(body, &result)

		if result.Status == "ready" {
			if tokenType == "turnstile" {
				return result.Solution.Token, nil
			}
			return result.Solution.GRecaptchaResponse, nil
		}

		if result.Status == "failed" {
			return "", fmt.Errorf("tarea fallida: %s", result.ErrorDescription)
		}

		if i%10 == 0 {
			log.Printf("Esperando solución... (%ds)", i)
		}
		time.Sleep(1 * time.Second)
	}

	return "", fmt.Errorf("tiempo agotado esperando solución")
}

// ============================================
// Detección de CAPTCHA
// ============================================

func DetectCaptcha(html string) *CaptchaInfo {
	// Verificar reCAPTCHA v2 (checkbox)
	if strings.Contains(html, "g-recaptcha") {
		siteKey := extractDataSiteKey(html, "g-recaptcha")
		if siteKey != "" {
			return &CaptchaInfo{
				Type:    RecaptchaV2,
				SiteKey: siteKey,
			}
		}
	}

	// Verificar Cloudflare Turnstile
	if strings.Contains(html, "cf-turnstile") || strings.Contains(html, "challenges.cloudflare.com/turnstile") {
		siteKey := extractDataSiteKey(html, "cf-turnstile")
		if siteKey != "" {
			return &CaptchaInfo{
				Type:    Turnstile,
				SiteKey: siteKey,
			}
		}
	}

	return nil
}

func extractDataSiteKey(html, className string) string {
	pattern := fmt.Sprintf(`class=['"][^'"]*%s[^'"]*['"][^>]*data-sitekey=['"]([^'"]+)['"]`, className)
	re := regexp.MustCompile(pattern)
	matches := re.FindStringSubmatch(html)
	if len(matches) > 1 {
		return matches[1]
	}

	// Patrón alternativo
	pattern = fmt.Sprintf(`data-sitekey=['"]([^'"]+)['"][^>]*class=['"][^'"]*%s`, className)
	re = regexp.MustCompile(pattern)
	matches = re.FindStringSubmatch(html)
	if len(matches) > 1 {
		return matches[1]
	}

	// Patrón genérico de sitekey
	re = regexp.MustCompile(`data-sitekey=['"]([^'"]+)['"]`)
	matches = re.FindStringSubmatch(html)
	if len(matches) > 1 {
		return matches[1]
	}

	return ""
}

// ============================================
// Inyección de token
// ============================================

func InjectToken(page *rod.Page, token string, captchaType CaptchaType) error {
	var js string

	switch captchaType {
	case RecaptchaV2:
		js = fmt.Sprintf(`
			(function() {
				var responseField = document.getElementById('g-recaptcha-response');
				if (responseField) {
					responseField.style.display = 'block';
					responseField.value = '%s';
				}

				var textareas = document.querySelectorAll('textarea[name="g-recaptcha-response"]');
				for (var i = 0; i < textareas.length; i++) {
					textareas[i].value = '%s';
				}

				if (typeof ___grecaptcha_cfg !== 'undefined') {
					var clients = ___grecaptcha_cfg.clients;
					for (var key in clients) {
						var client = clients[key];
						if (client) {
							try {
								var callback = client.callback ||
									(client.Q && client.Q.callback) ||
									(client.S && client.S.callback);
								if (typeof callback === 'function') {
									callback('%s');
								}
							} catch(e) {}
						}
					}
				}
				return true;
			})();
		`, token, token, token)

	case Turnstile:
		js = fmt.Sprintf(`
			(function() {
				var responseField = document.querySelector('[name="cf-turnstile-response"]');
				if (responseField) {
					responseField.value = '%s';
				}

				if (!responseField) {
					var input = document.createElement('input');
					input.type = 'hidden';
					input.name = 'cf-turnstile-response';
					input.value = '%s';
					var form = document.querySelector('form');
					if (form) form.appendChild(input);
				}

				if (window.turnstile && window.turnstileCallback) {
					window.turnstileCallback('%s');
				}
				return true;
			})();
		`, token, token, token)

	default:
		return fmt.Errorf("tipo de CAPTCHA no soportado: %s", captchaType)
	}

	_, err := page.Eval(js)
	return err
}

// ============================================
// Crawler
// ============================================

type CrawlResult struct {
	URL           string
	Title         string
	Success       bool
	CaptchaFound  bool
	CaptchaType   CaptchaType
	CaptchaSolved bool
	Error         string
}

func Crawl(browser *rod.Browser, client *CapsolverClient, targetURL string) *CrawlResult {
	result := &CrawlResult{
		URL:     targetURL,
Éxito: false,
	}

	// Navegar al objetivo
	log.Printf("Navegando a: %s", targetURL)
	page := browser.MustPage(targetURL)
	defer page.MustClose()

	page.MustWaitLoad()
	time.Sleep(2 * time.Second)

	// Obtener HTML de la página
	html := page.MustHTML()

	// Detectar CAPTCHA
	captchaInfo := DetectCaptcha(html)

	if captchaInfo != nil && captchaInfo.Type != Unknown {
		result.CaptchaFound = true
		result.CaptchaType = captchaInfo.Type

		log.Printf("CAPTCHA detectado: %s (siteKey: %s)", captchaInfo.Type, captchaInfo.SiteKey)

		// Resolver CAPTCHA
		log.Println("Resolviendo CAPTCHA con Capsolver...")
		token, err := client.Solve(captchaInfo, targetURL)
		if err != nil {
			result.Error = fmt.Sprintf("falló al resolver CAPTCHA: %v", err)
			log.Printf("Error: %s", result.Error)
			return result
		}

		log.Printf("Token recibido: %s...", token[:min(50, len(token))])

		// Inyectar token
		log.Println("Inyectando token...")
		err = InjectToken(page, token, captchaInfo.Type)
		if err != nil {
			result.Error = fmt.Sprintf("falló al inyectar token: %v", err)
			log.Printf("Error: %s", result.Error)
			return result
		}

		result.CaptchaSolved = true
		log.Println("Token inyectado correctamente!")

		// Intentar enviar formulario
		submitForm(page)
		time.Sleep(3 * time.Second)
	} else {
		log.Println("No se detectó CAPTCHA en la página")
	}

	// Obtener información de la página final
	result.Title = page.MustEval(`document.title`).String()
	result.Success = true

	return result
}

func submitForm(page *rod.Page) {
	selectors := []string{
		"button[type='submit']",
		"input[type='submit']",
		"#recaptcha-demo-submit",
		".submit-button",
	}

	for _, selector := range selectors {
		js := fmt.Sprintf(`
			(function() {
				var btn = document.querySelector('%s');
				if (btn && btn.offsetParent !== null) {
					btn.click();
					return true;
				}
				return false;
			})();
		`, selector)

		result := page.MustEval(js)
		if result.Bool() {
			log.Printf("Hizo clic en el botón de envío: %s", selector)
			return
		}
	}
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

// ============================================
// Principal
// ============================================

func main() {
	// Analizar banderas
	targetURL := flag.String("url", "https://www.google.com/recaptcha/api2/demo", "URL objetivo para navegar")
	headless := flag.Bool("headless", true, "Ejecutar el navegador en modo sin cabeza")
	checkBalance := flag.Bool("balance", false, "Solo verificar el saldo de la cuenta")
	flag.Parse()

	// Verificar clave de API
	if CAPSOLVER_API_KEY == "" {
		log.Fatal("La variable de entorno CAPSOLVER_API_KEY es requerida")
	}

	log.Println("==============================================")
	log.Println("Katana + Capsolver - Crawler de CAPTCHA universal")
	log.Println("==============================================")

	// Inicializar cliente
	client := NewCapsolverClient(CAPSOLVER_API_KEY)

	// Verificar saldo
	balance, err := client.GetBalance()
	if err != nil {
		log.Printf("Advertencia: No se pudo verificar el saldo: %v", err)
	} else {
		log.Printf("Saldo de Capsolver: $%.2f", balance)
	}

	if *checkBalance {
		return
	}

	// Iniciar navegador
	log.Println("Iniciando navegador...")
	path, _ := launcher.LookPath()
	u := launcher.New().Bin(path).Headless(*headless).MustLaunch()
	browser := rod.New().ControlURL(u).MustConnect()
	defer browser.MustClose()

	// Navegar
	result := Crawl(browser, client, *targetURL)

	// Mostrar resultados
	log.Println("==============================================")
	log.Println("RESULTADOS DE NAVEGACIÓN")
	log.Println("==============================================")
	log.Printf("URL: %s", result.URL)
	log.Printf("Título: %s", result.Title)
	log.Printf("Éxito: %v", result.Success)
	log.Printf("CAPTCHA detectado: %v", result.CaptchaFound)
	if result.CaptchaFound {
		log.Printf("Tipo de CAPTCHA: %s", result.CaptchaType)
		log.Printf("CAPTCHA resuelto: %v", result.CaptchaSolved)
	}
	if result.Error != "" {
		log.Printf("Error: %s", result.Error)
	}
	log.Println("==============================================")
}

Uso

bash Copy
# Crear proyecto
mkdir katana-universal-crawler
cd katana-universal-crawler
go mod init katana-universal-crawler

# Instalar dependencias
go get github.com/go-rod/rod@latest

# Establecer clave de API
export CAPSOLVER_API_KEY="SU_CLAVE_DE_API"

# Ejecutar con predeterminado (demo de reCAPTCHA v2)
go run main.go

# Ejecutar con URL personalizada
go run main.go -url "https://ejemplo.com"

# Verificar saldo solo
go run main.go -balance

# Ejecutar con navegador visible
go run main.go -headless=false

Buenas prácticas

1. Optimización del rendimiento

  • Usar tipos de tarea sin proxy: ReCaptchaV2TaskProxyLess utiliza proxies internos de Capsolver para resolver más rápido
  • Procesamiento paralelo: Iniciar la resolución de CAPTCHA mientras otros elementos de la página cargan
  • Caché de tokens: Los tokens de reCAPTCHA son válidos por ~2 minutos; cachear cuando sea posible

2. Gestión de costos

  • Detectar antes de resolver: Llamar a Capsolver solo cuando realmente haya un CAPTCHA
  • Validar claves de sitio: Asegurarse de que las claves extraídas sean válidas antes de las llamadas a la API
  • Monitorear uso: Rastrear llamadas a la API para gestionar costos eficazmente

3. Manejo de errores

go Copy
func SolveWithRetry(client *CapsolverClient, info *CaptchaInfo, url string, maxRetries int) (string, error) {
    var lastErr error

    for i := 0; i < maxRetries; i++ {
        token, err := client.Solve(info, url)
        if err == nil {
            return token, nil
        }

        lastErr = err
        log.Printf("Intento %d falló: %v", i+1, err)

        // Retroalimentación exponencial
        time.Sleep(time.Duration(i+1) * time.Second)
    }

    return "", fmt.Errorf("falló después de %d intentos: %w", maxRetries, lastErr)
}

4. Límites de velocidad

Implementar retrasos adecuados entre solicitudes para evitar detección:

go Copy
type RateLimiter struct {
    requests    int
    interval    time.Duration
    lastRequest time.Time
    mu          sync.Mutex
}

func (r *RateLimiter) Wait() {
    r.mu.Lock()
    defer r.mu.Unlock()

    elapsed := time.Since(r.lastRequest)
    if elapsed < r.interval {
        time.Sleep(r.interval - elapsed)
    }
    r.lastRequest = time.Now()
}

Solución de problemas

Errores comunes

Error Causa Solución
ERROR_ZERO_BALANCE Créditos insuficientes Recargar cuenta de Capsolver
ERROR_CAPTCHA_UNSOLVABLE Clave de sitio inválida Verificar lógica de extracción
ERROR_INVALID_TASK_DATA Parámetros faltantes Verificar estructura de tarea
context deadline exceeded Tiempo de espera Aumentar tiempo de espera o verificar red

Consejos de depuración

  1. Habilitar navegador visible: Establecer Headless(false) para ver lo que ocurre
  2. Registrar tráfico de red: Monitorear solicitudes para identificar problemas
  3. Guardar capturas de pantalla: Capturar estado de la página para depuración
  4. Validar tokens: Registrar formato de token antes de inyectarlo

Preguntas frecuentes

P: ¿Puedo usar Katana sin modo headless para páginas de CAPTCHA?
R: No, las páginas de CAPTCHA requieren renderizado de JavaScript, lo cual solo funciona en modo headless.

P: ¿Cuánto tiempo son válidos los tokens de CAPTCHA?
R: Tokens de reCAPTCHA: ~2 minutos. Turnstile: varía según configuración.

P: ¿Cuál es el tiempo promedio de resolución?
R: reCAPTCHA v2: 5-15s, Turnstile: 1-10s.

P: ¿Puedo usar mi propio proxy?
R: Sí, usar tipos de tarea sin el sufijo "ProxyLess" y proporcionar configuración de proxy.


Conclusión

Integrar Capsolver con Katana permite manejar CAPTCHA robusto para necesidades de raspado web. Los scripts completos anteriores se pueden copiar directamente y usar con proyectos de Go.

¿Listo para comenzar? Regístrese en Capsolver y potencia tus raspadores!

💡 Bonificación exclusiva para usuarios de integración Katana:
Para celebrar esta integración, ofrecemos un código de bonificación del 6% — Katana para todos los usuarios de CapSolver que se registren a través de este tutorial.
Simplemente ingrese el código durante el recarga en el Dashboard para recibir un 6% adicional de crédito de inmediato.


12. Documentación

Aviso de Cumplimiento: La información proporcionada en este blog es solo para fines informativos. CapSolver se compromete a cumplir con todas las leyes y regulaciones aplicables. El uso de la red de CapSolver para actividades ilegales, fraudulentas o abusivas está estrictamente prohibido y será investigado. Nuestras soluciones para la resolución de captcha mejoran la experiencia del usuario mientras garantizan un 100% de cumplimiento al ayudar a resolver las dificultades de captcha durante el rastreo de datos públicos. Fomentamos el uso responsable de nuestros servicios. Para obtener más información, visite nuestros Términos de Servicio y Política de Privacidad.

Máse

Solucionar errores 403 Prohibidos al crawlear sitios web con Python
Resolver errores 403 Prohibido al rastrear sitios web con Python

Aprende cómo superar errores 403 Prohibido al crawlear sitios web con Python. Este guía cubre la rotación de IP, el spoofing de user-agent, la limitación de solicitudes, el manejo de autenticación y el uso de navegadores headless para evadir restricciones de acceso y continuar con el scraping de web con éxito.

web scraping
Logo of CapSolver

Lucas Mitchell

13-Jan-2026

Agno con integración de CapSolver
Cómo resolver Captcha en Agno con integración de CapSolver

Aprende a integrar CapSolver con Agno para resolver desafíos de reCAPTCHA v2/v3, Cloudflare Turnstile y WAF en agentes de IA autónomos. Incluye ejemplos reales de Python para scraping web y automatización.

web scraping
Logo of CapSolver

Adélia Cruz

13-Jan-2026

Cómo resolver Captcha con Katana usando CapSolver
Integración de Katana con CapSolver: Resolución automatizada de CAPTCHA para rastreo de web

Aprende a integrar Katana con Capsolver para resolver automáticamente reCAPTCHA v2 y Cloudflare Turnstile en el crawling sin interfaz.

web scraping
Logo of CapSolver

Adélia Cruz

12-Jan-2026

Mejores bibliotecas de scraping web 2026
Mejores Bibliotecas de Scraping Web 2026

Explora las mejores librerías de scraping web en Python para 2026. Compara características, facilidad de uso y rendimiento para tus necesidades de extracción de datos. Incluye perspectivas de expertos y preguntas frecuentes.

web scraping
Logo of CapSolver

Aloísio Vítor

12-Jan-2026

Cómo resolver Captcha con Crawlab usando CapSolver
Integrar Crawlab con CapSolver: Resolución Automatizada de CAPTCHA para el Rastreo Distribuido

Aprende cómo integrar CapSolver con Crawlab para resolver reCAPTCHA y Cloudflare Turnstile a gran escala.

web scraping
Logo of CapSolver

Adélia Cruz

09-Jan-2026

Cómo eludir el desafío de Cloudflare durante el web scraping en 2025
Cómo sortear el desafío de Cloudflare durante el web scraping en 2026

Aprenda a omitir el desafío de Cloudflare y Turnstile en 2026 para un raspado web sin problemas. Descubra la integración de Capsolver, consejos sobre huellas dactilares TLS y soluciones para errores comunes para evitar el infierno del CAPTCHA. Ahorre tiempo y escale su extracción de datos.

web scraping
Logo of CapSolver

Emma Foster

07-Jan-2026