Ператворэнне сценарыю драматурга ў інструмент: зберагчык, павторныя спробы, CLI і алгорытмы карання AI
Як ендэвыдны сценарый драматурга ператвораецца ў інструмент для Node.js, які можна викорыстоўваць па-разныя разы, з функціямі запісу історыі ў SQLite, адзначэння дублікатав, паўтарных спроб, інтерфейсам Commander CLI, налашоўкамі і перакананымі задачамі, створанымі сяродовым AI.
Крыніцы: The JavaScript Automation Project I Built to Stop Repeating the Same Browser Tasks
Большая частка павтаральных задач у прынтара выконваліцца за адным і тым жа циклам: ачыць сайт, навігаваць, запісваць які-небудзь тэкст, чытаць рэзультат, зберагаць яго і павтарыць. Кожны крок ёсць простым, але якщо выконваць іх вручную ўсё дзень, гэта стварае значны тлумак. У этам кераванні цей цикл ператвараецца з дзесяткавай строчкі скрыпты Playwright у маленькі інструмент на Node.js з стойкай історыяю, адказвам на дуплікаты, можлівасцю павтарэння, інтерфейсам у командной лініи, цэнтральным налашчванням і захіщаным фронтэндам на натуральной мове.
Алгорытм работы такі: прыходзіць запит, яго выконвае JavaScript-процес, роботу выпрацоўвае справжній прынтар, рэзультаты у структураванам выконку падаюць у базу данных або звіт. У адным з суседніх матэрыялаў рассказваецца пра стварэнне модульнага інструменткіту для автаматызацыі на JavaScript з нуля; тут акцэнт ставяцца на рашэннях, якія забезпечваюць надзеямасць такога інструменту праз час.
Першы крок: запуск справжньага прыгледчыка
Playwright дазволяе JavaScript кераваць Chromium, Firefox або WebKit. Прыступны скрапт запускае видавальні прыгледчык, завантажае сторонку, чакае, пакуль DOM будзе готавы, выводзіць заголовак і замыкаеся.
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 дазволяе спазірваць процес автаматызацыі пад час разработкі, а waitUntil: „domcontentloaded“ рэшваецца, калі HTML будзе апрацаваны, замест таго каб чакаць на кожныя картынкі і скрапты. Усё, што следуе, ўтворана на адной з гэтых можлівасцей.
Абгортаваець первісныя дзеянні ў функцыі з іменам
Размешчэнне коду селектараў по проекце робіць кожную змяну сторонкі даскональнаю. У замен маленькія функцыі керуюць адной взаімадзеяннем, а функцыя вышэйшага роўня ўжывае іх разам.
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);
}
Локейтары выкарыстоўваюць getByRole, які знаходзі элементы па ўсёй доступной ролі і назеўцы. Ён зазвычай краща перажывае рэфакторынг маркапу, чым селектары класаў. Калекцэнтары тепер можаюць выразіць свою мету, не ведаючы прыроды гэтага процесу:
await performSearch(
page,
"JavaScript automation"
);
Якщо форма пошуку змяніцца, патрэбна будзе апдэйтаваць толькі fillSearchBox і clickSearch.
Ператвараць сторункі на структураваныя даны
Чысты HTML важка для працы. Наступная функцыя выбірае кожны элемент .article і, у контэксте сторункі, прыяносіць кожны з іх да простага об’екта з загаловкам і автаром.
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 выкананяе функцыю-вызывач у прыглушчыку для всіх падпадаючых элементаў і вяртае серыяваны рэзультат у Node. Апцыйональная ланцюгавая аперацыя (?.) запобегае выкліканню памылак, калі які-небудзь з поляў .title чы .author не існуе; такія поля проста становяцца undefined. Тады для ўжытку яго достатнья адна вызова:
const articles = await extractArticles(page);
console.log(articles);
Калі стораніца ператвараецца ў масэ об’ектаў, яе можна фільтрацыяваць, порэшчаць, зберагаць чы перадаваць як і любые іншы даны.
Задайце адзінку для інструмента з SQLite
Рэзультаты, якія зникаюць пасля завершэння процесу, маюць мяркавую корыстнасць. Пакет sqlite3 апраналяе базу дадзенаў, заснованую на файлах, а прадстаўленыя нижэй настройкі ствараюць табелю results пад час першага запуску, з автаматычна адрастаючым ID і значэнням падзеі за замовчаннем.
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
)
`);
Зберагчы рэяд практыкуецца памочны прыемак, які абгортае db.run, базаваны на калебаках, у праміс, ў тым ліку, каб ён могаў работаць з await. Заместыкі ? безпечна прыўязуюць значэнні, таму текст, выкараны з інтарацыямі, не можа спануліць або прайснуць у 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();
}
);
});
}
Історыя дазволяе ведаць пра змены, адрабатваць порэвання, ствараць атласы та лог актываў.
Што не трэба робіць: зберагчы однаковы результат два разы
Пад час паўтаральных адрабоцэнняях часта паводзіцца да таго, што апынуцца одныя і тыя ж элементы. Памочны прыемак для пошуку пераканальваецца, чы не існуе ўжо рэзультата з тым самым заглавлём для заданага джерела.
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));
}
);
});
}
Пасля чаго рабочы процес вставляе толькі новыя рэзультаты:
for (const result of articles) {
const exists = await resultExists(
result,
"example.com"
);
if (!exists) {
await saveResult(
result,
"example.com"
);
}
}
Такы падход «пераканаліць, потым дадзець» падходзіць для адного последовальнага процесу. Якщо колькая экзекуцыі можаў быць запусканы адночасна, два з іх могу пабачыць «не знайдзена» і дадаць дублікаты. Болей надзеяны варыянт — адначасна дадаць правілу UNIQUE на (title, source) і выкарыстаць INSERT OR IGNORE, што дазволяе базе дадзеных атамальна прыменяць гэта правіло.
Зробіце крокі, якія часта збываюцца, можлівым да перапрытку
Рэальная аўтоматызацыя менш прагнозаваная, чым дэманстрацыя. Сеті знікаюць, сторункі завантажаюцца медленна, серверы адпавядаюць разна, а элементы не завжды ўсё роўна є. Універсальны помачнік retry выканае асінхронную операцыю да заданага колькіства разоў, чакае между спробамі і зноў выкідае апошню паслявольную, якщо ўсе спробы збываюцца неудачна.
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;
}
Абгортанне навігацыі выглядае так:
await retry(
async () => {
await page.goto(
"https://example.com",
{
waitUntil:
"domcontentloaded"
}
);
},
3
);
Є два моменты, якія вартае звернуць увагу. Задзейсненне операцыі адбываецца за фіксаваным часам; для служб, якія можаць быць перавантажаныя, зазвычай лепей выкарыстоўваць падыход з адрастаючым часам задзейснення (экспанэнцыйны backoff). А перапрыбуткі маюць сэнс толькі для операцый, якія можна без парадокса выконваць знову: перазаваношчы сторунку — гэта нормальна, але падачка зноў формы для пакупкі — нягэта.
Аб’еднайце алгорытм работы ў класе
За дапамогою калькільных падказчыкаў адна аб’ектавая структура можа кераваць сторункай і адкрываць доступ да ее крокаў. Клас WebAutomation прымеў браузер, адкрывае сторунку ў методе start(), пераводзіцься між разнымі крокамі за дапамогою метода retry, выкананае пошук і пасля чаго сторунка закрываецца.
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();
}
}
}
Код аплікацыі тады выглядае як короткі скрыпт, які описвае намеры, пры чым цикл жыцця браузера залишаецца паза яго межамі, таму ўсё ж адны браузер можа обслужваць калькільных аб’ектаў для автаматызацыі.
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();
У працоўным режыме вызовы методу close() трэба размесціў у блоке finally.
Адаптаваць як інструмент уярэжбовайго рядка
Рэдагаванне коду для адкрыцьця іншых задач не ўзрастае эфективна. Бібліятэка Commander ператварае проект у інструмент уярэжбовайго рядка з назвай, апісаннем, версіяй і падкамандамі. Каманда visit, паказаная нижэй, прыме параметр URL, адкрые яго без відображэння і выведзе заголовак сторанкі.
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() выкарыстоўваецца замест parse(), таму што обробнік дзеяння ў асінхронным режыме. Пасля чаго інструмент запускаецца з тэрміналу:
node webtool.js visit https://example.com
У гэты момент скрыпт стае інструментам, які можна запускаць іншымі або за дапамогою задання cron.
Цэнтрызавацыя настройкаў
Значэння, заданыя як статычны код, быстра распространяюцца. Адна аб’ект настройкаў агрупавае параметры браузера, базы дадзенаў і выходных даных.
const config = {
browser: {
headless: true,
timeout: 30000
},
database: {
file: "automation.db"
},
output: {
directory: "results"
}
};
Код запуску пасля чаго чытае з яго данні, уключаючы стандартны таймаут, які прыменяецца да кожнага дзеяння Playwright на сторанцы:
const browser =
await chromium.launch({
headless:
config.browser.headless
});
const page =
await browser.newPage();
page.setDefaultTimeout(
config.browser.timeout
);
Зберагчыце настройкі ў аднам месца спрыяе лёгкам пераходу ад эксперымента да прыемнай для карэстаўвання аплікацыі.
Разместіце слой AI з пераканальнай перацэю спераду
Традыцыйны інструмент адкрывае фіксаваныя команды:
search
open
extract
save
report
Модель мовы можа заместа таго адразу распазнаць запит, напрыклад, пра шуканне і зберагчыце няўжоўныя статті на JavaScript з аднаго сайту, і ператворыць яго на структураваны об’ект задачы:
const task = {
action: "search",
query: "JavaScript",
saveResults: true
};
Модэлю ніколі не трэба дазволяць выкананне даколькі хоча бы коду. Кожная задача, яку ён стварыць, пераканальваецца па списку дазволеных дзеянняй прычымо да таго, калі ўсё будзе выканана.
const allowedActions = new Set([
"visit",
"search",
"extract",
"save"
]);
function validateTask(task) {
if (!allowedActions.has(task.action)) {
throw new Error(
"Unsupported action"
);
}
return true;
}
Рэзультатныя процесы раздзеляюць распазнаванне і выкананне:
Natural Language
↓
AI
↓
Structured Task
↓
Validation
↓
JavaScript Workflow
↓
Browser / Database
Модэль выявляе, чаго, як судзіцца, жадае корыстнік; прыкладнэць выявляе, што яму насправды дазволена робіць. У практыцы пераканальванне должна выходзіць за межы назвы дзеяння і таксама пераканаліваць параметры, напрыклад, чы рэгістры належаць да затверджаных домэнаў.
Для чаго насправды патрэбна гэта апраната
Корыстнікама важна тое, што зникла клопатная робота, а не тое, што у ўжыцці ёсць Playwright. Той самы двыгун падходзіць для легітымных выкарыстанняў, такіх як тэставанне якасці і автаматызаванае тэставанне, монітарынг сайтаў, внутрашняе кераванне процэсамі, збіранне дадзейнаў з автарызаваных источнікаў, періядычныя звёсткі, кераванне контентам і автаматызацыя бізнес-процэсаў. У комерчыскіх цялях гэта можа быць запакавана як наладжванне плюс індывідуальная налаштовка і стацыонарнае адтрыманне, або як падпіскі з лімітамі выкарыстання і прыемнымі функцыямі. Код — гэта двыгун; рабочы процес — гэта продукт.
Рэзультатныя архітектуры
Усі шары разам выглядаюць так. Cheerio з’яўляецца разам з Playwright як легкія альтернатыва для парсінгу статычнага HTML, калі не патрэбны цэлыя браузеры.
USER
│
▼
CLI / AI REQUEST
│
▼
TASK VALIDATOR
│
▼
WORKFLOW ENGINE
│ │
▼ ▼
PLAYWRIGHT CHEERIO
│ │
└────┬─────┘
▼
PROCESSED
DATA
│
▼
SQLITE
│
▼
REPORT / EXPORT
Заключанне
Этот інструмент ніколі не быў спроектаваны з самага пачатку. Ён розвіваўся шляхом дадавання новых шароў паштоўку, пры чым кожны з іх рашаў конкрэтную проблему:
- Функцыі з іменамі, якія выкарыстоўваюць локейтары, маюць наследкі змян на сайце.
- Структураваны экстракція плюс SQLite ператвараюць екзальтныя запускі на історыю.
- Усуненне дуплікацый і павторныя запускі робяць ўпорядкованымі; краща выкарыстоўваць обмежэння базы дадзеных і ідемпатныя операцыі.
- CLI і цэнтральная настройка дазволяюць іншым запускать інструмент без змены коду.
- Фронтэнд на базе AI ў безпекі толькі праз строгую пераканальвання кожнага створанага заведамства.
У замест на тое, каб пачаткаць з выбору фрэймворку для навучання, пачніце з проблемы, яую трэба рашыць, і нехай архітектура выклікаецца сама.