browser-automation

browser-automation

熱門

瀏覽器自動化為網頁測試、資料擷取與 AI Agent 互動提供強大支援。

4.3萬星標
6676分支
更新於 2026/7/15
SKILL.md
唯讀
名稱
browser-automation
描述

瀏覽器自動化為網頁測試、資料擷取與 AI Agent 互動提供強大支援。

瀏覽器自動化 (Browser Automation)

瀏覽器自動化為網頁測試、資料擷取與 AI Agent 互動提供強大支援。
腳本時好時壞與系統穩定可靠的差別,關鍵在於是否精通選取器、等待策略與防偵測模式。

本 Skill 涵蓋 Playwright(推薦)與 Puppeteer,並提供測試、資料擷取與 Agent 瀏覽器控制的實戰模式。核心洞察:Playwright 已在框架之爭中勝出。除非你需要 Puppeteer 的防偵測生態系或僅限 Chrome 環境,否則在 2025 年 Playwright 是更好的選擇。

關鍵區別:測試自動化(你可控且行為可預測的應用程式)vs 資料擷取/Agent 自動化(會主動反制的不可預測網站)。問題不同,解決方案也不同。

原則

  • 優先使用面向使用者的定位器(getByRole, getByText),而非 CSS/XPath
  • 切勿加入手動等待——Playwright 的自動等待(Auto-wait)機制會處理好一切
  • 每個測試/任務都應在全新的 Context 中完全隔離執行
  • 螢幕截圖與追蹤紀錄(Traces)是你的除錯命脈
  • CI 環境使用無頭模式(Headless),偵錯時使用有頭模式(Headed)
  • 防偵測是一場貓追老鼠的賽局——保持最新狀態,否則隨時會被封鎖

能力範圍

  • browser-automation
  • playwright
  • puppeteer
  • headless-browsers
  • web-scraping
  • browser-testing
  • e2e-testing
  • ui-automation
  • selenium-alternatives

相關範疇

  • api-testing → backend
  • load-testing → performance-thinker
  • accessibility-testing → accessibility-specialist
  • visual-regression-testing → ui-design

工具生態

框架

  • Playwright - 時機:預設首選——跨瀏覽器支援、內建自動等待、最佳開發者體驗 (DX)。備註:成功率 96%,平均執行時間 4.5 秒,微軟出品
  • Puppeteer - 時機:僅限 Chrome、需要防偵測外掛、維護現有程式碼庫。備註:大規模執行時成功率 75%,但擁有最佳防偵測生態系
  • Selenium - 時機:傳統舊系統、特定程式語言繫結。備註:速度較慢且繁瑣,但擁有最廣泛的瀏覽器支援

防偵測工具

  • puppeteer-extra-plugin-stealth - 時機:搭配 Puppeteer 需要繞過 Bot 偵測時。備註:防偵測的業界黃金標準
  • playwright-extra - 時機:Playwright 的防偵測外掛。備註:puppeteer-extra 生態系的移植版
  • undetected-chromedriver - 時機:Selenium 防偵測。備註:動態繞過自動化偵測

雲端瀏覽器

  • Browserbase - 時機:託管式無頭瀏覽器基礎設施。備註:內建防偵測模式、Session 管理
  • BrowserStack - 時機:大規模跨瀏覽器測試。備註:真實裝置支援、CI 整合

設計模式

測試隔離模式

每個測試都在完全隔離的全新狀態下執行

何時使用:自動化測試、任何需要可重複執行的自動化任務

測試隔離原則:

"""
每個測試各自擁有獨立的:

  • 瀏覽器 Context(Cookie、Storage)
  • 全新頁面
  • 乾淨無污染的狀態
    """

Playwright 測試範例

"""
import { test, expect } from '@playwright/test';

// 每個測試都在獨立的瀏覽器 Context 中執行
test('user can add item to cart', async ({ page }) => {
// 全新 Context - 無 Cookie,亦無其他測試殘留的 Storage
await page.goto('/products');
await page.getByRole('button', { name: 'Add to Cart' }).click();
await expect(page.getByTestId('cart-count')).toHaveText('1');
});

test('user can remove item from cart', async ({ page }) => {
// 完全隔離 - 購物車為空
await page.goto('/cart');
await expect(page.getByText('Your cart is empty')).toBeVisible();
});
"""

