<
  • 主题:
  • + -
  • 清除背景
  • 禁用背景

playwright

字数:31727 写于:2022-10-12
最新更新:2026-10-12 阅读本文预计花费您91分钟

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()关闭浏览器及其所有上下文/页面

持久化浏览器

默认情况下 browser = p.chromium.launch() context = browser.new_context() 此时 Context 是非持久化的,浏览数据不会作为长期浏览器用户数据保存到磁盘,如果希望保存Cookies、Local Storage、登录状态、浏览器站点数据,可以使用下述API
p.xxxx.launch_persistent_context("目录")

启动带持久化用户数据的浏览器,浏览器会将持久化数据写入该目录,该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.route("**/*.{png,jpg}", lambda route: route.abort()) # 修改 API 响应 def handle_api(route): if route.request.url.endswith("/api/user"): route.fulfill(status=200, body='{"name": "mock"}') else: route.continue_() context.route("\*\*/api/\*\*", handle_api)

注入代码

  • context.add_init_script(script)每个页面加载前注入JavaScript脚本
context.add_init_script(script="...") # 直接传 JS 字符串 context.add_init_script(path="init.js") # 传 JS 文件路径 # 隐藏 navigator.webdrive(绕反爬检测) from playwright.sync_api import sync_playwright

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")加载已保存的状态
# 获取 cookies = context.cookies() #添加 context.add_cookies([{ "name": "session", "value": "abc123", "domain": ".example.com", "path": "/" }])

跟踪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()
查看跟踪文件

复用登录状态案例

处理需要登录的网页时,虽然可以自动填入账号、密码,但登录页面通常还需要应对验证码。一种更可靠的方式是先手动登录一次,然后将登录会话内容保存到本地,之后一段时间内的自动化任务可以复用该登录状态,模板分为两部分:

  • 手动登录一次,并保存登录状态
  • 自动化登录,复用登录会话
#手动登录并保存登录状态
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")
  • wait_for_function()页面满足指定 JavaScript 条件时返回,适合等待动态数据、滚动加载结果或页面内部状态变化。表达式返回真值即完成等待;默认轮询方式为 requestAnimationFrame,也可以用毫秒数指定轮询间隔

#等待商品列表至少出现 20 个元素 page.wait_for_function( "() => document.querySelectorAll('.product').length >= 20" ) #等待页面滚动到接近底部 page.wait_for_function( "() => window.innerHeight + window.scrollY >= document.body.scrollHeight - 100" )
+ wait_for_timeout()暂停指定毫秒数后继续执行,如:page.wait_for_timeout(1000) #固定等待 1 秒

等待弹窗/页面

等待页面跳转

  • 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())
loginBtn = page.get_by_role("link", name="登录")

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_request("**/api/login") as info: page.click("#login") req = info.value print(req.method, req.url) print(req.post_data_json) print(req.headers.get("content-type")) print(req.resource_type)
  • 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
with page.expect_response("**/api/data") as info: page.click("#load") resp = info.value

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()取消下载
with page.expect_download() as info: page.click("#download") dl = info.value

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
with page.expect_event( "response", lambda r: r.url.endswith("/api/data") and r.status == 200 ) as info: page.click("#load") resp = info.value

注册持久监听器,每次事件触发都调用 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 上传文件
# 弹窗 page.on("dialog", lambda d: d.accept()) #新页面 page.on("popup", lambda p: print(p.url)) #下载 page.on("download", lambda d: d.save_as("file.zip")) #请求 / 响应 page.on("request", lambda r: print(">>", r.method, r.url)) page.on("response", lambda r: print("<<", r.status, r.url)) #失败请求 page.on("requestfailed", lambda r: print("fail", r.url)) #控制台 / JS 错误 page.on("console", lambda m: print(m.type, m.text)) page.on("pageerror", lambda e: print("JS error:", e)) #导航 page.once("framenavigated", lambda f: print("nav:", f.url)) #文件上传 page.on("filechooser", lambda fc: fc.set_files("a.txt"))

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.press( key, # 必填,如 "Enter"、"Control+A" delay=0, # 按下与松开之间的延迟(毫秒) timeout=30000, no_wait_after=False, ) locator.press_sequentially( text, # 必填,要输入的文本 delay=0, # 每个字符之间的延迟(毫秒),模拟用户输入 timeout=30000, no_wait_after=False, )
其他
  • 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() 方法
# 滚动页面 page.evaluate("window.scrollTo(0, document.body.scrollHeight)")

模板

# 爬取滚动加载页面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()
上一篇:Canvas
下一篇:python数据文件操作
z z z z z