Ir al contenido principal

Web scraping (parte 3)

 Requiero una introducción a Playwright con Python.

Leer 11 páginas web

Playwright es una biblioteca de automatización para navegadores moderna y potente, creada por Microsoft. Si tu proyecto de scraping implica lidiar con sitios web complejos y dinámicos (como el que describes para tu controlador de leads), Playwright puede ser tu mejor aliado. A continuación, te presento una introducción práctica para empezar a usarlo con Python.

🤔 ¿Por qué Playwright y no Selenium?

Ambas herramientas pueden controlar navegadores de forma automática, pero Playwright fue diseñado para resolver muchos de los dolores de cabeza de Selenium. Comparado con Selenium, Playwright destaca por su velocidad, ya que es notablemente más rápido en la ejecución de tareas, y por su sistema de espera automática, que evita que tu script falle porque un elemento aún no ha cargado, ahorrándote tener que escribir time.sleep() constantemente. Es una alternativa moderna que simplifica la automatización y el scraping complejo.

🛠️ Paso a Paso para tu Primer Proyecto con Playwright

1. Verifica tu entorno

Antes de empezar, asegúrate de tener Python instalado. Abre tu terminal y escribe:

bash
python --version
# o
python3 --version

Deberías ver una versión de Python 3.7 o superior. Si no es así, descárgalo desde la página oficial de Python.

2. Instalación (Solo dos comandos)

La instalación es muy sencilla. Primero, instala la librería de Playwright y luego descarga los navegadores que usarás para la automatización.

bash
# 1. Instalar la librería Playwright
pip install playwright

# 2. Instalar los navegadores (Chromium, Firefox, WebKit)
playwright install

Consejo: Si estás en China y la descarga de navegadores es lenta, puedes usar un espejo (mirror) para acelerar el proceso.

3. Escribe tu Primer Script (el "Hola Mundo")

Crea un archivo, por ejemplo, primer_scraping.py, y escribe el siguiente código que automatiza una búsqueda en Google:

python
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    # Lanza el navegador Chromium en modo visible para que veas la acción
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()

    # Ve a la URL de Google
    page.goto("https://www.google.com")

    # Encuentra la caja de búsqueda, escribe "Playwright" y presiona Enter
    page.locator('textarea[name="q"]').fill("Playwright Python")
    page.locator('textarea[name="q"]').press("Enter")

    # Espera a que carguen los resultados y toma una captura de pantalla
    page.wait_for_load_state("networkidle")
    page.screenshot(path="resultado_google.png")

    # Cierra el navegador
    browser.close()

Este script lanza un navegador, realiza una búsqueda en Google, espera a que la página cargue completamente y guarda una captura de pantalla llamada resultado_google.png.

🧠 Entendiendo los Conceptos Clave

  • API Síncrona vs. Asíncrona: Playwright te ofrece dos formas de escribir tu código.

    • Síncrona (sync_api): Es la más sencilla y directa. El código se ejecuta paso a paso y es ideal para la mayoría de los proyectos de scraping. Los ejemplos anteriores usan este enfoque.

    • Asíncrona (async_api): Es más avanzada y está pensada para proyectos que requieren manejar muchas tareas al mismo tiempo (concurrencia) para ser más eficientes.

  • Localizadores (Locators): Para interactuar con elementos de una página (botones, cajas de texto), necesitas "localizarlos". Playwright ofrece múltiples estrategias robustas y legibles.

    • page.locator('css_selector'): Usa selectores CSS.

    • page.get_by_text("Texto del elemento"): Busca por el texto visible.

    • page.get_by_role("button", name="Enviar"): Busca por su rol (recomendado por su estabilidad).

    • page.get_by_label("Usuario"): Busca por la etiqueta de un formulario.

  • Esperas Automáticas (Auto-Waiting): Esta es una de las características estrella de Playwright. Cuando usas un localizador para hacer click() o fill(), Playwright espera automáticamente a que el elemento exista y esté listo para la interacción, sin que tengas que añadir pausas manuales en tu código.

🚀 Herramientas que te Facilitarán la Vida

  • Playwright Inspector y Grabador (codegen): Esta herramienta es increíble para empezar. Al ejecutar playwright codegen en tu terminal, se abrirá un navegador y, mientras tú haces clics y escribes, Playwright irá generando el código Python correspondiente. Es una forma muy práctica de aprender la sintaxis y crear scripts rápidamente.

