
经常有朋友跑来问我Selenium 自动化测试入门先学什么我的答案一直很固定先把元素定位啃下来。别看框架 API 翻来覆去就那么几个真正决定一个用例能不能稳定跑下去的分水岭几乎都在定位这一环。按钮点不到、输入框找不到、脚本换个环境就崩十有八九是定位表达式扛不住变化。这篇东西我拖了很久今天干脆把自己这几年跟 Selenium 元素定位缠斗的经验整理一遍从八大基础定位方式到动态页面、iframe、横向滑动可见性这些让人头大的场景都往深里写。内容尽量保持“新手拿来就能做”的状态但也不想牺牲底层逻辑适合正在学 Selenium 或已经在写自动化用例、却被各种找不到元素整得没脾气的人。1. 元素定位为什么是自动化路上的第一道坎1.1 定位的本质是建立“操作目标”很多人一开始学 Selenium满脑子都是find_element这个方法的参数写法但很少去想它到底在做什么。我用一个生活里的例子解释你去食堂打饭师傅问你“几号窗口”你说“2号窗口”师傅才知道把饭递给你。Web 页面也一样你要点按钮、填输入框、拿标题文本必须先告诉 Selenium “哪个东西”。元素定位就是给浏览器窗口里的每个可操作对象发一个唯一门牌号。这个门牌号的底层是 DOMDocument Object Model。你可以把页面想象成一棵倒着长的树最顶部是html节点往下是head和body再往下是div、ul、li、input、button。每个标签都是树上的一片叶子Selenium 的定位就是在这棵树上找一个或多个符合条件的节点。理解这层关系特别重要因为后面所有 XPath 层级、CSS 选择器、父子兄弟关系本质上都是在描述“这棵树上的某条路径”。刚开始学的时候我犯过一个傻以为定位方式越多越好于是把每种定位的语法都背得很熟。后来做真实项目才发现定位最难的不是记 API而是判断一个页面里哪些属性是稳定的、哪些是会变的。比如有些按钮的id是后端生成的每次刷新都不一样有些class是前端框架在构建时自动加上的上线后一压缩又变了。定位的功夫一半在代码另一半在审视 DOM。1.2 踩坑率最高的不是语法而是“对象漂移”自动化测试界有个很形象的词叫“对象漂移”。意思是你昨天写好的定位表达式今天再跑就找不到了。页面没崩代码没动但按钮的文案从“立即登录”改成了“登录”或者某个class从btn-login被合并成了btn-primary。这种问题一点都不“基础”恰恰是实际维护中最痛的。我自己第一次被对象漂移坑得很惨。当时用了一个很长的绝对 XPath 定位用户头像类似/html/body/div[3]/div[div[2]/header/div[1]/img。项目组某次重构前面多包了一层div我的脚本全线挂掉排查了一上午才意识到是页面结构变了。从那以后我给自己立了一条规矩绝对路径能让脚本跑通但不代表它能扛住页面迭代。定位表达式应该尽量选择那些不太随界面调整而变化的特征比如语义化的id、固定的>from selenium import webdriver from selenium.webdriver.common.by import By driver webdriver.Chrome() # 通过 id 定位最推荐前提是 id 稳定 driver.find_element(By.ID, username) # 通过 name 属性定位一般用于表单控件 driver.find_element(By.NAME, email) # 通过 class 定位注意 class 可能包含多个值 driver.find_element(By.CLASS_NAME, form-control) # 通过标签名定位一般只适合某个类型只有一个目标时 driver.find_element(By.TAG_NAME, button) # 通过超链接完整文本定位仅可用于 a driver.find_element(By.LINK_TEXT, 立即注册) # 通过超链接部分文本定位适合文案偶尔小变动 driver.find_element(By.PARTIAL_LINK_TEXT, 注册)id之所以排在前面是因为 HTML 规范里一个页面中的id必须是唯一的这相当于给 Selenium 一个确定性。但现实项目里前端很难保证。name多用于表单比如注册页的 email、password比class语义强一点。CLASS_NAME是最容易出问题的因为一个元素可能有多个 class比如classbtn btn-primary btn-lg你只写btn-primary能匹配但写btn也会匹配到很多其他元素。TAG_NAME则更像“地图炮”除非页面上只有一个按钮否则慎用。还有一点要记住find_element返回找到的第一个元素找不到就抛NoSuchElementException。如果你想知道页面上有多少个匹配项或者需要遍历一组元素应该用find_elements带 s它返回列表一个都匹配不到时返回空列表而不是抛异常。这个区别在后面的轮播图实战里会用到。2.2 XPath最万能但最需要克制XPath 是定位方式里表达能力最强的。它可以按文本找、按属性找、按层级关系找、按兄弟节点找甚至可以判断“包含关系”。下面这几个是我项目里出现频率很高的写法# 通过 id 属性定位 driver.find_element(By.XPATH, //input[idusername]) # 通过文本定位按钮 driver.find_element(By.XPATH, //button[contains(text(),登录)]) # 通过属性部分匹配 driver.find_element(By.XPATH, //div[contains(class,login-form)]) # 通过层级关系定位 driver.find_element(By.XPATH, //div[classmodal]//input[nameemail])contains(text(),登录)是我很喜欢用的一个招。因为按钮文案经常是“登录”“立即登录”“登录并同步”这种变化用完全等号很容易挂而contains只要包含关键词就行。但克制的地方在于它可能同时匹配多个按钮比如“登录”和“登录失败”都包含“登录”所以最好再加上父级上下文限制。XPath 有两种常见前缀单斜杠/表示从根节点开始逐级找双斜杠//表示从当前位置往下任意层级找。浏览器开发者工具里直接右键复制出来的 XPath 往往是一种非常长的绝对路径不是不能用但页面结构一调整就断我强烈建议不要直接扔进脚本里。遇到这种情况自己分析一下目标元素附近有没有稳定属性手动写一个相对路径哪怕多跑一步也比复制长路径稳。2.3 CSS Selector效率高且更适合前端习惯如果你有前端基础CSS Selector 一定会让你感觉很亲切。它和写 CSS 样式的选择器语法几乎一样而且 Selenium 在执行时对 CSS 的解析性能通常优于 XPath。常见写法# id 定位用 # driver.find_element(By.CSS_SELECTOR, #username) # class 定位用 . driver.find_element(By.CSS_SELECTOR, .form-control) # 属性定位用中括号 driver.find_element(By.CSS_SELECTOR, input[nameemail]) # 层级定位空格表示任意后代 表示直接子节点 driver.find_element(By.CSS_SELECTOR, div.login-form input) # 伪类比如第一个子元素 driver.find_element(By.CSS_SELECTOR, ul#menu li:first-child)CSS 几乎是靠属性、类名、层级来描述的它的短板也很明显不能用文本内容定位。比如你看到一个按钮写着“提交订单”用 CSS 只能靠 class、id 这些如果这些属性都没有稳定值就得回头用 XPath 的//button[contains(text(),提交订单)]。另外CSS 定位里如果对class写得太精确也会遇到动态 class 的问题。比如classbtn btn-primary active中间的active是后来加进去的页面状态一变就没尽量不依赖它。我的选择习惯是有稳定id就用id没有的话优先 CSS遇到必须按文字定位或者层级特别绕的时候再切 XPath。虽然不能说绝对但这套顺序在大多数前端框架下都比较省事。2.4 定位方式选型对照表为了方便你快速决策我把常用的方式和适用场景整理成一个表定位方式示例优点缺点建议By.ID#username唯一性高速度快动态 id 会失效有稳定 id 时第一优先By.NAMEemail表单控件语义清晰页面里可能重复适合登录、注册表单By.CLASS_NAME.form-control贴近前端写法class 多值易误匹配配合限定父级使用By.TAG_NAMEbutton简单匹配范围大很少单独使用By.LINK_TEXT注册简单直观只适用于 a 标签适合静态导航链接By.PARTIAL_LINK_TEXT注册比全文稳一点可能匹配多个文本会小变动时用By.XPATH//div//span最强大支持文本/层级写不好易晦涩复杂路径首选By.CSS_SELECTOR.menu li简洁、性能好不能按文本定位优先级高于 XPath如果你是团队里第一次给自动化测试建立规范可以在项目里约定能加>driver.find_element(By.XPATH, //button[contains(id, user_)])但这样容易踩到另一个坑页面上可能有多处都包含user_结果find_element永远只点第一个。如果你知道按钮大致在哪个模块里就把范围缩小driver.find_element(By.XPATH, //div[classuser-panel]//button[contains(id, user_)])还有一种很实际的方案去和前端社区协调让开发在关键交互元素上加上>wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, [data-testidsubmit-order-btn])))这样即使前面的class全部重构只要契约还在测试就能稳。如果前端已经上线你没法改代码那就退而求其次找一个相对稳定的父节点配合按钮文字或 class 的唯一组合多写一层父子关系减少误匹配。3.2 iframe 和 Shadow DOM 里的元素定位iframe 是个老生常谈但非常容易困住新手的场景。页面上嵌了 iframe你在外层怎么find_element都找不到因为 iframe 里是另一个文档。比方说一个第三方支付弹窗里面的“确认支付”按钮你直接定位会报NoSuchElementException。正确做法是先切进去# 切成 iframe 的三种方式id、name、定位到的 iframe 元素 driver.switch_to.frame(payFrame) driver.find_element(By.XPATH, //button[contains(text(),确认支付)]).click() # 操作完切回默认内容 driver.switch_to.default_content()要注意的是切进去之后你定位的是 iframe 内部的文档树如果再想看外层元素必须先切回来。嵌套的 iframe 还需要一层层切进去调试时特别容易乱建议把切换 iframe 的步骤尽量放在操作动作附近。Shadow DOM 则更复杂。Selenium 4 还不能直接穿透 Shadow DOM 定位元素通常的做法是用 JavaScript 拿到shadowRoot再继续查询shadow_host driver.find_element(By.CSS_SELECTOR, custom-widget) shadow_root driver.execute_script(return arguments[0].shadowRoot;, shadow_host) inner_element shadow_root.find_element(By.CSS_SELECTOR, .inner-button)这个技术常用在自研组件、浏览器插件或某些复杂前端框架里。遇到这类页面我的建议是先确认业务是否必须自动化。如果只是要点击一个按钮直接准备一段 JS 摸到 shadowRoot 内部执行操作也可以但测试稳定性会差一些尽量走到>label手机号/label input typetext placeholder请输入手机号 /这时候可以先定位 label再通过following-sibling找到后面的 inputphone_input driver.find_element(By.XPATH, //label[text()手机号]/following-sibling::input)反过来如果你想定位某个节点前面的元素可以用preceding-sibling::。比如某个错误提示文字在当前输入框的后面你可以顺着 input 找下一个兄弟节点。这种写法虽然看起来路径多了点但比直接用input[1]这种索引可靠。因为索引会随着页面加一行字段就全乱了而相邻标签关系在需求不变时挺稳定。另外如果一个元素在多层div嵌套里需要回到上级再往下找可以用ancestor::或parent::。举个实际场景你定位到某个按钮文字但是想点它的父级容器里的另一个图标就可以driver.find_element(By.XPATH, //button[contains(text(),删除)]/parent::div/span[classicon-trash])这类写法不适合每一条定位都用但当你手头属性不稳定时它往往能救场。3.4 显式等待定位的“安全胶水”说到定位绝对不能跳过等待。很多定位不到的报错根本不是表达式的问题而是脚本跑得太快页面元素还没渲染出来。一个很典型的错误是刚打开页面就找登录按钮结果拿到的是“加载中”的空白页。你可以用time.sleep(5)硬等但那不是好习惯因为线上环境快慢波动大睡少了不行睡多了浪费用例时间。更专业的是显式等待from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait WebDriverWait(driver, 10) login_btn wait.until(EC.element_to_be_clickable((By.ID, login))) login_btn.click()element_to_be_clickable的意思是“元素存在、可见、可点击”比presence_of_element_located只在 DOM 里存在更严格。对点击类操作我几乎都用前者对输入框可以用visibility_of_element_located如果只是判断元素是否出现再用presence_of_element_located。使用等待的时候还有一个体验心得不要把隐式等待和显式等待混用虽然技术上可以但超时策略叠加后经常让人摸不着头脑。我的做法是在启动浏览器时设一个短一点的隐式等待比如 2 秒其余全部用显式等待精细控制。这样脚本既不会卡死也不至于每次都碰运气。4. 滚动可见性的坑左右滑动页面再点击4.1 为什么元素明明在 DOM 里就是点不到有段时间我经常被同学问“我明明能定位到元素打印出来标签也对但一点就报错这咋整”这种场面的幕后黑手多数是元素不在可视区内。Selenium 的 click 方法为了模拟真实用户会检查元素是否可见、是否在屏幕范围内、有没有被其他元素覆盖。如果你定位到一个在横向滚动条右边的轮播图卡片或者页面需要往下滚很长才能看到的提交按钮Selenium 不会自动帮你滚动。常见的报错有这两个ElementNotInteractableException表示元素不可交互ElementClickInterceptedException表示元素被别的层挡住了。后面这种经常出现在页面顶部有悬浮导航、右下角有客服浮窗时你明明点得到底部按钮但浮窗把坐标挡了。这也是“网页左右滑动”“左右滚动可见”这些词会火起来的原因。处理原则很简单先让目标元素滚到视口里再判断它是否可点击最后再动手点。不要一上来就硬点。4.2 水平滚动与垂直滚动的处理方式先分清滚动发生在哪里。如果是整个页面上下滚动直接操作windowdriver.execute_script(window.scrollTo(0, document.body.scrollHeight);)如果是水平很宽的页面可能需要设置document.documentElement.scrollLeftdriver.execute_script(window.scrollTo(500, 0);)但实际工作中我遇到更多是页面内部某个容器在横向滚动。比如电商首页的“为你推荐”外层是一个divoverflow-x: auto里面是一排商品卡片这时候全局滚动根本没用。你需要先定位到这个滚动容器再去改它的scrollLeftcarousel driver.find_element(By.CLASS_NAME, product-carousel) driver.execute_script(arguments[0].scrollLeft 400;, carousel)如果不知道目标元素在哪可以先滚动容器再检查目标元素是否在视口内element driver.find_element(By.CSS_SELECTOR, .product-item-5) carousel driver.find_element(By.CLASS_NAME, product-carousel) for _ in range(10): is_visible driver.execute_script( var r arguments[0].getBoundingClientRect(); return r.left 0 r.right window.innerWidth;, element ) if is_visible: break driver.execute_script(arguments[0].scrollLeft 300;, carousel)这段代码用getBoundingClientRect()判断元素左边界是否在窗口内。因为轮播图是横向的我们只看左右边界如果是纵向列表就看top 0 bottom window.innerHeight。还有一个更省事的 JS 方法scrollIntoView。如果你已经定位到了目标元素不管它在哪个滚动容器里浏览器都会自动把元素带进视口。driver.execute_script(arguments[0].scrollIntoView({block:center, inline:center});, element)block:center处理垂直居中inline:center处理水平居中这个组合对横向列表尤其好用。需要注意scrollIntoView可能会把页面整体滚动位置带偏影响后面的操作布局所以用完以后再稍等一下不要立刻点击。4.3 一个左右滑动轮播图的定位实战写一个典型场景页面上有一个横向商品卡片列表第 5 张卡片大部分隐藏在可视区右侧你需要点击它。第一步先用find_elements把所有卡片都取出来确认目标索引items driver.find_elements(By.CSS_SELECTOR, .product-card) target items[4] # 下标从 0 开始第5张第二步判断它是否在可视区。这里要区分元素虽然存在但如果它只漏出来一半Selenium 的 click 也可能认为无法点击。我一般要求右边界不要超过视口宽度太多最稳妥是让整个元素都进入driver.execute_script(arguments[0].scrollIntoView({inline:center, block:nearest});, target)第三步等它变成可点击状态后再点from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC WebDriverWait(driver, 5).until(EC.element_to_be_clickable(target)).click()第四步做一个封装方便以后复用。每次滚动前后打印目标元素的位置方便排查def scroll_into_view_and_click(driver, element): driver.execute_script(arguments[0].scrollIntoView({inline:center});, element) WebDriverWait(driver, 5).until(EC.element_to_be_clickable(element)).click()如果页面里的轮播图是循环结构元素已经存在于 DOM 中但我们看不到这种封装已经够用。还有一种特殊情况是“懒加载”只有滚动到附近才会真正渲染。这时你不仅要把容器滚过去还得等页面发出请求后再次定位。所以循环滚动时不要只滚一次要有“滚动-定位-判断-再滚动”的闭环。4.4 避免点击被遮挡等待可见 滚动 重试横向滚动问题解决了接着会遇到另一个问题滚动之后元素是可见了但页面顶部固定导航把可点击区域盖住了。Selenium 会报ElementClickInterceptedException。这时候不要慌先看是不是浮动层导致。最简单的方法是用 JS 判断元素位置和被遮挡情况或者干脆先把滚动位置微调一下让目标元素离开遮挡区driver.execute_script(arguments[0].scrollIntoView({block:center});, element)居中显示通常能避开顶部和底部的遮罩。如果还不行我才会考虑最后手段——直接执行 JavaScript 点击driver.execute_script(arguments[0].click();, element)但这条路不能作为首选因为 JS 点击绕过了 Selenium 的交互检查等于告诉用户“我不管你可能需要登录态或确认弹窗反正我强行触发点击”。这在业务上是危险的建议只在确认元素可见、只是因为坐标被改变时才用。另一个常见问题是滚动后页面有动画缓冲你立刻点击时机不对。可以在滚动代码后面加一个显式等待等到元素位置稳定或者用很短的time.sleep(0.3)过渡。虽然我平时不太喜欢 sleep但这种视觉动画造成的“暂时不可点”确实没有特别好的事件可以等待小睡一下是常见做法。5. 常见定位失败问题排查与调试技巧5.1 常见错误信息对照表我把这些年遇到最多的 Selenium 异常整理成一张表你可以把它当成速查手册异常可能原因处理方式NoSuchElementException定位表达式找不到元素检查表达式、是否在 iframe、是否未渲染ElementNotVisibleException元素存在但不可见等待可见、滚动到视口ElementClickInterceptedException元素被其他节点遮挡滚动居中、先关闭遮罩必要时 JS 点击StaleElementReferenceException页面或 DOM 更新旧引用失效重新定位元素避免复用旧对象InvalidSelectorException定位表达式语法错误在浏览器控制台验证表达式TimeoutException显式等待超时查看前一步是否成功或延长等待StaleElementReferenceException是很多进阶玩家也容易踩的。发生在你找到一个元素后页面对这个区域做了更新旧元素“死了”再用它就会报错。解决办法很简单重新执行一次find_element拿到新的引用。这也是我为什么建议不要在循环里保存一个元素引用反复用而是一次操作前都重新定位一次。5.2 调试工具浏览器控制台、截图与等待排查定位问题时我看到很多新手喜欢直接改代码重跑效率低。更聪明的方式是先在浏览器开发者工具里验证定位表达式。右键“检查”在 Console 里执行命令// 验证 XPath document.evaluate(//button[contains(text(),登录)], document).iterateNext() // 验证 CSS Selector document.querySelectorAll(.login-form input)能在浏览器里找到Selenium 里找不到那大概率是等待时机或 iframe 的问题在浏览器里也找不到那就说明表达式本身有问题或者页面没有你想的那个属性。这一步能帮你节省大量盲目重跑的时间。另一个实用技巧是截图。脚本报错时不要只看堆栈要保留现场图driver.save_screenshot(debug.png)如果是在 CI 流程里可以在异常处理里把截图一并打印出来。很多时候元素被遮住了、页面弹了错误框、或者布局被打乱截图一眼就看出来了比自己瞎猜快得多。我还会写一个打印元素信息的函数方便在调试时确认到底定位到了什么def print_element_info(element): print(tag:, element.tag_name) print(id:, element.get_attribute(id)) print(class:, element.get_attribute(class)) print(text:, element.text) print(location:, element.location)配合显式等待一套调试下来基本能把 90% 的定位问题定位清楚。5.3 稳定性的几个防守习惯我踩了足够多的坑之后慢慢形成一套习惯现在基本能保证定位代码的稳定性在一个比较高的水平。第一不用浏览器复制出来的绝对 XPath。那种路径又长又死页面任何一层发生变化都会挂。宁可多写几步把定位范围限制在稳定属性附近。第二少在业务代码里直接写定位表达式。我更喜欢用 Page Object 模式把每个页面的元素定位器统一放在一个类里。比如LoginPage类里定义一个LOGIN_BUTTON (By.ID, login)后续方法统一用self.driver.find_element(*self.LOGIN_BUTTON)。这样页面改版只需要改一个文件不用满项目去翻。第三定位器和前端形成契约。如果团队允许给关键元素加上>