Strona główna / Artykuły

Przekształcanie scenariusza dramaturga w narzędzie: przechowywanie, ponawianie prób, interfejs wiersza poleceń oraz zasady bezpieczeństwa oparte na AI

Jak jednorazowy skrypt Playwright przekształca się w narzędzie Node.js nadające się do wielokrotnego użycia, z historią w SQLite, funkcją usuwania duplikatów, możliwością ponawiania prób, interfejsem CLI Commander, konfiguracją oraz walidowanymi zadaniami wygenerowanymi przez AI.

Wydawca: REACTAPP.TOP9 min czytania1963 słów

Źródła: The JavaScript Automation Project I Built to Stop Repeating the Same Browser Tasks

Wiele powtarzalnych zadań w przeglądarce odbywa się według tego samego schematu: otwarcie strony, przeglądanie jej, wpisanie czegoś, odczytanie wyniku, jego zapisanie i powtórzenie. Każdy krok jest prosty, ale wykonywanie ich ręcznie każdego dnia skutkuje dużym obciążeniem. Ten przewodnik pokazuje, jak przekształcić taki schemat z dziesięcioliniowego skryptu Playwright w małe narzędzie Node.js z trwałą historią działań, wykrywaniem duplikatów, możliwością ponawiania prób, interfejsem wiersza poleceń, centralną konfiguracją oraz zabezpieczonym interfejsem w języku naturalnym.

Główny tok działań wygląda następująco: przychodzi żądanie, JavaScriptowy proces je przetwarza, praca jest wykonywana przez prawdziwą przeglądarkę, a ustrukturyzowane wyniki trafiają do bazy danych lub raportu. Inny temat dotyczy tworzenia modułowego zestawu narzędzi do automatyzacji w JavaScriptie od zera; tutaj nacisk kładziony jest na decyzje, które zapewniają niezawodność takiego narzędzia w miarę jego rozwoju.

Krok pierwszy: uruchomienie rzeczywistego przeglądarki

Playwright umożliwia JavaScriptowi sterowanie Chromium, Firefox lub WebKit. Skrypt startowy uruchamia widoczną przeglądarkę, ładuje stronę, czeka, aż DOM będzie gotowy, wyświetla tytuł i zamyka się.

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 pozwala obserwować proces automatyzacji podczas rozwoju, a waitUntil: „domcontentloaded” uruchamia się po zinterpretowaniu HTML, zamiast czekać na załadowanie każdego obrazka i skryptu. Wszystko, co następuje, opiera się na tych możliwościach.

Otoczenie surowych działań w nazwane funkcje

Rozproszenie kodu selektorów w projekcie sprawia, że każda zmiana strony jest kosztowna. Zamiast tego małe funkcje obsługują po jednej interakcji, a funkcja wyższego poziomu łączy je ze sobą.

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

Locatory wykorzystują getByRole, który znajduje elementy na podstawie ich dostępnej roli i nazwy. Zazwyczaj lepiej radzi sobie z modyfikacjami struktury markup niż selektory klasowe. Użytkownicy mogą teraz wyrazić swoje intencje bez konieczności znajomości mechanizmów działania:

await performSearch(
    page,
    "JavaScript automation"
);

Jeśli formularz wyszukiwania ulegnie zmianie, konieczne będzie aktualizowanie tylko funkcji fillSearchBox i clickSearch.

Zmiana stron na dane strukturalne

Czysty HTML sprawia trudności przy pracy. Następna funkcja wybiera wszystkie elementy .article i w kontekście strony mapuje każdy z nich na zwykły obiekt zawierający tytuł i autora.

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 uruchamia funkcję zwrotną w przeglądarce dla wszystkich dopasowanych elementów i zwraca seryjalizowany wynik do Node. Opcjonalne łączenie (?.) zapobiega błędom przy braku pola .title lub .author; takie pole jest po prostu ustawiane na undefined. Dzięki temu wystarczy jedna wywołanie:

const articles = await extractArticles(page);

console.log(articles);

