Skip to content

实战案例库

本章提供五个可直接套用到真实项目中的完整脚本模板,覆盖多账号自动化的最常见场景。每个案例提供 JavaScriptPython 两个版本,按需取用。


案例一:自动登录并保持状态

场景:自动将指定的候鸟账号环境登录到目标平台。

javascript
// JavaScript 版
const { chromium } = require('playwright');
const axios = require('axios');

const API_URL    = 'http://127.0.0.1:8186';
const SESSION_ID = '替换为您的环境ID';

async function autoLogin() {
  // ── 开启候鸟环境 ─────────────────────────────
  const { data } = await axios.post(`${API_URL}/api/v1/browser/start`, {
    Session_ID: SESSION_ID
  });
  if (data.code !== 0) throw new Error(`环境启动失败: ${data.message}`);
  const wsEndpoint = data.data.ws;

  const browser = await chromium.connectOverCDP(wsEndpoint);
  const context = browser.contexts()[0];
  const page    = context.pages()[0] || await context.newPage();

  try {
    // ── 打开登录页并填表 ────────────────────────
    await page.goto('https://example.com/login', { waitUntil: 'domcontentloaded' });

    // getByRole 是 Playwright 最推荐的定位方式,比 CSS 选择器更稳
    await page.getByLabel('邮箱').fill('my_account@email.com');
    await page.getByLabel('密码').fill('my_password_123');
    await page.getByRole('button', { name: '登 录' }).click();

    // 等待跳转到首页(登录成功的标志)
    await page.waitForURL('**/dashboard', { timeout: 15000 });
    console.log('✅ 登录成功!当前 URL:', page.url());

    // 候鸟会自动持久化本次 Cookie,下次开启该环境无需重新登录
    console.log('🔒 Cookie 已自动保存到候鸟环境');
  } catch (e) {
    console.error('❌ 登录失败:', e.message);
    await page.screenshot({ path: 'login_error.png', fullPage: true });
  } finally {
    await browser.close();
  }
}

autoLogin().catch(console.error);
python
# Python 版
import requests
from playwright.sync_api import sync_playwright

API_URL    = "http://127.0.0.1:8186"
SESSION_ID = "替换为您的环境ID"

def auto_login():
    resp = requests.post(
        f"{API_URL}/api/v1/browser/start",
        json={"Session_ID": SESSION_ID}, timeout=30
    )
    data = resp.json()
    if data["code"] != 0:
        print(f"❌ 环境启动失败: {data['message']}")
        return

    ws = data["data"]["ws"]
    with sync_playwright() as p:
        browser = p.chromium.connect_over_cdp(ws)
        context = browser.contexts[0]
        page    = context.pages[0] if context.pages else context.new_page()

        try:
            page.goto("https://example.com/login", wait_until="domcontentloaded")
            page.get_by_label("邮箱").fill("my_account@email.com")
            page.get_by_label("密码").fill("my_password_123")
            page.get_by_role("button", name="登 录").click()
            page.wait_for_url("**/dashboard", timeout=15000)
            print(f"✅ 登录成功!当前 URL: {page.url}")
        except Exception as e:
            print(f"❌ 登录失败: {e}")
            page.screenshot(path="login_error.png", full_page=True)
        finally:
            browser.close()

if __name__ == "__main__":
    auto_login()

场景:已有账号 Cookie 数据,无需走登录表单,直接注入后即进入登录状态。

javascript
// JavaScript 版
async function injectAndLogin(page, context) {
  // 注意:必须先打开目标域名,才能给该域注入 Cookie
  await page.goto('https://shop.example.com', { waitUntil: 'commit' });

  // 注入准备好的 Cookie 数组
  await context.addCookies([
    { name: 'session_id', value: 'SID_abcdef123456', domain: '.example.com', path: '/' },
    { name: 'auth_token', value: 'Bearer_xyz789',   domain: '.example.com', path: '/' }
  ]);

  // 刷新页面,Cookie 生效
  await page.reload({ waitUntil: 'domcontentloaded' });

  // 验证是否成功进入登录状态
  const accountEl = page.getByRole('button', { name: '我的账号' });
  if (await accountEl.isVisible()) {
    console.log('✅ Cookie 注入成功,已进入登录状态!');
  } else {
    console.warn('⚠️ Cookie 可能已过期,请重新采集');
  }
}
python
# Python 版
def inject_and_login(page, context):
    page.goto("https://shop.example.com", wait_until="commit")
    context.add_cookies([
        {"name": "session_id", "value": "SID_abcdef123456",
         "domain": ".example.com", "path": "/"},
        {"name": "auth_token", "value": "Bearer_xyz789",
         "domain": ".example.com", "path": "/"}
    ])
    page.reload(wait_until="domcontentloaded")

    if page.get_by_role("button", name="我的账号").is_visible():
        print("✅ Cookie 注入成功!")
    else:
        print("⚠️ Cookie 可能已过期")

NOTE

候鸟小秘密:在候鸟环境中成功登录过一次后,Cookie 会永久保存在该环境的数据目录中。下次重新开启该环境,无需重复注入,账号仍处于登录状态——这就是指纹浏览器"持久化环境"的核心价值!


