# Python + Playwright Web 前端自动化开发完全指南> 从零开始掌握 Playwright 自动化测试 - 元素定位、API 使用、实战场景、调试技巧一站式学习---## 文档版本信息- **版本**: 1.0- **创建日期**: 2024-03-26- **适用版本**: Playwright for Python 1.40+- **目标读者**: 前端自动化测试工程师、测试开发人员---## 目录### 第一部分:基础篇1. [Playwright 简介与环境搭建](#1-playwright-简介与环境搭建)2. [DOM 树与元素定位基础](#2-dom-树与元素定位基础)3. [Playwright 定位 API 总览](#3-playwright-定位-api-总览)### 第二部分:CSS 选择器篇4. [CSS 选择器基础语法](#4-css-选择器基础语法)5. [CSS 属性选择器详解](#5-css-属性选择器详解)6. [CSS 关系选择器与伪类](#6-css-关系选择器与伪类)### 第三部分:XPath 篇7. [XPath 定位法完全指南](#7-xpath-定位法完全指南)8. [XPath 轴定位详解](#8-xpath-轴定位详解)### 第四部分:Playwright API 篇9. [locator() - 通用选择器](#9-locator---通用选择器)10. [get_by_role() - ARIA 角色定位](#10-get_by_role---aria-角色定位)11. [get_by_text() - 文本内容定位](#11-get_by_text---文本内容定位)12. [get_by_label() - Label 关联定位](#12-get_by_label---label-关联定位)13. [get_by_placeholder() - 占位符定位](#13-get_by_placeholder---占位符定位)14. [get_by_alt_text() - 图片 Alt 定位](#14-get_by_alt_text---图片-alt-定位)15. [get_by_title() - Title 定位](#15-get_by_title---title-定位)16. [get_by_test_id() - 测试 ID 定位](#16-get_by_test_id---测试-id-定位)17. [frame_locator() - Iframe 定位](#17-frame_locator---iframe-定位)### 第五部分:高级篇18. [正则表达式与模糊匹配](#18-正则表达式与模糊匹配)19. [filter() 过滤方法高级用法](#19-filter-过滤方法高级用法)20. [动态值定位实战](#20-动态值定位实战)21. [JavaScript 表达式定位](#21-javascript-表达式定位)### 第六部分:实战篇22. [登录页面元素定位](#22-登录页面元素定位)23. [数据表格元素定位](#23-数据表格元素定位)24. [导航菜单元素定位](#24-导航菜单元素定位)25. [模态框/对话框元素定位](#25-模态框对话框元素定位)26. [表单验证错误定位](#26-表单验证错误定位)27. [动态加载内容处理](#27-动态加载内容处理)### 第七部分:技巧篇28. [API 选择决策树](#28-api-选择决策树)29. [元素定位调试技巧](#29-元素定位调试技巧)30. [常见问题与解决方案](#30-常见问题与解决方案)31. [最佳实践与性能优化](#31-最佳实践与性能优化)### 附录- [附录 A: CSS 选择器速查表](#附录-a-css-选择器速查表)- [附录 B: XPath 速查表](#附录-b-xpath-速查表)- [附录 C: Playwright API 速查表](#附录-c-playwright-api-速查表)- [附录 D: 正则表达式速查表](#附录-d-正则表达式速查表)---## 第一部分:基础篇### 1. Playwright 简介与环境搭建#### 1.1 什么是 PlaywrightPlaywright 是由微软开发的开源 Web 自动化测试框架,支持 Chromium、Firefox、WebKit 三大浏览器引擎。**核心特性:**- 🚀 快速、稳定、功能强大- 🌐 跨浏览器、跨平台- 🔧 自动等待、智能重试- 📱 支持移动端模拟- 🎭 支持无头模式和有头模式#### 1.2 环境搭建```bash# 1. 安装 Python 3.8+# 2. 安装 Playwrightpip install playwright# 3. 安装浏览器playwright install# 4. 安装浏览器依赖(Linux)playwright install-deps# 5. 验证安装python -c "from playwright.sync_api import sync_playwright; print('安装成功!')"```#### 1.3 第一个 Playwright 脚本```pythonfrom playwright.sync_api import sync_playwrightwith sync_playwright() as p: # 启动浏览器 browser = p.chromium.launch(headless=False) # 创建新页面 page = browser.new_page() # 访问网页 page.goto('https://example.com') # 元素定位与操作 page.get_by_role('button', name='More information').click() # 截图 page.screenshot(path='example.png') # 关闭浏览器 browser.close()```---### 2. DOM 树与元素定位基础#### 2.1 什么是 DOM 树**DOM(Document Object Model)** 是浏览器解析 HTML 文档后生成的树形数据结构。```HTML 源码 DOM 树结构─────────────────────────────────────────────────────────<html> Document <head> └── html <title>页面</title> ├── head </head> │ └── title <body> └── body <div id="app"> ├── div#app <h1>标题</h1> │ ├── h1 <p class="text">内容</p> │ └── p.text </div> └── ... </body></html>```#### 2.2 HTML、CSS、DOM 的关系| 概念 | 角色 | 作用 ||------|------|------|| **HTML** | 骨架/结构 | 定义网页有什么内容 || **CSS** | 皮肤/样式 | 定义内容长什么样 || **DOM** | 内存对象 | 浏览器解析后的可操作对象 || **选择器** | 地址 | 用来在 DOM 树中定位节点 |#### 2.3 可用于定位的 DOM 属性| 属性类型 | 属性名 | 示例 | 选择器语法 ||----------|--------|------|-----------|| 标签名 | tagName | div, button | `div`, `button` || ID | id | id="submit" | `#submit` || 类名 | class | class="btn" | `.btn` || 任意属性 | data-*, name, type | data-testid="login" | `[data-testid="login"]` || href/src | 链接地址 | href="/home" | `[href="/home"]` || 文本内容 | textContent | 按钮文本 | `:has-text("按钮")` || ARIA 属性 | role, aria-label | role="button" | `[role="button"]` |---### 3. Playwright 定位 API 总览#### 3.1 八大定位 API```┌─────────────────────────────────────────────────────────────────┐│ Playwright 定位 API │├─────────────────────────────────────────────────────────────────┤│ 1. locator() - 通用选择器(CSS/XPath) ││ 2. get_by_role() - ARIA 角色定位 ││ 3. get_by_text() - 文本内容定位 ││ 4. get_by_label() - Label 关联定位 ││ 5. get_by_placeholder() - 占位符定位 ││ 6. get_by_alt_text() - 图片 Alt 文本定位 ││ 7. get_by_title() - Title 属性定位 ││ 8. get_by_test_id() - 测试 ID 定位 ││ 9. frame_locator() - Iframe 内元素定位 │└─────────────────────────────────────────────────────────────────┘```#### 3.2 API 选择优先级```推荐优先级(从高到低):第 1 梯队(最推荐)★★★★★┌─────────────────────────────────────────────────────────────┐│ get_by_test_id() - 开发配合添加 data-testid 属性 ││ get_by_label() - 表单元素首选,语义化最好 ││ get_by_role() - 语义化定位,无障碍友好 │└─────────────────────────────────────────────────────────────┘第 2 梯队(推荐)★★★★┌─────────────────────────────────────────────────────────────┐│ get_by_placeholder() - 输入框专用,简单直接 ││ locator(#id) - 唯一 ID 定位,稳定可靠 │└─────────────────────────────────────────────────────────────┘第 3 梯队(可用)★★★┌─────────────────────────────────────────────────────────────┐│ get_by_alt_text() - 图片元素专用 ││ get_by_title() - 有 title 属性的元素 ││ get_by_text() - 文本稳定时可用 │└─────────────────────────────────────────────────────────────┘第 4 梯队(备选)★★┌─────────────────────────────────────────────────────────────┐│ locator(.class) - 类名稳定时可用 ││ locator([attr]) - 属性定位 │└─────────────────────────────────────────────────────────────┘```#### 3.3 从 HTML 中提取定位参数```python# ===== 示例 HTML ====="""<input id="username" ← 提取:'#username' 或 get_by_label() class="form-input required" ← 提取:'.form-input' name="username" ← 提取:'[name="username"]' type="text" ← 提取:'input[type="text"]' placeholder="请输入用户名" ← 提取:get_by_placeholder('请输入用户名') aria-label="用户名" ← 提取:get_by_role('textbox', name='用户名') data-testid="username-input" ← 提取:get_by_test_id('username-input')><label for="username">用户名</label> ← 提取:get_by_label('用户名')"""```---## 第二部分:CSS 选择器篇### 4. CSS 选择器基础语法#### 4.1 标签选择器```python# 语法:tagname# 匹配所有指定标签的元素page.locator('div') # 所有 div 元素page.locator('button') # 所有 button 元素page.locator('input') # 所有 input 元素page.locator('a') # 所有 a 链接page.locator('span') # 所有 span 元素page.locator('table') # 所有 table 元素page.locator('li') # 所有 li 元素```#### 4.2 ID 选择器```python# 语法:#id_value# 匹配 id 属性等于指定值的元素(ID 应该唯一)page.locator('#submit-btn') # id="submit-btn"page.locator('#username') # id="username"page.locator('button#submit-btn') # button 标签且 id="submit-btn"```#### 4.3 类选择器```python# 语法:.class_value# 匹配 class 属性包含指定值的元素page.locator('.btn') # 所有 class 包含 btn 的元素page.locator('.btn.primary') # 同时包含 btn 和 primary 类page.locator('button.btn') # button 标签且有 btn 类page.locator('div.container') # div 标签且有 container 类```#### 4.4 属性选择器```python# 语法:[attribute]# 匹配具有指定属性的元素page.locator('[type]') # 有 type 属性的元素page.locator('[type="text"]') # type 属性值为 textpage.locator('[name="username"]') # name 属性值为 usernamepage.locator('[data-testid="login"]') # data-testid 属性值为 loginpage.locator('[href="/home"]') # href 属性值为 /homepage.locator('[role="button"]') # role 属性值为 button```---### 5. CSS 属性选择器详解#### 5.1 精确匹配```python# 语法:[attribute=value]page.locator('[type="text"]') # type="text"page.locator('[type="password"]') # type="password"page.locator('[name="username"]') # name="username"page.locator('[value="提交"]') # value="提交"page.locator('[href="/home"]') # href="/home"page.locator('[data-status="active"]') # data-status="active"```#### 5.2 模糊匹配```python# ===== 包含匹配 [*] =====page.locator('[href*="example"]') # href 包含 "example"page.locator('[class*="btn"]') # class 包含 "btn"page.locator('[name*="user"]') # name 包含 "user"# ===== 开头匹配 [^] =====page.locator('[href^="https"]') # href 以 "https" 开头page.locator('[href^="/"]') # href 以 "/" 开头page.locator('[name^="user"]') # name 以 "user" 开头# ===== 结尾匹配 [$] =====page.locator('[href$=".pdf"]') # href 以 ".pdf" 结尾page.locator('[class$="-primary"]') # class 以 "-primary" 结尾page.locator('[id$="-btn"]') # id 以 "-btn" 结尾```#### 5.3 多属性组合```python# 语法:[attr1=val1][attr2=val2]page.locator('[type="text"][name="username"]') # type=text 且 name=usernamepage.locator('[type="submit"][class="btn"]') # type=submit 且 class=btnpage.locator('[type="text"][required]') # type=text 且有 required```---### 6. CSS 关系选择器与伪类#### 6.1 关系选择器```python# ===== 后代选择器(空格) =====page.locator('.container .btn') # container 内的所有.btn# ===== 子元素选择器(>) =====page.locator('.menu > .item') # menu 的直接子元素 item# ===== 相邻兄弟选择器(+) =====page.locator('h2 + p') # 紧跟 h2 的 p 元素# ===== 通用兄弟选择器(~) =====page.locator('h2 ~ p') # h2 后的所有 p 兄弟```#### 6.2 状态伪类```pythonpage.locator('button:hover') # 鼠标悬停的按钮page.locator('button:focus') # 获得焦点的按钮page.locator('input:disabled') # 禁用的输入框page.locator('input:enabled') # 启用的输入框page.locator('input:checked') # 被选中的复选框page.locator('input:required') # 必填字段page.locator('input:valid') # 验证通过的字段page.locator('input:invalid') # 验证失败的字段的```#### 6.3 结构伪类```pythonpage.locator('li:first-child') # 第一个 lipage.locator('li:last-child') # 最后一个 lipage.locator('li:nth-child(2)') # 第 2 个 lipage.locator('li:nth-child(odd)') # 奇数位置的 lipage.locator('li:nth-child(even)') # 偶数位置的 lipage.locator('li:not(:first-child)') # 非第一个 lipage.locator('li:not(.active)') # 不包含 active 类的 lipage.locator('li:empty') # 空的 li 元素```#### 6.4 高级伪类```python# :is() - 多选一page.locator(':is(h1, h2, h3)') # h1、h2 或 h3# :has() - 包含指定子元素page.locator('.card:has(img)') # 包含 img 的.cardpage.locator('div:has(> .btn)') # 包含直接子元素.btn 的 div# :not() - 否定page.locator('.btn:not(.disabled)') # 不包含.disabled 类的.btnpage.locator('button:not([disabled])') # 不禁用的按钮# :has-text() - 包含文本(Playwright 扩展)page.locator(':has-text("提交")') # 包含"提交"文本的元素page.locator('button:has-text("取消")') # 包含"取消"文本的 button```---## 第三部分:XPath 篇### 7. XPath 定位法完全指南#### 7.1 XPath 基础语法```python# 在 Playwright 中使用 XPath 需要加 'xpath=' 前缀# ===== 基础 XPath =====page.locator('xpath=//button') # 所有 button 元素page.locator('xpath=//*[@id="submit"]') # id="submit" 的元素page.locator('xpath=//*[@class="btn"]') # class="btn" 的元素# ===== 属性匹配 =====page.locator('xpath=//input[@type="text"]') # type="text"的 inputpage.locator('xpath=//input[@name="username"]') # name="username"的 inputpage.locator('xpath=//button[@type="submit"]') # type="submit"的 button# ===== 文本匹配 =====page.locator('xpath=//button[text()="提交"]') # 文本精确匹配page.locator('xpath=//button[contains(text(), "提")]') # 文本包含匹配```#### 7.2 属性模糊匹配```python# ===== contains() - 包含匹配 =====page.locator('xpath=//div[contains(@class, "btn")]') # class 包含 btnpage.locator('xpath=//a[contains(@href, "home")]') # href 包含 homepage.locator('xpath=//input[contains(@name, "user")]') # name 包含 user# ===== starts-with() - 开头匹配 =====page.locator('xpath=//a[starts-with(@href, "/")]') # href 以/开头page.locator('xpath=//input[starts-with(@name, "user")]') # name 以 user 开头```#### 7.3 位置索引```pythonpage.locator('xpath=//li[1]') # 第 1 个 lipage.locator('xpath=//li[2]') # 第 2 个 lipage.locator('xpath=//li[last()]') # 最后 1 个 lipage.locator('xpath=//li[position()=3]') # 第 3 个 li```---### 8. XPath 轴定位详解#### 8.1 parent 轴(父元素)```pythonpage.locator('xpath=//input[@id="username"]/parent::*') # username 的父元素page.locator('xpath=//input[@id="username"]/..') # username 的父元素(简写)page.locator('xpath=//input[@id="username"]/parent::div')# username 的父 div```#### 8.2 child 轴(子元素)```pythonpage.locator('xpath=//div[@class="container"]/child::button') # container 的子 buttonpage.locator('xpath=//div[@class="container"]/child::*') # container 的所有子元素```#### 8.3 ancestor 轴(祖先元素)```pythonpage.locator('xpath=//input[@id="username"]/ancestor::form') # username 的祖先 formpage.locator('xpath=//input[@id="username"]/ancestor::div') # username 的祖先 div```#### 8.4 descendant 轴(后代元素)```pythonpage.locator('xpath=//div[@id="app"]/descendant::button') # app 的后代 buttonpage.locator('xpath=//div[@id="app"]/descendant::*') # app 的所有后代```#### 8.5 following 轴(之后的元素)```pythonpage.locator('xpath=//label[text()="用户名"]/following::input') # label 后的 inputpage.locator('xpath=//label[text()="用户名"]/following::input[1]') # label 后的第 1 个 input```#### 8.6 following-sibling 轴(之后的兄弟)```pythonpage.locator('xpath=//h2/following-sibling::p') # h2 后的所有 p 兄弟page.locator('xpath=//h2/following-sibling::p[1]') # h2 后的第 1 个 p 兄弟```#### 8.7 preceding-sibling 轴(之前的兄弟)```pythonpage.locator('xpath=//input/preceding-sibling::label') # input 前的 labelpage.locator('xpath=//input/preceding-sibling::label[1]') # input 前的第 1 个 label```---## 第四部分:Playwright API 篇### 9. locator() - 通用选择器#### 9.1 API 签名```pythonpage.locator(selector, has=None, has_text=None)```**参数说明:**- `selector` (必需): CSS 选择器或 XPath 表达式- `has` (可选): 必须包含的子元素定位器- `has_text` (可选): 必须包含的文本内容#### 9.2 基础用法```python# CSS 选择器page.locator('#submit-btn')page.locator('.btn-primary')page.locator('button[type="submit"]')# XPath 选择器page.locator('xpath=//button[@type="submit"]')# 组合使用page.locator('form#login button[type="submit"]')```#### 9.3 has 和 has_text 参数```python# has - 必须包含指定子元素card_with_edit = page.locator('.card', has=page.locator('.edit'))# has_text - 必须包含指定文本home_menu = page.locator('.menu-item', has_text='首页')# 组合使用card = page.locator('.card', has=page.locator('.edit'), has_text='卡片')```---### 10. get_by_role() - ARIA 角色定位#### 10.1 API 签名```pythonpage.get_by_role(role, name=None, exact=False)```**参数说明:**- `role` (必需): ARIA 角色名称- `name` (可选): 元素的 Accessible Name- `exact` (可选): 是否精确匹配#### 10.2 常用角色```python# 按钮page.get_by_role('button')page.get_by_role('button', name='提交')# 链接page.get_by_role('link')page.get_by_role('link', name='首页')# 输入框page.get_by_role('textbox')page.get_by_role('textbox', name='用户名')# 复选框page.get_by_role('checkbox')page.get_by_role('checkbox', name='记住我')# 下拉框page.get_by_role('combobox')page.get_by_role('combobox', name='选择城市')# 标题page.get_by_role('heading', level=1) # h1page.get_by_role('heading', level=2) # h2# 其他角色page.get_by_role('dialog') # 对话框page.get_by_role('alert') # 警告框page.get_by_role('tab') # 标签页page.get_by_role('menu') # 菜单page.get_by_role('menuitem') # 菜单项page.get_by_role('listitem') # 列表项page.get_by_role('table') # 表格page.get_by_role('row') # 表格行page.get_by_role('cell') # 表格单元格page.get_by_role('navigation') # 导航page.get_by_role('banner') # 页眉page.get_by_role('main') # 主内容page.get_by_role('form') # 表单page.get_by_role('searchbox') # 搜索框page.get_by_role('img') # 图片```---### 11. get_by_text() - 文本内容定位#### 11.1 API 签名```pythonpage.get_by_text(text, exact=False)```**参数说明:**- `text` (必需): 要匹配的文本内容- `exact` (可选): 是否精确匹配,默认 False#### 11.2 用法示例```python# 模糊匹配(默认)page.get_by_text('提交') # 包含"提交"即可page.get_by_text('登录') # 包含"登录"即可# 精确匹配page.get_by_text('提交表单', exact=True) # 必须完全匹配# 链式调用page.get_by_role('button').filter(has_text='提交')```---### 12. get_by_label() - Label 关联定位#### 12.1 API 签名```pythonpage.get_by_label(label_text, exact=False)```**参数说明:**- `label_text` (必需): Label 元素的文本内容- `exact` (可选): 是否精确匹配#### 12.2 用法示例```python# HTML: <label for="username">用户名</label># <input id="username" type="text">page.get_by_label('用户名') # 模糊匹配page.get_by_label('用户名', exact=True) # 精确匹配# 嵌套 label# HTML: <label>密码<input type="password"></label>page.get_by_label('密码')```---### 13. get_by_placeholder() - 占位符定位#### 13.1 API 签名```pythonpage.get_by_placeholder(placeholder, exact=False)```**参数说明:**- `placeholder` (必需): input 元素的 placeholder 属性值- `exact` (可选): 是否精确匹配#### 13.2 用法示例```python# HTML: <input type="text" placeholder="请输入用户名">page.get_by_placeholder('用户名') # 模糊匹配page.get_by_placeholder('请输入用户名', exact=True) # 精确匹配```---### 14. get_by_alt_text() - 图片 Alt 定位#### 14.1 API 签名```pythonpage.get_by_alt_text(alt, exact=False)```**参数说明:**- `alt` (必需): img 元素的 alt 属性值- `exact` (可选): 是否精确匹配#### 14.2 用法示例```python# HTML: <img src="logo.png" alt="公司 Logo">page.get_by_alt_text('Logo') # 模糊匹配page.get_by_alt_text('公司 Logo', exact=True) # 精确匹配```---### 15. get_by_title() - Title 定位#### 15.1 API 签名```pythonpage.get_by_title(title, exact=False)```**参数说明:**- `title` (必需): 元素的 title 属性值- `exact` (可选): 是否精确匹配#### 15.2 用法示例```python# HTML: <img src="logo.png" title="公司 Logo">page.get_by_title('Logo') # 模糊匹配page.get_by_title('公司 Logo', exact=True) # 精确匹配```---### 16. get_by_test_id() - 测试 ID 定位#### 16.1 API 签名```pythonpage.get_by_test_id(test_id)```**参数说明:**- `test_id` (必需): 元素的 data-testid 属性值#### 16.2 用法示例```python# HTML: <button data-testid="login-btn">登录</button>page.get_by_test_id('login-btn')# 自定义测试 ID 属性名page.selectors.set_test_id_attribute('data-test-id')page.get_by_test_id('my-button') # 匹配 data-test-id="my-button"```---### 17. frame_locator() - Iframe 定位#### 17.1 API 签名```pythonpage.frame_locator(selector)```**参数说明:**- `selector` (必需): 定位 iframe 的选择器#### 17.2 用法示例```python# 基本用法frame = page.frame_locator('iframe[name="content"]')frame.locator('.save').click()# 嵌套 iframeouter_frame = page.frame_locator('iframe.outer')inner_frame = outer_frame.frame_locator('iframe.inner')inner_frame.locator('.deep').click()```---## 第五部分:高级篇### 18. 正则表达式与模糊匹配#### 18.1 为什么需要模糊匹配```python# 动态值问题示例:# id="btn-abc123" # 每次变化# class="_container_oq067_1" # React/Vue 哈希类名# 订单号:SO-202401150001 # 动态生成```#### 18.2 CSS 模糊匹配```python# 包含匹配 [*]page.locator('[id*="btn-"]')page.locator('[class*="container"]')# 开头匹配 [^]page.locator('[id^="btn-"]')page.locator('[class^="_container_"]')# 结尾匹配 [$]page.locator('[class$="-primary"]')page.locator('[id$="-btn"]')```#### 18.3 正则表达式 + filter()```pythonimport re# 匹配动态订单号page.locator('.order-item').filter( has_text=re.compile(r'SO-20240115\d{4}'))# 匹配特定 ID 范围page.locator('.user-card').filter( has_text=re.compile(r'ID:1000\d') # ID:10000-10009)# 匹配日志级别page.locator('.log').filter( has_text=re.compile(r'^(ERROR|WARNING):'))```#### 18.4 常用正则模式```pythonimport rere.compile(r'\d+') # 匹配数字re.compile(r'\d{4}-\d{2}-\d{2}')# 匹配日期:2024-01-15re.compile(r'SO-\d{8}') # 匹配订单号:SO-20240115re.compile(r'[A-Z]{2}\d{4}') # 匹配代码:AB1234re.compile(r'1[3-9]\d{9}') # 匹配手机号re.compile(r'¥[\d,]+\.?\d*') # 匹配价格```---### 19. filter() 过滤方法高级用法#### 19.1 API 签名```pythonlocator.filter(has=None, has_text=None, has_not=None, has_not_text=None)```#### 19.2 基础用法```python# has_text - 包含指定文本page.locator('.item').filter(has_text='产品 A').click()# has - 包含指定子元素page.locator('.item').filter( has=page.locator('.stock:has-text("有货")')).all()# has_not_text - 不包含指定文本page.locator('.item').filter(has_not_text='下架').all()# has_not - 不包含指定子元素page.locator('.item').filter( has_not=page.locator('.out-of-stock')).all()```#### 19.3 链式过滤```python# 多重条件过滤page.locator('.product').filter( has_text='电子产品').filter( has_text=re.compile(r'销量:[1-9]')).all()```---### 20. 动态值定位实战#### 20.1 动态 ID```python# HTML: <button id="btn-abc123">提交</button># CSS 开头匹配page.locator('[id^="btn-"]')# CSS 包含匹配page.locator('[id*="btn"]')# XPath containspage.locator('xpath=//button[contains(@id, "btn")]')```#### 20.2 React/Vue 动态类名```python# HTML: <div class="_container_oq067_1">内容</div>page.locator('[class*="_container"]')page.locator('[class^="_container_"]')```#### 20.3 动态文本```python# HTML: <span>欢迎,张三 (2024-01-15)</span># 模糊匹配page.get_by_text('欢迎')# 正则 + filterimport repage.locator('span').filter( has_text=re.compile(r'欢迎,.+'))```#### 20.4 动态属性```python# HTML: <input name="email_abc123">page.locator('[name^="email_"]')page.locator('[name*="email"]')```---### 21. JavaScript 表达式定位#### 21.1 evaluate 直接查询```python# querySelectorelement = page.evaluate('''() => { return document.querySelector('.btn-primary')}''')# querySelectorAllelements = page.evaluate('''() => { return Array.from(document.querySelectorAll('.btn'))}''')# XPath 查询element = page.evaluate('''() => { const xpath = '//button[@type="submit"]'; return document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;}''')```#### 21.2 $eval 和 $$eval```python# $eval - 查询单个元素并执行函数text = page.eval_on_selector('.btn', 'el => el.textContent')# $$eval - 查询所有元素并执行函数texts = page.eval_on_selector_all('.btn', 'els => els.map(el => el.textContent)')# 复杂操作count = page.eval_on_selector_all('.btn', 'els => els.filter(el => el.disabled).length')```---## 第六部分:实战篇### 22. 登录页面元素定位```python# ===== 典型登录页面 HTML ====="""<form id="login-form"> <label for="username">用户名</label> <input id="username" name="username" placeholder="请输入用户名"> <label for="password">密码</label> <input id="password" name="password" type="password"> <button id="submit-btn" type="submit">登录</button></form>"""# 用户名输入框page.get_by_test_id('username-input')page.get_by_label('用户名')page.locator('#username')page.locator('[name="username"]')page.get_by_placeholder('请输入用户名')# 密码输入框page.get_by_test_id('password-input')page.get_by_label('密码')page.locator('#password')page.locator('input[type="password"]')# 登录按钮page.get_by_test_id('login-submit')page.locator('#submit-btn')page.get_by_role('button', name='登录')page.locator('button[type="submit"]')```---### 23. 数据表格元素定位```python# ===== 典型数据表格 ====="""<table class="data-table" data-testid="user-table"> <tr data-row-id="1"> <td class="username">zhangsan</td> <td class="action"> <button data-action="edit">编辑</button> <button data-action="delete">删除</button> </td> </tr></table>"""# 表格page.get_by_test_id('user-table')# 数据行page.locator('[data-row-id="1"]')page.locator('tbody tr:first-child')# 按单元格内容定位行page.locator('tr:has-text("zhangsan")')# 操作按钮page.locator('[data-row-id="1"] .btn-edit')page.locator('tr:has-text("zhangsan") .btn-edit')```---### 24. 导航菜单元素定位```python# ===== 典型导航菜单 ====="""<nav class="main-nav" role="navigation"> <a href="/home" class="menu-link active">首页</a> <a href="/agent" class="menu-link">智能体</a> <a href="/qa" class="menu-link">问答</a></nav>"""# 导航容器page.locator('nav[role="navigation"]')page.locator('.main-nav')page.get_by_role('navigation')# 菜单项page.locator('.menu-link')page.locator('a[href="/home"]')page.get_by_text('首页')page.locator('.menu-link.active') # 当前激活项```---### 25. 模态框/对话框元素定位```python# ===== 典型模态框 ====="""<div class="modal" role="dialog"> <div class="modal-header"> <h3>确认删除</h3> <button class="btn-close" aria-label="关闭">×</button> </div> <div class="modal-body"> <p>确定要删除吗?</p> </div> <div class="modal-footer"> <button class="btn-cancel">取消</button> <button class="btn-danger" data-testid="confirm-delete">确认删除</button> </div></div>"""# 模态框容器page.get_by_role('dialog')page.locator('.modal')# 关闭按钮page.locator('.btn-close')page.locator('[aria-label="关闭"]')# 操作按钮page.locator('.btn-cancel')page.get_by_test_id('confirm-delete')```---### 26. 表单验证错误定位```python# ===== 带验证的表单 ====="""<div class="form-group has-error"> <input class="form-input error" aria-invalid="true"> <span class="error-message" role="alert">用户名已存在</span></div>"""# 错误状态容器page.locator('.has-error')# 错误输入框page.locator('input.error')page.locator('[aria-invalid="true"]')# 错误消息page.locator('.error-message')page.locator('[role="alert"]')```---### 27. 动态加载内容处理```pythonfrom playwright.sync_api import expect# 等待加载完成page.locator('.loading-overlay').wait_for(state='hidden', timeout=10000)# 等待数据行出现page.locator('tbody tr').first.wait_for(state='visible', timeout=10000)# 使用 expect 断言expect(page.locator('.loading-overlay')).to_be_hidden(timeout=10000)expect(page.locator('tbody tr')).to_have_count(10, timeout=10000)# 等待网络请求完成with page.expect_response('**/api/data'): page.locator('.refresh-btn').click()```---## 第七部分:技巧篇### 28. API 选择决策树``` 开始定位元素 │ ▼ ┌────────────────────────┐ │ 有 data-testid 属性? │ └───────────┬────────────┘ │ Yes ▼ ┌────────────────────────┐ │ get_by_test_id() │ └────────────────────────┘ │ No ▼ ┌────────────────────────┐ │ 是表单元素? │ └───────────┬────────────┘ │ Yes ┌─────────────┼─────────────┐ │ │ │ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ 有 label? │ │ 有 placeholder?│ │ 有 name? │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ Yes │ Yes │ Yes ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │get_by_label()│ │get_by_place- │ │ locator() │ │ │ │holder() │ │ [name="xxx"] │ └──────────────┘ └──────────────┘ └──────────────┘ │ No ▼ ┌────────────────────────┐ │ 有唯一 ID? │ └───────────┬────────────┘ │ Yes ▼ ┌────────────────────────┐ │ locator('#id') │ └────────────────────────┘ │ No ▼ ┌────────────────────────┐ │ 是按钮/链接/交互元素? │ └───────────┬────────────┘ │ Yes ▼ ┌────────────────────────┐ │ get_by_role() │ └────────────────────────┘ │ No ▼ ┌────────────────────────┐ │ locator(CSS/XPath) │ └────────────────────────┘```---### 29. 元素定位调试技巧#### 29.1 高亮元素```pythonlocator = page.locator('.btn-primary')locator.highlight() # 在浏览器中高亮显示```#### 29.2 获取元素信息```pythonlocator = page.locator('.btn')# 获取元素数量count = locator.count()print(f"找到 {count} 个元素")# 获取元素属性value = locator.get_attribute('value')print(f"value 属性:{value}")# 获取元素文本text = locator.text_content()print(f"文本:{text}")```#### 29.3 元素截图```pythonlocator = page.locator('.modal')locator.screenshot(path='modal.png')```#### 29.4 使用 Codegen```bash# 录制操作并生成代码npx playwright codegen https://example.com```#### 29.5 Trace Viewer```bash# 查看 tracenpx playwright show-trace trace.zip```---### 30. 常见问题与解决方案#### 30.1 元素在 iframe 中```python# 先切换到 iframeframe = page.frame_locator('iframe#content')frame.locator('.btn').click()```#### 30.2 元素在 Shadow DOM 中```python# 使用穿透语法page.locator('host-element >> .shadow-content').click()```#### 30.3 元素被遮挡```python# 等待遮挡元素消失page.locator('.loading-mask').wait_for(state='hidden')# 滚动到元素位置page.locator('.btn').scroll_into_view_if_needed()# 强制点击(慎用)page.locator('.btn').click(force=True)```#### 30.4 元素未加载```python# 添加显式等待page.locator('.content').wait_for(state='visible', timeout=10000)# 使用 expect 等待expect(page.locator('.result')).to_contain_text('完成', timeout=10000)```#### 30.5 选择器不唯一```python# 缩小范围page.locator('.modal').locator('.btn')# 添加更多条件page.locator('.btn.btn-primary')# 使用索引page.locator('.btn').nth(0)page.locator('.btn').first```---### 31. 最佳实践与性能优化#### 31.1 最佳实践```python# 1. 优先使用语义化 APIpage.get_by_test_id('submit-btn') # 最佳page.get_by_label('用户名') # 优秀page.get_by_role('button', name='提交') # 优秀# 2. 避免绝对路径# ❌ 不推荐page.locator('xpath=/html/body/div[2]/button')# ✓ 推荐page.locator('button:has-text("提交")')# 3. 使用模糊匹配处理动态值# ❌ 会失败page.locator('#btn-abc123')# ✓ 推荐page.locator('[id^="btn-"]')# 4. 添加显式等待page.locator('.dynamic-content').wait_for(state='visible', timeout=10000)# 5. 缓存定位器submit_btn = page.get_by_test_id('submit-btn')submit_btn.click()submit_btn.hover()```#### 31.2 性能优化```python# 1. 缩小搜索范围# ❌ 慢page.locator('[class*="btn"]')# ✓ 快page.locator('#form-container [class*="btn"]')# 2. 使用 CSS 而非 XPath# ✓ CSS(快)page.locator('[class*="btn"]')# XPath(稍慢)page.locator('xpath=//*[contains(@class, "btn")]')# 3. 避免过度使用正则# ✓ 简单快速page.locator('[id^="btn-"]')# 稍慢(但更灵活)page.locator('button').filter(has_text=re.compile(r'^btn-\d+'))```---## 附录### 附录 A: CSS 选择器速查表| 选择器 | 示例 | 说明 ||--------|------|------|| `*` | `*` | 所有元素 || `E` | `div` | 所有 div 元素 || `#id` | `#submit` | id="submit" || `.class` | `.btn` | class 包含 btn || `[attr]` | `[disabled]` | 有 disabled 属性 || `[attr=val]` | `[type="text"]` | type="text" || `[attr*=val]` | `[href*="http"]` | href 包含 http || `[attr^=val]` | `[href^="https"]` | href 以 https 开头 || `[attr$=val]` | `[href$=".pdf"]` | href 以.pdf 结尾 || `E F` | `div p` | div 内的 p(后代) || `E > F` | `div > p` | div 的直接子 p || `E + F` | `h2 + p` | 紧跟 h2 的 p || `E ~ F` | `h2 ~ p` | h2 后的所有 p || `:first-child` | `li:first-child` | 第一个 li || `:last-child` | `li:last-child` | 最后一个 li || `:nth-child(n)` | `li:nth-child(2)` | 第 2 个 li || `:not(.x)` | `button:not(.disabled)` | 非 disabled 的按钮 || `:has(selector)` | `div:has(> .btn)` | 包含.btn 子元素的 div |### 附录 B: XPath 速查表| 表达式 | 说明 ||--------|------|| `//tag` | 选择任意位置的 tag 元素 || `@attr` | 选择有 attr 属性的元素 || `@attr=val` | 选择 attr 属性值为 val 的元素 || `contains(@attr,val)` | 选择 attr 包含 val 的元素 || `starts-with(@attr,val)` | 选择 attr 以 val 开头的元素 || `text()=val` | 选择文本等于 val 的元素 || `contains(text(),val)` | 选择文本包含 val 的元素 || `[n]` | 选择第 n 个元素 || `[last()]` | 选择最后一个元素 || `parent::*` | 选择父元素 || `child::tag` | 选择子元素 || `ancestor::tag` | 选择祖先元素 || `descendant::tag` | 选择后代元素 || `following::tag` | 选择之后的元素 || `following-sibling::tag` | 选择之后的兄弟元素 |### 附录 C: Playwright API 速查表| 方法 | 参数 | 示例 ||------|------|------|| `locator()` | selector, has, has_text | `page.locator('.btn')` || `get_by_role()` | role, name, exact | `page.get_by_role('button')` || `get_by_text()` | text, exact | `page.get_by_text('提交')` || `get_by_label()` | label_text, exact | `page.get_by_label('用户名')` || `get_by_placeholder()` | placeholder, exact | `page.get_by_placeholder('请输入')` || `get_by_alt_text()` | alt, exact | `page.get_by_alt_text('logo')` || `get_by_title()` | title, exact | `page.get_by_title('提示')` || `get_by_test_id()` | test_id | `page.get_by_test_id('login')` || `frame_locator()` | selector | `page.frame_locator('iframe')` |### 附录 D: 正则表达式速查表| 模式 | 含义 | 示例 ||------|------|------|| `\d+` | 一个或多个数字 | `re.compile(r'\d+')` || `\d{4}` | 恰好 4 位数字 | `re.compile(r'\d{4}')` || `.*` | 任意字符任意次 | `re.compile(r'.*')` || `^abc` | 以 abc 开头 | `re.compile(r'^abc')` || `xyz$` | 以 xyz 结尾 | `re.compile(r'xyz$')` || `[abc]` | 字符集 | `re.compile(r'[a-z]+')` || `(a\|b)` | 或 | `re.compile(r'(ERROR\|WARNING)')` |---## 文档版本历史| 版本 | 日期 | 更新内容 ||------|------|----------|| 1.0 | 2024-03-26 | 初始版本,整合所有定位方法文档 |---**文档结束**