IndieBits - 独立开发碎碎念

探索海外独立开发 | 学习 · 思考 · 实践

Playwright 终极备忘单 (Cheatsheet)

Playwright 是一个由微软开发的开源框架,用于可靠地实现端到端 Web 自动化测试。它支持所有现代浏览器,并提供 Python 和 TypeScript/JavaScript 等多种语言的 API。

1. 安装

Playwright 的安装分为两步:安装库和安装所需的浏览器驱动。

Python TypeScript / JavaScript
安装库 pip install playwright npm init playwright@latest
(这是一个交互式向导,会自动创建配置文件和示例)
安装浏览器 playwright install
(会安装 Chromium, Firefox, WebKit)
(npm 初始化向导会自动执行此步骤)
如果需要手动安装,则运行 npx playwright install

2. 核心概念与常用 API

Playwright 的 API 在 Python 和 TS 中非常相似,主要区别在于 Python 使用 snake_case 命名法,而 TS 使用 camelCase

2.1 核心对象层级

Playwright -> Browser -> BrowserContext -> Page

  • Playwright: 入口点,用于启动浏览器实例。
  • Browser: 代表一个浏览器实例 (如 Chromium, Firefox)。
  • BrowserContext: 一个独立的浏览器会话(类似于隐身模式窗口)。上下文之间 Cookie 和本地存储是隔离的。这是管理会话和登录状态的最佳实践。
  • Page: 代表浏览器上下文中的一个标签页,是与网页交互的主要对象。

2.2 启动与导航

功能 Python TypeScript
同步启动 from playwright.sync_api import sync_playwright
with sync_playwright() as p:
  browser = p.chromium.launch(headless=False)
(TS 主要使用异步)
异步启动 from playwright.async_api import async_playwright
async with async_playwright() as p:
  browser = await p.chromium.launch()
import { chromium } from 'playwright';
const browser = await chromium.launch();
创建上下文 context = browser.new_context() const context = await browser.newContext();
创建页面 page = context.new_page() const page = await context.newPage();
访问 URL page.goto("https://example.com") await page.goto("https://example.com");
关闭 browser.close() await browser.close();

提示: headless=False 会以有头模式启动浏览器,方便调试时观察界面。

2.3 定位器 (Locators) - 最佳实践

定位器是 Playwright 中查找元素的核心机制。它们是自动等待严格的,这意味着 Playwright 会在执行操作前自动等待元素出现,并且如果定位器匹配到多个元素,会抛出错误(除非明确处理多个元素)。

功能 Python TypeScript
CSS 选择器 page.locator("#myId") page.locator('#myId')
文本内容 page.get_by_text("Submit") page.getByText('Submit')
角色 (ARIA) page.get_by_role("button", name="Sign in") page.getByRole('button', { name: 'Sign in' })
占位符 page.get_by_placeholder("Enter email") page.getByPlaceholder('Enter email')
链式定位 list_item = page.locator("ul > li").first
list_item.get_by_role("button").click()
const listItem = page.locator('ul > li').first();
await listItem.getByRole('button').click();
获取所有 all_items = page.locator("div").all() const allItems = await page.locator('div').all();

2.4 页面交互

功能 Python TypeScript
点击 page.get_by_role("button").click() await page.getByRole('button').click();
填充输入框 page.get_by_label("Password").fill("secret") await page.getByLabel('Password').fill('secret');
键盘按键 page.get_by_role("textbox").press("Enter") await page.getByRole('textbox').press('Enter');
截图 page.screenshot(path="screenshot.png") await page.screenshot({ path: 'screenshot.png' });
获取文本 text = page.locator("h1").text_content() const text = await page.locator('h1').textContent();
获取属性 href = page.locator("a").get_attribute("href") const href = await page.locator('a').getAttribute('href');

2.5 等待 (Waits)

尽管定位器会自动等待,但有时仍需要显式等待某些事件。

功能 Python TypeScript
等待 URL page.wait_for_url("**/dashboard") await page.waitForURL('**/dashboard');
等待加载状态 page.wait_for_load_state("networkidle") await page.waitForLoadState('networkidle');
等待选择器 page.wait_for_selector(".my-element", state="visible") await page.waitForSelector('.my-element', { state: 'visible' });

2.6 断言 (Assertions)

Playwright 内置了强大的断言库,它也会自动重试直到超时。

功能 Python TypeScript
检查可见性 expect(locator).to_be_visible() await expect(locator).toBeVisible();
检查文本 expect(locator).to_have_text("Welcome") await expect(locator).toHaveText('Welcome');
检查数量 expect(locator).to_have_count(5) await expect(locator).toHaveCount(5);

3. 实战 Demo:登录 GitHub 并读取仓库列表

