Playwright con Python: Tutorial Automatizar el Navegador

Playwright es una librería desarrollada por Microsoft que permite controlar navegadores web (Chromium, Firefox, Safari) mediante código. Puedes hacer clic en botones, rellenar formularios, esperar a que carguen elementos, extraer datos o hacer capturas de pantalla, todo de forma automática.

¿Por qué Playwright y no Selenium?

  • Es más rápido y moderno (lanzado en 2020 vs 2004)
  • Espera automáticamente a que los elementos estén listos (auto-wait)
  • Soporta páginas con JavaScript pesado (React, Vue, Angular) sin configuración extra
  • API más limpia y consistente
  • Soporte nativo para múltiples pestañas, iframes y descargas

Si ya conoces Selenium, Playwright te resultará familiar pero notarás inmediatamente que hay menos fricción.


1. Instalación

Necesitas Python 3.8 o superior. Instala la librería y los navegadores con dos comandos:

pip install playwright
playwright install

El segundo comando descarga Chromium, Firefox y WebKit (Safari). Si solo quieres Chromium para ahorrar espacio:

playwright install chromium

Verifica que funciona:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://example.com")
    print(page.title())
    browser.close()

Si ves Example Domain en la consola, todo está funcionando.


2. Conceptos clave antes de empezar

Antes de escribir código, entiende estos tres objetos que usarás constantemente:

ObjetoQué esAnalogía
BrowserEl navegador abiertoLa ventana de Chrome
PageUna pestaña del navegadorUna pestaña dentro de Chrome
LocatorUn elemento de la páginaUn botón, input o div concreto

La mayoría de tu código seguirá este patrón:

Browser → Page → Locator → Acción

3. Modos de ejecución: sync vs async

Playwright ofrece dos APIs:

Síncrona — más simple, ideal para scripts y automatizaciones lineales:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    # tu código aquí
    browser.close()

Asíncrona — para integrar con frameworks como FastAPI o cuando necesitas ejecutar varias tareas en paralelo:

import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()
        # tu código aquí
        await browser.close()

asyncio.run(main())

Para este tutorial usaremos la API síncrona. Es más legible y suficiente para el 90% de los casos.

4. Navegar y esperar contenido

Abrir una URL

page.goto("https://quotes.toscrape.com")

Por defecto, goto espera a que la página haya cargado completamente (load event). Puedes cambiar esto:

# Esperar solo a que el HTML esté disponible (más rápido)
page.goto("https://ejemplo.com", wait_until="domcontentloaded")

# Esperar a que la red esté inactiva (útil en SPAs)
page.goto("https://ejemplo.com", wait_until="networkidle")

Esperar un elemento específico

Una de las mejores características de Playwright: espera automáticamente a que el elemento exista y sea visible antes de interactuar con él.

# Playwright espera hasta 30 segundos por defecto
page.locator("h1").wait_for()

# Cambiar el timeout a 10 segundos
page.locator("h1").wait_for(timeout=10000)  # en milisegundos

5. Seleccionar elementos

Esta es la parte más importante. Playwright usa locators para encontrar elementos.

Por texto visible

# Encuentra un elemento que contenga exactamente ese texto
page.get_by_text("Iniciar sesión")

# Texto parcial
page.get_by_text("Iniciar", exact=False)

Por rol ARIA (recomendado para formularios)

page.get_by_role("button", name="Enviar")
page.get_by_role("link", name="Ver más")
page.get_by_role("textbox", name="Email")

Por placeholder

page.get_by_placeholder("Escribe tu email")

Por selector CSS (el más flexible)

page.locator("h1")                    # etiqueta
page.locator(".card-title")           # clase
page.locator("#submit-btn")           # id
page.locator("ul.results li")         # anidado
page.locator("input[type='email']")   # atributo

Por selector XPath (para casos complejos)

page.locator("//div[@class='producto']//span[@class='precio']")

Regla práctica: usa get_by_role y get_by_text cuando puedas. Usa CSS cuando necesites precisión. Recurre a XPath solo si no hay otra opción.

6. Acciones fundamentales

Hacer clic

page.locator("#boton-login").click()

# Clic derecho
page.locator(".elemento").click(button="right")

# Doble clic
page.locator(".elemento").dbl_click()

Rellenar inputs

# Limpia el campo y escribe
page.locator("input[name='email']").fill("usuario@ejemplo.com")

# Escribe carácter a carácter (útil para campos con validación en tiempo real)
page.locator("input[name='email']").type("usuario@ejemplo.com", delay=50)

Seleccionar en un <select>

page.locator("select#pais").select_option("ES")         # por valor
page.locator("select#pais").select_option(label="España")  # por texto visible

Checkbox y radio buttons

page.locator("input[type='checkbox']").check()
page.locator("input[type='checkbox']").uncheck()
page.locator("input[value='opcion2']").check()

Subir archivos

page.locator("input[type='file']").set_input_files("documento.pdf")