⚙️ Modo de Depuración

Cuando estés desarrollando, usa el parámetro slow_mo en launch() para ralentizar las acciones y ver qué está pasando. Esto es muy útil para depurar errores.

python
# En el script de ejemplo
browser = p.chromium.launch(headless=False, slow_mo=500) # 500ms de pausa entre acciones

En resumen, Playwright con Python te da un control total sobre navegadores modernos con una API limpia y potente. Es una herramienta que, sin duda, te ayudará a superar los desafíos de scraping de tu proyecto.

¿Te gustaría que profundice en algún aspecto concreto, como usar localizadores más complejos o manejar el escenario de los CAPTCHA con Playwright?

En la documentación oficial se guía al lector a usar Playwright con Pytest argumentando que la principal intención de Playwright es desarrollar end-to-end tests. Yo no estoy seguro de si mi requerimiento involucra usar Playwright de esa manera.
Leer 15 páginas web

Tu intuición es correcta. Aunque la documentación oficial de Playwright está muy orientada a pruebas, no estás obligado a usarlo de esa manera. La belleza de Playwright es que es una herramienta de automatización de navegadores muy versátil, y para tu proyecto de scraping y leads, puedes usarla de la forma que mejor se adapte a tus necesidades.

Vamos a verlo en detalle para que tomes la mejor decisión.

🤔 ¿Por qué la documentación enfatiza las pruebas (E2E)?

La razón principal es ecosistema y herencia, no una limitación técnica.

  • El caso de uso principal: Playwright fue creado por Microsoft para ser una herramienta de pruebas end-to-end (E2E) confiable y moderna, y en eso es excelente . Su objetivo es automatizar navegadores para simular usuarios reales en entornos de pruebas.

  • La elección de Pytest en Python: La comunidad de Python ya tenía un estándar de facto para pruebas: Pytest. En lugar de crear su propio sistema desde cero para Python, el equipo de Playwright creó el plugin pytest-playwright, que integra de forma nativa las potentes funcionalidades de Playwright con la estructura y los fixtures de Pytest . Por eso, la documentación te guía hacia este camino: es la forma más "oficial" y estructurada de usar Playwright en Python, especialmente para quienes vienen del mundo del testing.

🛠️ Dos formas de usar Playwright para tu proyecto

Tienes dos caminos perfectamente válidos:

1. Scripts Independientes (El camino "libre")

Este es el que más se ajusta a lo que tienes en mente. Consiste en escribir scripts de Python que usen directamente la API de Playwright (sync_playwright o async_playwright) para controlar el navegador y realizar tareas de automatización.

  • Cómo se hace: Creas un archivo .py, importas playwright y controlas el ciclo de vida del navegador (abrirlo, navegar, interactuar, cerrar) dentro de un bloque with sync_playwright() as p: .

  • Ventajas:

    • Máxima flexibilidad: Tienes control total sobre el flujo de tu script, sin la estructura "impuesta" por un test runner.

    • Menor curva de aprendizaje inicial: Te centras en la lógica de tu scraper, no en cómo estructurar pruebas.

  • Para tu caso: Es perfecto para un script que:

    1. Recibe un número de documento.

    2. Abre el navegador con Playwright.

    3. Llena el formulario en la web.

    4. Resuelve el desafío matemático (usando la lógica que ya hemos diseñado).

    5. Extrae el resultado (activo/inactivo).

    6. Guarda el resultado y cierra el navegador.

Este es, probablemente, el enfoque más directo para la fase de prototipado y para un proyecto unipersonal.

2. Pytest + Plugin pytest-playwright (El camino "estructurado")

Este enfoque integra Playwright con Pytest. Cada "prueba" que escribes es, en realidad, una automatización que se ejecuta en un contexto aislado.

  • Cómo se hace: Usas los fixtures que el plugin te proporciona, como page o browser, directamente en tus funciones de prueba . Playwright se encarga de abrir y cerrar el navegador por ti.

  • Ventajas:

    • Fixtures y aislamiento: El plugin se encarga de la gestión del navegador, y cada prueba tiene su propio contexto de navegador, evitando que el estado de una afecte a otra .

    • Integración con herramientas: Puedes usar fácilmente plugins de Pytest para ejecución en paralelo (pytest-xdist), generar reportes, etc. .

  • Para tu caso: Podrías escribir "pruebas" que, en realidad, verifican el estado de un lead. Cada lead sería un escenario de prueba. Aunque esto es técnicamente posible, puede resultar menos natural que un script simple.