这个 Demo 分为两个脚本:

  1. 脚本一 login.py / login.ts: 登录 GitHub 并将包含 Cookie 和本地存储的认证信息保存到文件中 (auth.json)。
  2. 脚本二 scrape.py / scrape.ts: 加载认证文件,跳过登录直接访问用户的仓库页面,并打印仓库名称列表。

准备工作:
为了安全,请将你的 GitHub 用户名和密码设置为环境变量 GITHUB_USERGITHUB_PASS

Python Demo 🐍

脚本一: login.py (登录并保存状态)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import os
from playwright.sync_api import sync_playwright, expect

AUTH_FILE = "auth.json"

def run():
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
context = browser.new_context()
page = context.new_page()

page.goto("https://github.com/login")

# 使用环境变量进行登录
username = os.getenv("GITHUB_USER")
password = os.getenv("GITHUB_PASS")
if not username or not password:
raise ValueError("请设置 GITHUB_USER 和 GITHUB_PASS 环境变量")

page.get_by_label("Username or email address").fill(username)
page.get_by_label("Password").fill(password)
page.get_by_role("button", name="Sign in").click()

# 等待登录成功后的仪表盘页面标志性元素出现
# 注意:如果启用了2FA,你需要手动在浏览器中输入验证码,Playwright会等待
dashboard_locator = page.get_by_role("link", name="Dashboard")
expect(dashboard_locator).to_be_visible(timeout=60000)
print("登录成功!")

# 保存认证状态到文件
context.storage_state(path=AUTH_FILE)
print(f"认证状态已保存到 {AUTH_FILE}")

browser.close()

if __name__ == "__main__":
run()

脚本二: scrape.py (使用状态并抓取数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import os
from playwright.sync_api import sync_playwright, expect

AUTH_FILE = "auth.json"

def run():
username = os.getenv("GITHUB_USER")
if not username:
raise ValueError("请设置 GITHUB_USER 环境变量")

with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
# 从文件加载认证状态,创建预登录的上下文
context = browser.new_context(storage_state=AUTH_FILE)

page = context.new_page()

# 直接访问需要登录才能访问的页面
repo_url = f"https://github.com/{username}?tab=repositories"
page.goto(repo_url)
print(f"已访问仓库页面: {repo_url}")

# 定位仓库列表
repo_list_locator = page.locator("#user-repositories-list li")
expect(repo_list_locator.first).to_be_visible()

print("\n--- 你的公开仓库列表 ---")
# 遍历并打印仓库名称
for repo_item in repo_list_locator.all():
repo_name = repo_item.locator("a[itemprop='name codeRepository']").text_content()
print(repo_name.strip())

print("------------------------\n抓取完成!")
browser.close()

if __name__ == "__main__":
run()

TypeScript Demo 🟦

脚本一: login.ts (登录并保存状态)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { test, expect, chromium } from '@playwright/test';
import * as fs from 'fs';

const AUTH_FILE = 'auth.json';

async function runLogin() {
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();

await page.goto('https://github.com/login');

const username = process.env.GITHUB_USER;
const password = process.env.GITHUB_PASS;
if (!username || !password) {
throw new Error('请设置 GITHUB_USER 和 GITHUB_PASS 环境变量');
}

await page.getByLabel('Username or email address').fill(username);
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Sign in' }).click();

// 等待登录成功
const dashboardLocator = page.getByRole('link', { name: 'Dashboard' });
await expect(dashboardLocator).toBeVisible({ timeout: 60000 });
console.log('登录成功!');

// 保存认证状态
const storageState = await context.storageState();
fs.writeFileSync(AUTH_FILE, JSON.stringify(storageState, null, 2));
console.log(`认证状态已保存到 ${AUTH_FILE}`);

await browser.close();
}

runLogin();

脚本二: scrape.ts (使用状态并抓取数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { test, expect, chromium } from '@playwright/test';

const AUTH_FILE = 'auth.json';

async function runScrape() {
const username = process.env.GITHUB_USER;
if (!username) {
throw new Error('请设置 GITHUB_USER 环境变量');
}

const browser = await chromium.launch({ headless: false });
// 从文件加载认证状态
const context = await browser.newContext({ storageState: AUTH_FILE });
const page = await context.newPage();

const repoUrl = `https://github.com/${username}?tab=repositories`;
await page.goto(repoUrl);
console.log(`已访问仓库页面: ${repoUrl}`);

const repoListLocator = page.locator("#user-repositories-list li");
await expect(repoListLocator.first()).toBeVisible();

console.log('\n--- 你的公开仓库列表 ---');
// 遍历并打印
const repoItems = await repoListLocator.all();
for (const item of repoItems) {
const repoName = await item.locator("a[itemprop='name codeRepository']").textContent();
console.log(repoName?.trim());
}

console.log('------------------------\n抓取完成!');
await browser.close();
}

runScrape();
0%