共享驗證狀態模式

"""
// 儲存一次驗證狀態,跨測試重複使用
// setup.ts
import { test as setup } from '@playwright/test';

setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();

// 等待驗證完成
await page.waitForURL('/dashboard');

// 儲存驗證狀態
await page.context().storageState({
path: './playwright/.auth/user.json'
});
});

// playwright.config.ts
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /.*.setup.ts/ },
{
name: 'tests',
dependencies: ['setup'],
use: {
storageState: './playwright/.auth/user.json',
},
},
],
});
"""

面向使用者定位器模式

以使用者看待介面的方式來選取元素

何時使用:永遠優先使用——定位器的預設標準做法

面向使用者定位器:

"""
優先順序:

  1. getByRole - 最佳:對齊無障礙樹 (Accessibility tree)
  2. getByText - 良好:比對可見內容
  3. getByLabel - 良好:比對表單 Label
  4. getByTestId - 備用:明確的測試契約
  5. CSS/XPath - 最後手段:脆弱易失效,應盡量避免
    """

良好範例(面向使用者)

"""
// By role - 最佳選擇
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('link', { name: 'Sign up' }).click();
await page.getByRole('heading', { name: 'Dashboard' }).isVisible();
await page.getByRole('textbox', { name: 'Search' }).fill('query');

// By text content
await page.getByText('Welcome back').isVisible();
await page.getByText(/Order #\d+/).click(); // 支援正則表達式

// By label (表單)
await page.getByLabel('Email address').fill('user@example.com');
await page.getByLabel('Password').fill('secret');

// By placeholder
await page.getByPlaceholder('Search...').fill('query');

// By test ID (當沒有面向使用者的選項可用時)
await page.getByTestId('submit-button').click();
"""

不佳範例(脆弱易失效)

"""
// 請勿使用 - 與 DOM 結構高度綁定的 CSS 選取器
await page.locator('.btn-primary.submit-form').click();
await page.locator('#header > div > button:nth-child(2)').click();

// 請勿使用 - 與 DOM 結構綁定的 XPath
await page.locator('//div[@class="form"]/button[1]').click();

// 請勿使用 - 自動產生的選取器
await page.locator('[data-v-12345]').click();
"""

篩選與鏈結

"""
// 依包含文字篩選
await page.getByRole('listitem')
.filter({ hasText: 'Product A' })
.getByRole('button', { name: 'Add to cart' })
.click();

// 依不包含文字篩選
await page.getByRole('listitem')
.filter({ hasNotText: 'Sold out' })
.first()
.click();

// 鏈結定位器
const row = page.getByRole('row', { name: 'John Doe' });
await row.getByRole('button', { name: 'Edit' }).click();
"""

自動等待模式

讓 Playwright 自動處理等待,絕不加入手動等待

何時使用:使用 Playwright 時永遠適用

自動等待模式:

"""
Playwright 會自動等待以下條件:

  • 元素已附加至 DOM (Attached)
  • 元素可見 (Visible)
  • 元素已穩定(無動畫中)
  • 元素可接收事件
  • 元素處於啟用狀態 (Enabled)

絕對不要手動加入硬性等待!
"""

錯誤做法 - 手動硬性等待

"""
// 請勿這樣做
await page.goto('/dashboard');
await page.waitForTimeout(2000); // 不可!隨意設定的等待時間
await page.click('.submit-button');

// 請勿這樣做
await page.waitForSelector('.loading-spinner', { state: 'hidden' });
await page.waitForTimeout(500); // 「以防萬一」——不可!
"""

正確做法 - 讓自動等待機制發揮作用

"""
// 自動等待按鈕變為可點擊狀態
await page.getByRole('button', { name: 'Submit' }).click();

// 自動等待文字出現
await expect(page.getByText('Success!')).toBeVisible();

// 自動等待頁面導向完成
await page.goto('/dashboard');
// 頁面已準備就緒——無需手動等待
"""

