Playwright 入门指南:从零开始轻松搞定 Web 自动化
Playwright 是一个强大的现代开源自动化库,专门为端到端测试而设计。它由微软的团队开发并维护,支持使用单一 API 来操控多种浏览器。
Playwright 之所以迅速流行,是因为它解决了许多传统自动化工具(如 Selenium)的痛点。
跨浏览器支持
支持所有现代浏览器引擎:Chromium(Chrome, Edge)、Firefox 和 WebKit(Safari)。
这意味着你可以在 Windows 上测试 Safari,确保了跨平台的一致性。
跨平台、多语言
平台: 支持 Windows, macOS, Linux(包括在 Docker 容器中)。
语言: 提供对 TypeScript/JavaScript、Python、.NET (C#) 和 Java 的一流支持,API 在不同语言间保持一致。
强大的自动化能力
自动等待: Playwright 在执行操作前会自动等待元素可用的状态(如可点击、可见),无需手动添加
sleep或复杂等待,大大提高了测试的稳定性和编写效率。网络拦截: 可以轻松地模拟和修改网络请求,例如拦截 API 调用、模拟慢速网络或离线状态。
多标签页、多上下文: 能够处理多个浏览器标签页、甚至无痕浏览器上下文,这对于测试多用户场景或单点登录非常有用。
文件下载与上传: 原生支持文件上传和下载操作的自动化。
Shadow DOM 支持: 对 Web Components 中的 Shadow DOM 有很好的支持,可以直接定位其中的元素。
速度快、可靠性高
无头模式: 默认在无头模式下运行,速度极快。
浏览器上下文: 通过创建独立的浏览器上下文,可以实现并行测试,且彼此隔离,互不干扰。
追踪与调试: 内置强大的工具,可以记录测试执行过程的视频、截图和操作日志,方便调试失败的测试。
现代化的测试框架集成
可以轻松与流行的测试运行器集成,如 Jest、Mocha、Playwright Test(其自带的、功能丰富的测试运行器)、Pytest 等。
相关网址:
playwright中文文档 Nodejs.cn(https://playwright.nodejs.cn/python/docs/library)
安装
pip install playwright使用如下命令来安装playwright所需的引擎
playwright installQuickStart
在我们的第一个脚本中,我们将导航到 https://playwright.nodejs.cn/ 并在 WebKit 中截取屏幕截图。你可以使用 Python 脚本运行 import Playwright,并启动 3 个浏览器(chromium、firefox 和 webkit)中的任意一个。
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("http://www.baidu.com")
page.screenshot(path="./example.png")
browser.close()默认情况下,Playwright 以无头模式运行浏览器。要查看浏览器 UI,请将 headless 选项设置为 False。你还可以使用 slow_mo 来减慢执行速度。在调试工具 section 中了解更多信息。
chromium.launch(headless=False, slow_mo=50)Playwright 支持两种 API 变体:同步和异步。如果你的现代项目使用 asyncio,则应该使用异步 API:
import asyncio
from playwright.async_api import async_playwright
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page()
await page.goto("http://www.baidu.com")
print(await page.title())
await browser.close()
asyncio.run(main())浏览器操作
打开浏览器
with sync_playwright() as p:
browser = p.chromium.launch() # 打开chrome
browser = p.firefox.launch() # 打开firefox
browser = p.webkit.launch() # 打开webkit浏览器启动参数
p.chromium.launch()中可以通过设置启动参数改变浏览器的启动行为:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(
# 基本参数
headless=False, # 是否无头模式,False显示浏览器窗口
executable_path="/path/to/browser", # 指定浏览器可执行文件路径
# 性能和控制参数
slow_mo=1000, # 操作间隔延迟(毫秒)
timeout=30000, # 操作超时时间(毫秒)
# 设置浏览器参数
args=[
"--disable-web-security", # 禁用web安全
"--no-sandbox", # 禁用沙盒
"--disable-setuid-sandbox", # 禁用setuid沙盒
"--disable-dev-shm-usage", # 禁用/dev/shm使用
"--disable-accelerated-2d-canvas",
"--disable-gpu", # 禁用GPU硬件加速
"--window-size=1920,1080", # 窗口大小
],
# 环境变量
env={"ENV_VAR": "value"},
# 代理设置
proxy={
"server": "http://proxy.example.com:8080",
"username": "user", # 可选
"password": "pass" # 可选
},
# 下载设置
downloads_path="/path/to/downloads", # 下载文件保存路径
# 其他参数
devtools=False, # 是否自动打开开发者工具
handle_sigint=True, # 是否处理SIGINT信号
handle_sigterm=True, # 是否处理SIGTERM信号
handle_sighup=True, # 是否处理SIGHUP信号
# Chromium 特有但其他浏览器也兼容的参数
ignore_default_args=False, # 是否忽略默认参数
ignore_https_errors=False, # 是否忽略HTTPS错误
# 高级参数
firefox_user_prefs={}, # Firefox 用户首选项
chromium_sandbox=False, # 是否启用Chromium沙盒
)具体的浏览器参数
窗口和显示
common_args = [
"--window-size=1920,1080",
"--start-maximized",
"--kiosk",
]安全和权限
common_args = [
"--disable-web-security",
"--allow-running-insecure-content",
"--disable-features=VizDisplayCompositor",
"--allow-insecure-localhost",
]性能和资源
common_args = [
"--disable-background-timer-throttling",
"--disable-backgrounding-occluded-windows",
"--disable-renderer-backgrounding",
"--disable-dev-shm-usage",
"--disable-accelerated-2d-canvas",
"--disable-gpu",
"--no-sandbox",
"--disable-setuid-sandbox",
"--memory-pressure-off",
]功能和扩展
common_args = [
"--disable-extensions",
"--disable-plugins",
"--disable-translate",
"--disable-sync",
"--disable-default-apps",
"--disable-component-extensions-with-background-pages",
]自动化和测试
common_args = [
"--disable-blink-features=AutomationControlled",
"--enable-automation",
"--remote-debugging-port=0",
"--password-store=basic",
"--use-mock-keychain",
]用户界面
common_args = [
"--hide-scrollbars",
"--mute-audio",
"--no-first-run",
"--no-default-browser-check",
"--disable-popup-blocking",
]网络和代理
common_args = [
"--proxy-bypass-list=*",
"--proxy-server=http://proxy:8080",
]语言和区域
common_args = [
"--lang=zh-CN",
"--accept-lang=zh-CN,zh;q=0.9,en;q=0.8",
]Chromium/Chrome 特有参数
chromium_specific_args = [
"--disable-features=TranslateUI",
"--disable-ipc-flooding-protection",
"--disable-hang-monitor",
"--disable-back-forward-cache",
"--disable-site-isolation-trials",
]Firefox 特有参数
firefox_specific_args = [
"--safe-mode",
"--new-instance",
"--width=1920",
"--height=1080",
]WebKit 特有参数
webkit_specific_args = [
"--disable-web-security",
"--allow-running-insecure-content",
]关闭浏览器
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
browser.close() # 关闭浏览器标签页管理
新建标签页
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page() # 新建标签页获取所有标签页
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
# 获取浏览器中的所有标签页
all_pages = browser.contexts[0].pages # 第一个上下文中的所有页面
print(f"标签页数量: {len(all_pages)}")
# 遍历所有标签页
for i, page in enumerate(all_pages):
print(f"标签页 {i}: {page.url}")
# 获取特定上下文中的标签页
browser.close()获取标签页属性
# 基本属性
print(f"URL: {page.url}")
print(f"标题: {page.title()}")
print(f"页面HTML: {page.content()}")
# 页面状态
print(f"是否关闭: {page.is_closed()}")
print(f"页面加载状态: {page.evaluate('() => document.readyState')}")
# 视口信息
viewport = page.viewport_size
print(f"视口大小: {viewport}")
# 用户代理
user_agent = page.evaluate("() => navigator.userAgent")
print(f"User Agent: {user_agent}")标签导航
# 前进后退
page.go_back() # 后退
page.go_forward() # 前进
page.reload() # 刷新
# 获取导航历史
print(f"可以后退: {page.can_go_back()}")
print(f"可以前进: {page.can_go_forward()}")标签页截图和PDF
# 截取整个页面
page.screenshot(path="fullpage.png", full_page=True)
# 截取可见区域
page.screenshot(path="viewport.png")
# 截取特定元素
element = page.locator(".header")
element.screenshot(path="header.png")
# 生成PDF
page.pdf(path="page.pdf", format="A4")关闭标签页
page.close()元素定位
使page.locator()可以进行元素定位
选择器语法
在介绍元素定位前,首先介绍一下CCS选择器语法。下面详细解释这些语法和其他常见的 CSS 选择器。
基础选择器
/* 元素选择器 */
div /* 选择所有 div 元素 */
button /* 选择所有 button 元素 */
span /* 选择所有 span 元素 */
/* 类选择器 */
.submit /* 选择 class 包含 submit 的元素 */
.btn.primary /* 选择同时有 btn 和 primary 类的元素 */
/* ID 选择器 */
#username /* 选择 id 为 username 的元素 */
#main /* 选择 id 为 main 的元素 */
/* 通配符选择器 */
* /* 选择所有元素 */
/* 属性选择器 */
[type] /* 选择有 type 属性的元素 */组合选择器
/* 后代选择器(空格) */
div span /* 选择 div 内的所有 span(任意嵌套) */
.form .input /* 选择 .form 内的所有 .input */
/* 子元素选择器(>) */
div > span /* 选择直接子元素 span */
ul > li /* 选择 ul 的直接子元素 li */
/* 相邻兄弟选择器(+) */
h1 + p /* 选择紧接在 h1 后面的 p 元素 */
.error + .input /* 选择紧接在 .error 后面的 .input */
/* 通用兄弟选择器(~) */
h1 ~ p /* 选择 h1 后面的所有 p 兄弟元素 */属性选择器
/* 基础属性选择器 */
[type] /* 选择有 type 属性的元素 */
[type="text"] /* 选择 type="text" 的元素 */
[class~="btn"] /* 选择 class 包含完整单词 "btn" 的元素 */
/* 子字符串匹配属性选择器 */
[class*="error"] /* 选择 class 包含 "error" 的元素 */
[href^="https"] /* 选择 href 以 "https" 开头的元素 */
[src$=".jpg"] /* 选择 src 以 ".jpg" 结尾的元素 */
[class|="lang"] /* 选择 class 以 "lang-" 开头的元素 */
/* 大小写敏感匹配 */
[type="submit" i] /* 不区分大小写匹配 type 属性 */伪类选择器
/* 状态伪类 */
:hover /* 鼠标悬停时的元素 */
:focus /* 获得焦点的元素 */
:active /* 被激活的元素 */
:visited /* 已访问的链接 */
:checked /* 被选中的复选框/单选按钮 */
:disabled /* 被禁用的元素 */
:enabled /* 可用的元素 */
/* 结构伪类 */
:first-child /* 父元素的第一个子元素 */
:last-child /* 父元素的最后一个子元素 */
:nth-child(n) /* 父元素的第 n 个子元素 */
:nth-last-child(n) /* 父元素的倒数第 n 个子元素 */
:only-child /* 父元素唯一的子元素 */
:first-of-type /* 同类型中的第一个 */
:last-of-type /* 同类型中的最后一个 */
:nth-of-type(n) /* 同类型中的第 n 个 */
:only-of-type /* 同类型中唯一的 */
/* 其他伪类 */
:not(selector) /* 不匹配选择器的元素 */
:empty /* 没有子元素的元素 */
:root /* 文档的根元素 */伪元素选择器
::before /* 在元素内容前插入 */
::after /* 在元素内容后插入 */
::first-line /* 元素的第一行 */
::first-letter /* 元素的第一个字母 */
::selection /* 用户选中的内容 */基础定位
CCS定位
element = page.locator("button.submit") # 选择所有 class 包含 submit 的 button 元素
element = page.locator("#login-btn") # 选择 id 为 login-btn 的元素
element = page.locator("div > span.highlight") # 选择所有直接父元素是 div 且 class 包含 highlight 的 span 元素文本定位
element = page.locator("text=登录")
element = page.locator("text='Welcome'")
element = page.locator("text=/hello/i") # 正则表达式XPath 定位
element = page.locator("xpath=//button[@id='submit']")
element = page.locator("//div[@class='container']//a")属性定位
element = page.locator("[data-testid='submit-button']")
element = page.locator("[type='email']")
element = page.locator("[placeholder*='search']") # 包含组合定位
element = page.locator("form.login >> input[name='username']") # 首先找到 class 为"login"的 form 元素,然后在该表单内查找 name 为"username"的 input 元素
element = page.locator(".modal >> .close-btn") # 首先找到 class 为"modal"的元素,然后在其内部查找 class 为"close-btn"的元素高级定位策略
按位置定位
first_element = page.locator("button").first
last_element = page.locator("button").last
nth_element = page.locator("li").nth(2) # 第三个元素过滤定位
visible_elements = page.locator("button").filter(has_text="Submit")
hidden_elements = page.locator("div").filter(has=page.locator(".hidden"))链式定位
element = (page.locator("form")
.filter(has=page.locator("input[required]"))
.locator("button"))包含其他元素的定位
element = page.locator("div", has=page.locator("p.description"))
element = page.locator("li", has_text="Item").filter(
has=page.locator(".badge")
)相对定位
base_element = page.locator(".container")
child_element = base_element.locator(".item")
sibling_element = base_element.locator("xpath=following-sibling::div")元素状态检查
可见性检查
is_visible = element.is_visible()
is_hidden = element.is_hidden()
is_enabled = element.is_enabled()
is_disabled = element.is_disabled()
is_editable = element.is_editable()选中状态检查
is_checked = element.is_checked()等待元素状态
element.wait_for(state="visible") # 等待可见
element.wait_for(state="hidden") # 等待隐藏
element.wait_for(state="attached") # 等待附加到DOM
element.wait_for(state="detached") # 等待从DOM分离获取元素的属性和内容
获取文本内容
text = element.text_content()
inner_text = element.inner_text()
all_texts = element.all_text_contents() # 多个匹配元素获取属性
class_name = element.get_attribute("class")
data_value = element.get_attribute("data-value")
all_attributes = element.evaluate("el => el.attributes")获取输入值
input_value = element.input_value()获取内部HTML
html_content = element.inner_html()获取元素的样式和尺寸
获取计算样式
color = element.evaluate("el => getComputedStyle(el).color")
font_size = element.evaluate("el => getComputedStyle(el).fontSize")获取元素尺寸和位置
bounding_box = element.bounding_box() # {x, y, width, height}
is_visible = bounding_box is not None检查元素是否包含其他元素
contains_child = element.locator(".child").count() > 0元素等待
等待元素出现
element = page.locator(".dynamic-content")
element.wait_for() # 等待元素附加到DOM等待元素可见
element.wait_for(state="visible")等待元素满足条件
element.wait_for(
state="visible",
timeout=10000 # 10秒超时
)自定义等待条件
page.wait_for_function("""
() => document.querySelector('.result').textContent.includes('成功')
""")元素交互
点击
基础点击
page.click("button#submit")
page.locator(".btn").click()点击选项
page.click("button",
button="right", # 右键点击
delay=1000, # 按下和释放之间的延迟
force=True, # 强制点击,即使元素不可点击
no_wait_after=True, # 点击后不等待导航
modifiers=["Shift"] # 修饰键
)双击
page.dblclick(".item")输入
基础输入
page.fill("#username", "myusername")
page.locator("input[type='password']").fill("mypassword")类型输入(模拟键盘)
page.type("#search", "keyword", delay=100) # 带延迟的输入清空
page.fill("#input", "") # 方法1
page.locator("#input").clear() # 方法2文件上传
page.set_input_files("#file-input", "path/to/file.txt")
page.set_input_files("#multi-file", ["file1.txt", "file2.jpg"])通过如下方式清除文件
page.set_input_files("#file-input", [])选择和拖拽
下拉选择框
page.select_option("#country", "china") # 按值
page.select_option("#colors", label="Red") # 按标签
page.select_option("#multi-select", value=["opt1", "opt2"]) # 多选复选框和单选框
page.check("#agree-terms")
page.uncheck("#newsletter")
page.set_checked("#option1", True)拖拽
page.drag_and_drop("#source", "#target")通过如下方式进行手动拖拽
source = page.locator("#draggable")
target = page.locator("#droppable")
source.drag_to(target)键盘模拟
在元素上操作键盘
element = page.locator("#input")
element.press("Enter")
element.press("Control+A") # 全选页面级键盘操作
page.keyboard.type("Hello World!")
page.keyboard.press("ArrowDown")
page.keyboard.down("Shift") # 按住Shift
page.keyboard.up("Shift") # 释放Shift组合键
page.keyboard.press("Control+A")
page.keyboard.press("Control+C")鼠标模拟
悬停
page.hover(".menu-item")
page.locator(".dropdown").hover()精确鼠标控制
page.mouse.move(100, 200) # 移动到坐标
page.mouse.click(150, 250) # 点击坐标
page.mouse.dblclick(300, 400) # 双击坐标
page.mouse.down() # 按下鼠标
page.mouse.up() # 释放鼠标滚轮
page.mouse.wheel(0, 100) # 向下滚动100px元素的截图和焦点
元素截图
element.screenshot(path="element.png")带选项的截图
element.screenshot(
path="highlighted.png",
type="jpeg",
quality=80,
omit_background=True
)焦点
element.focus()
element.blur()检查焦点
has_focus = element.evaluate("el => el === document.activeElement") # 通过javascript来获取焦点执行 JavaScript
在页面上下文中执行
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
page.goto("https://example.com")
# 执行简单的 JavaScript 并返回值
title = page.evaluate("() => document.title")
print(f"页面标题: {title}")
# 执行带参数的 JavaScript
result = page.evaluate("(a, b) => a + b", 5, 3)
print(f"5 + 3 = {result}")
# 执行复杂的 JavaScript 代码块
page_info = page.evaluate("""
() => {
return {
title: document.title,
url: window.location.href,
userAgent: navigator.userAgent,
viewport: {
width: window.innerWidth,
height: window.innerHeight
}
};
}
""")
print(f"页面信息: {page_info}")
browser.close()使page.evaluate_handle()返回 JSHandle 对象
# 返回 JSHandle,可以传递到其他 evaluate 调用中
element_handle = page.evaluate_handle("() => document.body")
text_content = page.evaluate("(body) => body.textContent", element_handle)
# JSHandle 可以调用方法
bounding_box = element_handle.bounding_box()在元素上下文中执行
# 获取元素并在其上下文中执行 JavaScript
element = page.locator(".content")
# 获取元素的文本内容
text = element.evaluate("(el) => el.textContent")
# 获取元素属性
class_name = element.evaluate("(el) => el.className")
# 修改元素样式
element.evaluate("""
(el) => {
el.style.backgroundColor = 'yellow';
el.style.padding = '10px';
}
""")
# 执行复杂的元素操作
element.evaluate("""
(el) => {
// 获取计算样式
const style = window.getComputedStyle(el);
return {
tagName: el.tagName,
className: el.className,
fontSize: style.fontSize,
color: style.color,
isVisible: el.offsetParent !== null
};
}
""")使element.evaluate_all()在多个元素上执行
# 在多个匹配的元素上执行 JavaScript
all_links = page.locator("a")
# 获取所有链接的 href 和文本
link_data = all_links.evaluate_all("""
(elements) => {
return elements.map(el => ({
text: el.textContent.trim(),
href: el.href,
target: el.target
}));
}
""")
print(f"找到 {len(link_data)} 个链接")
for link in link_data:
print(f"链接: {link['text']} -> {link['href']}")操作 Cookie
Cookie 属性详解
cookie_template = {
'name': 'cookie_name', # 必需: Cookie 名称
'value': 'cookie_value', # 必需: Cookie 值
# 以下三选一(指定 Cookie 的作用范围)
'url': 'https://example.com', # 方式1: 完整 URL
'domain': '.example.com', # 方式2: 域名(包含子域名用 . 开头)
'path': '/', # 方式3: 路径(需要与 domain 一起使用)
# 可选属性
'expires': 1698765432, # 过期时间(Unix 时间戳)
'httpOnly': True, # 是否仅 HTTP 可访问
'secure': True, # 是否仅 HTTPS 传输
'sameSite': 'Strict', # SameSite 策略: 'Strict', 'Lax', 'None'
}
# 注意:必须提供 url 或 domain+path 来指定 Cookie 的作用范围获取 Cookie
获取所有 Cookie
cookies = browser.cookies()获取特定 URL 的 Cookie
site_cookies = browser.cookies("https://example.com")添加 Cookie
使用如下方式添加单个 Cookie
# 添加基本的 Cookie
browser.add_cookies([{
'name': 'user_preference',
'value': 'dark_mode',
'url': 'https://example.com'
}])
# 添加完整的 Cookie(包含所有属性)
browser.add_cookies([{
'name': 'session_id',
'value': 'abc123xyz',
'domain': '.example.com',
'path': '/',
'expires': int((datetime.now() + timedelta(days=7)).timestamp()), # 7天后过期
'httpOnly': True,
'secure': True,
'sameSite': 'Lax'
}])当想批量添加 Cookie 的时候,如下:
# 批量添加 Cookie
cookies_to_add = [
{
'name': 'user_id',
'value': '12345',
'url': 'https://example.com'
},
{
'name': 'theme',
'value': 'dark',
'url': 'https://example.com'
},
{
'name': 'language',
'value': 'zh-CN',
'url': 'https://example.com'
}
]
browser.add_cookies(cookies_to_add)删除 Cookie
browser.clear_cookies()