En mi empresa anterior, desarrollé un trabajo por lotes que rastreaba métricas en redes sociales, como Twitter, LinkedIn, Mastodon, Bluesky, Reddit, etc. Luego me di cuenta de que podía duplicarlo para mi propia "personalidad". El problema es que algunos medios no ofrecen una API HTTP para las métricas que quiero. Estas son las métricas que quiero en LinkedIn:
Busqué durante mucho tiempo, pero no encontré acceso a la API para las métricas anteriores. Busqué las métricas manualmente todas las mañanas durante mucho tiempo y finalmente decidí automatizar esta tediosa tarea. Esto es lo que aprendí.
El trabajo se realiza en Python, por lo que quiero mantenerme en la misma pila tecnológica. Después de una rápida investigación, encontré Playwright , una herramienta de automatización del navegador con un par de API de lenguaje, incluido Python. El caso de uso principal de Playwright es la prueba de extremo a extremo, pero también puede administrar el navegador fuera de un contexto de prueba.
Estoy usando Poetry para administrar las dependencias. Instalar Playwright es tan fácil como:
poetry add playwright
En este punto, Playwright está listo para usarse. Ofrece dos API distintas, una sincrónica y otra asincrónica . Debido a mi caso de uso, la primera versión es más que suficiente.
Me gusta abordar el desarrollo de forma incremental.
A continuación se muestra un extracto de la API:
Se traduce al siguiente código:
from playwright.sync_api import Browser, Locator, Page, sync_playwright with (sync_playwright() as pw): #1 browser: Browser = pw.chromium.launch() #2 page: Page = browser.new_page() #3 page.goto('https://www.linkedin.com/login') #4 page.locator('#username').press_sequentially(getenv('LINKEDIN_USERNAME')) #5 page.locator('#password').press_sequentially(getenv('LINKEDIN_PASSWORD')) #5 page.locator('button[type=submit]').press('Enter') #6 page.goto('https://www.linkedin.com/dashboard/') #4 metrics_container: Locator = page.locator('.pcd-analytic-view-items-container') metrics: List[Locator] = metrics_container.locator('p.text-body-large-bold').all() #7 impressions = atoi(metrics[0].inner_text()) #8 # Get other metrics browser.close() #9
Consigue un objeto playwright
.
Inicie una instancia del navegador. Hay varios tipos de navegadores disponibles; elegí Chromium por capricho. Tenga en cuenta que debería haber instalado el navegador específico previamente, es decir , playwright install --with-deps chromium
.
De forma predeterminada, el navegador se abre sin interfaz gráfica , no aparece. Te recomiendo ejecutarlo de forma visible al principio para facilitar la depuración: headless = True
.
Abra una nueva ventana del navegador.
Navegar a una nueva ubicación.
Localice los campos de entrada especificados y complételos con mis credenciales.
Localice el botón especificado y presiónelo.
Localizar todos los elementos especificados.
Obtenga el texto interno del primer elemento.
Cierre el navegador para limpiar.
Lo anterior funcionó como se esperaba. El único inconveniente es que recibí un correo electrónico de LinkedIn cada vez que ejecuté el script:
Hola Nicolás,
Has activado con éxito Recordarme en un nuevo dispositivo HeadlessChrome, <OS> en <ciudad>, <región>, <país >. Obtén más información sobre cómo funciona Recordarme en un dispositivo.
También conocí a Fabien Vauchelles en la conferencia JavaCro . Se especializa en web scraping y me dijo que la mayoría de las personas en este campo aprovechan los perfiles del navegador. De hecho, si inicias sesión en LinkedIn, obtendrás un token de autenticación almacenado como cookies y no necesitarás autenticarlo nuevamente antes de que caduque. Afortunadamente, Playwright ofrece esa función con su método launch_persistent_context
.
Podemos reemplazar el launch
anterior por el siguiente:
with sync_playwright() as pw: playwright_profile_dir = f'{Path.home()}/.social-metrics/playwright-profile' context: BrowserContext = pw.chromium.launch_persistent_context(playwright_profile_dir) #1 try: #2 page: Page = context.new_page() #3 page.goto('https://www.linkedin.com/dashboard/') #4 if 'session_redirect' in page.url: #4 page.locator('#username').press_sequentially(getenv('LINKEDIN_USERNAME')) page.locator('#password').press_sequentially(getenv('LINKEDIN_PASSWORD')) page.locator('button[type=submit]').press('Enter') page.goto('https://www.linkedin.com/dashboard/') metrics_container: Locator = page.locator('.pcd-analytic-view-items-container') # Same as in the previous snippet except Exception as e: #2 logger.error(f'Could not fetch metrics: {e}') finally: #5 context.close()
Playwright almacenará el perfil en la carpeta especificada y lo reutilizará en cada ejecución.
Mejorar el manejo de excepciones.
BrowserContext
también puede abrir páginas.
Intentamos navegar hasta el panel de control. LinkedIn nos redireccionará a la página de inicio de sesión si no estamos autenticados; entonces podremos autenticarnos.
Cerrar el contexto sea cual sea el resultado.
En este punto, solo necesitamos autenticarnos con ambas credenciales la primera vez. En ejecuciones posteriores, depende.
Me sorprendió ver que el código anterior no funcionaba de manera confiable. Funcionó en la primera ejecución y, a veces, en las posteriores. Debido a que estoy almacenando el perfil del navegador en varias ejecuciones, cuando necesito autenticarme, LinkedIn solo solicita la contraseña, ¡no el nombre de usuario! Debido a que el código intenta ingresar el nombre de usuario, falla en este caso. La solución es bastante sencilla:
username_field = page.locator('#username') if username_field.is_visible(): username_field.press_sequentially(getenv('LINKEDIN_USERNAME')) page.locator('#password').press_sequentially(getenv('LINKEDIN_PASSWORD'))
Aunque no soy un experto en Python, logré lo que quería con Playwright. Preferí usar la API de sincronización porque hace que el código sea un poco más fácil de entender y no tengo ningún requisito de rendimiento. Solo utilicé las funciones básicas que ofrece Playwright. Playwright permite grabar videos en el contexto de las pruebas, lo que es muy útil cuando una prueba falla durante la ejecución de una secuencia de comandos de CI.
Para ir más allá:
Publicado originalmente en A Java Geek el 19 de enero de 2024