Strona główna / Artykuły / Tworzenie modułowego narzędzia do automatyzacji w JavaScript od zera

Tworzenie modułowego narzędzia do automatyzacji w JavaScript od zera

Dowiedz się, jak połączyć Playwright, Cheerio, SQLite i Commander w wielokrotnie używalny silnik procesów Node.js, który rozwija się z prostego skryptu w produkt automatyzacji nadający się do sprzedaży.

2080 słów

1. Powtarzający się problem stał się punktem wyjścia

W pewnym momencie zauważasz, że raz po raz wykonywasz ten sam rodzaj zadania.

Otwierasz stronę.

Szukasz na niej czegoś przydatnego.

Wybierasz istotne informacje.

Zapisujesz je gdzieś na później.

Przechodzisz do następnej strony.

Ponawiasz ten cykl.

Żaden z tych kroków sam w sobie nie jest trudny, ale sama ich powtarzalność jest absurdalna.

Zamiast więc bierać kolejną ramę JavaScript tylko po to, by stworzyć kolejny panel sterowania, bardziej przydatnym rozwiązaniem jest stworzenie czegoś, co faktycznie można wykorzystać: narzędzia do automatyzacji w JavaScript i zbierania danych.

Idea jest prosta:

Poddaj programowi zadanie → pozwól JavaScript zająć się powtarzalną częścią → otrzymaj ustrukturyzowane wyniki.

Wcześniejsza wersja takiego narzędzia wymaga zaledwie kilku technologii:

  • Node.js
  • Playwright
  • Cheerio
  • SQLite
  • Commander

To połączenie wystarcza, by przekształcić mały skrypt w coś, co zaczyna przypominać prawdziwy produkt.

2. Playwright jako pierwszy element budulcowy

Celem było umożliwienie JavaScriptowi sterowania rzeczywistym przeglądarką.

Playwright ułatwia to znacznie bardziej, niż się spodziewano.

const { chromium } = require("playwright");

async function visitWebsite(url) {
    const browser = await chromium.launch({
        headless: false
    });

    const page = await browser.newPage();

    await page.goto(url, {
        waitUntil: "domcontentloaded"
    });

    console.log(
        "Page title:",
        await page.title()
    );

    console.log(
        "Current URL:",
        page.url()
    );

    await browser.close();
}

visitWebsite(
    "https://example.com"
);

Kiedy po raz pierwszy uruchamiasz coś takiego, wydaje się to niemal zbyt proste.

JavaScript otwiera przeglądarkę.

JavaScript przechodzi na określoną stronę.

JavaScript odczytuje jej zawartość.

JavaScript zamyka przeglądarkę.

Samo to daje podstawę, na której można budować narzędzia do testowania przeglądarek, monitoringu, wykonywania powtarzalnych zadań oraz ogólnej automatyzacji.

3. Przejście od współrzędnych do elementów

Jedną rzeczą, której należy unikać od samego początku, jest krucha automatyzacja.

Mówienie programowi czegoś w stylu:

Kliknij dokładnie w to miejsce.

Oznacza, że automatyzacja może przestać działać w momencie najmniejszej zmiany układu.

Lepszym podejściem jest pisanie kodu, który opisuje to, z czym faktycznie interaguje użytkownik, a nie to, gdzie co znajduje się na ekranie.

async function searchPage(page, query) {
    await page
        .getByRole("textbox")
        .fill(query);

    await page
        .getByRole("button", {
            name: "Search"
        })
        .click();

    await page.waitForLoadState(
        "domcontentloaded"
    );
}

To o wiele łatwiejsze do utrzymania z biegiem czasu.

Kod nie mówi:

Kliknij to, co znajduje się w współrzędnych 742, 381.

Mówi natomiast:

Znajdź pole tekstowe i przycisk wyszukiwania.

To niewielki wybór projektowy, ale znacznie ułatwia późniejszą automatyzację przeglądarki.

4. Wyodrębnianie danych z strony

Gdy przeglądarka potrafi sama poruszać się po stronie, kolejnym krokiem jest pozyskiwanie przez nią informacji.

Wyobraźmy sobie na przykład stronę wypełnioną kartami produktów.