En resumen: Pytest es una opción, no una imposición.  dejan claro que pytest-playwright es un plugin sobre el núcleo de Playwright, y puedes usar este último de forma totalmente independiente. Wikipedia también lo define como una "librería de automatización para pruebas y web scraping" .

💡 Decisión Final para tu Proyecto

Para tu proyecto, donde el objetivo es un scraper funcional y no un test suite, te recomiendo que empieces con un script independiente.

  • Script independiente: Es más rápido de escribir y depurar, y te da control total sobre el flujo de trabajo. Una vez que tengas la lógica de scraping funcionando perfectamente, podrías, si lo deseas, envolverlo en una función para que Laravel lo llame.

  • Si luego necesitas escalar: Si tu proyecto crece y necesitas procesar muchos leads en paralelo o generar reportes detallados de fallos, entonces considerar el enfoque con Pytest puede tener sentido para estructurar y mantener el código. Pero para empezar, no lo necesitas.

¿Te gustaría que te ayude a diseñar la estructura de un script Python con Playwright para tu caso específico (abrir web, llenar formulario, resolver desafío) que puedas probar de forma aislada?

Sí.

Perfecto. Vamos a diseñar la estructura de un script Python con Playwright para tu caso específico. Te daré una base sólida que puedas probar y adaptar a tus necesidades.


🏗️ Estructura Base del Script

python
from playwright.sync_api import sync_playwright
import re
import time
from datetime import datetime

