ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Selenium 4相对定位器详解:告别脆弱XPath,用空间关系稳定定位元素

2026/9/16 3:53:15 拓冰建站 浏览量
Selenium 4相对定位器详解:告别脆弱XPath,用空间关系稳定定位元素 1. 为什么相对定位器值得被重视传统定位的痛点复盘1.1 脆弱的绝对路径与一点点改动就崩的噩梦做 Web UI 自动化的朋友大概率都有过这样的经历昨天还跑得绿油油的全量回归今天一早起来就红成一片点开失败日志一看不是功能出了问题而是前端同事把某个div挪了个位置或者给中间加了一层容器。最气人的是你花半天时间把 XPath 修好下个迭代他又改了。这种脆弱性的根源在于我们用了太多从根节点一路数下来的绝对定位。比如// 典型的绝对路径 XPathDOM 稍微一变就废 driver.findElement(By.xpath(/html/body/div[2]/div[3]/form/div[4]/input))这种定位方式把测试和 DOM 的内部实现细节绑死了。前端重构的时候没人会记得你的 XPath 长什么样他们只管自己代码结构清晰于是你的用例就跟着遭殃。那 CSS 选择器会不会好一点好一点但也有限。一旦元素本身没有稳定的id、class、name这些属性或者这些属性被前端框架动态生成比如 Vue、React 渲染出来的随机 class你照样没辙。我在项目里就见过不少这种动态 classdiv classel-form-item__content css-1h8k3l input classel-input__inner placeholder请输入用户名 / /divcss-1h8k3l这种哈希类名构建一次变一次完全不能用。你需要的是按相对关系来描述元素拿到某个已知稳定的锚点元素然后告诉 Selenium——我要找它下面那个输入框我要找它右边那个按钮。这正是 Selenium 4 相对定位器Relative Locators的用武之地。1.2 XPath 轴定位的拐弯抹角其实在相对定位器出现之前XPath 已经提供了类似的相对能力那就是轴Axes定位比如following-sibling、preceding-sibling、ancestor、descendant。理论上你能用轴实现所有相对定位器的功能。但问题在于轴定位的语法可读性真的很差。举个例子要找用户名输入框下面那个校验错误提示用 XPath 写是这样的//input[idusername]/following::div[contains(class,error)][1]这段表达式看起来还行但如果再加一层条件比如在表单区域内、且位于密码输入框左边、且 class 包含 error表达式会迅速膨胀到让人头皮发麻。而且following::这个轴是全文档扫描性能开销不小文档一大就慢。更关键的是XPath 轴表达的是 DOM 树上的逻辑顺序关系而人眼在看页面的时候用的是空间位置关系。你说输入框下面那个红字这明显是几何描述不是树描述。相对定位器恰恰把这两种描述方式统一了——你写代码的思路跟你看页面的直觉是一致的维护成本自然低很多。1.3 相对定位器解决的是什么Selenium 4 把相对定位器作为一个正式特性推出来说白了就是想让自动化测试的定位方式更接近人类直觉。它不要求你知道元素的绝对层级不要求你数.和/的个数只需要你找到一个稳定的锚点元素再按上下左右的空间关系去描述目标。这套 API 最早在 Selenium 4.0 里以 Friendly Locators 的名字出现后来正式定名为 Relative Locators。它支持的浏览器包括 Chrome、Firefox、Edge 和 Safari 的 WebDriver 实现覆盖了绝大多数测试场景。对测试框架来说这是纯 WebDriver 层面的能力不需要额外装插件、不需要改前端代码只要升级到 Selenium 4 就能直接用。2. 五个核心方法的用法与底层判断逻辑2.1 withTagName 加 above / below / toLeftOf / toRightOf / near相对定位器的核心是一组以某个锚点元素为参照的方法。先用findElement拿到锚点然后用相对关系描述目标最后调用findElement真正执行定位。Java 版本的基本写法import org.openqa.selenium.support.locators.RelativeLocator; // 锚点用户名输入框 WebElement usernameInput driver.findElement(By.id(username)); // 定位用户名输入框下方的错误提示 WebElement errorTip driver.findElement( RelativeLocator.with(By.tagName(div)) .below(usernameInput) );Python 版本的写法from selenium.webdriver.support.relative_locator import locate_with # 锚点用户名输入框 username_input driver.find_element(By.ID, username) # 定位用户名输入框下方的错误提示 error_tip driver.find_element( locate_with(By.TAG_NAME, div).below(username_input) )五个核心方法各自对应一种空间关系方法含义典型使用场景above()位于锚点元素上方定位标题、Label、表头单元格below()位于锚点元素下方定位输入框下方的校验提示、说明文字toLeftOf()位于锚点元素左侧定位复选框左侧的文本标签toRightOf()位于锚点元素右侧定位输入框右侧的图标、按钮near()位于锚点元素附近50px 以内定位与锚点相邻但方向不固定的元素with(By.tagName(div))这里的By不只是tagName你可以换成By.id、By.className、By.xpath等任何你熟悉的定位方式。这相当于给要查找的目标先画了一个轮廓约束再叠加空间关系过滤。2.2 near() 的距离阈值到底怎么算near()是这几个方法里最容易让人误解的一个。它表示距离锚点元素 50 像素以内的元素注意这个 50 像素是默认值而且计算的是元素边缘之间的最近距离不是中心点距离。在 Selenium 4.6 之前这个阈值是写死的50不能传参。4.6 开始near()支持传入自定义距离// 找距离锚点 30px 以内的元素 driver.findElement( RelativeLocator.with(By.tagName(button)) .near(anchorElement, 30) );Python 对应写法driver.find_element(locate_with(By.TAG_NAME, button).near(anchor_element, 30))为什么默认 50px 而不是 10px我个人的理解是Web 页面里常见的元素间距、内边距、外边距通常都在 8~24px 之间加上元素自身的高度宽度50px 这个值能覆盖大多数相邻场景。但也正因为这个默认值偏大near()在元素密集的页面上很容易一下子捞出多个候选定位结果不稳定。这种场景我建议要么显式传小一点的阈值要么干脆用above()、toRightOf()这种方向明确的方法。2.3 多条件组合与链式调用相对定位器最强的地方在于它支持多条件链式组合。比如位于锚点元素左下方这种同时涉及 x 和 y 两个方向的关系单靠一个方法表达不了但链式调用就能搞定WebElement target driver.findElement( RelativeLocator.with(By.tagName(span)) .below(anchorElement) .toLeftOf(otherElement) );链式调用的语义是所有条件同时满足——目标元素必须在锚点下方同时又在另一个参照元素左侧。这跟 XPath 里用and连接多个谓词是一个思路但可读性高得多。我实际测试过链式调用的匹配逻辑是基于两个元素中心点坐标的几何关系判断目标元素的中心点 y 坐标大于锚点中心点 y 坐标就认为在下方x 坐标小于参照元素中心点 x 坐标就认为在左侧。两个条件取交集最终返回第一个匹配的元素。这里有一个很实用的技巧如果页面里同时存在多个满足条件的元素而且你想拿到的是第二个或最后一个可以用findElements配合下标ListWebElement tips driver.findElements( RelativeLocator.with(By.tagName(div)) .below(usernameInput) ); // 取第 2 个提示 WebElement secondTip tips.get(1);这在处理动态列表、循环渲染的表单项时特别好用。3. 我在真实项目里最常用的四个场景3.1 表单校验错误提示的定位表单页是相对定位器最典型的应用场景没有之一。以登录页为例用户点击登录但没填用户名前端会在输入框下方渲染一个红色提示文字。这个提示元素通常没有稳定的idclass 可能是动态的但你不需要纠结它长什么样——你知道它一定在用户名输入框下方。WebElement usernameInput driver.findElement(By.id(username)); WebElement usernameError driver.findElement( RelativeLocator.with(By.className(ant-form-item-explain-error)) .below(usernameInput) ); assertTrue(usernameError.getText().contains(请输入用户名));这套写法的好处是不管前端在页面上加了多少别的元素只要错误提示确实渲染在输入框下方它就能被稳定找到。哪怕整个表单从页面顶部移到中部锚点跟着移动相对关系不变定位依然有效。3.2 表格里图标-文案-状态的关联断言后台管理系统里最不缺的就是表格。一个单元格里可能同时有状态图标、状态文字、操作按钮。传统做法是写一长串 XPath用../../..往上回溯好几层再横向找兄弟节点。这种 XPath 看一眼就头大。相对定位器的写法直观得多。比如要断言某个用户所在行的状态列显示的是已启用// 锚点用户名单元格 WebElement userNameCell driver.findElement(By.xpath(//td[contains(text(), 张三)])); // 状态文字通常在用户名的右侧 WebElement statusText driver.findElement( RelativeLocator.with(By.className(status-text)) .toRightOf(userNameCell) ); assertEquals(已启用, statusText.getText());用toRightOf()之前我确实没想到定位能这么干净。它把行内横向关联这一高频操作从 XPath 轴的地狱里解放出来了。配合表格列的固定顺序即便前端调整了列的宽度、加了新的辅助列只要目标列和锚点列的相对左右关系不变测试就不用动。3.3 日历组件与日期格子的筛选日期选择器是另一个让我印象深刻的场景。很多日历组件渲染出来的是一个div网格里面所有日期格子都是同样的 class唯一的区别是文本内容。想选当前显示月份的第 2 周的星期三这种组合条件XPath 极难写而相对定位器配合锚点就轻松了。// 拿到日历面板 WebElement panel driver.findElement(By.className(el-date-picker)); // 定位15号右边的16号 WebElement day16 driver.findElement( RelativeLocator.with(By.className(available)) .toRightOf(driver.findElement(By.xpath(//td[contains(class,available) and text()15]))) .below(panel) );这招对那种相邻日期格子间距一致、class 相似的日历特别有效。当然如果项目里日期组件支持按文本直接选择那还是优先用文本相对定位器更适合无法直接按文本匹配的复杂场景。3.4 消息通知与悬浮气泡消息通知、Tooltip、Popover 这一类临时生成的元素是定位的重灾区。它们通常被挂在body末尾离触发它的按钮十万八千里DOM 层级上和触发按钮毫无关系。用 XPath 找要么靠文本要么靠 class一旦文案变化就崩。相对定位器的思路是Tooltip 一定出现在触发元素的附近上、下、左、右都有可能取决于组件配置而且距离很近。这时候near()就是最合适的选择trigger driver.find_element(By.ID, info-icon) tooltip driver.find_element( locate_with(By.CLASS_NAME, el-tooltip__popper).near(trigger) ) assert 订单编号格式为 12 位数字 in tooltip.text用near()的好处是它不像above()那样严格要求方向——Tooltip 可能从上面冒出来也可能从下面冒出来near()只要求足够近容错性高很多。4. 踩坑实录相对定位器没那么完美4.1 不可见元素直接翻车相对定位器的底层实现依赖元素在页面上的实际渲染位置。它的工作原理大致是通过 JavaScript 获取锚点元素和目标候选元素的 border box 矩形信息getBoundingClientRect()然后比较中心点与边界坐标。一旦目标元素处于以下状态定位就会失败或者定位到错误元素display: none的元素不会参与匹配元素在视口外但未被滚动到可见区域坐标计算可能不准确元素被遮挡或处于visibility: hidden同样拿不到有效矩形通过 CSS 动画渐入的元素如果在动画开始前匹配位置可能是初始值。这意味着你不能指望用相对定位器去定位一个需要先滚动到视口内的元素。解决办法是先scrollIntoView()把页面滚动到位再执行相对定位。这个坑我踩过很多次特别是配合Actions滚动或 JS 滚动时动作执行和定位之间的时序不对就会偶发性失败。4.2 near() 的就近不等于期望的最近near()默认 50px 的阈值在元素密集区域会带来误匹配。举个例子一个表单里用户名输入框和邮箱输入框垂直间距可能只有 20px如果你以用户名输入框为锚点调用near()很可能同时匹配到用户名自己的错误提示和邮箱输入框的错误提示因为它们在几何距离上都小于 50px。我一个真实项目里就出现过这种诡异 bug用例明明断言的是重置密码成功的绿色提示结果定位到了一个隐藏的正在校验手机号的蓝色提示上两个元素都在按钮附近 50px 内。排查了很久才发现是near()范围太宽。解决方案有两个方向。一是收窄阈值把 50 改成 10 或 15。二是优先使用方向明确的方法能用below()就不用near()只有确确实实方向不确定时才用near()。方向上多给一个约束结果稳定性会指数级提升。4.3 iframe、Shadow DOM 与浏览器兼容性相对定位器目前跨 iframe 是失效的。锚点元素在 iframe 里目标元素在 iframe 外这俩的坐标体系根本不在同一个坐标系相对关系无从谈起。遇到这种场景得先switchTo()到对应 iframe再在 iframe 内部做相对定位。Shadow DOM 也类似。如果目标元素或锚点元素处于 Shadow DOM 内部相对定位器无法直接穿透 shadow root 进行几何匹配。你仍然需要先通过常规方式比如shadowRoot.findElement或 Selenium 4 的 shadow 定位支持拿到 shadow 内部的元素再以其为锚点做相对定位。浏览器兼容性方面Chrome、Firefox、Edge 的主流程没有问题Safari 的实现则相对保守个别版本对near()的距离参数支持不完整。如果你维护的测试矩阵里 Safari 权重高我建议在引入相对定位器之前先用一个冒烟用例把所有相对方法在 Safari 上跑一遍确认版本行为符合预期。4.4 性能损耗与定位速度相对定位器不是免费的。above()、below()这类方法底层会先找出页面中所有匹配with()条件的候选元素再逐个比较几何关系。如果with(By.xpath(//div))这种宽泛条件页面上有几百个 div每个都要做一次矩形计算和坐标比较定位耗时会明显增加。我在一个后台报表页面上实测过用RelativeLocator.with(By.tagName(td)).below(anchor)定位一个表格单元格单次定位耗时约 500ms而同样逻辑用 XPath 的following::加上精确条件只有 80ms 左右。当然500ms 对大多数测试场景完全能接受但如果你的用例数量上去了每步操作都慢半秒全量跑下来差距就很明显。优化建议很朴素让with()里的条件尽量精确。with(By.tagName(div))这种条件会在整个页面扫描所有 div而with(By.className(form-item-error))只扫描符合条件的少数元素性能差异立竿见影。相对定位器是让代码更好写的功能不是替代 XPath的银弹该精确的地方还是要精确。5. 相对定位器与 XPath 轴定位怎么选5.1 两者定位思想的核心差异XPath 轴定位描述的是DOM 树上的逻辑顺序相对定位器描述的是页面上的空间几何关系。这个差异决定了它们各自的适用边界。DOM 树的逻辑顺序关系比如父元素下的第 3 个子元素某个元素之前的所有兄弟节点这些用 XPath 轴表达非常自然。而空间关系比如在锚点下方在锚点右侧用相对定位器表达更直观。还有一个实际差异稳定性。XPath 轴依赖 DOM 层级关系前端一旦调整嵌套结构就容易失效相对定位器依赖视觉位置只要元素在页面上的相对位置不变哪怕 DOM 结构大改也能扛住。5.2 一组对照场景的选型判断我根据自己的项目经验把几类常见场景的选型建议整理成了一张表场景推荐方案原因元素的id/name/>public class BasePage { protected WebElement findBelow(WebElement anchor, By target) { return driver.findElement(RelativeLocator.with(target).below(anchor)); } protected WebElement findRightOf(WebElement anchor, By target) { return driver.findElement(RelativeLocator.with(target).toRightOf(anchor)); } protected WebElement findNear(WebElement anchor, By target, int distance) { return driver.findElement(RelativeLocator.with(target).near(anchor, distance)); } }这样封装的好处有三个第一调用方只关心我要锚点下方的哪个元素不用关心 API 细节第二如果未来 Selenium 版本调整了 API 签名比如改了方法名你只需要改基类一个地方第三方便统一加等待和日志——每次定位失败时自动打出一句尝试定位 anchorxxx 下方的 targetxxx这样的调试信息。6.2 调试相对定位器的快速手段相对定位器出问题的时候最怕的就是不知道选错了哪个元素。Chrome DevTools 的 Console 里可以直接跑一小段 JS 来模拟 Selenium 的矩形计算逻辑// 在 Console 中获取元素的位置信息 function rectInfo(el) { const r el.getBoundingClientRect(); return { x: r.x, y: r.y, width: r.width, height: r.height }; } // 手动比较锚点元素和候选元素的空间关系 const anchor document.querySelector(#username); const candidates document.querySelectorAll(.form-error); candidates.forEach(el { const a rectInfo(anchor), b rectInfo(el); console.log(el.textContent, 下方:, b.y a.y a.height, 上方:, b.y b.height a.y, 右侧:, b.x a.x a.width); });这段脚本能帮你快速确认你以为在锚点下方的元素在页面上真实的矩形坐标到底是不是下方。很多时候前端用了绝对定位或者 transform 位移视觉位置和文档流位置不一致导致相对定位器的判断和你的直觉对不上。用getBoundingClientRect()一量所有问题都清楚了。6.3 写在最后的实战建议跑了小半年相对定位器之后我的总体感受是它不会替代你现有的定位方式但能把一批以前很难写、写了很难维护的用例变简单。特别是表单校验提示、表格操作列、消息气泡这三类用相对定位器之后用例的失败率确实降了不少。如果只让我留三条经验给正在评估相对定位器的团队第一锚点元素的稳定性比什么都重要。锚点选得好相对定位器就稳锚点本身是动态的那相对定位器只会放大不稳定性。优先选id、>