Transformar un guion de dramaturgo en una herramienta: almacenamiento, intentos repetidos, interfaz de línea de comandos y límites impuestos por la IA
Cómo un script único de Playwright se transforma en una herramienta reutilizable para Node.js con historial en SQLite, eliminación de duplicados, intentos repetidos, una interfaz CLI Commander, configuraciones y tareas generadas por IA validadas.
Fuentes: The JavaScript Automation Project I Built to Stop Repeating the Same Browser Tasks
Muchas tareas repetitivas en el navegador siguen el mismo ciclo: abrir un sitio, navegar, escribir algo, leer el resultado, guardarlo y repetir. Cada paso es sencillo, pero hacerlos a mano todos los días suma. Esta guía transforma ese ciclo de un script de Playwright de diez líneas en una pequeña herramienta de Node.js con historial persistente, detección de duplicados, reintentos, una interfaz de línea de comandos, configuración centralizada y una interfaz de usuario en lenguaje natural protegida.
El flujo general es el siguiente: llega una solicitud, un flujo de trabajo en JavaScript la procesa, un navegador real realiza el trabajo, los resultados estructurados se almacenan en una base de datos o un informe. Un artículo relacionado trata sobre crear un kit de automatización en JavaScript modular desde cero; aquí el énfasis está en las decisiones que garantizan la fiabilidad de dicha herramienta a medida que crece.
Paso uno: ejecutar un navegador real
Playwright permite que JavaScript controle Chromium, Firefox o WebKit. El script inicial inicia un navegador visible, carga una página, espera a que el DOM esté listo, muestra el título y lo cierra.
const { chromium } = require("playwright");
async function openWebsite(url) {
const browser = await chromium.launch({
headless: false
});
const page = await browser.newPage();
await page.goto(url, {
waitUntil: "domcontentloaded"
});
console.log("Title:", await page.title());
await browser.close();
}
openWebsite("https://example.com");
headless: false te permite observar la automatización mientras desarrollas, y waitUntil: "domcontentloaded" se resuelve una vez que el HTML está analizado en lugar de esperar a que se carguen todas las imágenes y scripts. Todo lo que sigue está estructurado en torno a estas capacidades.
Rodear acciones básicas en funciones nombradas
Distribuir el código de selección por todo el proyecto hace que cada cambio de página sea costoso. En su lugar, pequeñas funciones se encargan de una interacción cada una, y una función de nivel superior las combina.
async function fillSearchBox(page, query) {
const searchBox = page.getByRole("textbox");
await searchBox.fill(query);
}
async function clickSearch(page) {
await page.getByRole("button", {
name: "Search"
}).click();
}
async function performSearch(page, query) {
await fillSearchBox(page, query);
await clickSearch(page);
}
Los localizadores utilizan getByRole, que encuentra elementos según su rol y nombre accesibles. Tiende a sobrevivir mejor a las refacturaciones del marcado que los selectores de clase. Los llamantes ahora pueden expresar su intención sin conocer los mecanismos subyacentes:
await performSearch(
page,
"JavaScript automation"
);
Si el formulario de búsqueda cambia, solo es necesario actualizar fillSearchBox y clickSearch.
Convierte las páginas en datos estructurados
El HTML sin procesar es difícil de trabajar. La siguiente función selecciona cada elemento .article y, dentro del contexto de la página, lo mapea a un objeto simple con título y autor.
async function extractArticles(page) {
return await page
.locator(".article")
.evaluateAll(elements => {
return elements.map(element => {
const title = element
.querySelector(".title")
?.textContent
?.trim();
const author = element
.querySelector(".author")
?.textContent
?.trim();
return {
title,
author
};
});
});
}
evaluateAll ejecuta la función de callback en el navegador sobre todos los elementos coincidentes y devuelve el resultado serializado a Node. La cadena opcional (?.) evita que la falta de .title o .author genere un error; ese campo simplemente se considera undefined. Por lo tanto, su uso implica una sola llamada:
const articles = await extractArticles(page);
console.log(articles);
Una vez que la página se convierte en un array de objetos, se puede filtrar, comparar, almacenar o transmitir como cualquier otro dato.
Dale a la herramienta memoria con SQLite
Los resultados que desaparecen al finalizar el proceso tienen un uso limitado. El paquete sqlite3 proporciona una base de datos basada en archivos, y la configuración a continuación crea una tabla results la primera vez que se ejecuta, con un ID de incremento automático y una fecha y hora por defecto.
const sqlite3 = require("sqlite3").verbose();
const db = new sqlite3.Database(
"automation.db"
);
db.run(`
CREATE TABLE IF NOT EXISTS results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
author TEXT,
source TEXT,
created_at DATETIME
DEFAULT CURRENT_TIMESTAMP
)
`);
Al guardar una fila se utiliza una función auxiliar que envuelve a db.run, basado en callbacks, dentro de una promesa para que funcione con await. Los marcadores ? vinculan los valores de forma segura, por lo que el texto extraído que contenga comillas no puede causar errores ni inyectarse en la consulta SQL.
function saveResult(result, source) {
return new Promise((resolve, reject) => {
db.run(
`
INSERT INTO results
(title, author, source)
VALUES (?, ?, ?)
`,
[
result.title,
result.author,
source
],
error => {
if (error) {
reject(error);
return;
}
resolve();
}
);
});
}
El historial permite el seguimiento de cambios, realizar comparaciones, generar informes y mantener un registro de actividades.
Evite almacenar el mismo resultado dos veces
Las ejecuciones repetidas suelen mostrar los mismos elementos. Una función auxiliar de búsqueda verifica si ya existe un resultado con el mismo título para una fuente determinada.
function resultExists(result, source) {
return new Promise((resolve, reject) => {
db.get(
`
SELECT id
FROM results
WHERE title = ?
AND source = ?
LIMIT 1
`,
[
result.title,
source
],
(error, row) => {
if (error) {
reject(error);
return;
}
resolve(Boolean(row));
}
);
});
}
El flujo de trabajo solo inserta los resultados que son nuevos:
for (const result of articles) {
const exists = await resultExists(
result,
"example.com"
);
if (!exists) {
await saveResult(
result,
"example.com"
);
}
}
Este enfoque de verificar primero y luego insertar funciona bien para un proceso secuencial único. Si varias ejecuciones pudieran escribir al mismo tiempo, dos de ellas podrían ver “no encontrado” e insertar duplicados. Una opción más sólida es agregar una restricción UNIQUE en (title, source) y utilizar INSERT OR IGNORE, lo que permite que la base de datos aplique la regla de forma atómica.
Hacer que los pasos problemáticos sean reintentables
La automatización real es menos predecible que una demostración. Las redes fallan, las páginas se cargan lentamente, los servidores responden de manera diferente y los elementos no siempre están presentes de inmediato. Una función auxiliar genérica retry ejecuta una operación asíncrona un número determinado de veces, espera entre intentos y vuelve a lanzar el último error si todos fallan.
async function retry(
operation,
attempts = 3,
delay = 1500
) {
let lastError;
for (
let attempt = 1;
attempt <= attempts;
attempt++
) {
try {
return await operation();
} catch (error) {
lastError = error;
console.log(
`Attempt ${attempt} failed`
);
if (attempt < attempts) {
await new Promise(resolve => {
setTimeout(
resolve,
delay
);
});
}
}
}
throw lastError;
}
El formato para envolver una navegación es el siguiente:
await retry(
async () => {
await page.goto(
"https://example.com",
{
waitUntil:
"domcontentloaded"
}
);
},
3
);
Hay dos aspectos que merecen atención. La demora es fija; para servicios que podrían quedar sobrecargados, una demora creciente (retroceso exponencial) suele ser más adecuada. Además, las reintentos solo tienen sentido en operaciones que se pueden repetir sin problemas: volver a cargar una página está bien, pero volver a enviar un formulario de compra no.
Agrupar el flujo de trabajo en una clase
Con varios auxiliares, un único objeto puede gestionar la página y exponer sus pasos. WebAutomation recibe un navegador, abre una página en start(), navega mediante retry, realiza la búsqueda y cierra la página.
class WebAutomation {
constructor(browser) {
this.browser = browser;
this.page = null;
}
async start() {
this.page =
await this.browser.newPage();
}
async open(url) {
await retry(async () => {
await this.page.goto(url, {
waitUntil:
"domcontentloaded"
});
});
}
async search(query) {
await this.page
.getByRole("textbox")
.fill(query);
await this.page
.getByRole("button", {
name: "Search"
})
.click();
}
async close() {
if (this.page) {
await this.page.close();
}
}
}
El código de la aplicación se lee entonces como un breve script que describe las intenciones, manteniendo el ciclo de vida del navegador al margen para que un mismo navegador pueda gestionar varios objetos de automatización.
const browser =
await chromium.launch({
headless: true
});
const automation =
new WebAutomation(browser);
await automation.start();
await automation.open(
"https://example.com"
);
await automation.search(
"JavaScript"
);
await automation.close();
await browser.close();
En entornos de producción, colocar las llamadas a close() dentro de un bloque finally.
Exponerlo como herramienta de línea de comandos
Editar el código para ejecutar una tarea diferente no es escalable. La biblioteca Commander convierte el proyecto en una CLI con nombre, descripción, versión y subcomandos. El comando visit que se muestra a continuación acepta un argumento de URL, lo abre sin interfaz gráfica y muestra el título de la página.
const { Command } = require("commander");
const program = new Command();
program
.name("webtool")
.description(
"Browser automation utility"
)
.version("1.0.0");
program
.command("visit")
.description(
"Visit a webpage"
)
.argument("<url>")
.action(async url => {
const browser =
await chromium.launch({
headless: true
});
const page =
await browser.newPage();
await page.goto(url);
console.log(
await page.title()
);
await browser.close();
});
program.parseAsync();
Se utiliza parseAsync() en lugar de parse() porque el manejador de acciones es asíncrono. Luego, la herramienta se ejecuta desde una terminal:
node webtool.js visit https://example.com
En este punto, el script se ha convertido en una herramienta que otros o un job cron pueden llamar.
Centralizar la configuración
Los valores codificados de forma fija se propagan rápidamente. Un único objeto de configuración agrupa los ajustes del navegador, la base de datos y la salida.
const config = {
browser: {
headless: true,
timeout: 30000
},
database: {
file: "automation.db"
},
output: {
directory: "results"
}
};
El código de inicio luego lee de este objeto, incluyendo un tiempo de espera predeterminado aplicado a cada acción de Playwright en la página:
const browser =
await chromium.launch({
headless:
config.browser.headless
});
const page =
await browser.newPage();
page.setDefaultTimeout(
config.browser.timeout
);
Tener los ajustes en un solo lugar facilita el paso de un experimento a una aplicación mantenible.
Coloque una capa de IA validada al frente
Una herramienta convencional expone comandos fijos:
search
open
extract
save
report
En cambio, un modelo de lenguaje puede interpretar una solicitud como la de buscar y almacenar artículos recientes en JavaScript de un sitio, y convertirla en un objeto de tarea estructurado:
const task = {
action: "search",
query: "JavaScript",
saveResults: true
};
Nunca se debe permitir que el modelo ejecute código arbitrario. Cada tarea que genera se verifica contra una lista de acciones permitidas antes de que se ejecute cualquier cosa.
const allowedActions = new Set([
"visit",
"search",
"extract",
"save"
]);
function validateTask(task) {
if (!allowedActions.has(task.action)) {
throw new Error(
"Unsupported action"
);
}
return true;
}
El pipeline resultante mantiene separadas la interpretación y la ejecución:
Natural Language
↓
AI
↓
Structured Task
↓
Validation
↓
JavaScript Workflow
↓
Browser / Database
El modelo decide qué parece que quiere el usuario; la aplicación decide qué se le permite hacer realmente. En la práctica, la validación debería ir más allá del nombre de la acción y también verificar los parámetros, por ejemplo que las URLs pertenezcan a dominios aprobados.
Para qué sirve realmente la herramienta
A los usuarios les importa que una tarea tediosa haya desaparecido, no que esté involucrado Playwright. El mismo motor sirve para usos legítimos como pruebas de calidad y pruebas automatizadas, monitoreo de sitios, flujos de trabajo internos, recopilación de datos de fuentes autorizadas, informes recurrentes, gestión de contenido y automatización de procesos empresariales. Desde el punto de vista comercial, esto puede presentarse como un servicio de configuración, personalización y mantenimiento continuo, o como una suscripción con límites de uso y funciones premium. El código es el motor; el flujo de trabajo es el producto.
La arquitectura resultante
En conjunto, las capas se ven así. Cheerio aparece junto a Playwright como una opción más ligera para analizar HTML estático cuando no es necesario utilizar un navegador completo.
USER
│
▼
CLI / AI REQUEST
│
▼
TASK VALIDATOR
│
▼
WORKFLOW ENGINE
│ │
▼ ▼
PLAYWRIGHT CHEERIO
│ │
└────┬─────┘
▼
PROCESSED
DATA
│
▼
SQLITE
│
▼
REPORT / EXPORT
Conclusión
La herramienta nunca se diseñó desde un principio. Se fue desarrollando capa por capa, cada una resolviendo un problema concreto:
- Las funciones nombradas relacionadas con los localizadores acarrean el costo de los cambios en el sitio.
- La extracción estructurada junto con SQLite convierten las ejecuciones puntuales en registros históricos.
- La deduplicación y las reintentos hacen que las ejecuciones repetidas sin supervisión sean fiables; es mejor utilizar restricciones de base de datos y operaciones idempotentes.
- Una interfaz de línea de comandos y una configuración centralizada permiten que otros la utilicen sin tocar el código.
- Una interfaz de usuario basada en IA solo es segura si cada tarea generada pasa por una validación estricta.
En lugar de empezar por decidir qué framework aprender, comience con un problema repetitivo que valga la pena resolver y deje que la arquitectura surja de ello.
Lecturas relacionadas
- Construyendo un kit de automatización en JavaScript modular desde cero — Aprenda cómo combinar Playwright, Cheerio, SQLite y Commander en un motor de flujo de trabajo reutilizable para Node.js que evoluciona de un script a un producto de automatización vendible.
- Moviendo el JWT de una app React fuera de localStorage hacia cookies HttpOnly — Aprenda por qué los JWT en localStorage son vulnerables a cualquier ataque XSS, cómo las cookies HttpOnly con SameSite, rotación y un BFF reducen el riesgo, y qué problemas persisten en las apps React.