class LeadScraper:
    def __init__(self, headless=False, slow_mo=100):
        """
        Inicializa el scraper.
        
        Args:
            headless: True para ejecutar sin interfaz gráfica, False para ver el navegador
            slow_mo: Milisegundos de pausa entre acciones (útil para depuración)
        """
        self.headless = headless
        self.slow_mo = slow_mo
        self.browser = None
        self.page = None
        
    def iniciar_navegador(self):
        """Inicia el navegador y abre una nueva página"""
        self.playwright = sync_playwright().start()
        self.browser = self.playwright.chromium.launch(
            headless=self.headless,
            slow_mo=self.slow_mo
        )
        self.page = self.browser.new_page()
        
    def cerrar_navegador(self):
        """Cierra el navegador y limpia recursos"""
        if self.browser:
            self.browser.close()
        if self.playwright:
            self.playwright.stop()
            
    def navegar_a_consulta(self, url):
        """Navega a la página de consulta"""
        self.page.goto(url)
        # Espera a que la página cargue completamente
        self.page.wait_for_load_state("networkidle")
        
    def llenar_formulario(self, numero_documento):
        """
        Llena el campo del número de documento.
        NOTA: Los selectores son ejemplos; deberás ajustarlos.
        """
        # Buscar el campo de documento (ajusta el selector)
        campo_documento = self.page.locator('input[name="documento"]')
        campo_documento.fill(numero_documento)
        
    def extraer_desafio(self):
        """
        Extrae el texto del desafío matemático o CAPTCHA.
        NOTA: Los selectores son ejemplos; deberás ajustarlos.
        """
        try:
            # Busca el texto del desafío (ajusta el selector)
            desafio_element = self.page.locator('.captcha-text, .math-challenge, #captcha-text')
            desafio_texto = desafio_element.text_content()
            print(f"📝 Desafío encontrado: {desafio_texto}")
            return desafio_texto
        except Exception as e:
            print(f"❌ No se pudo extraer el desafío: {e}")
            return None
            
    def resolver_desafio_matematico(self, texto_desafio):
        """
        Resuelve el desafío matemático usando la lógica que desarrollamos.
        """
        # Aquí va la lógica que ya hemos diseñado
        # (función resolver_captcha que vimos antes)
        return self.resolver_captcha(texto_desafio)
        
    def resolver_captcha(self, texto):
        """Versión simplificada - Puedes expandirla con tu lógica completa"""
        # Extrae números y operación
        patron = r'(\d+)\s*([+\-×÷*/])\s*(\d+)'
        match = re.search(patron, texto)
        
        if not match:
            return None
            
        num1 = int(match.group(1))
        operador = match.group(2)
        num2 = int(match.group(3))
        
        # Verifica si hay instrucción de exclusión
        if "no tenga en cuenta" in texto.lower() or "ignore" in texto.lower():
            # Busca qué número ignorar
            patron_ignorar = r'(?:no tenga en cuenta|ignore)(?:\s+el\s+número)?\s+(\d+)'
            match_ignorar = re.search(patron_ignorar, texto.lower())
            if match_ignorar:
                numero_a_ignorar = int(match_ignorar.group(1))
                if num1 == numero_a_ignorar:
                    num1 = 0
                if num2 == numero_a_ignorar:
                    num2 = 0
                    
        # Resuelve la operación
        ops = {
            '+': lambda a, b: a + b,
            '-': lambda a, b: a - b,
            '×': lambda a, b: a * b,
            '*': lambda a, b: a * b,
            '÷': lambda a, b: a // b,
            '/': lambda a, b: a // b
        }
        
        resultado = ops.get(operador, lambda a, b: a + b)(num1, num2)
        print(f"🧮 Resultado calculado: {resultado}")
        return str(resultado)
        
    def enviar_respuesta_desafio(self, respuesta):
        """
        Envía la respuesta del desafío.
        NOTA: Los selectores son ejemplos; deberás ajustarlos.
        """
        try:
            campo_respuesta = self.page.locator('input[name="captcha"], input[name="resultado"]')
            campo_respuesta.fill(respuesta)
            
            # Envía el formulario (puede ser un botón o presionar Enter)
            boton_enviar = self.page.locator('button[type="submit"], input[type="submit"]')
            boton_enviar.click()
            print("✅ Desafío enviado")
            return True
        except Exception as e:
            print(f"❌ Error al enviar respuesta: {e}")
            return False
            
    def extraer_resultado_lead(self):
        """
        Extrae si el lead está activo o inactivo de la página de resultados.
        NOTA: Los selectores son ejemplos; deberás ajustarlos.
        """
        # Espera a que cargue la página de resultados
        self.page.wait_for_load_state("networkidle")
        
        # Busca indicadores de "activo" o "inactivo" en la página
        pagina_html = self.page.content()
        
        # Detecta palabras clave en el HTML (ajusta según la página real)
        if "activo" in pagina_html.lower() or "encontrado" in pagina_html.lower():
            return "activo"
        elif "inactivo" in pagina_html.lower() or "no encontrado" in pagina_html.lower() or "error" in pagina_html.lower():
            return "inactivo"
        else:
            return "desconocido"
            
    def procesar_lead(self, numero_documento, url_consulta):
        """
        Flujo principal para procesar un lead.
        """
        try:
            print(f"🚀 Procesando lead: {numero_documento}")
            
            # 1. Navegar a la página
            self.navegar_a_consulta(url_consulta)
            
            # 2. Llenar formulario
            self.llenar_formulario(numero_documento)
            
            # 3. Extraer desafío
            desafio_texto = self.extraer_desafio()
            if not desafio_texto:
                print("⚠️ No se pudo extraer el desafío. Requiere intervención manual.")
                return {"estado": "error", "motivo": "desafío_no_encontrado"}
                
            # 4. Resolver desafío
            respuesta = self.resolver_desafio_matematico(desafio_texto)
            
            if respuesta is None:
                print("⚠️ No se pudo resolver automáticamente. Requiere intervención manual.")
                # Toma captura de pantalla para revisión manual
                self.page.screenshot(path=f"captcha_manual_{numero_documento}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png")
                return {"estado": "pendiente_manual", "motivo": "desafío_no_resuelto"}
                
            # 5. Enviar respuesta
            if not self.enviar_respuesta_desafio(respuesta):
                return {"estado": "error", "motivo": "fallo_envio_desafio"}
                
            # 6. Extraer resultado
            # Espera breve para que la página procese la respuesta
            time.sleep(2)
            resultado = self.extraer_resultado_lead()
            
            print(f"✅ Lead {numero_documento} procesado: {resultado}")
            return {"estado": "completado", "resultado": resultado}
            
        except Exception as e:
            print(f"❌ Error procesando lead {numero_documento}: {e}")
            return {"estado": "error", "motivo": str(e)}

