¿Playwright sync o async? Cuál elegir en Python

Para la mayoría de los scripts conviene la API síncrona, que es la más simple. La asíncrona se reserva para tres casos: aplicaciones que ya son asíncronas, como FastAPI o un bot, notebooks de Jupyter y tareas que deben abrir varias páginas a la vez.

Las dos versiones ofrecen los mismos métodos con los mismos nombres, y solo cambia la forma de llamarlos. Por eso elegir mal no es grave, pero sí molesta: pasar de una a otra obliga a añadir async y await en muchas líneas, y en Jupyter la síncrona ni siquiera funciona.

El error que aparece al usar la API síncrona dentro de Jupyter tiene una causa sencilla y una solución de una línea, y se explica al final junto con un ejemplo de varias páginas en paralelo.

Te puede interesar: Playwright con Python: Tutorial para automatizar el navegador

La API síncrona

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")
    print(page.title())
    browser.close()

Cada línea termina antes de que empiece la siguiente, como en cualquier script normal. Es la forma más fácil de leer y de depurar, y encaja con las automatizaciones lineales: abrir una página, hacer algo y guardar un resultado. Para la mayoría de los scripts es suficiente, y por eso es la que se usa en el resto de la serie.

La API asíncrona

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()
        await page.goto("https://quotes.toscrape.com")
        print(await page.title())
        await browser.close()


asyncio.run(main())

La estructura es la misma, con tres cambios. La importación viene de playwright.async_api, las funciones se declaran con async def y cada llamada que habla con el navegador lleva await. Al final, asyncio.run() pone todo en marcha.

Esta versión se elige cuando el programa ya es asíncrono, como ocurre con FastAPI o con un bot de Telegram, o cuando se necesita hacer varias cosas a la vez.

Varias páginas al mismo tiempo

La ventaja real de la API asíncrona aparece al trabajar con varias páginas. Mientras una espera la respuesta de la red, otra puede avanzar, y el tiempo total se acerca al de la tarea más lenta en lugar de a la suma de todas.

import asyncio
from playwright.async_api import async_playwright


async def obtener_titulo(browser, url):
    page = await browser.new_page()
    await page.goto(url)
    titulo = await page.title()
    await page.close()
    return titulo


async def main():
    urls = [
        "https://quotes.toscrape.com/page/1/",
        "https://quotes.toscrape.com/page/2/",
        "https://quotes.toscrape.com/page/3/",
    ]
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        titulos = await asyncio.gather(*(obtener_titulo(browser, u) for u in urls))
        print(titulos)
        await browser.close()


asyncio.run(main())

La función asyncio.gather() lanza todas las tareas juntas y devuelve los resultados en el mismo orden que las direcciones.

Abrir cientos de pestañas a la vez consume mucha memoria y puede saturar el sitio que se está consultando. Para poner un límite se usa un semáforo, que deja pasar solo un número fijo de tareas simultáneas:

semaforo = asyncio.Semaphore(3)


async def obtener_titulo_limitado(browser, url):
    async with semaforo:
        return await obtener_titulo(browser, url)

Un ejemplo cotidiano: revisar si varias páginas responden

Un uso muy frecuente de la API asíncrona es revisar una lista de direcciones, por ejemplo para encontrar enlaces rotos. Cada comprobación espera sobre todo a la red, así que hacerlas a la vez ahorra mucho tiempo.

import asyncio

from playwright.async_api import async_playwright, Error

URLS = [
    "https://quotes.toscrape.com/",
    "https://quotes.toscrape.com/page/2/",
    "https://quotes.toscrape.com/pagina-que-no-existe/",
    "https://sitio-que-no-existe.invalid/",
]


async def revisar(browser, semaforo, url):
    async with semaforo:
        page = await browser.new_page()
        try:
            respuesta = await page.goto(url, timeout=15000)
            return url, respuesta.status
        except Error as error:
            return url, str(error).splitlines()[0]
        finally:
            await page.close()


async def main():
    semaforo = asyncio.Semaphore(3)
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        resultados = await asyncio.gather(*(revisar(browser, semaforo, u) for u in URLS))
        await browser.close()

    for url, estado in resultados:
        print(estado, url)


asyncio.run(main())

El script distingue dos tipos de resultado. Cuando la página responde, aunque sea con un 404, goto() devuelve el código de estado, y por eso la tercera dirección aparece como 404 sin provocar ningún error. Cuando el dominio no existe, en cambio, goto() lanza una excepción, que el bloque except convierte en un mensaje legible. El semáforo limita a tres las páginas abiertas al mismo tiempo, y finally garantiza que cada página se cierre incluso si algo falla.

El error dentro de Jupyter

Si se usa la API síncrona en un entorno que ya tiene un bucle de eventos en marcha, Playwright lanza este error: “It looks like you are using Playwright Sync API inside the asyncio loop. Please use the Async API instead”. Ocurre en Jupyter y en otros notebooks, y también dentro de aplicaciones asíncronas.

La solución es pasar a la API asíncrona. En un notebook, además, no se llama a asyncio.run(), porque el bucle ya existe y esa función falla. En su lugar se escribe await main() directamente en la celda, ya que Jupyter admite await en el nivel superior.

Cómo elegir

SituaciónAPI recomendada
Script que se ejecuta de principio a finSíncrona
Notebook de JupyterAsíncrona
Aplicación FastAPI u otra asíncronaAsíncrona
Muchas páginas en paraleloAsíncrona

Si hay dudas, se empieza con la síncrona. El paso a la asíncrona es mecánico, porque basta con añadir async y await donde corresponda.

Similar Posts

Leave a Reply

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