ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Selenium文本定位与操作:解决元素定位频繁变动

Selenium文本定位与操作:解决元素定位频繁变动 写完一套 Selenium 脚本最让人抓狂的往往不是业务逻辑而是元素定位说崩就崩。上午还好好的find_element(By.ID, submit-btn)下午前端同学换了个组件库id 就变成了btn-7f3a91脚本当场瘫掉。这种时候我一般会做一件事把定位方式从机器生成的属性换成人眼能读到的文本。说白了Selenium 通过文本定位并实现操作就是让脚本像真人一样看到页面上写着登录提交订单确认删除这几个字就把光标挪过去、点一下、或者往里填内容。它解决的正是属性频繁变动带来的维护成本问题适合已经会用 Selenium 基础 API、但被元素定位折磨过的测试开发和自动化从业者也适合刚入门想把脚本写稳一点的朋友。我见过太多团队在元素定位上反复返工最后不得不专门拉一个人维护 locator 文件。其实文本定位这条路走顺了脚本的存活周期能从一次发版延长到半年不动。当然它也不是银弹后面会详细讲清楚它的代价和边界。下面我把自己这些年在这条路上的做法、踩过的坑和可以直接抄的代码一次性摊开讲。1. 为什么用文本找元素在真实项目里越来越吃香1.1 三种属性定位集体失效的典型场景先说清楚我们为什么要换思路。在真实项目里有几种情况几乎一定会遇到而它们都会让基于属性的定位失效。第一种是现代化的前端构建链路。React、Vue 这类框架配合 CSS Modules 或 styled-components编译出来的 class 名往往是css-1q2w3e这种哈希串。构建工具一跑哈希就变昨天能用的选择器今天必然报错。id 也类似很多脚手架会给组件自动生成前缀加随机后缀的 id人工根本没法提前猜到。第二种是组件库带来的深层嵌套。Element UI、Ant Design 这类库把一个小小的确定按钮包成了div div button span四层结构。你就算用 CSS 选择器一路写下去链条又长又脆中间任何一层加了个包装 div整条选择器就废了。第三种是多环境差异。测试环境的按钮 id 是btn_001预发环境是btn_002生产又是另一套。这种情况下维护三套选择器纯属自找麻烦。而在这三种场景里唯一相对稳定的东西是什么是用户能看见的那几个字。产品经理改文案的概率远低于前端改 class 名的概率。所以文本定位的价值就在这儿它锚定的是产品的语义而不是实现的细节。1.2 文本定位的代价与适用边界话说回来我不建议你把所有定位都改成文本。它有三个明确的代价心里得有数。第一文案是会变的。运营做个 A/B 测试把立即购买改成马上抢购你的脚本就挂了。所以文本定位更适合那些产品形态已经稳定、文案经过评审冻结的模块比如后台管理系统的固定功能按钮、导航菜单、表格操作列。第二文本可能重复。页面上有两个删除按钮——一个是删除单行一个是批量删除。这时候单纯用//button[text()删除]会抛ElementNotInteractableException或者干脆点错。解决办法是用find_elements拿到列表后按索引或者按父节点范围过滤这一点在第四章会展开。第三XPath 文本匹配的执行效率略低于 id 和 CSS 选择器。浏览器对 id 有原生索引而 XPath 需要遍历 DOM 树求值。不过在单个页面上这个差异通常在毫秒级除非你在一个上千行的大表格里循环查找否则感受不到。所以我的实践原则是能用稳定的 data 属性就用属性属性不稳的时候优先文本文本重复的时候用文本 层级范围组合。这三层优先级排下来脚本的健壮性会有明显提升。2. 三种文本定位写法逐个拆解2.1 link_text 和 partial_link_text只对链接生效的专才这是 Selenium 原生提供的两个文本定位方式用法极简from selenium.webdriver.common.by import By # 精确匹配链接的完整可见文本 driver.find_element(By.LINK_TEXT, 忘记密码).click() # 匹配链接文本的一部分 driver.find_element(By.PARTIAL_LINK_TEXT, 忘记).click()它们的优点是写法短、语义清楚不需要拼 XPath。但限制也很硬只对a标签生效。你拿它去找一个span或者button必然报NoSuchElementException不管那个元素的文字写得多明显。另一个坑是匹配范围。LINK_TEXT比对的是链接的textContent去掉首尾空白后的值注意是去掉首尾中间的空格、换行、全角字符都会保留。如果链接长这样a href/reset 忘记span密码/span /a那么它的完整文本是忘记密码中间没有空格LINK_TEXT可以匹配。但如果 HTML 里有换行符被渲染成空白比如忘记 \n 密码那LINK_TEXT就匹配不上了因为中间冒出来一个空格。这种情况下只能用 XPath 的normalize-space下一节会讲。PARTIAL_LINK_TEXT稍微宽容一点只要链接文本里包含你给的子串就命中。但宽容也是双刃剑页面上同时存在查看详情和查看详情并下载你写PARTIAL_LINK_TEXT查看它会返回第一个匹配到的未必是你想要的那个。所以我在实际项目里PARTIAL_LINK_TEXT只在子串区分度足够高的时候才用。2.2 XPath 文本定位contains 与 normalize-space 的组合拳XPath 才是文本定位的主力因为它对所有标签一视同仁。下面这几种写法我在不同场景下都用过# 1. 精确匹配文本必须完全等于给定值含首尾空白 driver.find_element(By.XPATH, //button[text()登录]) # 2. 包含匹配只看子串 driver.find_element(By.XPATH, //button[contains(text(),登录)]) # 3. 归一化后匹配自动去掉首尾空白、合并中间连续空白 driver.find_element(By.XPATH, //button[normalize-space(text())登录]) # 4. 用点号匹配后代文本跨子节点也能命中 driver.find_element(By.XPATH, //button[contains(., 登录)])这里有个特别容易混淆的点值得单独拎出来说text()和.的区别。text()取的是当前节点的直接文本子节点它不会往下钻。如果按钮结构是buttonspan登/spanspan录/span/button那么button的直接文本子节点其实是空的text()返回空字符串text()登录匹配失败。而.表示当前节点的字符串值它会把所有后代的文本拼起来结果就是登录能匹配成功。这个坑我在做国际化项目时踩过——前端为了做逐字动画把按钮文字拆成了多个 span结果所有text()写法的定位全部失效换成.或者contains(string(.), ...)之后才恢复。所以我现在的习惯是能确定文本在单个文本节点里用text()不确定直接用.。反正.的容错性更高代价只是略微慢一点点。再说normalize-space。它做的事是去掉字符串首尾空白并把中间连续的空白字符空格、Tab、换行压缩成一个空格。这个函数对付HTML 源码里换行缩进导致文本里混进空白的情况特别有效。举个真实的例子td classamount 1,280.00 /td这个单元格的textContent前后各有一堆缩进空白。你写//td[text()1,280.00]会失败但//td[normalize-space(text())1,280.00]稳稳命中。所以只要是从带缩进的 HTML 里取文本我基本都会套一层normalize-space。2.3 精确匹配和模糊匹配到底该选哪个这两种方式没有绝对优劣取决于你对文本稳定性的判断。我整理了一张对照表方便直接查匹配方式写法示例命中条件适用场景主要风险精确匹配//a[text()提交]文本完全一致文案固定、唯一的功能入口多一个空格就失效归一化精确//a[normalize-space()提交]去空白后完全一致源码有缩进、换行中间有意的多空格会被压缩子串匹配//a[contains(., 提交)]包含给定子串文本较长、带图标或动态后缀可能命中多个元素前缀匹配//a[starts-with(., 提交)]以给定串开头文本后面带数量或状态前缀区分度不够时误命中归一化子串//a[contains(normalize-space(.), 提交)]归一化后包含长文本加缩进缩排写法略长我自己的取舍逻辑是默认上精确加归一化只有在文本内容会动态变化时才退到子串匹配。因为精确匹配能天然帮你排掉大部分重复元素而子串匹配的误伤率明显更高。曾经有个项目里我偷懒用了contains(., 删除)结果页面上有个已删除的筛选项也被命中脚本点了它之后列表直接变空排查了小半天才定位到问题。另外一个实用技巧如果文本本身写在value属性或者title属性里比如按钮不给文字而是给个图标加 tooltip那就把text()换成title或者aria-labeldriver.find_element(By.XPATH, //button[aria-label关闭])现代组件库基本都会给纯图标按钮加aria-label这算是个隐藏福利值得优先利用。3. 从定位到操作一条完整的实操链路3.1 环境准备与代码骨架先说安装。Selenium 4.6 之后自带 Selenium Manager也就是浏览器驱动的自动下载和版本匹配基本不用再手动装 chromedriver 了。所以现在环境准备就一句pip install selenium如果你用的是更早的版本或者公司网络有代理限制不方便自动拉驱动那就补一个webdriver-managerpip install selenium webdriver-manager启动浏览器的骨架代码我一般这么写from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--window-size1440,900) options.add_experimental_option(excludeSwitches, [enable-logging]) driver webdriver.Chrome(optionsoptions) driver.implicitly_wait(0) # 建议关掉隐式等待统一用显式等待 wait WebDriverWait(driver, 10, poll_frequency0.3)这里有个我坚持了很多年的习惯把隐式等待关掉全部用显式等待。原因在于隐式等待是全局的它和显式等待叠加时会互相干扰导致某些元素实际等了 20 秒才报错排查起来一头雾水。统一用WebDriverWait之后超时时间在每一处调用点都是显式的读代码的人一眼就知道这里最多等多久。3.2 封装一个按文本点击的通用函数零散地写 XPath 早晚会乱我在每个项目里都会先封装两个基础函数一个负责按文本找元素一个负责按文本点击def find_by_text(driver, wait, text, tag*, exactTrue, timeout10): 按可见文本查元素返回单个 WebElement if exact: xpath f//{tag}[normalize-space(.){_quote(text)}] else: xpath f//{tag}[contains(normalize-space(.), {_quote(text)})] return wait.until(EC.presence_of_element_located((By.XPATH, xpath))) def click_by_text(driver, wait, text, tag*, exactTrue, timeout10): 按文本找到元素并点击点击前确保它可点 if exact: xpath f//{tag}[normalize-space(.){_quote(text)}] else: xpath f//{tag}[contains(normalize-space(.), {_quote(text)})] el wait.until(EC.element_to_be_clickable((By.XPATH, xpath))) driver.execute_script(arguments[0].scrollIntoView({block:center});, el) el.click() return el这两个函数里有三处细节值得说明。第一处是 XPath 字符串的引号问题。中文文本里虽然不太会出现单引号但英文文案里可能出现Dont save这种带撇号的内容直接拼进单引号包裹的 XPath 会语法错误。所以我会写一个_quote辅助函数如果文本里包含单引号就改用双引号包裹两边都有就用 XPath 的concat()拼接。这是很多教程不会提但线上脚本一定会碰到的细节。第二处是element_to_be_clickable和presence_of_element_located的区别。前者要求元素既存在于 DOM 中又是可见的、且没有被disabled属性挡住。用后者找到的元素不一定能点尤其在按钮进页面时还处于禁用状态的情况下。第三处是scrollIntoView。页面上有浮动表头或者固定底栏的时候目标元素虽然存在、虽然可点但它可能被遮挡在视口之外直接click()会抛ElementClickInterceptedException。先滚到视口中间再点能规避掉很大一部分这类问题。3.3 把光标定位到文本框并输入内容热搜里有个说法叫将鼠标的光标定位到某个文本框中这个词其实要澄清一下Selenium 并不会真的去移动操作系统的鼠标指针。它做的是通过 WebDriver 协议向浏览器发送输入事件浏览器内部再把焦点交给目标元素。从页面效果上看和用户用鼠标点一下输入框是一个结果但本质不同。所以定位到文本框这件事在 Selenium 里的正确做法是先按文本找到和输入框关联的 label 或外层容器再用相对 XPath 定位到 input。表单场景里这个套路用得最多div classform-item label用户名/label input typetext nameuser / /div# 通过 label 文本找到它后面的 input xpath //label[normalize-space(.)用户名]/following-sibling::input username wait.until(EC.element_to_be_clickable((By.XPATH, xpath))) username.click() # 让浏览器把焦点交给它 username.clear() # 清掉可能的默认值 username.send_keys(tester_01)这里click()那一步的作用就是把光标定位进去。有些输入框带readonly或者被遮罩层覆盖不先点一下send_keys会抛ElementNotInteractableException。另外clear()在 Selenium 4 里的行为是清空value属性值对那种受控组件React 的受控 input有时候清不干净遇到这种情况的替代方案是全选删除from selenium.webdriver.common.keys import Keys username.send_keys(Keys.CONTROL, a) username.send_keys(Keys.DELETE)还有一类场景是输入框没有 label只有placeholder。这时候可以用//input[placeholder请输入手机号]虽然它不是严格意义上的文本定位但逻辑是一样的——锚定人类可读的文字而不是机器生成的属性。3.4 非原生下拉框 divulli 的实操拆解这是我在面试和带新人时被问得最多的一个点也是最容易翻车的场景。原生下拉框是select加optionSelenium 提供了Select类直接搞定from selenium.webdriver.support.ui import Select select Select(driver.find_element(By.ID, city)) select.select_by_visible_text(杭州)但现代前端基本不用原生 select 了。原因很简单——原生 select 的样式没法自由定制。于是绝大多数组件库都用div ul li自己拼一个下拉框。这种结构的特点是选项默认不在 DOM 里或者虽然存在但被display:none隐藏必须点击触发器之后才会渲染出来。这就意味着你不能直接去找 li得先展开。完整流程我通常写成四步# 第一步点击触发器展开下拉框 trigger wait.until(EC.element_to_be_clickable( (By.XPATH, //div[contains(class,select-trigger)]) )) trigger.click() # 第二步等待选项面板可见 wait.until(EC.visibility_of_element_located( (By.XPATH, //ul[contains(class,select-dropdown)]) )) # 第三步按文本找到目标选项 option wait.until(EC.element_to_be_clickable( (By.XPATH, //ul[contains(class,select-dropdown)]//li[normalize-space(.)上海]) )) # 第四步点击选项并确认下拉框收起来了 option.click() wait.until(EC.invisibility_of_element_located( (By.XPATH, //ul[contains(class,select-dropdown)]) ))这四步里每一步都有存在的理由。第一步不点第二步等什么都是白等。第二步用visibility而不是presence是因为面板容器可能一直在 DOM 里只是display:nonepresence会立刻返回然后在第三步点击时失败。第三步把 li 的查找范围限制在ul.select-dropdown之内是为了避免页面上其他位置的同名文字被误命中比如页面底部恰好有个上海的地名标签。第四步的确认动作是可选的但如果脚本接下来要读下拉框上显示的选中值这个等待能保证读到的是新值而不是旧值。还要提一个变体有些下拉框点开之后选项不是 li而是div加自定义属性或者干脆是虚拟滚动列表只有可视区的那几个选项真实存在于 DOM 中。虚拟滚动列表的应对办法是先在搜索框里输入关键字把目标项筛出来再点而不是一路滚动找。4. 动态页面下的进阶处理思路4.1 文本被拆散或拼接时的兜底写法前面提过.和text()的区别这里再往深一层。有一类页面文本不是被拆成多个 span而是同一个元素里混着图标文字和状态后截比如spani classicon/i待审核/span。normalize-space(.)得到的结果就是待审核因为图标元素本身没有文本。这种情况没问题。但如果结构变成spani/i待审核3 天/span你想匹配的语义是待审核后面那个天数会随状态变。这时候精确匹配就废了得退到containsxpath //span[contains(normalize-space(.), 待审核)]再狠一点的场景文本里混了零宽字符或者不换行空格nbsp;。nbsp;在 HTML 里表现为\xa0它不属于普通空格normalize-space会把它当成空白处理吗严格来说XPath 的空白定义包含空格、Tab、换行、回车\xa0在部分浏览器的实现里也算但并不保证一致。我遇到过一次很诡异的匹配失败最后是把目标元素的textContent打印出来用repr()一照才发现藏着\xa0。解决办法是用translate()把它先替换掉xpath (//span[normalize-space(translate(., \xa0, ))提交订单])这个写法看着啰嗦但在处理老系统的时候是真管用。4.2 文案会变的时候怎么让定位活下来多语言站点或者文案经常调整的模块单条文本定位就是个定时炸弹。我的做法是维护一个文本候选池按优先级依次尝试def click_by_any_text(driver, wait, texts, tagbutton, timeout8): for t in texts: try: xpath f//{tag}[normalize-space(.){_quote(t)}] el WebDriverWait(driver, timeout).until( EC.element_to_be_clickable((By.XPATH, xpath)) ) el.click() return t except Exception: continue raise AssertionError(f候选项全部未命中: {texts}) # 调用新老文案都兜住 click_by_any_text(driver, wait, [立即购买, 马上抢购, 去下单])注意这里每次尝试都重新创建了WebDriverWait而不是复用同一个。原因是复用的时候总超时是共享的第一个候选就吃掉全部时间后面几个没机会试。分开创建之后每个候选有独立的 8 秒窗口整体最长等待时间会变成候选数乘以超时时间所以候选项别放太多三到五个足够。另外一个值得提的思路是双通道定位主通道用文本兜底通道用>def robust_click(driver, wait, text, testidNone): try: return click_by_text(driver, wait, text) except Exception: if testid: el wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, f[data-testid{testid}]))) el.click() return el raise这种写法的好处是即使文案改了、脚本也不会立刻失效而是走备用路径继续跑。等哪天有空了再回头更新文案池。4.3 页面元素枚举与只存定位元数据当页面上一批元素的文本遵循同一规律时比如一个表格的每一行末尾都有一个查看链接逐个写死 XPath 就太笨了。这时候用find_elements做枚举更合适rows driver.find_elements(By.XPATH, //table[idorder]//tbody/tr) for i, row in enumerate(rows): # 取出这一行里的状态文本 status row.find_element(By.XPATH, .//td[contains(class,status)]).text.strip() if status 待发货: # 只在满足条件的行里点按钮避免误点其他行 row.find_element(By.XPATH, .//a[normalize-space(.)发货]).click()这里有个容易忽略的细节行内查找的 XPath 必须以.开头比如.//td。不加那个点//td会从整个文档根开始找你以为是限定在行内实际上找的是全页面第一个匹配的 td。这个坑我见过至少三个同事踩过症状是明明只该点第二行结果每次都点第一行。再说一个概念枚举得到的是元素引用不是定位信息。find_elements返回的WebElement对象内部持有的是当前页面上下文的引用。一旦页面发生跳转、局部刷新或者 SPA 的视图切换这些引用就全部失效了再调用它们的.text或者.click()会抛StaleElementReferenceException。所以在需要跨多次操作、跨页面复用的场景下正确的做法是只存储定位元数据需要的时候现查。所谓定位元数据就是(By.XPATH, xpath_string)这样的元组而不是元素对象本身# 不好的做法缓存元素对象 elements_cache {row.text: row for row in rows} # 页面一刷新就全废 # 好的做法缓存定位元数据 locator_cache {} for i, row in enumerate(rows): locator_cache[row.find_element(By.XPATH, .//td[1]).text] ( By.XPATH, f//table[idorder]//tbody/tr[{i 1}]//a[normalize-space(.)发货] ) # 要用的时候现场查 by, value locator_cache[订单号8891] wait.until(EC.element_to_be_clickable((by, value))).click()这个习惯一旦养成脚本的稳定性会有质的提升因为它天然规避了元素过期的问题也顺手把页面刷新了怎么办这个隐患解决了。5. 常见报错与排查速查表5.1 NoSuchElementException 的几种常见成因报找不到元素的时候绝大多数情况不是定位写错了而是时机或者上下文不对。我按出现频率列一下。第一元素还没渲染出来。尤其是 SPA 页面路由切换之后首屏是骨架屏真实内容要等接口返回才挂上去。这种情况下把WebDriverWait加上基本就解决了。如果加了等待还是找不到那说明不是时机问题。第二元素在 iframe 里。这是最典型的明明能在浏览器里看到脚本就是找不到。解决方法是切进去wait.until(EC.frame_to_be_available_and_switch_to_it((By.ID, pay-frame))) # 操作完之后记得切回来 driver.switch_to.default_content()很多人只记得切进去忘了切回来导致后面所有定位都失败查半天查不出来。我现在写这类代码时会在切进去的下一行就写好切回来的注释提醒自己。第三文本里混了空白或者特殊字符。前面讲过normalize-space和translate的用法这里不重复。排查方法很简单先用 XPath 定位到元素把.get_attribute(textContent)打印出来用repr()看一下真相立刻就出来了。第四点击触发器之后元素才出现但代码没等。下拉框、弹窗、折叠面板都属于这类。解决办法是把等待放在点击触发器的后面而不是前面。第五元素在 Shadow DOM 里。Web Components 越来越常见Shadow DOM 内部的节点对普通 XPath 是不可见的。Selenium 4 提供了shadow_root属性来穿透host driver.find_element(By.CSS_SELECTOR, my-component) shadow host.shadow_root shadow.find_element(By.CSS_SELECTOR, button).click()不过要注意shadow_root只能穿透一层嵌套 shadow DOM 需要逐层往下钻。第六被滚动容器限制。元素在滚动区域下方虽然 DOM 里存在但因为滚动容器的 overflow 属性它不占视口。这时候需要先滚到它或者直接对它做execute_script滚动。判断依据是浏览器里的开发者工具可以找到它但 Selenium 报元素不可交互。5.2 点击了但没反应或者点了别的元素这类问题比找不到更麻烦因为不报错脚本继续往下跑最后在断言的时候才炸此时现场早就没了。最常见的原因是元素被遮挡。页面上有固定的导航栏或者浮动的客服按钮正好盖在目标元素上方。Selenium 的click()会先计算目标元素的中心坐标然后在那儿派发点击事件如果那个坐标被别的元素占了浏览器收到事件的就是遮挡物。这也就是为什么加一行scrollIntoView往往能解决问题——把元素滚到视口中央浮层就盖不到了。第二个原因是动画没结束。按钮点下去之后有个 300 毫秒的缩放动画元素在动画中位置一直在变click()算出来的坐标可能是动画中间态的。这类情况我会在点击后加一个短等待from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 等元素位置稳定 wait.until(lambda d: d.find_element(By.XPATH, xpath).is_displayed()) # 或者等 loading 遮罩消失 wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, .loading-mask)))第三个原因是匹配到多个元素。find_element在多匹配时返回第一个这个第一个是按 DOM 顺序不是按视觉顺序。如果页面上有两个同名按钮一个在隐藏的弹窗里一个在主界面上find_element很可能会返回弹窗里那个隐藏的。解决办法是把查找范围收窄到某个稳定的容器内xpath //div[idmain-content]//button[normalize-space(.)提交]养成先框定范围、再按文本定位的习惯能挡掉相当多的误命中。5.3 问题排查速查表现象高概率原因快速验证方法处理方式NoSuchElementException元素未渲染手动加time.sleep(3)看是否恢复换用 WebDriverWait 显式等待NoSuchElementException在 iframe 内看 DOM 树里目标元素上方有没有 iframeswitch_to.frame切入完成后切回NoSuchElementException文本含空白或\xa0打印repr(textContent)normalize-space或translate处理点击无效果被浮层遮挡用元素中心坐标比对遮挡物scrollIntoView后点击点击无效果匹配到了隐藏元素打印命中元素的is_displayed()收窄 XPath 范围StaleElementReferenceException页面局部刷新看失败是否总发生在刷新动作之后只存定位元数据用前重新查找ElementNotInteractableException元素被 disabled检查disabled属性等按钮启用后再点下拉框选项点不到面板未展开或用了原生 select看目标是不是select原生用 Select 类自定义的按文本找 li文本定位命中多个同名文案重复用find_elements打印长度限定父级容器范围这张表我一般是直接贴在内部分享文档里的新人照着查能解决八成的定位问题。6. 几个我踩过之后才记住的实操细节6.1 空格、全角半角、不可见字符中文页面里的空白字符是个大坑值得单独说。除了前面提到的nbsp;还有几个常见的一个是全角空格\u3000运营在后台配置文案时输入法没切回来就会打出来。它在视觉上和普通空格几乎一样肉眼根本看不出来但字符串比较一定失败。处理办法是把它也纳入translatexpath //span[normalize-space(translate(., \xa0, ))限时抢购]另一个是 BOM 或者零宽字符\u200b常见于从 Excel 或者文档里复制过来的文案。这类字符在页面上完全不可见但会让精确匹配失效。排查的通用手段还是那句打印repr()然后对着\u开头的转义码看。还有一个不那么常见但确实存在的场景数字里的千分位符号。有系统用半角逗号1,280有的用全角逗号1280还有的用空格1 280。如果你的定位要落在金额上最好别用完整金额文本而是只用金额前面的固定前缀来匹配比如contains(., 订单金额)然后从元素里读值再自己解析。6.2 把定位元数据集中放别散落在代码里这一条是我这几年最受益的工程习惯。刚开始写脚本的时候我习惯把 XPath 内联在调用处一个文件里散落着几十条 XPath 字符串。等到前端改一次版我要靠全局搜索去一条条改改漏一条就留个雷。后来我改成统一放到一个模块里# locators.py class OrderPageLocators: SEARCH_INPUT (By.XPATH, //label[normalize-space(.)订单号]/following-sibling::input) SUBMIT_BTN (By.XPATH, //button[normalize-space(.)查询]) EXPORT_BTN (By.XPATH, //button[contains(normalize-space(.), 导出)]) # test_order.py from locators import OrderPageLocators as L wait.until(EC.element_to_be_clickable(L.SUBMIT_BTN)).click()这样改版的时候只需要动一个文件而且能一眼看出全站用了多少条基于文本的定位便于评估风险。再进一步可以给每个定位加个备注写清楚这条 XPath 是基于哪个版本的文案万一文案改了回溯起来也快。6.3 我的稳定性优先级排序最后说说我在实际项目里怎么排定位方式的优先级这个顺序帮我把脚本的月度维护工作量压到了很低。第一优先是>
返回列表