案例三:利用网络拦截抓取 API 数据

场景:目标网站的数据通过 XHR/Fetch 请求加载,比直接解析网页 HTML 更方便、更稳定。

javascript
// JavaScript 版
async function interceptAndCollect(page) {
  const collectedData = [];

  // ── 监听目标 API 的响应 ──────────────────────────
  page.on('response', async response => {
    if (response.url().includes('/api/products') && response.status() === 200) {
      try {
        const json = await response.json();
        if (json.data?.list) {
          collectedData.push(...json.data.list);
          console.log(`📦 已采集 ${collectedData.length} 条商品数据`);
        }
      } catch (e) {
        // 部分响应不是 JSON,忽略
      }
    }
  });

  // ── 打开商品列表页,自动触发 API 请求 ───────────
  await page.goto('https://example.com/products');

  // ── 模拟翻页,采集多页数据 ──────────────────────
  for (let i = 0; i < 5; i++) {
    const nextBtn = page.getByRole('button', { name: '下一页' });
    if (!await nextBtn.isVisible()) break;
    // 点击翻页,并等待数据请求完成
    await Promise.all([
      page.waitForResponse(r => r.url().includes('/api/products')),
      nextBtn.click()
    ]);
  }

  console.log(`✅ 共采集 ${collectedData.length} 条数据`);
  return collectedData;
}
python
# Python 版(Python Playwright 默认是同步 API)
def intercept_and_collect(page):
    collected_data = []

    def handle_response(response):
        if "/api/products" in response.url and response.status == 200:
            try:
                json_data = response.json()
                if json_data.get("data", {}).get("list"):
                    collected_data.extend(json_data["data"]["list"])
                    print(f"📦 已采集 {len(collected_data)} 条数据")
            except Exception:
                pass

    page.on("response", handle_response)
    page.goto("https://example.com/products")

    for _ in range(5):
        next_btn = page.get_by_role("button", name="下一页")
        if not next_btn.is_visible():
            break
        with page.expect_response(lambda r: "/api/products" in r.url):
            next_btn.click()

    print(f"✅ 共采集 {len(collected_data)} 条数据")
    return collected_data

案例四:多账号并发批量操作

场景:同时操控多个候鸟环境,实现真正的并行效率(JavaScript Promise.all 天然支持)。

javascript
// JavaScript 版 —— 并发操作多个账号
const { chromium } = require('playwright');
const axios = require('axios');

const API_URL = 'http://127.0.0.1:8186';

// 定义要并发操作的环境列表
const SESSION_LIST = [
  '环境ID_1',
  '环境ID_2',
  '环境ID_3',
  '环境ID_4',
  '环境ID_5',
];

async function operateOneAccount(sessionId, index) {
  console.log(`[线程 ${index}] 启动环境 ${sessionId.slice(0, 8)}...`);
  try {
    const { data } = await axios.post(`${API_URL}/api/v1/browser/start`, {
      Session_ID: sessionId
    });
    if (data.code !== 0) {
      console.error(`[线程 ${index}] ❌ 启动失败: ${data.message}`);
      return;
    }

    const browser = await chromium.connectOverCDP(data.data.ws);
    const context = browser.contexts()[0];
    const page    = context.pages()[0] || await context.newPage();

    // ── 在此处放您的账号业务逻辑 ────────────────
    await page.goto('https://example.com');
    console.log(`[线程 ${index}] ✅ 操作完成: ${await page.title()}`);

    await browser.close();
  } catch (e) {
    console.error(`[线程 ${index}] ❌ 异常: ${e.message}`);
  } finally {
    // 关闭候鸟环境(释放内存)
    await axios.post(`${API_URL}/api/v1/browser/stop`, { Session_ID: sessionId })
      .catch(() => {});
  }
}

async function batchOperate() {
  // Promise.all 同时启动所有账号操作
  await Promise.all(
    SESSION_LIST.map((sessionId, i) => operateOneAccount(sessionId, i + 1))
  );
  console.log('\n🎉 所有账号操作完毕!');
}

batchOperate().catch(console.error);
python
# Python 版 —— 使用 threading 实现并发
import threading
import requests
from playwright.sync_api import sync_playwright

API_URL      = "http://127.0.0.1:8186"
SESSION_LIST = ["环境ID_1", "环境ID_2", "环境ID_3", "环境ID_4", "环境ID_5"]


def operate_one_account(session_id, thread_id):
    print(f"[线程 {thread_id}] 启动环境 {session_id[:8]}...")
    try:
        resp = requests.post(
            f"{API_URL}/api/v1/browser/start",
            json={"Session_ID": session_id}, timeout=30
        )
        data = resp.json()
        if data["code"] != 0:
            print(f"[线程 {thread_id}] ❌ 失败: {data['message']}")
            return

        ws = data["data"]["ws"]
        with sync_playwright() as p:
            browser = p.chromium.connect_over_cdp(ws)
            context = browser.contexts[0]
            page    = context.pages[0] if context.pages else context.new_page()

            # ── 您的账号业务逻辑 ─────────────────
            page.goto("https://example.com")
            print(f"[线程 {thread_id}] ✅ 完成: {page.title()}")

            browser.close()
    except Exception as e:
        print(f"[线程 {thread_id}] ❌ 异常: {e}")
    finally:
        requests.post(
            f"{API_URL}/api/v1/browser/stop",
            json={"Session_ID": session_id}, timeout=10
        )