# --- Función Principal ---
def main():
    """
    Función principal para ejecutar el scraper.
    """
    # Configuración
    URL_CONSULTA = "https://ejemplo.com/consulta"  # ¡CAMBIA ESTO!
    NUMERO_DOCUMENTO = "12345678"  # ¡CAMBIA ESTO!
    
    # Crea el scraper (headless=False para ver el navegador)
    scraper = LeadScraper(headless=False, slow_mo=300)
    
    try:
        scraper.iniciar_navegador()
        resultado = scraper.procesar_lead(NUMERO_DOCUMENTO, URL_CONSULTA)
        print(f"\n📊 Resultado final: {resultado}")
    finally:
        scraper.cerrar_navegador()

if __name__ == "__main__":
    main()

📋 Pasos para Probar el Script

1. Configuración Inicial

bash
# Instala Playwright si no lo has hecho
pip install playwright
playwright install

2. Ajusta los Selectores

Los selectores en el script son genéricos. Deberás inspeccionar la página real y reemplazar:

Lugar en el scriptSelector ejemploQué debes cambiar
llenar_formulario()'input[name="documento"]'El selector real del campo de documento
extraer_desafio()'.captcha-text'El selector real donde aparece el desafío
enviar_respuesta_desafio()'input[name="captcha"]'El selector real del campo de respuesta
enviar_respuesta_desafio()'button[type="submit"]'El selector real del botón de enviar

3. Cómo encontrar los selectores reales

  1. Abre la página web en Chrome o Firefox.

  2. Haz clic derecho en el elemento que te interesa (campo de texto, botón, etc.).

  3. Selecciona "Inspeccionar" o "Inspeccionar elemento".

  4. En las herramientas de desarrollo, busca el atributo id, name, class o una combinación que identifique únicamente al elemento.

Ejemplos de selectores que podrías usar:

  • Por ID: '#documento_input'

  • Por nombre: 'input[name="documento"]'

  • Por clase: '.campo-documento'

  • Por texto visible: 'button:has-text("Enviar")'

4. Ejecuta y Observa

bash
python lead_scraper.py

Con headless=False verás el navegador abrirse y realizar las acciones paso a paso, lo que te permite depurar visualmente.


🚀 Próximos Pasos para Integrar con Laravel

Una vez que el script funcione correctamente de forma aislada, puedes integrarlo con Laravel usando cualquiera de estos enfoques:

Opción 1: Llamada directa desde PHP

php
// En tu controlador o job de Laravel
$documento = $lead->documento;
$resultado = shell_exec("python3 /ruta/al/script.py $documento 2>&1");
// Procesa $resultado (JSON, texto, etc.)

Opción 2: API HTTP (recomendado para mayor control)

python
# script_api.py - Versión con Flask
from flask import Flask, request, jsonify
from lead_scraper import LeadScraper

app = Flask(__name__)

@app.route('/procesar', methods=['POST'])
def procesar():
    data = request.json
    documento = data.get('documento')
    url = data.get('url')
    
    scraper = LeadScraper(headless=True)
    scraper.iniciar_navegador()
    resultado = scraper.procesar_lead(documento, url)
    scraper.cerrar_navegador()
    
    return jsonify(resultado)

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=5000)

Luego desde Laravel:

php
use Illuminate\Support\Facades\Http;

$response = Http::post('http://127.0.0.1:5000/procesar', [
    'documento' => $lead->documento,
    'url' => config('app.consulta_url')
]);

$resultado = $response->json();

💡 Consejos para el Desarrollo

  1. Usa headless=False durante el desarrollo para ver qué está pasando.

  2. Añade slow_mo=300 para que las acciones sean más lentas y puedas seguir el flujo.

  3. Guarda capturas de pantalla cuando ocurran errores, te ayudarán a depurar.

  4. Loggea todo: guarda el texto del desafío, la respuesta calculada, el resultado extraído, etc.

  5. Prueba con diferentes números de documento para ver cómo varía el comportamiento de la página.

¿Te gustaría que profundice en alguna parte específica, como la integración con Laravel o cómo manejar casos donde la página usa iframes o ventanas emergentes?

Comentarios