Gdy strona staje się tablicą obiektów, można ją filtrować, porównywać, przechowywać lub przekazywać dalej jak każde inne dane.

Daj narzędziu pamięć za pomocą SQLite

Wyniki, które znikają po zakończeniu procesu, mają ograniczone zastosowanie. Pakiet sqlite3 umożliwia tworzenie bazy danych opartej na plikach, a poniższa konfiguracja tworzy tabelę results przy pierwszym uruchomieniu, zawierającą automatycznie rosnący ID oraz domyślny zapis daty i godziny.

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

Zapisanie wiersza odbywa się za pośrednictwem narzędzia pomocniczego, które otacza funkcję db.run opartą na callbackach obietnicą, aby mogła być używana z await. Zastępcze wartości ? bezpiecznie łączą dane, dzięki czemu tekst pobrany z sieci zawierający cudzysłowy nie może spowodować błędu ani zostać włączony do zapytania 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();
            }
        );
    });
}

Historia umożliwia śledzenie zmian, porównywanie wyników, generowanie raportów oraz prowadzenie dziennika aktywności.

Unikaj przechowywania tego samego wyniku dwukrotnie

Powtarzane wykonywania często pokazują te same elementy. Narzędzie pomocnicze sprawdza, czy dla danego źródła już istnieje wynik o tym samym tytule.

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

Następnie proces tylko wstawia nowe wyniki:

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

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

Ten podejście polegające na sprawdzeniu, a następnie wstawieniu danych jest w porządku dla pojedynczego, sekwencyjnego procesu. Jeśli kilka prób może działać jednocześnie, dwa z nich mogą otrzymać komunikat „nie znaleziono” i wstawić duplikaty. Lepszą opcją jest dodanie ograniczenia UNIQUE dla parametrów (title, source) oraz użycie instrukcji INSERT OR IGNORE, która pozwala bazie danych egzekwować to prawilo w sposób atomowy.

Zmiana kroków niestabilnych na możliwe do ponownej próby

Prawdziwa automatyzacja jest mniej przewidywalna niż demonstracja. Sieci mogą się rozłączyć, strony ładują się powoli, serwery odpowiadają inaczej, a elementy nie zawsze są dostępne od razu. Użytkownikowy helper retry uruchamia operację asynchroniczną maksymalnie określoną liczbę razy, czeka pomiędzy próbami i ponownie rzutuje ostatni błąd, jeśli wszystkie zawiodą.

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

Opakowanie operacji nawigacyjnej wygląda w ten sposób:

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

Warto zwrócić uwagę na dwie kwestie. Opóźnienie jest stałe; w przypadku usług, które mogą być przeciążone, zwykle lepsze jest rosnące opóźnienie (exponential backoff). Ponadto ponawianie prób ma sens tylko wtedy, gdy operacje można bezpiecznie powtarzać: ponowne załadowanie strony jest w porządku, natomiast ponowna wysyłka formularza zakupowego nie.

Zgrupuj procesy w klasie

Dzięki kilku pomocnikom pojedynczy obiekt może zarządzać stroną i udostępniać poszczególne kroki. WebAutomation otrzymuje przeglądarkę, otwiera stronę w metodzie start(), przemieszcza się za pomocą metody retry, wykonuje wyszukiwanie i zamyka stronę.

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

Kod aplikacji wtedy przypomina krótki skrypt opisujący zamierzenia, przy czym cykl życia przeglądarki pozostaje na zewnątrz, dzięki czemu jedna przeglądarka może obsługiwać kilka obiektów automatyzacji.

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

W środowisku produkcyjnym umieść wywołania close() w bloku finally.

Ujawnij to jako narzędzie wiersza poleceń

Edytowanie kodu w celu wykonywania innych zadań nie jest skalowalne. Biblioteka Commander przekształca projekt w CLI z nazwą, opisem, wersją oraz podpoleceniami. Poniższe polecenie visit przyjmuje argument w postaci URL, otwiera go bez interfejsu graficznego i wyświetla tytuł strony.

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

