¿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ón | API recomendada |
|---|---|
| Script que se ejecuta de principio a fin | Síncrona |
| Notebook de Jupyter | Asíncrona |
| Aplicación FastAPI u otra asíncrona | Asíncrona |
| Muchas páginas en paralelo | Así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.