確實需要等待的特殊時機

"""
// 等待特定的網路請求
const responsePromise = page.waitForResponse(
response => response.url().includes('/api/data')
);
await page.getByRole('button', { name: 'Load' }).click();
const response = await responsePromise;

// 等待 URL 變更
await Promise.all([
page.waitForURL('**/dashboard'),
page.getByRole('button', { name: 'Login' }).click(),
]);

// 等待檔案下載
const downloadPromise = page.waitForEvent('download');
await page.getByText('Export CSV').click();
const download = await downloadPromise;
"""

防偵測瀏覽器模式

用於爬蟲時避免被 Bot 偵測機制封鎖

何時使用:擷取帶有反爬蟲保護機制的網站

防偵測瀏覽器模式:

"""
Bot 偵測機制主要檢查:

  • navigator.webdriver 屬性
  • Chrome DevTools Protocol 特徵殘留
  • 瀏覽器指紋不一致
  • 行為模式(時間間隔過度完美、完全無滑鼠移動)
  • Headless 無頭模式特徵
    """

Puppeteer Stealth(最佳防偵測)

"""
import puppeteer from 'puppeteer-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';

puppeteer.use(StealthPlugin());

const browser = await puppeteer.launch({
headless: 'new',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-blink-features=AutomationControlled',
],
});

const page = await browser.newPage();

// 設定逼真的 Viewport
await page.setViewport({ width: 1920, height: 1080 });

// 逼真的 User Agent
await page.setUserAgent(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
'(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
);

// 模擬真人行為進行導向
await page.goto('https://target-site.com', {
waitUntil: 'networkidle0',
});
"""

Playwright Stealth

"""
import { chromium } from 'playwright-extra';
import stealth from 'puppeteer-extra-plugin-stealth';

chromium.use(stealth());

const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 },
userAgent: 'Mozilla/5.0 ...',
locale: 'en-US',
timezoneId: 'America/New_York',
});
"""

模擬真人行為

"""
// 操作之間的隨機延遲
const randomDelay = (min: number, max: number) =>
new Promise(r => setTimeout(r, Math.random() * (max - min) + min));

await page.goto(url);
await randomDelay(500, 1500);

// 點擊前的滑鼠移動
const button = await page.$('button.submit');
const box = await button.boundingBox();
await page.mouse.move(
box.x + box.width / 2,
box.y + box.height / 2,
{ steps: 10 } // 像真人一樣分步移動滑鼠
);
await randomDelay(100, 300);
await button.click();

// 自然平滑滾動
await page.evaluate(() => {
window.scrollBy({
top: 300 + Math.random() * 200,
behavior: 'smooth'
});
});
"""

錯誤復原模式

透過螢幕截圖與重試機制優雅地處理失敗

何時使用:任何正式環境的自動化任務

錯誤復原模式:

失敗時自動截圖

"""
// playwright.config.ts
export default defineConfig({
use: {
screenshot: 'only-on-failure',
trace: 'retain-on-failure',
video: 'retain-on-failure',
},
retries: 2, // 重試失敗的測試
});
"""

附帶除錯資訊的 Try-Catch 處理

"""
async function scrapeProduct(page: Page, url: string) {
try {
await page.goto(url, { timeout: 30000 });

const title = await page.getByRole('heading', { level: 1 }).textContent();
const price = await page.getByTestId('price').textContent();

return { title, price, success: true };

} catch (error) {
// 擷取除錯資訊
const screenshot = await page.screenshot({
path: errors/${Date.now()}-error.png,
fullPage: true
});

const html = await page.content();
await fs.writeFile(`errors/${Date.now()}-page.html`, html);

console.error({
  url,
  error: error.message,
  currentUrl: page.url(),
});

return { success: false, error: error.message };

}
}
"""

具備指數退避的重試機制

"""
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> {
let lastError: Error;

for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;

  if (attempt < maxRetries - 1) {
    const delay = baseDelay * Math.pow(2, attempt);
    const jitter = delay * 0.1 * Math.random();
    await new Promise(r => setTimeout(r, delay + jitt

<!-- truncated for translation batch; full body continues in source -->