Używana jest funkcja parseAsync() zamiast parse(), ponieważ obsługa działań jest asynchroniczna. Narzędzie to następnie uruchamia się z terminala:

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

W tym momencie skrypt stał się narzędziem, które mogą wywołać inni użytkownicy lub zadań typu cron.

Zcentralizuj konfigurację

Wartości zapisane w sposób sztywny szybko się rozprzestrzeniają. Jeden obiekt konfiguracji grupuje ustawienia przeglądarki, bazy danych oraz wyników.

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

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

    output: {
        directory: "results"
    }
};

Kod uruchamiający następnie odczytuje z niego dane, w tym domyślny czas timeoutu stosowany do każdej akcji Playwright na stronie:

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

const page =
    await browser.newPage();

page.setDefaultTimeout(
    config.browser.timeout
);

Przechowywanie ustawień w jednym miejscu ułatwia przejście od eksperymentów do aplikacji łatwej w utrzymaniu.

Umieść warstwę AI sprawdzoną na początku

Klasyczne narzędzia udostępniają stałe polecenia:

search
open
extract
save
report

Model językowy może zamiast tego zinterpretować żądanie, np. prośbę o znalezienie i przechowanie najnowszych artykułów w JavaScript na danej stronie, i przekształcić je w zstrukturyzowany obiekt zadania:

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

Nigdy nie wolno pozwalać modelowi na uruchamianie dowolnego kodu. Każde zadanie, które tworzy, jest sprawdzane pod kątem listy dozwolonych działań przed wykonaniem jakichkolwiek operacji.

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

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

    return true;
}

Wynikowy proces oddziela interpretację od wykonywania:

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

Model decyduje o tym, czego użytkownik chce; aplikacja określa, co jest mu faktycznie dozwolone. W praktyce walidacja powinna wykraczać poza nazwę działania i sprawdzać również parametry, na przykład to, czy adresy URL należą do zatwierdzonych domen.

Dla czego naprawdę służy to narzędzie

Użytkownikom zależy na tym, że nudne obowiązki zniknęły, a nie na tym, że użyto Playwrighta. Ten sam silnik nadaje się do legalnych zastosowań, takich jak testy jakościowe i automatyczne testowanie, monitorowanie stron, wewnętrzne procesy pracy, zbieranie danych z uprawnionych źródeł, regularne raporty, zarządzanie treścią oraz automatyzacja procesów biznesowych. Z komercyjnego punktu widzenia może to być oferowane jako pakiet obejmujący konfigurację, dostosowanie oraz ciągłą obsługę, albo jako subskrypcja z ograniczeniami korzystania i dodatkowymi funkcjami premium. Kod to silnik; proces pracy to produkt.

Otrzymana architektura

W połączeniu warstwy wyglądają w ten sposób. Cheerio pojawia się obok Playwright jako lżejsza alternatywa do parsowania statycznego HTML, gdy pełny przeglądarka nie jest konieczny.

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

Podsumowanie

Narzędzie nie zostało zaprojektowane od początku. Rozwijano je warstwa po warstwie, przy czym każda z nich rozwiązywała konkretny problem:

  • Funkcje o nazwach odpowiadających lokalizatorom obejmują koszty zmian na stronie.
  • Strukturyzowane wydobywanie danych w połączeniu z SQLite przekształca jednorazowe wykonywania w historię działań.
  • Deduplikacja i ponawianie prób sprawiają, że powtarzane, nie nadzorowane wykonywania są wiarygodne; lepiej używać ograniczeń bazy danych i operacji idempotentnych.
  • Interfejs CLI oraz centralna konfiguracja umożliwiają innym uruchamianie narzędzia bez modyfikacji kodu.
  • Interfejs oparty na AI jest bezpieczny tylko przy ścisłej weryfikacji każdego utworzonego zadań.

Zamiast zaczynać od decyzji, jakiego frameworka się nauczyć, zacznij od problemu powtarzalnego, który warto rozwiązać, i pozwól architekturze się rozwijać.