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

Adélia Cruz
Neural Network Developer
12-Jan-2026

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
# Requiere Go 1.24+
CGO_ENABLED=1 go install github.com/projectdiscovery/katana/cmd/katana@latest
Uso básico
bash
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:
- Crear tarea: Enviar parámetros de CAPTCHA (tipo, siteKey, URL)
- Obtener ID de tarea: Recibir un identificador único de tarea
- Consultar resultado: Verificar el estado de la tarea hasta que la solución esté lista
- Recibir token: Obtener el token de CAPTCHA resuelto
Requisitos previos
Antes de comenzar, asegúrate de tener:
- Go 1.24+ instalado
- Clave de API de Capsolver - Regístrate aquí
- Navegador Chrome (para el modo sin cabeza)
Establece tu clave de API como variable de entorno:
bash
export CAPSOLVER_API_KEY="TU_CLAVE_DE_API"
Arquitectura de integración
┌─────────────────────────┐
│ 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
// 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
# 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
// 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,
}
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,
}
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_-]+)['"],
}
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';
}
// 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")
}
// 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
}
### 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
# 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:
ReCaptchaV2TaskProxyLessutiliza 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
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
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
- Habilitar navegador visible: Establecer
Headless(false)para ver lo que ocurre - Registrar tráfico de red: Monitorear solicitudes para identificar problemas
- Guardar capturas de pantalla: Capturar estado de la página para depuración
- 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

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.

Lucas Mitchell
13-Jan-2026

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.

Adélia Cruz
13-Jan-2026

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.

Adélia Cruz
12-Jan-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.

Aloísio Vítor
12-Jan-2026

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.

Adélia Cruz
09-Jan-2026

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.

Emma Foster
07-Jan-2026