Pulsar teclas

page.keyboard.press("Enter")
page.keyboard.press("Tab")
page.keyboard.press("Control+A")  # seleccionar todo

7. Extraer información

Texto de un elemento

titulo = page.locator("h1").inner_text()
print(titulo)

Atributo de un elemento

enlace = page.locator("a.leer-mas").get_attribute("href")
imagen = page.locator("img.portada").get_attribute("src")

Múltiples elementos

Cuando hay varios elementos que coinciden con el selector, usa all():

titulos = page.locator(".card-title").all()
for titulo in titulos:
    print(titulo.inner_text())

# O en una línea con list comprehension
textos = [el.inner_text() for el in page.locator(".card-title").all()]

Ejemplo completo: extraer una lista de productos

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://quotes.toscrape.com")

    productos = []
    items = page.locator(".quote").all()

    for item in items:
        texto = item.locator(".text").inner_text()
        autor = item.locator(".author").inner_text()
        productos.append({"texto": texto, "autor": autor})

    browser.close()

for p in productos:
    print(f"{p['autor']}: {p['texto'][:60]}...")

8. Paginación

La mayoría de sitios dividen el contenido en páginas. Aquí tienes el patrón estándar:

from playwright.sync_api import sync_playwright

resultados = []

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://quotes.toscrape.com")

    while True:
        # Extraer datos de la página actual
        items = page.locator(".quote").all()
        for item in items:
            resultados.append({
                "texto": item.locator(".text").inner_text(),
                "autor": item.locator(".author").inner_text()
            })

        # Buscar botón de siguiente página
        siguiente = page.locator("li.next a")
        if siguiente.count() == 0:
            break  # No hay más páginas

        siguiente.click()
        page.wait_for_load_state("domcontentloaded")

    browser.close()

print(f"Total extraídos: {len(resultados)}")

9. Formularios y autenticación

Automatizar un login es uno de los casos más comunes:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)  # headless=False para ver el navegador
    page = browser.new_page()

    # Ir al login
    page.goto("https://quotes.toscrape.com/login")

    # Rellenar credenciales
    page.get_by_label("Username").fill("admin")
    page.get_by_label("Password").fill("password123")

    # Enviar formulario
    page.get_by_role("button", name="Login").click()

    # Verificar que el login fue correcto
    page.wait_for_url("**/")  # espera a que la URL cambie al home
    print("Login correcto:", page.url)

    # A partir de aquí puedes navegar como usuario autenticado
    page.goto("https://quotes.toscrape.com/")

    browser.close()

Guardar la sesión para no hacer login cada vez

# Primera ejecución: hacer login y guardar cookies
context = browser.new_context()
page = context.new_page()
# ... proceso de login ...
context.storage_state(path="sesion.json")

# Siguientes ejecuciones: cargar sesión guardada
context = browser.new_context(storage_state="sesion.json")
page = context.new_page()
page.goto("https://app.ejemplo.com/dashboard")
# Ya estás autenticado sin hacer login

10. Screenshots y PDFs

Captura de pantalla

# Captura de la vista actual
page.screenshot(path="captura.png")

# Captura de la página completa (scroll completo)
page.screenshot(path="pagina-completa.png", full_page=True)

# Captura de un elemento específico
page.locator(".grafico-ventas").screenshot(path="grafico.png")

Generar PDF (solo funciona en Chromium)

page.pdf(path="reporte.pdf")

# Con configuración de página
page.pdf(
    path="reporte.pdf",
    format="A4",
    print_background=True,
    margin={"top": "2cm", "bottom": "2cm", "left": "2cm", "right": "2cm"}
)

Esto es especialmente útil para generar reportes: cargas una página HTML con tus datos y la exportas a PDF automáticamente.

11. Manejar diálogos y ventanas emergentes

Alertas JavaScript

# Aceptar automáticamente cualquier alert/confirm
page.on("dialog", lambda dialog: dialog.accept())

# O rechazar
page.on("dialog", lambda dialog: dialog.dismiss())

# Leer el mensaje antes de decidir
def manejar_dialogo(dialog):
    print(f"Tipo: {dialog.type}, Mensaje: {dialog.message}")
    dialog.accept()

page.on("dialog", manejar_dialogo)

Nueva pestaña o ventana

# Esperar a que se abra una nueva página al hacer clic
with page.expect_popup() as popup_info:
    page.locator("a[target='_blank']").click()

nueva_pagina = popup_info.value
nueva_pagina.wait_for_load_state()
print(nueva_pagina.url)

12. Modo headless y configuración del navegador

Headless vs visible

# Headless: sin interfaz gráfica (más rápido, para producción)
browser = p.chromium.launch(headless=True)  # por defecto es True

# Con ventana visible (para desarrollo y debugging)
browser = p.chromium.launch(headless=False, slow_mo=500)
# slow_mo=500 añade 500ms entre acciones para seguirlo con la vista

Simular un dispositivo móvil

