Accueil / Articles

Transformer un script de dramaturge en outil : stockage, tentatives répétées, interface ligne de commande et contraintes liées à l’IA

Comment un script unique écrit par un dramaturge se transforme en outil Node.js réutilisable doté d’un historique SQLite, de fonctionnalités de suppression des doublons, de tentatives répétées, d’une interface CLI Commander, de paramètres de configuration et de tâches générées par l’IA validées.

Éditeur : REACTAPP.TOP9 min de lecture1963 mots

Sources: The JavaScript Automation Project I Built to Stop Repeating the Same Browser Tasks

De nombreuses tâches répétitives dans le navigateur suivent le même cycle : ouvrir un site, naviguer, taper quelque chose, lire le résultat, le sauvegarder, et répéter. Chaque étape est simple, mais les effectuer manuellement tous les jours finit par peser lourd. Cette présentation transforme ce cycle, issu d’un script Playwright de dix lignes, en une petite application Node.js dotée d’une historique persistante, d’un détecteur de doublons, de tentatives automatiques, d’une interface en ligne de commande, d’une configuration centrale et d’une interface utilisateur en langage naturel sécurisée.

Le flux de travail est le suivant : une demande arrive, un workflow JavaScript la traite, un vrai navigateur effectue le travail, puis des résultats structurés sont générés et stockés dans une base de données ou un rapport. Un article connexe traite de la création d’un outil d’automatisation JavaScript modulaire à partir de zéro ; ici, l’accent est mis sur les décisions qui permettent à un tel outil de rester fiable au fil de son développement.

Étape un : exécuter un vrai navigateur

Playwright permet à JavaScript de contrôler Chromium, Firefox ou WebKit. Le script de démarrage lance un navigateur visible, charge une page, attend que le DOM soit prêt, affiche le titre et ferme le navigateur.

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 vous permet de surveiller l’automatisation pendant le développement, et waitUntil: „domcontentloaded“ déclenche l’action une fois que l’HTML a été analysé, au lieu d’attendre le chargement de chaque image et script. Tout ce qui suit est structuré autour de ces fonctionnalités.

Encadrer les actions brutes dans des fonctions nommées

Déployer du code de sélection un peu partout dans un projet rend chaque modification de page coûteuse. Au lieu de cela, de petites fonctions gèrent chacune une interaction unique, tandis qu’une fonction de niveau supérieur les combine.

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);
}

Les locateurs utilisent getByRole, qui trouve les éléments en fonction de leur rôle et de leur nom accessibles. Ils ont tendance à mieux résister aux refacteurs de balisage que les sélecteurs de classe. Les appelsurs sont désormais capables d’exprimer leur intention sans connaître les mécanismes sous-jacents :

await performSearch(
    page,
    "JavaScript automation"
);

Si le formulaire de recherche change, seuls fillSearchBox et clickSearch doivent être mis à jour.

Transformer les pages en données structurées

L’HTML brut est difficile à manipuler. La fonction suivante sélectionne tous les éléments .article et, dans le contexte de la page, associe chacun d’eux à un objet simple contenant un titre et un auteur.

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 exécute la fonction de rappel dans le navigateur sur tous les éléments correspondants et renvoie le résultat serialisé à Node. La chaînage optionnel (?.) empêche qu’une absence de .title ou .author ne provoque une erreur ; ce champ vaut alors simplement undefined. L’utilisation se fait donc par une seule appel :

const articles = await extractArticles(page);

console.log(articles);

Lorsque la page est un tableau d’objets, vous pouvez la filtrer, la comparer, la stocker ou la transmettre comme n’importe quel autre type de données.

Fournir à l’outil une mémoire avec SQLite

Les résultats qui disparaissent à la fin du processus ont une utilité limitée. Le package sqlite3 fournit une base de données basée sur des fichiers, et la configuration ci-dessous crée une table results lors du premier exécution, avec un ID incrémental automatique et une valeur par défaut représentant une date et une heure.

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
    )
