瀏覽器自動化為網頁測試、資料擷取與 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',
},
},
],
});
"""
面向使用者定位器模式
以使用者看待介面的方式來選取元素
何時使用:永遠優先使用——定位器的預設標準做法
面向使用者定位器:
"""
優先順序:
- getByRole - 最佳:對齊無障礙樹 (Accessibility tree)
- getByText - 良好:比對可見內容
- getByLabel - 良好:比對表單 Label
- getByTestId - 備用:明確的測試契約
- 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 -->






