feat(browser): funciones anti-deteccion + perfiles para web_scraping
Funciones nuevas del dominio browser (grupo navegator): - cdp_move_mouse_human / cdp_click_human: movimiento de raton con curva de Bezier cubica, easing y micro-jitter para imitar comportamiento humano y reducir deteccion de automatizacion. - cdp_wait_idle: espera network-idle contando requests en vuelo via eventos CDP Network.*; inmune a extensiones que mutan el DOM (Dark Reader, uBlock) y a animaciones JS. - list_chrome_profiles: lista perfiles de un user-data-dir (extensiones, nombre legible, preferencias). - prepare_chrome_profile (bash): clona un user-data-dir conservando solo una whitelist de extensiones (default uBlock Origin Lite). Modificadas: - chrome_launch: Linux-first (chromium/google-chrome/brave antes que chrome.exe), KeepExtensions y Setpgid para matar el arbol con cdp_close. - cdp_close: kill por grupo de proceso. Todas con tests verdes (go test ./functions/browser ok).
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CdpClickHuman hace click en el elemento identificado por selector CSS con
|
||||
// movimiento humano: obtiene el bbox, calcula un punto destino ligeramente
|
||||
// desplazado del centro, mueve el ratón por una trayectoria de Bézier cúbica
|
||||
// y luego despacha mousePressed/mouseReleased con una micro-pausa entre ellos.
|
||||
//
|
||||
// opts controla la trayectoria del movimiento previo al click.
|
||||
// Para configurar el origen del movimiento usa opts.FromX / opts.FromY.
|
||||
func CdpClickHuman(c *CDPConn, selector string, opts MouseHumanOpts) error {
|
||||
if c == nil {
|
||||
return fmt.Errorf("cdp click human: conexion nula")
|
||||
}
|
||||
|
||||
// Obtener bounding box del selector
|
||||
js := fmt.Sprintf(`(function() {
|
||||
var el = document.querySelector(%q);
|
||||
if (!el) return null;
|
||||
var r = el.getBoundingClientRect();
|
||||
return JSON.stringify({x: r.left, y: r.top, w: r.width, h: r.height});
|
||||
})()`, selector)
|
||||
|
||||
bboxStr, err := CdpEvaluate(c, js)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cdp click human: obtener bbox de %q: %w", selector, err)
|
||||
}
|
||||
if bboxStr == "" || bboxStr == "null" {
|
||||
return fmt.Errorf("cdp click human: elemento %q no encontrado en el DOM", selector)
|
||||
}
|
||||
|
||||
bboxStr = strings.Trim(bboxStr, `"`)
|
||||
bx, by, bw, bh, err := parseBbox(bboxStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cdp click human: parsear bbox %q: %w", bboxStr, err)
|
||||
}
|
||||
|
||||
// Scroll al elemento para que sea visible
|
||||
scrollJS := fmt.Sprintf(`document.querySelector(%q).scrollIntoView({block:'center'})`, selector)
|
||||
if _, err := CdpEvaluate(c, scrollJS); err != nil {
|
||||
_ = err // no fatal
|
||||
}
|
||||
|
||||
// Punto destino: centro + pequeño offset aleatorio (±15% del tamaño)
|
||||
offX := (rand.Float64()*2 - 1) * bw * 0.15
|
||||
offY := (rand.Float64()*2 - 1) * bh * 0.15
|
||||
toX := bx + bw/2 + offX
|
||||
toY := by + bh/2 + offY
|
||||
|
||||
// Mover el ratón con trayectoria humana
|
||||
if err := CdpMoveMouseHuman(c, toX, toY, opts); err != nil {
|
||||
return fmt.Errorf("cdp click human: mover raton: %w", err)
|
||||
}
|
||||
|
||||
// mousePressed
|
||||
clickParams := map[string]any{
|
||||
"type": "mousePressed",
|
||||
"x": toX,
|
||||
"y": toY,
|
||||
"button": "left",
|
||||
"clickCount": 1,
|
||||
}
|
||||
if _, err := c.sendCDP("Input.dispatchMouseEvent", clickParams); err != nil {
|
||||
return fmt.Errorf("cdp click human: mousePressed: %w", err)
|
||||
}
|
||||
|
||||
// Micro-pausa humana entre press y release (30–90 ms)
|
||||
pauseMs := 30 + rand.Intn(61)
|
||||
time.Sleep(time.Duration(pauseMs) * time.Millisecond)
|
||||
|
||||
// mouseReleased
|
||||
clickParams["type"] = "mouseReleased"
|
||||
if _, err := c.sendCDP("Input.dispatchMouseEvent", clickParams); err != nil {
|
||||
return fmt.Errorf("cdp click human: mouseReleased: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseBbox extrae left, top, width, height de un JSON como {"x":10,"y":20,"w":100,"h":40}.
|
||||
func parseBbox(s string) (left, top, width, height float64, err error) {
|
||||
// Reutiliza el mismo parser manual que parseCoords para evitar encoding/json
|
||||
s = strings.TrimSpace(s)
|
||||
s = strings.TrimPrefix(s, "{")
|
||||
s = strings.TrimSuffix(s, "}")
|
||||
|
||||
for part := range strings.SplitSeq(s, ",") {
|
||||
kv := strings.SplitN(strings.TrimSpace(part), ":", 2)
|
||||
if len(kv) != 2 {
|
||||
continue
|
||||
}
|
||||
k := strings.Trim(strings.TrimSpace(kv[0]), `"`)
|
||||
var v float64
|
||||
if _, e := fmt.Sscanf(strings.TrimSpace(kv[1]), "%f", &v); e != nil {
|
||||
err = fmt.Errorf("parsear valor %q: %w", kv[1], e)
|
||||
return
|
||||
}
|
||||
switch k {
|
||||
case "x":
|
||||
left = v
|
||||
case "y":
|
||||
top = v
|
||||
case "w":
|
||||
width = v
|
||||
case "h":
|
||||
height = v
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user