feat: Implement cookie extraction script for Chrome v20 and enhance browser interaction
This commit is contained in:
+100
-26
@@ -1,54 +1,58 @@
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
import random
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.ScrappingWeb.Tab import Tab
|
||||
from .Tab import Tab
|
||||
|
||||
class ElementoWeb:
|
||||
def __init__(self, tab: "Tab", object_id: str):
|
||||
def __init__(self, tab: "Tab", object_id: Optional[str]):
|
||||
self.tab = tab
|
||||
self.object_id = object_id
|
||||
self._node_id = None # Lazy resolved
|
||||
|
||||
@classmethod
|
||||
def from_node(cls, tab: "Tab", node_id: int) -> "ElementoWeb":
|
||||
inst = cls(tab, object_id=None)
|
||||
inst._node_id = node_id
|
||||
return inst
|
||||
|
||||
async def _asegurar_object_id(self):
|
||||
if not self.object_id and self._node_id:
|
||||
try:
|
||||
resolved = await self.tab._enviar("DOM.resolveNode", {"nodeId": self._node_id})
|
||||
self.object_id = resolved["object"]["objectId"]
|
||||
except Exception as e:
|
||||
print(f"⚠️ No se pudo resolver objectId desde nodeId: {e}")
|
||||
|
||||
async def scroll_into_view(self):
|
||||
try:
|
||||
await self._asegurar_object_id()
|
||||
await self.tab._enviar("Runtime.callFunctionOn", {
|
||||
"objectId": self.object_id,
|
||||
"functionDeclaration": "function() { this.scrollIntoView({block: 'center'}); }",
|
||||
"awaitPromise": True
|
||||
})
|
||||
print("📜 Elemento desplazado a la vista.")
|
||||
if self.tab.verbose:
|
||||
print("📜 Elemento desplazado a la vista.")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al hacer scroll hacia el elemento: {e}")
|
||||
|
||||
@classmethod
|
||||
def from_node(cls, tab: "Tab", node_id: int) -> "ElementoWeb":
|
||||
# Creamos un objectId a partir del nodeId usando DOM.resolveNode
|
||||
cls._node_id = node_id
|
||||
cls._resolved_object_id = None # Lazy resolution opcional
|
||||
return cls(tab, object_id=None)
|
||||
|
||||
async def click(self):
|
||||
try:
|
||||
await self.scroll_into_view()
|
||||
|
||||
# Resolver objectId si es necesario
|
||||
if not self.object_id and hasattr(self, "_node_id"):
|
||||
resolved = await self.tab._enviar("DOM.resolveNode", {"nodeId": self._node_id})
|
||||
self.object_id = resolved["object"]["objectId"]
|
||||
|
||||
await self._asegurar_object_id()
|
||||
if not self.object_id:
|
||||
raise ValueError("No se puede obtener objectId del elemento para hacer click.")
|
||||
|
||||
# Obtener nodeId
|
||||
# Intenta obtener coordenadas del nodo
|
||||
node_result = await self.tab._enviar("DOM.describeNode", {
|
||||
"objectId": self.object_id
|
||||
})
|
||||
|
||||
node_id = node_result["node"]["nodeId"]
|
||||
|
||||
# Obtener coordenadas con fallback
|
||||
try:
|
||||
box_model = await self.tab._enviar("DOM.getBoxModel", {"nodeId": node_id})
|
||||
content = box_model["model"]["content"]
|
||||
@@ -60,7 +64,12 @@ class ElementoWeb:
|
||||
x = (quad[0] + quad[4]) / 2
|
||||
y = (quad[1] + quad[5]) / 2
|
||||
|
||||
# Simular movimiento humano del mouse
|
||||
# 🧠 Enfocar el elemento antes de clickear
|
||||
await self.tab._enviar("DOM.focus", {
|
||||
"objectId": self.object_id
|
||||
})
|
||||
|
||||
# 🎯 Movimiento humanoide opcional
|
||||
start_x, start_y = x + random.uniform(-100, 100), y + random.uniform(-100, 100)
|
||||
steps = random.randint(5, 12)
|
||||
for i in range(1, steps + 1):
|
||||
@@ -73,7 +82,7 @@ class ElementoWeb:
|
||||
})
|
||||
await asyncio.sleep(random.uniform(0.01, 0.05))
|
||||
|
||||
# Click humano
|
||||
# 👆 Mouse Down
|
||||
await self.tab._enviar("Input.dispatchMouseEvent", {
|
||||
"type": "mousePressed",
|
||||
"x": x,
|
||||
@@ -81,7 +90,10 @@ class ElementoWeb:
|
||||
"button": "left",
|
||||
"clickCount": 1
|
||||
})
|
||||
|
||||
await asyncio.sleep(random.uniform(0.05, 0.15))
|
||||
|
||||
# 👇 Mouse Up
|
||||
await self.tab._enviar("Input.dispatchMouseEvent", {
|
||||
"type": "mouseReleased",
|
||||
"x": x,
|
||||
@@ -90,27 +102,89 @@ class ElementoWeb:
|
||||
"clickCount": 1
|
||||
})
|
||||
|
||||
print(f"🖱️ Click humano simulado en ({x:.1f}, {y:.1f})")
|
||||
await asyncio.sleep(random.uniform(0.01, 0.05))
|
||||
|
||||
# 🖱️ Click manual adicional
|
||||
await self.tab._enviar("Input.dispatchMouseEvent", {
|
||||
"type": "mouseClicked",
|
||||
"x": x,
|
||||
"y": y,
|
||||
"button": "left",
|
||||
"clickCount": 1
|
||||
})
|
||||
|
||||
if self.tab.verbose:
|
||||
print(f"🖱️ Click humano simulado en ({x:.1f}, {y:.1f})")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al hacer click físico: {e}")
|
||||
print("🧪 Intentando fallback con JavaScript click()...")
|
||||
await self.click_js()
|
||||
|
||||
|
||||
async def click_js(self):
|
||||
try:
|
||||
await self._asegurar_object_id()
|
||||
if not self.object_id:
|
||||
print("⚠️ No se puede hacer click JS: objectId no disponible.")
|
||||
return
|
||||
await self.tab._enviar("Runtime.callFunctionOn", {
|
||||
"objectId": self.object_id,
|
||||
"functionDeclaration": "function() { this.click(); }",
|
||||
"awaitPromise": True
|
||||
})
|
||||
print("🖱️ Click simulado por JavaScript (element.click())")
|
||||
if self.tab.verbose:
|
||||
print("🖱️ Click simulado por JavaScript (element.click())")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al ejecutar click en JS: {e}")
|
||||
|
||||
async def obtener_texto(self) -> Optional[str]:
|
||||
return await self.tab.evaluar_js(f'document.getElementById("{self.object_id}").textContent')
|
||||
try:
|
||||
await self._asegurar_object_id()
|
||||
result = await self.tab._enviar("Runtime.callFunctionOn", {
|
||||
"objectId": self.object_id,
|
||||
"functionDeclaration": "function() { return this.textContent; }",
|
||||
"returnByValue": True
|
||||
})
|
||||
return result.get("result", {}).get("value")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al obtener texto del elemento: {e}")
|
||||
return None
|
||||
|
||||
async def escribir_texto(self, texto: str):
|
||||
await self.tab.evaluar_js(f'document.getElementById("{self.object_id}").value = "{texto}"')
|
||||
try:
|
||||
await self._asegurar_object_id()
|
||||
await self.tab._enviar("Runtime.callFunctionOn", {
|
||||
"objectId": self.object_id,
|
||||
"functionDeclaration": f"function() {{ this.value = {json.dumps(texto)}; this.dispatchEvent(new Event('input')); }}",
|
||||
"awaitPromise": True
|
||||
})
|
||||
if self.tab.verbose:
|
||||
print(f"⌨️ Texto escrito en elemento: '{texto}'")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al escribir texto: {e}")
|
||||
|
||||
|
||||
async def encontrar_hijo_clickeable(self) -> Optional["ElementoWeb"]:
|
||||
try:
|
||||
await self._asegurar_object_id()
|
||||
resultado = await self.tab._enviar("Runtime.callFunctionOn", {
|
||||
"objectId": self.object_id,
|
||||
"functionDeclaration": """
|
||||
function() {
|
||||
const candidatos = this.querySelectorAll("span, div, a, button");
|
||||
for (const el of candidatos) {
|
||||
const style = window.getComputedStyle(el);
|
||||
const visible = style.display !== "none" && style.visibility !== "hidden";
|
||||
const interactivo = style.pointerEvents !== "none";
|
||||
if (visible && interactivo) return el;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
""",
|
||||
"returnByValue": False
|
||||
})
|
||||
if "result" in resultado and "objectId" in resultado["result"]:
|
||||
return ElementoWeb(self.tab, resultado["result"]["objectId"])
|
||||
except Exception as e:
|
||||
print(f"⚠️ No se pudo encontrar hijo clickeable: {e}")
|
||||
return None
|
||||
@@ -87,9 +87,9 @@ class Navegador:
|
||||
f"--user-data-dir={self.user_data_dir}",
|
||||
"--disable-blink-features=AutomationControlled",
|
||||
"--no-sandbox",
|
||||
"--disable-web-security",
|
||||
# "--disable-web-security",
|
||||
# "--disable-extensions",
|
||||
"--disable-dev-shm-usage",
|
||||
# "--disable-dev-shm-usage",
|
||||
"--disable-infobars",
|
||||
"--disable-popup-blocking",
|
||||
"--disable-default-apps",
|
||||
|
||||
@@ -2,7 +2,10 @@ import aiohttp
|
||||
import websockets
|
||||
import json
|
||||
import asyncio
|
||||
from src.ScrappingWeb.Tab import Tab
|
||||
from .Tab import Tab
|
||||
from typing import Optional
|
||||
|
||||
|
||||
|
||||
class Scrapper:
|
||||
def __init__(self, debugging_url: str = "http://127.0.0.1:9222"):
|
||||
@@ -56,14 +59,80 @@ class Scrapper:
|
||||
|
||||
raise RuntimeError("No se pudo obtener el WebSocket de la nueva pestaña")
|
||||
|
||||
async def nueva_tab(self, url: str, wait_time: float = 5.0) -> Tab:
|
||||
async def nueva_tab(self, url: str = "", wait_time: float = 5.0) -> Tab:
|
||||
websocket_url = await self._crear_tab_websocket_url()
|
||||
tab = await Tab.crear_desde_websocket(websocket_url)
|
||||
self.tabs.append(tab)
|
||||
await tab.navegar(url, wait_time)
|
||||
|
||||
if url:
|
||||
print(f"🌍 Navegando a: {url}")
|
||||
await tab.navegar(url, wait_time)
|
||||
else:
|
||||
print("⚠️ No se especificó URL. La pestaña se creó pero no se navegó a ninguna página.")
|
||||
|
||||
return tab
|
||||
|
||||
async def cerrar_todos(self):
|
||||
for tab in list(self.tabs):
|
||||
await tab.cerrar()
|
||||
self.tabs.clear()
|
||||
self.tabs.clear()
|
||||
|
||||
def get_tab(self, identifier: str) -> Optional[Tab]:
|
||||
"""
|
||||
Devuelve una instancia de Tab según su WebSocket URL o su ID final (extraído del WebSocket URL).
|
||||
Acepta:
|
||||
- ws_url completo: ws://127.0.0.1:9222/devtools/page/XYZ
|
||||
- id directo: XYZ
|
||||
"""
|
||||
for tab in self.tabs:
|
||||
# Comparar directamente contra ws_url
|
||||
if tab.ws_url == identifier:
|
||||
return tab
|
||||
|
||||
# Comparar contra el ID extraído
|
||||
ws_id = tab.ws_url.rsplit("/", 1)[-1]
|
||||
if ws_id == identifier:
|
||||
return tab
|
||||
|
||||
return None
|
||||
|
||||
async def obtener_tabs_existentes(self) -> list[Tab]:
|
||||
"""
|
||||
Recupera todas las pestañas de tipo 'page' que no están ya en self.tabs,
|
||||
las conecta y devuelve como lista. Muestra resumen limpio por consola.
|
||||
"""
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"{self.debugging_url}/json") as resp:
|
||||
if resp.status != 200:
|
||||
raise RuntimeError("No se pudo obtener la lista de pestañas")
|
||||
|
||||
tabs_info = await resp.json()
|
||||
|
||||
print("\n🧾 Pestañas activas (filtradas: solo 'type': 'page'):\n")
|
||||
nuevas_tabs = []
|
||||
for idx, tab_info in enumerate(tabs_info, start=1):
|
||||
tipo = tab_info.get("type")
|
||||
if tipo != "page":
|
||||
continue # Filtrar todo lo que no sea página visible
|
||||
|
||||
ws_url = tab_info.get("webSocketDebuggerUrl")
|
||||
tab_id = tab_info.get("id")
|
||||
title = tab_info.get("title", "<Sin título>")
|
||||
url = tab_info.get("url", "<Sin URL>")
|
||||
|
||||
# Verifica si ya la tienes cargada
|
||||
if any(t.ws_url == ws_url for t in self.tabs):
|
||||
continue
|
||||
|
||||
# Conectar
|
||||
try:
|
||||
tab = await Tab.crear_desde_websocket(ws_url)
|
||||
self.tabs.append(tab)
|
||||
nuevas_tabs.append(tab)
|
||||
except Exception as e:
|
||||
print(f"⚠️ No se pudo conectar a pestaña {tab_id}: {e}")
|
||||
|
||||
if not nuevas_tabs:
|
||||
print("⚠️ No se encontraron nuevas pestañas para agregar.\n")
|
||||
|
||||
return nuevas_tabs
|
||||
+74
-32
@@ -2,21 +2,29 @@ import asyncio
|
||||
import json
|
||||
import base64
|
||||
import websockets
|
||||
from typing import Optional
|
||||
from typing import List
|
||||
from src.ScrappingWeb.ElementoWeb import ElementoWeb
|
||||
from typing import Optional, List
|
||||
from .ElementoWeb import ElementoWeb
|
||||
import os
|
||||
|
||||
|
||||
class Tab:
|
||||
def __init__(self, websocket: websockets.WebSocketClientProtocol, ws_url: str):
|
||||
def __init__(self, websocket: websockets.WebSocketClientProtocol, ws_url: str, verbose: bool = True):
|
||||
self.websocket = websocket
|
||||
self.ws_url = ws_url
|
||||
self._message_id = 0
|
||||
self._pending = {}
|
||||
self._load_event = asyncio.Event()
|
||||
self.verbose = verbose
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
await self.cerrar()
|
||||
|
||||
@classmethod
|
||||
async def crear_desde_websocket(cls, ws_url: str) -> "Tab":
|
||||
websocket = await websockets.connect(ws_url)
|
||||
websocket = await websockets.connect(ws_url, max_size=10 * 1024 * 1024)
|
||||
tab = cls(websocket, ws_url)
|
||||
asyncio.create_task(tab._recibir_eventos())
|
||||
await tab._enviar("Page.enable")
|
||||
@@ -28,11 +36,14 @@ class Tab:
|
||||
data = json.loads(mensaje)
|
||||
if "id" in data and data["id"] in self._pending:
|
||||
future = self._pending.pop(data["id"])
|
||||
future.set_result(data.get("result"))
|
||||
if "result" in data:
|
||||
future.set_result(data["result"])
|
||||
elif "error" in data:
|
||||
future.set_exception(Exception(data["error"]))
|
||||
elif data.get("method") == "Page.loadEventFired":
|
||||
self._load_event.set()
|
||||
|
||||
async def _enviar(self, metodo: str, parametros: Optional[dict] = None) -> dict:
|
||||
async def _enviar(self, metodo: str, parametros: Optional[dict] = None, timeout: float = 10.0) -> dict:
|
||||
self._message_id += 1
|
||||
msg_id = self._message_id
|
||||
mensaje = {
|
||||
@@ -44,15 +55,17 @@ class Tab:
|
||||
future = asyncio.get_event_loop().create_future()
|
||||
self._pending[msg_id] = future
|
||||
await self.websocket.send(json.dumps(mensaje))
|
||||
return await future
|
||||
return await asyncio.wait_for(future, timeout=timeout)
|
||||
|
||||
async def navegar(self, url: str, wait_time: float = 5.0):
|
||||
self._load_event.clear()
|
||||
print(f"🌍 Navegando a: {url}")
|
||||
if self.verbose:
|
||||
print(f"🌍 Navegando a: {url}")
|
||||
await self._enviar("Page.navigate", {"url": url})
|
||||
try:
|
||||
await asyncio.wait_for(self._load_event.wait(), timeout=wait_time)
|
||||
print("✅ Página cargada correctamente.")
|
||||
if self.verbose:
|
||||
print("✅ Página cargada correctamente.")
|
||||
except asyncio.TimeoutError:
|
||||
print(f"⚠️ Tiempo de espera agotado ({wait_time}s) al cargar la página.")
|
||||
|
||||
@@ -62,11 +75,40 @@ class Tab:
|
||||
"expression": js_code,
|
||||
"returnByValue": True
|
||||
})
|
||||
return result["result"]["value"]
|
||||
if "exceptionDetails" in result:
|
||||
raise Exception(result["exceptionDetails"])
|
||||
return result.get("result", {}).get("value")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al ejecutar JS: {e}")
|
||||
return None
|
||||
|
||||
async def inyectar_archivo_js(self, ruta_archivo: str, reemplazos: dict = None) -> Optional[str]:
|
||||
if not os.path.exists(ruta_archivo):
|
||||
print(f"❌ Archivo JS no encontrado: {ruta_archivo}")
|
||||
return None
|
||||
|
||||
with open(ruta_archivo, "r", encoding="utf-8") as f:
|
||||
js_code = f.read()
|
||||
|
||||
if reemplazos:
|
||||
for key, value in reemplazos.items():
|
||||
js_code = js_code.replace(f"{{{{{key}}}}}", str(value))
|
||||
|
||||
# 🔧 Eliminamos el `return` externo
|
||||
js_code_final = f"(async () => {{\n{js_code}\n}})();"
|
||||
|
||||
try:
|
||||
result = await self._enviar("Runtime.evaluate", {
|
||||
"expression": js_code_final,
|
||||
"returnByValue": True
|
||||
})
|
||||
if "exceptionDetails" in result:
|
||||
raise Exception(result["exceptionDetails"])
|
||||
return result.get("result", {}).get("value")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al inyectar JS desde {ruta_archivo}: {e}")
|
||||
return None
|
||||
|
||||
async def obtener_user_agent(self) -> Optional[str]:
|
||||
return await self.evaluar_js("navigator.userAgent")
|
||||
|
||||
@@ -76,66 +118,57 @@ class Tab:
|
||||
data = result["data"]
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(base64.b64decode(data))
|
||||
print(f"📸 Screenshot guardado como {output_path}")
|
||||
if self.verbose:
|
||||
print(f"📸 Screenshot guardado como {output_path}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al capturar screenshot: {e}")
|
||||
|
||||
async def cerrar(self):
|
||||
try:
|
||||
await self.websocket.close()
|
||||
print("🛑 WebSocket cerrado.")
|
||||
if not self.websocket.closed:
|
||||
await self.websocket.close()
|
||||
if self.verbose:
|
||||
print("🛑 WebSocket cerrado.")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al cerrar pestaña: {e}")
|
||||
|
||||
async def obtener_html_completo(self) -> Optional[str]:
|
||||
"""
|
||||
Devuelve el HTML completo de la página actual.
|
||||
"""
|
||||
try:
|
||||
result = await self._enviar("Runtime.evaluate", {
|
||||
"expression": "document.documentElement.outerHTML",
|
||||
"returnByValue": True
|
||||
})
|
||||
html = result["result"]["value"]
|
||||
print("📄 HTML completo obtenido.")
|
||||
return html
|
||||
return result.get("result", {}).get("value")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al obtener HTML: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def obtener_dominio(self) -> Optional[str]:
|
||||
"""
|
||||
Devuelve el dominio (hostname) de la página actual, por ejemplo: 'example.com'.
|
||||
"""
|
||||
try:
|
||||
dominio = await self.evaluar_js("window.location.hostname")
|
||||
print(f"🌐 Dominio actual: {dominio}")
|
||||
if self.verbose and dominio:
|
||||
print(f"🌐 Dominio actual: {dominio}")
|
||||
return dominio
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al obtener dominio: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def get_element_by_selector_node(self, selector: str) -> Optional["ElementoWeb"]:
|
||||
try:
|
||||
# Obtener nodo raíz del documento
|
||||
doc = await self._enviar("DOM.getDocument")
|
||||
root_node_id = doc["root"]["nodeId"]
|
||||
|
||||
# Buscar el nodo desde el DOM (más confiable que Runtime.evaluate)
|
||||
result = await self._enviar("DOM.querySelector", {
|
||||
"nodeId": root_node_id,
|
||||
"selector": selector
|
||||
})
|
||||
node_id = result["nodeId"]
|
||||
node_id = result.get("nodeId")
|
||||
|
||||
if not node_id:
|
||||
print(f"⚠️ Nodo no encontrado con selector: {selector}")
|
||||
return None
|
||||
|
||||
return ElementoWeb.from_node(self, node_id=node_id)
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al buscar nodo desde DOM.querySelector: {e}")
|
||||
return None
|
||||
@@ -157,8 +190,17 @@ class Tab:
|
||||
for prop in props["result"]:
|
||||
if "value" in prop and "objectId" in prop["value"]:
|
||||
elementos.append(ElementoWeb(self, prop["value"]["objectId"]))
|
||||
print(f"🔍 Se encontraron {len(elementos)} elementos con el selector CSS '{selector}'.")
|
||||
if self.verbose:
|
||||
print(f"🔍 Se encontraron {len(elementos)} elementos con el selector CSS '{selector}'.")
|
||||
return elementos
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al buscar elementos por selector CSS '{selector}': {e}")
|
||||
return []
|
||||
return []
|
||||
|
||||
async def enfocar(self):
|
||||
try:
|
||||
await self._enviar("Page.bringToFront")
|
||||
if self.verbose:
|
||||
print("🪟 Pestaña enfocada (bringToFront).")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al enfocar pestaña: {e}")
|
||||
|
||||
Reference in New Issue
Block a user