Ein Drehbuch eines Dramatikers in ein Tool umwandeln: Speicherung, Wiederholungsversuche, CLI und AI-Schutzmechanismen
Wie ein einmaliger Playwright-Skript zu einem wiederverwendbaren Node.js-Tool mit SQLite-Historie, Doppelungsbeseitigung, Wiederholungsversuchen, einer Commander CLI, Konfiguration sowie validierten von KI erzeugten Aufgaben wird.
Quellen: The JavaScript Automation Project I Built to Stop Repeating the Same Browser Tasks
Viele wiederkehrende Aufgaben im Browser folgen demselben Muster: Eine Seite öffnen, navigieren, etwas eingeben, das Ergebnis lesen, speichern und wiederholen. Jeder Schritt ist unkompliziert, doch wenn man sie täglich manuell ausführt, summieren sich die Aufwände. In dieser Anleitung wird dieses Muster von einem zehnzeiligen Playwright-Skript in ein kleines Node.js-Tool umgewandelt, das eine dauerhafte Historie, Duplikatenerkennung, Wiederholungsversuche, eine Kommandozeilenschnittstelle, zentrale Konfiguration sowie ein geschütztes Frontend in natürlicher Sprache bietet.
Der Ablauf ist wie folgt: Es kommt eine Anfrage, ein JavaScript-Workflow führt sie aus, ein echter Browser erledigt die Arbeit, strukturierte Ergebnisse entstehen und landen in einer Datenbank oder einem Bericht. Ein verwandtes Thema behandelt das Erstellen eines modularen JavaScript-Automatisierungs-Toolkits von Grund auf; hier liegt der Schwerpunkt auf den Entscheidungen, die dafür sorgen, dass ein solches Tool im Laufe seiner Entwicklung zuverlässig bleibt.
Schritt eins: Einen echten Browser ausführen
Playwright ermöglicht es JavaScript, Chromium, Firefox oder WebKit zu steuern. Das Startskript lädt einen sichtbaren Browser, lädt eine Seite, wartet, bis der DOM bereit ist, gibt den Titel aus und schließt den Browser wieder.
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 ermöglicht es, die Automatisierung während der Entwicklung zu beobachten, und waitUntil: „domcontentloaded“ führt aus, sobald die HTML-Struktur parset wurde, anstatt auf jedes Bild und jede Skriptdatei zu warten. Alles Weitere basiert auf diesen Funktionen.
Raue Aktionen in benannte Funktionen einbetten
Die Verbreitung von Selektorcode über ein Projekt macht jeden Seitenwechsel aufwändig. Stattdessen übernehmen kleine Funktionen jeweils eine Interaktion, und eine höherstufige Funktion kombiniert sie.
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);
}
Die Lokatoren verwenden getByRole, welcher Elemente anhand ihrer zugänglichen Rolle und ihres Namens findet. Er übersteht Überschreibungen der Markup-Struktur in der Regel besser als Klassenselktoren. Die Aufrufer können nun ihre Absicht ausdrücken, ohne die dahinterliegenden Mechanismen kennen zu müssen:
await performSearch(
page,
"JavaScript automation"
);
Falls sich das Suchformular ändert, müssen nur fillSearchBox und clickSearch aktualisiert werden.
Seiten in strukturierte Daten umwandeln
Rohes HTML ist schwer zu verarbeiten. Die folgende Funktion wählt jedes .article-Element aus und ordnet innerhalb des Seitenkontexts jedes davon einem einfachen Objekt mit Titel und Autor zu.
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 führt den Callback im Browser auf allen gefundenen Elementen aus und gibt das serialisierte Ergebnis an Node zurück. Die optionale Kettensuche (?.) verhindert, dass das Fehlen von .title oder .author zu einem Fehler führt; in diesem Fall wird das Feld einfach als undefined behandelt. Dadurch reicht eine einzige Aufrufstelle aus:
const articles = await extractArticles(page);
console.log(articles);
Sobald die Seite als Array von Objekten vorliegt, kann man sie filtern, vergleichen, speichern oder wie jede andere Datenmenge weitergeben.
Geben Sie der Anwendung eine SQLite-Datenbank
Ergebnisse, die beim Beenden des Prozesses verschwinden, sind nur eingeschränkt nützlich. Das sqlite3-Paket bietet eine auf Dateien basierende Datenbank, und die untenstehende Einrichtung erstellt beim ersten Start eine results-Tabelle mit einer automatisch inkrementierenden ID sowie einem standardmäßigen Zeitstempel.
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
)
`);
Das Speichern einer Zeile erfolgt über einen Hilfsfunktion, der die auf Callbacks basierende db.run-Funktion in eine Promise einhüllt, damit sie mit await verwendet werden kann. Die ?-Platzhalter binden die Werte sicher, sodass gescrapteter Text mit Anführungszeichen den SQL-Abfragen nicht schaden oder sich darin einfügen kann.
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();
}
);
});
}
Die Historie ermöglicht das Verfolgen von Änderungen, Durchführung von Vergleichen, Erstellung von Berichten sowie ein Aktivitätsprotokoll.
Vermeiden Sie das Mehrfachspeichern desselben Ergebnisses
Bei wiederholten Ausführungen werden oft dieselben Einträge angezeigt. Ein Abfragenhilfsfunktion prüft, ob bereits ein Ergebnis mit dem gleichen Titel für eine bestimmte Quelle existiert.
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));
}
);
});
}
Der Workflow fügt anschließend nur neue Ergebnisse ein:
for (const result of articles) {
const exists = await resultExists(
result,
"example.com"
);
if (!exists) {
await saveResult(
result,
"example.com"
);
}
}
Der Ansatz „Prüfen und dann einfügen“ eignet sich für einen einzigen sequenziellen Prozess. Wenn mehrere Ausführungen gleichzeitig schreiben könnten, könnten zwei von ihnen beide „nicht gefunden“ sehen und Duplikate einfügen. Eine robustere Option ist es, eine UNIQUE-Beschränkung für (title, source) hinzuzufügen und INSERT OR IGNORE zu verwenden, wodurch die Datenbank die Regel atomar durchsetzen kann.
Mögliche Fehlerquellen in Schritten wiederholbar machen
Echte Automatisierungen sind weniger vorhersehbar als Demonstrationen. Netzwerke brechen ab, Seiten laden langsam, Server antworten unterschiedlich und Elemente sind nicht immer sofort vorhanden. Ein generischer retry-Hilfsfunktion führt eine asynchrone Operation bis zu einer bestimmten Anzahl von Malen aus, wartet zwischen den Versuchen und wirft den letzten Fehler erneut auf, wenn alle fehlschlagen.
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;
}
Die Umhüllung einer Navigation sieht so aus:
await retry(
async () => {
await page.goto(
"https://example.com",
{
waitUntil:
"domcontentloaded"
}
);
},
3
);
Zwei Dinge sind erwähnenswert. Die Verzögerung ist festgelegt; für Dienste, die möglicherweise überlastet sein können, ist eine zunehmende Verzögerung (exponentieller Backoff) in der Regel besser. Zudem machen Wiederholungsversuche nur dann Sinn, wenn die Operationen sicher wiederholt werden können: Das Neu laden einer Seite ist in Ordnung, während das erneute Absenden eines Kaufformulars nicht geeignet ist.
Den Workflow in einer Klasse bündeln
Mit mehreren Hilfsfunktionen kann ein einzelnes Objekt die Seite verwalten und die Schritte bereitstellen. WebAutomation erhält einen Browser, öffnet eine Seite in start(), navigiert über retry, führt die Suche durch und schließt anschließend die Seite.
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();
}
}
}
Der Anwendungscode liest sich somit wie ein kurzer Skript mit Angaben zu den Absichten, wobei der Lebenszyklus des Browsers außerhalb bleibt, sodass ein einziger Browser mehrere Automatisierungsobjekte bedienen kann.
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();
In der Produktion sollten die close()-Aufrufe in einen finally-Block gelegt werden.
Als Befehlszeilenwerkzeug bereitstellen
Das Anpassen des Codes, um eine andere Aufgabe auszuführen, ist nicht skalierbar. Die Commander-Bibliothek verwandelt das Projekt in ein CLI mit Namen, Beschreibung, Version und Unterbefehlen. Der unten stehende visit-Befehl nimmt ein URL-Argument entgegen, öffnet es ohne grafische Oberfläche und gibt den Seitentitel aus.
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() wird anstelle von parse() verwendet, weil der Aktionsersteller asynchron ist. Das Werkzeug wird anschließend aus einer Terminal-Schnittstelle ausgeführt:
node webtool.js visit https://example.com
Zu diesem Zeitpunkt ist das Skript zu einem Werkzeug geworden, das von anderen oder durch einen Cron-Job aufgerufen werden kann.
Konfiguration zentralisieren
Hart kodierte Werte verbreiten sich schnell. Ein einziges Konfigurationsobjekt gruppiert Einstellungen für Browser, Datenbanken und Ausgabe.
const config = {
browser: {
headless: true,
timeout: 30000
},
database: {
file: "automation.db"
},
output: {
directory: "results"
}
};
Der Startcode liest anschließend daraus ab, einschließlich einer Standard-Timeout-Einstellung, die auf jede Playwright-Aktion auf der Seite angewendet wird:
const browser =
await chromium.launch({
headless:
config.browser.headless
});
const page =
await browser.newPage();
page.setDefaultTimeout(
config.browser.timeout
);
Die Aufbewahrung der Einstellungen an einem Ort erleichtert den Übergang von Experimenten zu wartbaren Anwendungen.
Fügen Sie eine validierte KI-Schicht voran
Ein herkömmliches Tool stellt feste Befehle zur Verfügung:
search
open
extract
save
report
Ein Sprachmodell kann hingegen eine Anfrage wie das Suchen und Speichern aktueller JavaScript-Artikel auf einer Website interpretieren und sie in ein strukturiertes Aufgab Objekt umwandeln:
const task = {
action: "search",
query: "JavaScript",
saveResults: true
};
Es darf dem Modell unter keinen Umständen erlaubt werden, willkürlichen Code auszuführen. Jede von ihm erzeugte Aufgabe wird vor der Ausführung gegen eine Erlaubnisliste unterstützter Aktionen überprüft.
const allowedActions = new Set([
"visit",
"search",
"extract",
"save"
]);
function validateTask(task) {
if (!allowedActions.has(task.action)) {
throw new Error(
"Unsupported action"
);
}
return true;
}
Der resultierende Pipeline trennt Interpretation und Ausführung voneinander:
Natural Language
↓
AI
↓
Structured Task
↓
Validation
↓
JavaScript Workflow
↓
Browser / Database
Das Modell entscheidet, was der Benutzer zu wollen scheint; die Anwendung entscheidet, was tatsächlich erlaubt ist. In der Praxis sollte die Validierung über den Namen der Aktion hinausgehen und auch Parameter überprüfen, beispielsweise ob URLs zu zugelassenen Domains gehören.
Zweck des Tools
Es ist den Benutzern wichtig, dass eine mühsame Aufgabe verschwunden ist – nicht, dass Playwright involviert ist. Derselbe Motor eignet sich für legitime Anwendungen wie QA- und automatisierte Tests, Site-Monitoring, interne Workflows, Datenerfassung aus autorisierten Quellen, wiederkehrende Berichte, Content-Management sowie Automatisierung von Geschäftsprozessen. Kommerziell kann dies als Setup inklusive Anpassung und kontinuierlicher Wartung oder als Abonnement mit Nutzungsgrenzen und Premium-Funktionen angeboten werden. Der Code ist der Motor; der Workflow ist das Produkt.
Die resultierende Architektur
Insgesamt sehen die Schichten so aus. Cheerio tritt neben Playwright als leichtere Alternative zum Parsen statischer HTMLs auf, bei denen ein vollständiger Browser nicht notwendig ist.
USER
│
▼
CLI / AI REQUEST
│
▼
TASK VALIDATOR
│
▼
WORKFLOW ENGINE
│ │
▼ ▼
PLAYWRIGHT CHEERIO
│ │
└────┬─────┘
▼
PROCESSED
DATA
│
▼
SQLITE
│
▼
REPORT / EXPORT
Zusammenfassung
Das Tool wurde nicht von Anfang an geplant. Es entwickelte sich Schicht für Schicht, wobei jede Schicht ein konkretes Problem löste:
- Namenlose Funktionen für Lokatoren beinhalten die Kosten von Änderungen auf der Website.
- Gestrukturierte Extraktion in Kombination mit SQLite verwandeln einmalige Ausführungen in historische Aufzeichnungen.
- Deduplizierung und Wiederholungsversuche sorgen dafür, dass wiederholte, unüberwachte Ausführungen zuverlässig sind; man sollte Datenbankbeschränkungen sowie idempotente Operationen bevorzugen.
- Eine CLI und zentrale Konfiguration ermöglichen es anderen, das Tool auszuführen, ohne den Code anzufassen.
- Ein KI-basiertes Frontend ist nur sicher, wenn jede generierte Aufgabe streng überprüft wird.
Anstatt mit der Entscheidung zu beginnen, welches Framework man lernen soll, sollte man mit einem wiederkehrenden Problem starten, das beseitigt werden muss, und die Architektur daraus entwickeln lassen.