def batch_operate():
    threads = [
        threading.Thread(target=operate_one_account, args=(sid, i + 1))
        for i, sid in enumerate(SESSION_LIST)
    ]
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    print("\n🎉 所有账号操作完毕!")


if __name__ == "__main__":
    batch_operate()

IMPORTANT

并发数量建议

  • 16GB 内存 → 同时开启 10-15 个候鸟环境
  • 32GB 内存 → 同时开启 20-30 个候鸟环境
    过多并发会导致内存不足,浏览器响应迟钝甚至崩溃,请根据实际硬件配置控制并发量。

案例五:生产级脚本框架(含重试与日志)

场景:正式生产环境使用,需要稳定性、可观测性和容错能力。

python
# Python 生产级框架
import threading
import requests
import logging
import time
from playwright.sync_api import sync_playwright, TimeoutError as PWTimeoutError

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(threadName)s] %(levelname)s %(message)s",
    datefmt="%H:%M:%S"
)
logger = logging.getLogger(__name__)

API_URL = "http://127.0.0.1:8186"


class MBPlaywrightSession:
    """候鸟 Playwright 会话管理器(支持自动重试、上下文管理)"""

    def __init__(self, session_id: str, max_retry: int = 3):
        self.session_id = session_id
        self.max_retry  = max_retry
        self._pw        = None
        self._browser   = None

    def start(self):
        """启动候鸟环境并初始化 Playwright 连接(失败自动重试)"""
        for attempt in range(1, self.max_retry + 1):
            try:
                logger.info(f"[{self.session_id[:8]}] 尝试开启环境(第 {attempt} 次)")
                resp = requests.post(
                    f"{API_URL}/api/v1/browser/start",
                    json={"Session_ID": self.session_id},
                    timeout=30
                )
                data = resp.json()
                if data["code"] == 0:
                    ws = data["data"]["ws"]
                    self._pw      = sync_playwright().start()
                    self._browser = self._pw.chromium.connect_over_cdp(ws)
                    logger.info(f"[{self.session_id[:8]}] ✅ 环境就绪")
                    return self
                logger.warning(f"[{self.session_id[:8]}] 开启失败: {data.get('message')}")
            except Exception as e:
                logger.error(f"[{self.session_id[:8]}] 异常: {e}")
            time.sleep(2 ** attempt)  # 指数退避:2s、4s、8s

        raise RuntimeError(f"环境 {self.session_id} 连续 {self.max_retry} 次启动失败")

    @property
    def page(self):
        """返回当前上下文的第一个页面(懒加载)"""
        context = self._browser.contexts[0]
        return context.pages[0] if context.pages else context.new_page()

    @property
    def context(self):
        return self._browser.contexts[0]

    def stop(self, close_browser: bool = False):
        """关闭会话"""
        try:
            if self._browser:
                self._browser.close()
            if self._pw:
                self._pw.stop()
        except Exception:
            pass
        if close_browser:
            requests.post(
                f"{API_URL}/api/v1/browser/stop",
                json={"Session_ID": self.session_id},
                timeout=10
            )
        logger.info(f"[{self.session_id[:8]}] 会话已关闭")

    def __enter__(self):
        return self.start()

    def __exit__(self, *args):
        self.stop()
        return False


# ── 使用示例 ──────────────────────────────────────────────
SESSION_ID = "替换为您的环境ID"

with MBPlaywrightSession(SESSION_ID) as session:
    page = session.page
    try:
        page.goto("https://www.mbbrowser.com")
        logger.info(f"页面标题: {page.title()}")
        page.screenshot(path="result.png")
    except PWTimeoutError:
        logger.error("页面超时,请检查代理或网络")
    except Exception as e:
        logger.error(f"执行异常: {e}")
        page.screenshot(path="error.png")

与 Selenium / Puppeteer 的对比总结

对比维度PuppeteerSeleniumPlaywright
候鸟接入字段data.wsdata.httpdata.ws
连接方法connectOverCDP(ws)debuggerAddressconnectOverCDP(ws)
主要语言JavaScriptPython/JavaJS / Python / Java
自动等待手写 waitForSelector手写 WebDriverWait内置,默认开启
网络拦截有限不支持强大(route/intercept)
多标签页支持支持原生优秀
推荐场景快速脚本/JS 开发者已有代码库/Python 爬虫全场景,新项目首选

TIP

🎉 恭喜!您已掌握候鸟 + Playwright 的完整自动化技术栈。

如需深入了解 Playwright 官方文档,请访问 playwright.dev

如果您想在候鸟客户端内管理和调试您的脚本,请参阅 自动化脚本管理器使用