async function extractProducts(page) {
    return page
        .locator(".product-card")
        .evaluateAll(cards => {
            return cards.map(card => {

                const name =
                    card
                        .querySelector(".product-name")
                        ?.textContent
                        ?.trim();

                const price =
                    card
                        .querySelector(".price")
                        ?.textContent
                        ?.trim();

                return {
                    name,
                    price
                };
            });
        });
}

W tym momencie przeglądarka już tylko nie odwiedza stron.

Przekształca to, co znajduje się na stronie, w obiekty JavaScript.

Dzięki temu dane stają się przydatne do dalszej obróbki.

const products =
    await extractProducts(page);

console.log(
    JSON.stringify(
        products,
        null,
        2
    )
);

Gdy informacje mają już określoną strukturę, można je przechowywać, porównywać, analizować lub przekazywać do innej części aplikacji.

5. Wykorzystanie Cheerio do parsowania HTML

Playwright sprawdza się, gdy naprawdę potrzebujesz działającej przeglądarki.

Często jednak masz już HTML pod ręką i w ogóle nie musisz uruchamiać Chromium.

Wtedy właśnie Cheerio odgrywa kluczową rolę.

const cheerio = require("cheerio");

function parseProducts(html) {
    const $ = cheerio.load(html);

    const products = [];

    $(".product-card").each(
        (_, element) => {

            const name = $(element)
                .find(".product-name")
                .text()
                .trim();

            const price = $(element)
                .find(".price")
                .text()
                .trim();

            products.push({
                name,
                price
            });
        }
    );

    return products;
}

Posiadanie obu tych narzędzi jest prawdziwą zaletą.

Playwright zajmuje się interakcją z przeglądarką.

Cheerio służy do lekkiego parsowania HTML.

Dzięki temu pełna instancja przeglądarki jest używana tylko wtedy, gdy jest to rzeczywiście konieczne, a prosty parser zajmuje się resztą.

6. Zapewnienie pamięci dla automatyzacji za pomocą SQLite

Kolejnym wyzwaniem do rozwiązania była przechowywanie danych.

Jeśli skrypt zbiera dane dzisiaj, gdzie one będą znajdować się jutro?

Rozwiązaniem okazało się wprowadzenie SQLite.

const sqlite3 = require("sqlite3").verbose();

const db = new sqlite3.Database(
    "automation.db"
);

db.run(`
    CREATE TABLE IF NOT EXISTS products (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        price TEXT,
        source TEXT,
        created_at DATETIME
            DEFAULT CURRENT_TIMESTAMP
    )
`);

Następnie dedykowana funkcja zajmowała się zapisywaniem każdego wyniku.

function saveProduct(product, source) {
    return new Promise(
        (resolve, reject) => {

            db.run(
                `
                INSERT INTO products
                (name, price, source)
                VALUES (?, ?, ?)
                `,
                [
                    product.name,
                    product.price,
                    source
                ],
                error => {

                    if (error) {
                        reject(error);
                        return;
                    }

                    resolve();
                }
            );
        }
    );
}

To dodatek znacząco zmienił strukturę projektu.