iphone = p.devices["iPhone 13"]
context = browser.new_context(**iphone)
page = context.new_page()
page.goto("https://ejemplo.com")

Configurar viewport y user agent

context = browser.new_context(
    viewport={"width": 1920, "height": 1080},
    user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0"
)

Bloquear imágenes para ir más rápido

def bloquear_recursos(route):
    if route.request.resource_type in ["image", "font", "stylesheet"]:
        route.abort()
    else:
        route.continue_()

page.route("**/*", bloquear_recursos)

13. Manejo de errores

Playwright lanza excepciones cuando algo falla. Envuelve las operaciones críticas en try/except:

from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://ejemplo.com")

    try:
        # Espera 5 segundos máximo
        page.locator(".precio").wait_for(timeout=5000)
        precio = page.locator(".precio").inner_text()
    except PlaywrightTimeout:
        print("El elemento .precio no apareció en 5 segundos")
        precio = None

    browser.close()

Verificar si un elemento existe antes de interactuar

boton = page.locator(".ver-mas")

if boton.count() > 0:
    boton.click()
else:
    print("No hay botón 'ver más' en esta página")

14. Script completo de ejemplo

Este script combina todo lo anterior: navega varias páginas, extrae datos, maneja errores y guarda el resultado en CSV.

import csv
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout

def extraer_citas():
    resultados = []

    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)

        # Configurar el contexto con viewport realista
        context = browser.new_context(
            viewport={"width": 1280, "height": 800}
        )
        page = context.new_page()

        # Bloquear imágenes para ir más rápido
        page.route("**/*.{png,jpg,jpeg,gif,svg}", lambda r: r.abort())

        print("Navegando a la página...")
        page.goto("https://quotes.toscrape.com", wait_until="domcontentloaded")

        pagina_num = 1

        while True:
            print(f"Extrayendo página {pagina_num}...")

            try:
                page.locator(".quote").first.wait_for(timeout=5000)
            except PlaywrightTimeout:
                print(f"No se cargaron elementos en página {pagina_num}")
                break

            # Extraer todas las citas de la página actual
            quotes = page.locator(".quote").all()

            for quote in quotes:
                try:
                    texto = quote.locator(".text").inner_text()
                    autor = quote.locator(".author").inner_text()
                    tags = [t.inner_text() for t in quote.locator(".tag").all()]

                    resultados.append({
                        "texto": texto,
                        "autor": autor,
                        "tags": ", ".join(tags),
                        "pagina": pagina_num
                    })
                except Exception as e:
                    print(f"Error extrayendo cita: {e}")
                    continue

            # Siguiente página
            siguiente = page.locator("li.next a")
            if siguiente.count() == 0:
                print("Última página alcanzada.")
                break

            siguiente.click()
            page.wait_for_load_state("domcontentloaded")
            pagina_num += 1

        browser.close()

    return resultados

def guardar_csv(datos, archivo="citas.csv"):
    if not datos:
        print("No hay datos para guardar.")
        return

    with open(archivo, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=["texto", "autor", "tags", "pagina"])
        writer.writeheader()
        writer.writerows(datos)

    print(f"✓ {len(datos)} citas guardadas en {archivo}")

if __name__ == "__main__":
    citas = extraer_citas()
    guardar_csv(citas)

15. Consejos para proyectos reales

Sé respetuoso con los sitios que automatizas. Añade pausas entre peticiones y no hagas scraping masivo sin revisar el robots.txt del sitio.

import time
time.sleep(1)  # pausa de 1 segundo entre páginas

Usa variables de entorno para credenciales, nunca las hardcodees en el script:

import os
usuario = os.getenv("APP_USER")
password = os.getenv("APP_PASS")

Estructura tus scripts como funciones o clases desde el principio. Un script que hace todo en línea es difícil de mantener cuando crece.

Playwright tiene un generador de código. Si no sabes qué selector usar, ejecuta:

playwright codegen https://ejemplo.com

Se abre el navegador y graba cada acción que haces, generando el código Python automáticamente.

Próximos pasos

Con lo que has visto en este tutorial puedes construir:

  • Scripts de scraping que extraen y guardan datos de cualquier web
  • Bots de formularios que automatizan registros, pedidos o reservas
  • Generadores de reportes en PDF o capturas de pantalla programadas
  • Tests automáticos que verifican que tu propia web funciona correctamente

Los siguientes artículos de esta serie profundizan en cada uno de estos casos con proyectos reales de cliente a cliente:

  • → Introduccion al web scraping con Python y BeautifulSoup
  • → Cómo hacer web scraping con Playwright y Python paso a paso
  • → Automatizar formularios web con Playwright: casos reales
  • → Captura de screenshots y generación de PDF con Playwright
  • → Playwright para pruebas E2E: guía práctica

¿Tienes un proceso en tu empresa que podría automatizarse? Cuéntanoslo — hacemos una auditoría gratuita para ver si es viable.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *