playwright
Playwright 是 Microsoft 开源的现代化浏览器自动化库,支持 Chromium、Firefox、WebKit 三大浏览器引擎,它会启动真实的浏览器并执行 JavaScript,因此适合处理动态网页、需要点击/输入/登录的网站,以及传统 HTTP 请求难以直接获取的数据,它支持:
- 爬取 JavaScript 动态渲染后的网页
- 自动点击、输入、提交表单
- 登录网站、维持 Cookie/Session
- 获取 DOM、文本、属性等页面内容
- 截图、生成 PDF
- Web UI 自动化测试
- 模拟不同设备、浏览器环境
安装
Playwright 属于第三方库,需要手动安装
pip install playwright
Playwright 可以调用当前计算机中已经安装好的浏览器,但更推荐使用库推荐的浏览器内核,以提供更好的兼容性和代码运行保障:
# 安装 Playwright 所需的浏览器程序
playwright install
# 可以只安装指定浏览器
playwright install chromium
导入
官方 Python API 分为同步和异步两套,大多数脚本可以优先使用同步 API;如果程序本身使用 asyncio,则使用异步 API。两套 API 的方法基本对应,只是异步 API 需要 await
# 同步 API
from playwright.sync_api import sync_playwright
# 异步 API
from playwright.async_api import async_playwright
使用流程
Playwright 通过以下对象层次来完成一条完整的 Web 自动化操作链:
- Playwright:库本身,也是整个库的总入口,负责管理浏览器自动化能力
- Browser:浏览器实例,后续操作都需要在该实例中执行
- BrowserContext:一个独立的浏览器会话,不同 Context 之间默认不共享 Cookie、缓存等浏览数据
- Page:一个标签页,负责网页级操作,可以获取网页信息、监听网络、处理弹窗、截图等
- Locator:用于定位网页元素,是 Playwright 中最核心的网页操作对象之一
# 操作模板示例
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
# 启动浏览器(可换 firefox / webkit)
browser = p.chromium.launch(headless=False, slow_mo=500)
# 创建上下文(相当于独立会话,隔离 cookie)
context = browser.new_context(
viewport={"width": 1280, "height": 720},
user_agent="Mozilla/5.0 ...",
locale="zh-CN"
)
# 创建页面
page = context.new_page()
page.goto("https://example.com")
# ... 操作 ...
print(page.title())
print(page.locator("body").inner_text())
context.close()
browser.close()
代码助手Codegen
Playwright 的代码助手(Codegen)是一个内置的代码生成工具,用于帮助用户快速编写 Playwright 测试脚本。在打开codegen后,用户在浏览器里的所有操作(点击、输入、导航等)都会被自动转换成 Playwright 代码,可以帮助我们自动生成稳定、可读性好的选择器,以及快速验证页面交互等
playwright codegen运行 codegen 打开浏览器
- url:可以指定URL,用于直接在该页面中打开Codegen,如:playwright codegen https://example.com
- --target 语言指定输出语言
- -o 文件保存到文件
- --load-storage=auth.json使用已保存的登录状态
- --device="iPhone 13"模拟移动设备,如:npx playwright codegen --device="iPhone 13" https://example.com
Browser浏览器实例
浏览器控制主要负责:启动浏览器、选择浏览器版本、配置浏览器环境、创建独立会话,以及连接到已经运行的浏览器,核心方法为:
启动新的浏览器环境
- p.chromium.launch() p 为Playwright 库对象,下文相同
- p.firefox.launch()
- p.webkit.launch()
启动新的浏览器,默认启动的浏览器是无界面的(headless),支持以下参数:- headless:指定是否显示浏览器窗口,默认headless=True,开发、调试时可修改以显式窗口:browser = p.chromium.launch(headless=False)
- 指定 Playwright 所使用的浏览器,建议使用 Playwright 安装的内核,这些内核参数被调试的更适配Playwright,推荐使用的优先级为:Playwright自带的内核 > channel指定 > executable_path指定
- channel指定使用哪个浏览器发行版,常用值有:chromium、chrome、chrome-beta、chrome-dev chrome-canary、msedge等,如:可通过p.chromium.launch(channel="chrome")让 Playwright 启动本机安装的 Google Chrome,而不是 Playwright 自带的 Chromium,Playwright 会自行找到其启动位置
- executable_path:直接指定浏览器程序路径,如:browser = p.chromium.launch(executable_path = r"C:\path\to\chrome.exe")
- slow_mo:指定一个值,单位为毫秒,让 Playwright 的在每个操作之间增加指定延迟,主要用于观察和调试,如:browser = p.chromium.launch( headless=False, slow_mo=500 ),让 Playwright 执行步骤延迟500ms
- proxy:为浏览器配置代理,支持以下方式:
- downloads_path="路径":指定浏览器下载文件的目录,但下载行为建议通过 BrowserContext / Page 的下载 API 管理
- args :向 浏览器内核 传递启动参数,Playwright 自己会向浏览器传递一组经过测试的启动参数,不建议自行传入参数,如:args=["--start-maximized"]
# 指定代理 browser = p.chromium.launch( proxy={ "server": "http\://127.0.0.1:7890" } ) # 带认证 proxy={ "server": "http://proxy\.example.com:8080", "username": "user", "password": "password" } # 使用 SOCKS proxy={ "server": "socks5://127.0.0.1:1080" }
# 示例
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(
headless=False, # 显示窗口
slow_mo=500 # 减缓操作,方便调试
)
常用属性和方法
- browser.contexts返回当前浏览器中所有已打开的上下文,可迭代,或通过索引访问
- browser.browser_type返回创建该浏览器的 BrowserType(chromium/firefox/webkit)
- browser.version浏览器版本号
- browser.is_connected()浏览器是否仍处于连接状态
- browser.close()关闭浏览器及其所有上下文/页面
持久化浏览器
启动带持久化用户数据的浏览器,浏览器会将持久化数据写入该目录,该API直接返回 Context,而不是 Browser,关闭这个 Context 时,浏览器也会一起关闭。注意!不要直接使用 Chrome 正常日常使用的 Profile 给 Playwright ,而是独立建立一个专门用于自动化的独立目录,且每个浏览器实例应该使用不同目录,否则容易产生 Profile 锁定和数据冲突
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
context = p.chromium.launch_persistent_context(
"playwright_profile", # 专门用于自动化的目录
headless=False,
channel="chrome"
)
page = context.new_page()
page.goto("https://example.com")
context.close()
# 下一访问仍然使用playwright_profile目录中的浏览器数据
context = p.chromium.launch_persistent_context(
"playwright_profile"
)
连接现有浏览器
在使用上述API创建浏览器实例,调试 Playwright 代码时,每次执行都会重新启动浏览器、创建会话、访问目标页面,导致调试、修改功能代码很繁琐,Playwright 提供了以下方法,可以通过 Chrome DevTools Protocol(CDP,Chrome 开发者工具协议) 连接已经启动的 Chromium 浏览器,实现快速重新运行脚本
启动带调试端口的 Chrome
首先需要启动一个允许 CDP 远程调试的浏览器实例
# Windows
chrome.exe --remote-debugging-port=9222 --user-data-dir="D:\chrome-debug-profile"
#或完整路径启动
"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 --user-data-dir="D:\chrome-debug-profile"
#Linux
google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug
参数:
- --remote-debugging-port=9222:开放 CDP 调试端口,供 Playwright 连接。
- --user-data-dir=...:指定浏览器用户数据目录,用于保存 Cookie、登录状态、缓存等数据,应使用专门的调试目录,不要直接与正在运行的日常浏览器共用同一个用户数据目录
启动完成后,可通过以下地址检查调试端口是否已正确打开,如果返回包含 webSocketDebuggerUrl 的 JSON 信息,说明调试接口已经开放。该端口应仅供本机可信程序访问,不要暴露到公网或不可信网络
http://127.0.0.1:9222/json/version
连接浏览器
- p.connect_over_cdp()通过 Chrome DevTools Protocol 连接浏览器,适合复用 Chrome 或 Edge 的现有页面
- p.connect()使用 Playwright 自身的通信协议,连接由 Playwright 驱动的浏览器服务,通常要求两端 Playwright 版本兼容
from playwright.sync_api import sync_playwright
def main():
with sync_playwright() as p:
# 连接已启动的浏览器,而不是 launch
browser = p.chromium.connect_over_cdp("http://localhost:9222")
# 复用已有的 context 没有才新建
target_url="test.com"
context = browser.contexts[0] if browser.contexts else browser.new_context()
# 查找页面
pages = [
p for p in context.pages
if p.url.startswith(target_url)
]
if len(pages) != 1:
raise RuntimeError(
f"预期找到 1 个页面,实际找到 {len(pages)} 个"
)
page = pages[0]
# 业务代码,改完只重跑这段
links = page.locator("a").all()
print("链接数量:", len(links))
# ↑↑↑ 不要调用 page.close() / context.close() / browser.close() ↑↑↑
# 只断开 CDP 连接,不关闭浏览器
browser.close()
if __name__ == "__main__":
main()
context浏览器会话
创建浏览器会话
context = browser.new_context() 创建浏览器会话,其中browser为已经创建好的实例,支持以下参数:
- viewport设置页面视口,Playwright Context 默认使用固定 viewport,如果希望使用实际浏览器窗口尺寸,可以 viewport=None,或手动指定值context = browser.new_context( viewport={ "width": 1920, "height": 1080 } )
- user_agent指定 UA,如:user_agent="Mozilla/5.0 ..."
- locale设置浏览器语言环境,如:locale="zh-CN",它会影响navigator.language,Accept-Language,数字/日期等格式
- timezone_id设置时区
- ignore_https_errors = True忽略 HTTPS 证书错误,主要用于开发环境
- extra_http_headers给所有请求增加 HTTP Header,如:extra_http_headers={ "Authorization": "Bearer xxx" }
- java_script_enabled是否启用JS,默认为True
- storage_state用于复用登录状态,它所指定的文件通常是context.storage_state导出的,如:context = browser.new_context(storage_state=r"D:\playwrightAuth\xxx.json")
context的常用属性和方法
- context.pages返回当前 context 中所有已打开页面的列表,可迭代
- context.browser返回 context 所属 browser 对象
- context.close()关闭 context 及其所有页面
- context.is_closed()检查context 会话是否已关闭
事件监听 - context.on("事件", 回调函数)监听事件,执行回调,支持以下事件:
- page:新页面打开时触发
- close:context 关闭时触发
- request:任意请求发出时触发
- response:收到响应时触发
- requestfailed:请求失败时触发
- console:控制台消息
context.on("page", lambda page: print("新页面:", page.url)) context.on("close", lambda: print("context 已关闭")) # 拦截请求 context.on("request", lambda req: print("请求:", req.url))路由与网络拦截 - context.route(url, handler)拦截并处理匹配的请求
- context.unroute(url)取消路由拦截
- context.route_from_har(...)从 HAR 文件回放网络请求
注入代码
- context.add_init_script(script)每个页面加载前注入JavaScript脚本
with sync_playwright() as p:
browser = p.chromium.launch()
context = browser.new_context()
context.add_init_script("""
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
});
""")
page = context.new_page()
page.goto("https://example.com")
print(page.evaluate("navigator.webdriver")) # None
browser.close()
Cookie与存储状态
- context.cookies(urls=None)获取所有/指定 URL 的 cookie
- context.add_cookies(cookies)添加 cookie
- context.clear_cookies()清空所有 cookie
- context.storage_state(path=None)导出登录状态(cookie + localStorage)到文件
- browser.new_context(storage_state="auth.json")加载已保存的状态
跟踪Trace
Trace 是 Playwright 的调试与事后分析工具。它把一次测试运行的完整过程记录下来,包括:
- 每个操作的截图(可逐帧回放)
- DOM 快照(可以查看每个时刻的页面结构)
- 网络请求(请求/响应、耗时、状态码)
- 控制台日志(console 输出、报错)
- 源码位置(哪个操作对应哪一行代码)
- 时间线(操作之间的等待、耗时)
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=False, slow_mo=500)
context = browser.new_context(
viewport={"width": 1280, "height": 720},
user_agent="Mozilla/5.0 ...",
locale="zh-CN"
)
# 启用追踪
context.tracing.start(snapshots=True,sources=True,screenshots=True)
# 创建页面
page = context.new_page()
page.goto("https://example.com")
# ... 操作 ...
page.locator("#input").fill("python")
page.locator("#go").click()
# 停止跟踪
context.tracing.stop(path="trace.zip")
context.close()
browser.close()
查看跟踪文件
- 通过命令
playwright show-trace 追踪文件查看 - 通过微软的网站https://trace.playwright.dev/,将文件上传检查
复用登录状态案例
处理需要登录的网页时,虽然可以自动填入账号、密码,但登录页面通常还需要应对验证码。一种更可靠的方式是先手动登录一次,然后将登录会话内容保存到本地,之后一段时间内的自动化任务可以复用该登录状态,模板分为两部分:
- 手动登录一次,并保存登录状态
- 自动化登录,复用登录会话
#手动登录并保存登录状态
from pathlib import Path
from playwright.sync_api import sync_playwright
LOGIN_URL = "https://xxx.com/login.html"
STATE_DIR = Path(r"D:\playwrightAuth")
STATE_FILE = STATE_DIR / "state.json"
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
context = browser.new_context()
page = context.new_page()
page.goto(LOGIN_URL)
input("请前往浏览器完成登录,完成登录后按任意键继续..")
print("当前页面",page.url)
STATE_DIR.mkdir(exist_ok=True)
#导出登录状态信息
context.storage_state(path=str(STATE_FILE))
print(f"会话文件已经保存在{str(STATE_FILE)}")
browser.close()
#后期自动化任务,复用登录状态
from playwright.sync_api import sync_playwright
from pathlib import Path
TARGET_URL = "https://xxxx.com"
STATE_DIR = Path(r"D:\playwrightAuth")
STATE_FILE = STATE_DIR / "state.json"
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
#复用登录状态
context = browser.new_context(storage_state=str(STATE_FILE))
page = context.new_page()
page.goto(TARGET_URL)
print(f"当前位于{page.url}")
#爬取逻辑
input("按任意键继续...")
browser.close()
Page页面对象
Page对象指的是浏览器上下文中的单个标签页或弹出窗口,每个 BrowserContext 可以有多个页面,可通过以下方式创建:
- page = browser.new_page()通过浏览器实例对象直接创建,适合简单场景
- page = context.new_page()通过 BrowserContext 创建,适用于需要处理 Cookie、Storage、代理、权限等浏览器会话环境
页面导航
- page.goto("URL",*args)打开指定URL,返回一个 Response(意味着可以通过 response.status 获取状态码),goto 语句自带等待,推荐添加 wait_until 参数以指示页面加载完成时机,支持以下参数:
- url目标地址
- timeout超时时间,单位 ms
- wait_until认为导航完成的时机,常用值为:
- "load":默认值,load 事件
- "commit":HTTP 响应收到、文档开始加载
- "domcontentloaded":DOMContentLoaded
- "networkidle":网络连接持续空闲
- page.reload()重新加载当前页面,常用参数与 goto() 类似
- page.go_back()浏览器后退
- page.go_forward()浏览器前进
page.goto(
"https\://example.com",
wait_until="domcontentloaded",
timeout=30000
)#Sync同步
page = context.new_page()
page.goto('http://example.com')
page.locator('#search').fill('query')
page.locator('#submit').click()
#Async异步
page = await context.new_page()
await page.goto('http://example.com')
await page.locator('#search').fill('query')
await page.locator('#submit').click()
页面属性与方法
- page.url获取当前URL,注意它是属性
- page.title()获取当前页面标题
- page.content()获取当前页面完整 HTML,常用于将当前页面本地保存,如:with open("page.html", "w", encoding="utf-8") as f: f.write(page.content())
- page.screenshot(path="page.png", full_page=True) 整页截图
- page.set_content("<h1>Hello</h1>")设置页面内容,用于测试或临时构造 HTML
- page.viewport_size获取视口尺寸,属性
- page.frames获取所有 frame,属性
- page.main_frame获取主 frame,属性
- page.context获取所属 browser context,属性
- page.workers获取所有 Web Worker,属性
- page.set_default_timeout()设置全局超时,单位为ms
等待事件
wait_for_load_state()等待页面达到指定的加载状态
- 参数 state 支持以下三种值:
- "domcontentloaded":HTML 文档解析完成,DOMContentLoaded 事件触发,不要求图片等资源加载完成
- "load":页面及其依赖资源加载完成
- "networkidle":网络连接至少连续 500 ms 没有活动;官方不推荐将其作为判断页面就绪的主要方式
- timeou:指定超时,如:page.wait_for_load_state("load", timeout=15000)
#页面导航已提交后,等待指定加载状态 page.wait_for_load_state("load")- 参数 state 支持以下三种值:
wait_for_function()页面满足指定 JavaScript 条件时返回,适合等待动态数据、滚动加载结果或页面内部状态变化。表达式返回真值即完成等待;默认轮询方式为 requestAnimationFrame,也可以用毫秒数指定轮询间隔
等待弹窗/页面
等待页面跳转
- wait_for_url()等待页面 URL 匹配指定值或模式,适合点击链接、提交表单后发生导航的场景page.wait_for_url("**/page/2",timeout=10000) #**是通配符,两个**可以跨路径分隔符/
等待新页面:新页面指通过点击 target="_blank" 链接或触发 window.open() 打开的新的标签页,如果是在当前页跳转,使用wait_for_url()
with context.expect_page() as ..断言并捕获当前context内新打开的页面(新标签页/新窗口),返回 Page 对象
- 新页面指通过点击 target="_blank" 链接或触发 window.open() 打开的页面
- timeout:等待超时时间(毫秒),默认使用 context 默认超时
- predicate:接收 Page 对象,返回布尔值,只有满足条件的页面才会被返回
- expect_page() 用于断言页面打开,通常用于测试,如果需要等待页面打开应该使用 wait_for_event('page')
- 通常使用 with 管理上下文
- 注意新打开页面后,新的页面需要重新捕获
with context.expect_page(
predicate=lambda page: "example" in page.url,
timeout=5000
) as new_page_info:
page.click('a[target="_blank"]') # 触发新页面
new_page = new_page_info.value
new_page.wait_for_load_state()
print(new_page.title())
with context.expect_page() as new_page_info:
loginBtn.click()
#捕获新页面
new_page = new_page_info.value
new_page.wait_for_load_state() # 等待新页面加载完成
#对新页面的操作
new_page.locator("#name").fill("your_username")
等待弹窗页:弹窗指该页面内打开的新窗口,如:OAuth 第三方登录:用于登录、授权、支付的弹窗
- with page.expect_popup() as ..监听当前页面通过 window.open() 或 target="_blank" 打开的弹窗,返回 Page 对象
- 支持的参数同 context.expect_page()
- 通常使用 with 管理上下文
#使用 Google 账号自动化登录
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
context = browser.new_context()
page = context.new_page()
page.goto("https://example.com/login")
# 点击登录,等待 OAuth 弹窗
with page.expect_popup() as popup_info:
page.get_by_text("使用 Google 登录").click()
popup = popup_info.value
popup.wait_for_load_state()
# 在弹窗中填写登录信息
popup.fill('input[type="email"]', 'user@example.com')
popup.fill('input[type="password"]', 'password')
popup.click('button[type="submit"]')
# 弹窗关闭后回到主页面
popup.wait_for_event("close")
page.wait_for_url("**/dashboard")
browser.close()
等待请求/响应/下载
- with page.expect_request(url) as ..等待符合条件的请求发出,返回Request对象,该对象支持的属性和方法:
- request.url请求 URL
- request.method请求方法(GET/POST…)
- request.headers请求头(dict,key 小写)
- request.post_dataPOST 请求体(字符串,可能为 None)
- request.post_data_jsonPOST 请求体解析为 JSON
- request.post_data_buffer请求体原始 bytes
- request.resource_type资源类型(document/xhr/fetch/image…)
- request.is_navigation_request()是否为导航请求
- request.redirected_from重定向来源 Request
- request.redirected_to重定向目标 Request
- request.timing请求各阶段耗时
- request.all_headers()返回全部请求头(含 cookie 等)
- request.header_value(name)获取某个请求头
- request.response()获取对应的 Response(可能为 None)
- request.failure失败原因(字符串,成功为 None)
- with page.expect_response(url) as ..等待符合条件的响应返回,返回Response对象,该对象支持属性和方法:
- response.url响应 URL
- response.statusHTTP 状态码(200/404…)
- response.status_text状态文本(OK/Not Found)
- response.ok是否 2xx/3xx(成功)
- response.headers响应头(dict,key 小写)
- response.request对应的 Request
- response.frame所属 Frame
- response.server_addr服务器地址
- response.from_service_worker是否来自 Service Worker
- response.all_headers()全部响应头
- response.header_value(name)获取某个响应头
- response.header_values(name)获取同名响应头列表
- response.body()响应体 bytes
- response.text()响应体文本
- response.json()响应体解析为 JSON
- response.finished()等待响应体下载完成,返回失败原因或 None
print(resp.status, resp.ok, resp.url)
print(resp.headers.get("content-type"))
data = resp.json() # 解析 JSON
text = resp.text() # 文本
raw = resp.body() # bytes
- with page.expect_download() as ..等待下载事件
- download.url下载 URL
- download.suggested_filename建议文件名
- download.page所属 Page
- download.failure()失败原因(成功为 None)
- download.path()返回下载到本地的临时路径
- download.save_as(path)保存到指定路径
- download.delete()删除已下载的临时文件
- download.create_read_stream()返回可读流
- download.cancel()取消下载
print(dl.suggested_filename)
dl.save_as(f"downloads/{dl.suggested_filename}") # 保存
#path = dl.path() # 临时路径
事件监听/等待
用于等待某个事件触发一次,并在触发后拿到事件对象,适用于指定某个操作后,等待该事件发生并处理
- with page.expect_event(event, predicate=None, timeout=None) as ..等待指定页面事件,支持参数:
- event 事件名(同 page.on() 支持的事件)
- predicate 可选,过滤函数,接收事件对象,返回 True 才认为匹配
- timeout 可选,超时毫秒数
- wait_for_event()等待事件,这会阻塞代码,直到事件发生或超时,更推荐使用page.expect_event
注册持久监听器,每次事件触发都调用 handler,适用于持续监听请求、响应、错误等
- page.on(event, handler)监听事件。触发回调
- page.once(event, handler)只监听一次事件,触发后自动移除监听器
- page.remove_listener(event, handler)移除指定事件的监听器
- page.remove_all_listeners(event=None)移除指定事件或全部事件的监听器
常用的event
| 事件 | 触发时机 | handler 参数 | 典型用途 |
|---|---|---|---|
| "dialog" | 出现 alert / confirm / prompt | Dialog | 自动接受或取消弹窗 |
| "popup" | 打开新页面/窗口 | Page | 捕获新标签页 |
| "download" | 触发文件下载 | Download | 保存下载文件 |
| "request" | 发出请求 | Request | 记录/拦截请求 |
| "response" | 收到响应 | Response | 记录响应状态、抓接口数据 |
| "requestfailed" | 请求失败 | Request | 监听失败请求 |
| "console" | 控制台输出 | ConsoleMessage | 打印页面日志、调试 |
| "pageerror" | 未捕获的 JS 异常 | Error | 捕获页面报错 |
| "framenavigated" | frame 导航完成 | Frame | 监听页面跳转 |
| "filechooser" | 点击<input type=file> | FileChooser | 上传文件 |
Locator
Locator 是一个元素定位器,在 playwright 中用于捕获元素,并返回一个 Locator 对象,该对象支持的完整方法参考官方文档:class-locator,playwright 中的元素定位/操作有以下特点:
- 自动等待:Locator 的操作不一定立即执行点击,而会等待元素满足操作所需要的条件(actionable),例如元素出现、可见、可操作等,无需手动 sleep,这保障了playwright对动态网页的操作稳定
- 推荐优先使用语义定位:get_by_role > get_by_label > get_by_text > CSS/XPath。
- 支持链式操作:locator.filter(...).get_by_role(...).first.click()。
- 严格模式:默认 strict 模式,有多个匹配时操作会报错,进行操作时使用 .first / .nth() 明确指定操作哪个元素
- 重新定位:每次操作时,Playwright 会重新查找元素,不保存旧 DOM 引用,能有效避免元素变更带来的操作失效
定位元素
以下方法会返回 Locator,因此可以在拿到元素后链式调用,如:page.get_by_role(...).click()
get_by_role()
- page.get_by_role(),按照 ARIA 角色(role)定位元素,是最推荐的方式,语义清晰、贴近可访问性。示例:page.get_by_role("button", name="登录"),常用参数有:
- name:按可访问名称匹配,支持字符串和正则。名称可能来自元素文本、关联 <label>、aria-label、aria-labelledby、图片 alt 等
- exact:字符串默认不区分大小写、匹配子串;设为 True 后区分大小写并要求整个名称匹配,但仍会去除首尾空白。正则匹配时该参数不生效
- checked:匹配复选框、单选框等支持 checked 状态的角色。
- disabled:匹配禁用状态;可能来自原生 disabled 或 aria-disabled。
- expanded:匹配展开状态,通常对应 aria-expanded。
- pressed:匹配切换按钮的按下状态,通常对应 aria-pressed。
- selected:匹配选中状态,常用于 option、tab 等。
- level:匹配语义层级,最常见是 heading 的 1~6,对应 h1-h6
- include_hidden:默认 False,控制是否包含按照 ARIA 规则被视为隐藏的元素;不是简单判断宽高是否为 0,也不等同于 CSS 可见性检测。
| HTML标签 | 对应role | 示例 |
|---|---|---|
| <input type="search"> | searchbox | get_by_role("searchbox") |
| <input type="number"> | spinbutton | get_by_role("spinbutton") |
| <input type="range"> | slider | get_by_role("slider") |
| <input type="submit"> | button | get_by_role("button") |
| <input type="button"> | button | get_by_role("button") |
| <a href="..."> | link | get_by_role("link") |
| <h1> ~ <h6> | heading | get_by_role("heading", level=1) |
| <img alt="..."> | img | get_by_role("img", name="Logo") |
| <nav> | navigation | get_by_role("navigation") |
| <main> | main | get_by_role("main") |
| <header>(页面级) | banner | get_by_role("banner") |
| <footer>(页面级) | contentinfo | get_by_role("contentinfo") |
| <form>(具有可访问名称时) | form | get_by_role("form", name="登录") |
| <dialog> | dialog | get_by_role("dialog") |
| <ul> / <ol> | list | get_by_role("list") |
| <li> | listitem | get_by_role("listitem") |
| <p> | paragraph | get_by_role("paragraph") |
| <table> | table | get_by_role("table") |
| <tr> | row | get_by_role("row") |
| <th>(列标题) | columnheader | get_by_role("columnheader") |
| <td> | cell | get_by_role("cell") |
| <input type="text"> | textbox | get_by_role("textbox") |
| <textarea> | textbox | get_by_role("textbox") |
| <input type="email"> | textbox | get_by_role("textbox") |
| <input type="password"> | 通常无可用的 textbox role | 优先 get_by_label() |
| <input type="checkbox"> | checkbox | get_by_role("checkbox") |
| <input type="radio"> | radio | get_by_role("radio") |
| <select>(单选) | combobox | get_by_role("combobox") |
| <select multiple> | listbox | get_by_role("listbox") |
| <option> | option | get_by_role("option") |
get_by_xxx
- page.get_by_text(),按照元素可见文本定位。示例:page.get_by_text("欢迎回来")。常用参数:exact(默认 False,子串匹配;True 为完全匹配)。支持字符串或正则表达式。
- page.get_by_label(),按照关联的
- page.get_by_placeholder(),按照输入框的 placeholder 属性定位。示例:page.get_by_placeholder("请输入密码")。常用参数:exact。
- page.get_by_alt_text(),按照图片的 alt 属性定位(通常用于img标签)。示例:page.get_by_alt_text("公司 Logo")。常用参数:exact。
- page.get_by_title(),按照元素的 title 属性定位。示例:page.get_by_title("提交表单")。常用参数:exact。
- page.get_by_test_id(),按照 data-testid(可自定义)属性定位,专为测试设计,最稳定。示例:page.get_by_test_id("submit-button")。可通过 selectors.set_test_id_attribute() 自定义属性名。
locator()通过CSS/Xpath定位
- page.locator(),使用 CSS 或 XPath 选择器定位,最灵活通用,使用示例:
page.locator("#submit") #id 为 submit 的元素
page.locator(".btn") #class 含 btn 的元素
page.locator("div.card") #同时有 div 标签且 class 含 card
page.locator("div button") #div 内任意后代 button(不限层级)
page.locator("div > button") #div 的直接子 button
page.locator("input[type='text']") #type 属性为 text 的 input
page.locator("a[href^='https']") #href 以 https 开头的 a
page.locator("a[href$='.pdf']") #href 以 .pdf 结尾的 a
page.locator("a[href*='login']") #href 含 login 的 a
page.locator("button:has-text('登录')") #内部文本含"登录"的 button
page.locator("li:nth-child(2)") #父元素下第 2 个 li
page.locator("li:first-child") #父元素下第 1 个 li
page.locator("li:last-child") #父元素下最后 1 个 li
page.locator("div:not(.hidden)") #class 不含 hidden 的 div
page.locator("button >> text=登录") #链式:button 中文本为登录的(Playwright 专有 >> 语法)
page.locator("css=div.card") #显式声明用 css 引擎
page.locator("xpath=//div[@class='card']") #显式声明用 xpath 引擎
page.locator("text=登录") #文本含"登录"的元素
page.locator("text='登录'") #文本精确为"登录"(引号表示精确)
page.locator("li").filter(has_text="产品") #li 中再筛出含"产品"的
page.locator("div").filter(has=page.locator("button")) #div 中再筛出含 button 的
page.locator("li").nth(2) #匹配结果里第 3 个(下标从 0 开始)
page.locator("button").first #匹配结果里第 1 个
page.locator("button").last #匹配结果里最后 1 个
page.locator("ul > li").locator("a") #链式:ul 下 li,再往下找 a
page.locator("div.card", has_text="促销") #构造时直接带 has_text 过滤
集合与筛选
- locator.first:第一个
- locator.last:最后一个
- locator.nth(index):第 index 个(从 0 开始)
- locator.count():匹配元素个数
- locator.all():返回所有匹配的 Locator 列表
- locator.locator(selector):在其内部继续定位,也可以 locator.locator("..") 向父级移动,应该优先使用上面的语句来定位元素,而不是大量依赖XPath
- locator.filter(has_text=..., has=..., has_not=...):筛选元素元素#文本筛选 locator.filter(has_text="Python") #根据内部因素筛选 items.filter( has=page.get_by_role("button", name="购买") )
- locator.and_(other):组合条件,要求两个 Locator 同时匹配
- locator.or_(other):或条件,两个 Locator 取并集
操作参数
大多数针对 Locator 的操作和方法支持以下通用参数:
- timeoutPlaywright 操作前会自动等待元素满足条件(如:元素存在,可见,可点击等),该参数用于指定最大等待时间(毫秒),默认值为30000,0 表示禁用超时,超时则抛出 TimeoutError,如:locator.fill("hello", timeout=3000)
- force 强制执行操作,跳过 actionability 检查(可见/稳定/可用/可接收事件),如:locator.click(force=True),适合点击隐藏或覆盖层、自动化测试绕过 UI 限制
- no_wait_after是否等待操作触发的页面跳转完成,默认情况下Playwright会等待导航完成后才继续执行,no_wait_after=True 可以立即继续执行操作,适合不关心跳转或页面跳转很慢时,自己管理等待
- trial只检测操作是否可执行,不真正执行
- position指定元素内相对于左上角的坐标,很多鼠标操作支持该参数,单位为px,locator.click(position={"x":100,"y":50})
交互类
以下大多数操作(click()、fill()、check() 等)内置自动等待,它们会等待元素出现在 DOM 中,或者元素可见、可交互,动画结束、稳定等,然后再执行
点击/触摸/hover
- locator.click(**kwargs) / .dblclick(**kwargs):单击 / 双击元素,支持以下参数:
- button:指定按下的键,值可为"left" | "right" | "middle",默认 "left"
- click_count:点击次数(click 专用)
- modifiers:点击时同时按下键盘修饰键,指定一个list:"Alt"、"Control"、"Meta"(Windows/Command)、"Shift",如:locator.click(modifiers=["Control"])
- delay:鼠标按下和释放之间的延迟,单位为ms,用于模拟真人点击,如:locator.click(delay=100)
- locator.hover(**kwargs):鼠标悬停,可通过 position 指定悬停位置
- locator.tap(**kwargs):触摸点击(移动端)
- locator.drag_to(target, **kwargs):拖拽到目标元素,支持以下特殊参数:
- 拖动起点,如:source_position={"x":0,"y":0}
- 目标位置:target_position={"x":100,"y":100}
文本操作
- locator.fill(value, **kwargs):清空后填充文本
- locator.clear(**kwargs):清空输入框
表单操作
- locator.check(**kwargs):勾选复选框/单选框
- locator.uncheck(**kwargs):取消勾选
- locator.set_checked(checked, **kwargs):设置勾选状态
- locator.input_value(**kwargs):获取表单控件当前值
- locator.set_input_files(files, **kwargs):上传文件locator.set_input_files(["a.jpg", "b.jpg"])
- locator.select_option(value=..., **kwargs):选择 <select> 选项# 三种选择方式只能选一种 locator.select_option( value="value1", # 按 value 选择(可传 str 或 list) label="Label 1", # 按 label 选择(可传 str 或 list) index=0, # 按索引选择(可传 int 或 list) force=False, timeout=30000, no_wait_after=False, )
键盘操作
- locator.press(key, **kwargs):按下按键(如 "Enter"),如:locator.press("Control+A")
- locator.press_sequentially(text, **kwargs):模拟逐字输入
其他
- locator.focus():聚焦元素
- locator.blur():移除焦点
- Frame / 内容访问
- locator.element_handle(**kwargs):获取 ElementHandle
- locator.content_frame():获取 iframe 的 Frame
- locator.owner_frame():获取所属 Frame
- locator.scroll_into_view_if_needed():滚动到可见
获取内容与属性
- locator.inner_text(**kwargs):获取内部文本(渲染后)
- locator.inner_html(**kwargs):获取内部 HTML
- locator.text_content(**kwargs):获取文本内容
- locator.input_value(**kwargs):获取输入框的值
- locator.get_attribute(name, **kwargs):获取指定属性值
- locator.all_inner_texts():所有匹配元素的内部文本列表
- locator.all_text_contents():所有匹配元素的文本内容列表
状态判断(返回 bool)
- locator.is_visible(**kwargs):是否可见
- locator.is_hidden(**kwargs):是否隐藏
- locator.is_enabled(**kwargs):是否可用
- locator.is_disabled(**kwargs):是否禁用
- locator.is_checked(**kwargs):判断 checkbox / radio 是否选中
- locator.is_editable(**kwargs):是否可编辑
等待方法
- locator.wait_for(state="visible", **kwargs):等待元素可见
- locator.wait_for(state="attached"):等待DOM被添加
- locator.wait_for(state="detached"):等待从 DOM 移除
- locator.wait_for(state="hidden"):等待隐藏
截图与高亮
- locator.screenshot(path=..., **kwargs):元素截图,还支持:
- type:指定格式type="jpeg"
- quality:指定质量,值为0-100
- locator.highlight():高亮元素(调试用)
- locator.bounding_box(**kwargs):获取元素边界框
断言(配合 expect() 使用)
- expect(locator).to_be_visible():断言可见
- expect(locator).to_be_hidden():断言隐藏
- expect(locator).to_be_enabled():断言可用
- expect(locator).to_be_disabled():断言禁用
- expect(locator).to_be_checked():断言已勾选
- expect(locator).to_have_text("xxx"):断言文本
- expect(locator).to_contain_text("xxx"):断言包含文本
- expect(locator).to_have_value("xxx"):断言值
- expect(locator).to_have_attribute("name", "value"):断言属性
- expect(locator).to_have_class("xxx"):断言 class
- expect(locator).to_have_count(3):断言数量
- expect(locator).to_have_css("color", "red"):断言 CSS 样式
.evaluate()执行JS代码
.evaluate() 用于在浏览器页面上下文中执行 JavaScript 代码,可访问 window、document、DOM 等,执行完毕会将结果返回给 Python。;
obj.evaluate(expression, arg=None)
- obj:evaluate()方法可在 Page、Frame、ElementHandle、JSHandle、Locator 等对象上调用,可以使用Locator.evaluate_all()方法对所有匹配到的Locator对象执行代码
- expression:要执行的 JS 代码字符串,如 "el => el.textContent"
- arg:可选,传给 JS 函数的参数,可传多个参数,如果调用对象是Locator、ElementHandle(元素)、JSHandle(JS对象),则传入的第一个参数是该obj本身
- 返回值必须可序列化;DOM 元素、函数等不能直接返回,返回值为JS对象时,需要使用 evaluate_handle() 方法
模板
# 爬取滚动加载页面https://scrapingsandbox.com/infinite-scroll
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError
URL = "https://scrapingsandbox.com/infinite-scroll"
TARGET_COUNT = 60 # 最多抓取 60 个商品
with sync_playwright() as p:
# launch() 启动独立的浏览器实例;headless=False 可以观察滚动过程
browser = p.chromium.launch(headless=False)
page = browser.new_page(viewport={"width": 1280, "height": 800})
# domcontentloaded 只表示初始 HTML 已解析,不代表动态商品已加载完成
page.goto(URL, wait_until="domcontentloaded")
# locator() 创建定位器,不会立即提取元素内容
# 以下商品卡片及字段选择器需要与页面实际 DOM 结构一致
cards = page.locator(".product-card")
# wait_for() 等待至少一个商品卡片进入 DOM,避免页面刚打开就开始提取
cards.first.wait_for(state="visible", timeout=15000)
products = []
seen_names = set()
while len(products) < TARGET_COUNT:
# 每次重新获取当前卡片数量,避免只处理初始批次
card_count = cards.count()
old_count = card_count
for i in range(card_count):
card = cards.nth(i)
# locator() 可以在当前商品卡片内部继续查找字段
name_locator = card.locator(".product-title")
price_locator = card.locator(".product-price")
# inner_text() 读取页面渲染后的文本
# 商品名称和价格选择器应按实际 DOM 调整
if name_locator.count() == 0 or price_locator.count() == 0:
continue
name = name_locator.first.inner_text().strip()
price = price_locator.first.inner_text().strip()
# 滚动加载时,已存在的商品会再次被遍历,因此去重
if name and name not in seen_names:
seen_names.add(name)
products.append({
"name": name,
"price": price,
})
print(f"{len(products):02d}. {name} | {price}")
if len(products) >= TARGET_COUNT:
break
if len(products) >= TARGET_COUNT:
break
# evaluate() 在浏览器页面上下文中执行 JavaScript。
# window.scrollTo() 将页面滚动至当前文档底部,触发滚动加载逻辑。
page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
try:
# 等待新商品出现,而不是固定 sleep 若干秒。
# 如果商品数量一直不变,说明本次滚动可能没有触发新批次加载。
page.wait_for_function(
"""oldCount =>
document.querySelectorAll('.product-card').length > oldCount
""",
arg=old_count,
timeout=5000,
)
except PlaywrightTimeoutError:
# 没有新商品时退出,避免无限循环
print("等待新商品超时,结束滚动。")
break
print(f"\n共抓取 {len(products)} 个商品")
browser.close()