`);

Enregistrement d’une ligne : cela passe par un outil d’aide qui enveloppe db.run, basé sur des appels de fonction, dans une promesse afin qu’il puisse être utilisé avec await. Les placeholders ? lient les valeurs de manière sécurisée, de sorte que le texte extrait contenant des guillemets ne peut ni endommager la requête SQL ni s’y injecter.

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();
            }
        );
    });
}

L’historique permet le suivi des modifications, la comparaison de résultats, l’émission de rapports et un journal d’activités.

Éviter de stocker deux fois le même résultat

Lors de plusieurs exécutions, on observe souvent les mêmes éléments. Un outil de recherche vérifie s’un résultat ayant le même titre existe déjà pour une source donnée.

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));
            }
        );
    });
}

Le flux de travail n’insère alors que les résultats qui sont nouveaux :

for (const result of articles) {
    const exists = await resultExists(
        result,
        "example.com"
    );

    if (!exists) {
        await saveResult(
            result,
            "example.com"
        );
    }
}

Cette approche de vérification puis d’insertion convient pour un processus séquentiel unique. Si plusieurs exécutions peuvent écrire en même temps, deux d’entre elles pourraient toutes deux afficher « non trouvé » et insérer des doublons. Une solution plus fiable consiste à ajouter une contrainte UNIQUE sur (title, source) et à utiliser INSERT OR IGNORE, ce qui permet à la base de données d’appliquer la règle de manière atomique.

Rendre les étapes instables reproductibles

L’automatisation réelle est moins prévisible qu’une démonstration. Les réseaux tombent en panne, les pages se chargent lentement, les serveurs répondent différemment, et les éléments ne sont pas toujours présents immédiatement. Un outil générique retry exécute une opération asynchrone un certain nombre de fois, attend entre les tentatives et relance la dernière erreur si toutes échouent.

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;
}

L’enveloppement d’une navigation se fait de la manière suivante :

await retry(
    async () => {
        await page.goto(
            "https://example.com",
            {
                waitUntil:
                    "domcontentloaded"
            }
        );
    },
    3
);

Deux points méritent d’être notés. Le délai est fixe ; pour les services qui peuvent être surchargés, un délai croissant (backoff exponentiel) est généralement préférable. De plus, les tentatives de réexécution n’ont sens que pour des opérations qui peuvent être répétées sans risque : recharger une page est acceptable, tandis que soumettre à nouveau un formulaire de commande ne l’est pas.

Grouper le flux de travail dans une classe

Avec plusieurs outils d’aide, un seul objet peut gérer la page et exposer les étapes à suivre. WebAutomation reçoit un navigateur, ouvre une page via start(), navigue à l’aide de retry, effectue la recherche et ferme ensuite la page.

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();
        }
    }
}

Le code d’application se lit alors comme un bref script décrivant les intentions, le cycle de vie du navigateur étant géré séparément afin qu’un seul navigateur puisse servir plusieurs objets d’automatisation.

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 environnement de production, placer les appels à close() dans un bloc finally.

Le présenter comme un outil en ligne de commande

Modifier le code pour exécuter une tâche différente n’est pas scalable. La bibliothèque Commander transforme le projet en une CLI dotée d’un nom, d’une description, d’une version et de sous-commandes. La commande visit ci-dessous prend un argument URL, l’ouvre en mode sans interface graphique et affiche le titre de la page.

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();

parseAsync() est utilisé à la place de parse() car le gestionnaire d’action est asynchrone. L’outil est ensuite exécuté depuis une terminal :

node webtool.js visit https://example.com

À ce stade, le script est devenu un outil que d’autres personnes ou un job cron peuvent appeler.

Centraliser la configuration

Les valeurs codées en dur se propagent rapidement. Un seul objet de configuration regroupe les paramètres du navigateur, de la base de données et des résultats.

const config = {
    browser: {
        headless: true,
        timeout: 30000
    },

    database: {
        file: "automation.db"
    },

    output: {
        directory: "results"
    }
};

Le code d’exécution lit ensuite ses valeurs, y compris un délai d’attente par défaut appliqué à chaque action Playwright sur la page :

const browser =
    await chromium.launch({
        headless:
            config.browser.headless
    });

const page =
    await browser.newPage();

page.setDefaultTimeout(
    config.browser.timeout
);

Conserver les paramètres en un seul endroit facilite le passage d’une expérience à une application facile à maintenir.

Placer une couche d’IA validée en premier

Un outil conventionnel expose des commandes fixes :

search
open
extract
save
report

Un modèle de langage peut quant à lui interpréter une demande, comme celle de trouver et de stocker des articles récents en JavaScript sur un site, pour la transformer en un objet de tâche structuré :

const task = {
    action: "search",
    query: "JavaScript",
    saveResults: true
};

Il ne faut jamais permettre au modèle d’exécuter du code arbitraire. Chaque tâche qu’il génère est vérifiée contre une liste autorisée des actions prises en charge avant toute exécution.

const allowedActions = new Set([
    "visit",
    "search",
    "extract",
    "save"
]);

function validateTask(task) {
    if (!allowedActions.has(task.action)) {
        throw new Error(
            "Unsupported action"
        );
    }

    return true;
}

Le pipeline résultant sépare l’interprétation de l’exécution :

Natural Language
       ↓
      AI
       ↓
Structured Task
       ↓
Validation
       ↓
JavaScript Workflow
       ↓
Browser / Database

Le modèle détermine ce que l’utilisateur semble vouloir ; l’application décide de ce qui lui est réellement permis de faire. En pratique, la validation devrait aller au-delà du nom de l’action et vérifier également les paramètres, par exemple que les URL appartiennent à des domaines autorisés.

À quoi sert vraiment cet outil

Les utilisateurs se soucient du fait qu’une tâche fastidieuse a disparu, et non du fait que Playwright est impliqué. Le même moteur convient à des utilisations légitimes telles que les tests de qualité et les tests automatisés, la surveillance des sites, les workflows internes, la collecte de données provenant de sources autorisées, les rapports récurrents, la gestion de contenu et l’automatisation des processus métier. Sur le plan commercial, cela peut être proposé sous forme de mise en place accompagnée de personnalisation et de maintenance continue, ou sous forme d’abonnement avec des limites d’utilisation et des fonctionnalités premium. Le code est le moteur ; le workflow est le produit.

L’architecture résultante

Ensemble, les couches ressemblent à ceci. Cheerio apparaît aux côtés de Playwright en tant qu’option plus légère pour parser du HTML statique lorsque un navigateur complet n’est pas nécessaire.

USER
                      │
                      ▼
              CLI / AI REQUEST
                      │
                      ▼
                TASK VALIDATOR
                      │
                      ▼
               WORKFLOW ENGINE
                 │          │
                 ▼          ▼
             PLAYWRIGHT   CHEERIO
                 │          │
                 └────┬─────┘
                      ▼
                  PROCESSED
                    DATA
                      │
                      ▼
                   SQLITE
                      │
                      ▼
              REPORT / EXPORT

Conclusion

Cet outil n’a jamais été conçu dès le départ. Il s’est développé couche par couche, chacune résolvant un problème concret :

  • Les fonctions nommées autour des locateurs englobent les coûts liés aux modifications du site.
  • L’extraction structurée combinée à SQLite transforme les exécutions ponctuelles en historique.
  • La déduplication et les tentatives de réexécution rendent fiables les exécutions répétées sans surveillance ; privilégiez les contraintes de base de données et les opérations idempotentes.
  • Une interface CLI et une configuration centrale permettent à d’autres de l’utiliser sans modifier le code.
  • Un interface utilisateur basée sur l’IA n’est sûre que s’il existe une validation stricte de chaque tâche générée.

Au lieu de commencer par déterminer quel framework apprendre, partez d’un problème récurrent à résoudre et laissez l’architecture évoluer en conséquence.