Dzięki bazie danych dane historyczne mogły się z czasem gromadzić. To otworzyło drogę do odpowiadania na praktyczne pytania takie jak:

  • Co się zmieniło?
  • Co pojawiło się ostatnio?
  • Co zniknęło?
  • Kiedy po raz ostatni zebrano te dane?
  • Które źródło stworzyło dany rekord?
  • Automatyzacja staje się znacznie bardziej przydatna, gdy potrafi pamiętać to, co wydarzyło się wcześniej.

    7. Budowa wielokrotnie używalnego silnika procesów pracy

    W tym etapie projekt składał się z kilku niezależnych elementów: sterowania przeglądarką, parsowania HTML oraz przechowywania danych w bazie. Brakowało mu jednak struktury — istniało realne ryzyko, że funkcje będą rozproszone wszędzie zamiast tworzyć spójny system.

    Aby to naprawić, klasa procesu pracy połączyła wszystko razem.

    class AutomationWorkflow {
    
        constructor(browser) {
            this.browser = browser;
            this.page = null;
        }
    
        async start() {
            this.page =
                await this.browser.newPage();
        }
    
        async visit(url) {
            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 getTitle() {
            return this.page.title();
        }
    
        async close() {
            await this.page.close();
        }
    }
    

    Gdy ta klasa została wprowadzona, realizacja pełnego zadania staje się prosta i przejrzysta od początku do końca.

    const browser =
        await chromium.launch({
            headless: false
        });
    
    const workflow =
        new AutomationWorkflow(
            browser
        );
    
    await workflow.start();
    
    await workflow.visit(
        "https://example.com"
    );
    
    await workflow.search(
        "JavaScript automation"
    );
    
    console.log(
        await workflow.getTitle()
    );
    
    await workflow.close();
    
    await browser.close();
    

    To jest moment, w którym projektowanie obiektowe zaczęło przynosić efekty. Instancja klasy modeluje sam proces pracy, a jej metody reprezentują poszczególne operacje. Reszta bazy kodu nie musi wiedzieć, jak każdy krok jest implementowany wewnętrznie.

    8. Radzenie sobie z błędami za pomocą logiki ponawiania

    Skrypty automatyzacyjne mają tendencję do bezbłędnego działania przez dziewięć razy, a potem zawalenia się przy dziesiątym. Sieć zwalnia, strona nie jest w pełni załadowana, serwer ma problemy lub element potrzebuje więcej czasu niż oczekiwano na wyświetlenie.

    Aby temu zaradzić, wprowadzono uniwersalną funkcję pomocniczą do ponawiania prób.

    async function retry(
        operation,
        attempts = 3,
        delay = 2000
    ) {
        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;
    }
    

    To narzędzie umożliwiło otoczenie kluczowych kroków automatycznym ponawianiem prób.

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

    Wniosek był prosty: automatyzacja na poziomie produkcyjnym musi uwzględniać awarie jako zjawisko normalne, a nie wyjątek. Tutorials zazwyczaj zakładają idealnie współpracującą sieć. Prawdziwe systemy nie mogą sobie pozwolić na takie założenia.

    9. Umieszczenie w interfejsie linii poleceń

    W pewnym momencie otwieranie pliku źródłowego za każdym razem tylko po to, by zmienić URL, stało się męczące.

    Cel przesunął się w kierunku sprawienia, by narzędzie zachowywało się jak prawdziwa aplikacja linii poleceń.

    Commander ułatwił osiągnięcie tego celu.

    const { Command } = require("commander");
    
    const program = new Command();
    
    program
        .name("webpilot")
        .description(
            "JavaScript automation toolkit"
        )
        .version("1.0.0");
    
    program
        .command("visit")
        .description(
            "Open a webpage"
        )
        .argument("<url>")
        .action(async url => {
    
            const browser =
                await chromium.launch({
                    headless: false
                });
    
            const page =
                await browser.newPage();
    
            await page.goto(url);
    
            console.log(
                await page.title()
            );
    
            await browser.close();
        });
    
    program.parseAsync();
    

    Gdy to zostało zaimplementowane, narzędzie można było uruchomić bezpośrednio z terminala.

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

    Wygląda to na niewielką korektę, ale fundamentalnie zmienia sposób interakcji z oprogramowaniem.

    Zamiast edytować program za każdym razem, gdy czegoś potrzebujesz,

    wystarczy go po prostu uruchomić.

    10. Traktowanie AI jako interfejsu użytkownika

    To rodzi nowe pytanie:

    Dlaczego użytkownik w ogóle musi zapamiętywać dokładną składnię poleceń?

    Zamiast wpisywać coś takiego jak:

    node webpilot.js screenshot https://example.com
    

    osoba może po prostu opisać intencję w zwykłym języku:

    ">Zrób zrzut ekranu strony głównej."

    Szara warstwa AI może przetłumaczyć to zdanie na strukturalny obiekt zadania.

    const task = {
        action: "screenshot",
        url: "https://example.com",
        output: "homepage.png"
    };
    

    Jednak AI nigdy nie otrzyma pozwolenia na bezpośrednie uruchamianie dowolnego JavaScriptu.

    Każda żądana akcja najpierw przechodzi przez krok walidacji.

    const allowedActions = new Set([
        "visit",
        "search",
        "screenshot",
        "download"
    ]);
    
    function validateTask(task) {
    
        if (
            !allowedActions.has(
                task.action
            )
        ) {
            throw new Error(
                "Unsupported action."
            );
        }
    
        if (
            task.url &&
            !task.url.startsWith("https://")
        ) {
            throw new Error(
                "Invalid URL."
            );
        }
    
        return true;
    }
    

    To zapewnia czyste rozdzielenie obowiązków:

    AI interpretuje to, czego chce użytkownik.

    Warstwa JavaScriptu decyduje, co jest faktycznie dozwolone.

    Playwright wykonywaje tylko zatwierdzoną akcję.

    Taki układ warstwowy to znacznie bezpieczniejsze podejście niż przekazanie modelowi AI bezpośredniej, nieograniczonej kontroli nad maszyną.

    11. Przekształcenie projektu w produkt sprzedażowy

    Na tym etapie uwaga przeniosła się z podstawowych bibliotek na rzecz rzeczywistego klienta.

    Prezentacja nigdy nie miała brzmieć tak:

    ">Aplikacja do automatyzacji przeglądarki stworzona przy użyciu Playwright i JavaScript."

    Nikt nie szuka konkretnie Playwright – szukają rezultatu.

    Dlatego ofertą stał sam rezultat. Kilka przykładów:

    Nadzór nad stronami internetowymi

    Firmy mogą śledzić swoje strony internetowe i otrzymywać powiadomienia, gdy kluczowe strony ulegną zmianie lub przestaną działać.

    Automaticzne testy jakości

    Zespoły inżynieryjne mogą przeprowadzać spójne, powtarzalne testy przeglądarek w odniesieniu do własnych aplikacji.

    Autoryzacja procesów wewnętrznych

    Organizacje mogą zautomatyzować powtarzalne zadania oparte na przeglądarce w narzędziach, do których już mają dostęp.

    Autoryzacja raportowania

    Zadanie zaplanowane może automatycznie zbierać zatwierdzone dane, je przechowywać i kompilować je w raport.

    Autoryzacja dla agencji

    Agencja może projektować dostosowane do potrzeb klientów ścieżki automatyzacji oraz naliczać opłaty za konfigurację i ciągłą obsługę.

    Ceny mogą przybierać kilka form:

    • Jednorazowa stała opłata za konfigurację
    • Miesięczne opłaty za utrzymanie
    • Ceny za poszczególne procesy
    • Licencjonowanie oparte na zespołach
    • Dostosowane prace integracyjne
    • Dostęp oparty na subskrypcji i hostowaniu

    Kluczem jest powiązanie oferowanego rozwiązania z konkretnym, określonym problemem, a nie z zestawem technologii.

    12. Małe skrypty mogą przerodzić się w prawdziwe produkty

    Na końcu architektura całego systemu wyglądała mniej więcej w ten sposób:

    User
                       │
                       ▼
                 CLI / AI Input
                       │
                       ▼
                 Task Validator
                       │
                       ▼
                 Workflow Engine
                       │
              ┌────────┼────────┐
              ▼        ▼        ▼
         Playwright  Cheerio  SQLite
              │        │        │
              └────────┼────────┘
                       ▼
                  Result Data
                       │
                       ▼
                 Report / API
    

    To jest ta część, nad którą warto się skupić.

    Prawdziwy problem nigdy nie dotyczył samej automatyzacji przeglądarki.

    Chodziło o eliminację powtarzalnej pracy ręcznej.

    Biblioteki — Playwright, Cheerio, SQLite, Commander — były jedynie mechanizmami służącymi do przekształcenia tej powtarzalności w działający oprogramowanie.

    Takie podejście kształtuje teraz sposób traktowania nowych projektów w JavaScript.

    Każdy raz, gdy sekwencja kroków ręcznych zaczyna się powtarzać po raz dwudziesty, instynkt nie jest taki:

    ">Czas poszukać nowego frameworku."

    Jest taki:

    ">Czy ten proces może zostać przekształcony w funkcję?"

    Jeśli odpowiedź brzmi tak, to jest to zalążek projektu automatyzacji.

    A gdy ta automatyzacja zaoszczędzi komuś wystarczająco dużo czasu i wysiłku, może rozwinąć się w coś, co warto sprzedać.

    Powiązane materiały

  • Ataki na łańcuch dostaw npm: jak działają i jak chronić Node.js — Wyjaśnia, w jaki sposób ataki na łańcuch dostaw npm, takie jak przejmowanie kont, typosquatting i pomyłki w zależnościach, funkcjonują, oraz podaje konkretne kroki do wzmocnienia instalacji Node.js.