手写XPath完全指南:语法、定位技巧与自动化测试实践
手写XPath这两年越来越被低估了。很多人一上来就装插件、靠开发者工具右键复制结果复制出来的路径又长又脆页面一改就挂。我做了几年自动化测试和爬虫现在反而回到手写XPath的老路上不是保守是这玩意儿在实战里最稳。这篇把基础语法、定位技巧、调试手段一次讲透适合刚接触自动化、想系统补XPath基础的同学也适合写了好久但只会照抄工具结果的人。1. 为什么要手写XPath而不是直接复制1.1 工具复制的路径为什么这么脆弱用Chrome开发者工具右键元素选择Copy Copy XPath复制出来往往是一长串绝对路径长这样/html/body/div[2]/div[3]/div[1]/div[2]/div[1]/div[2]/ul/li[4]/a这种绝对路径是从html根节点一路数下来的中间任何一个层级变动、样式调整、动态渲染插入节点整个路径就断了。我见过最夸张的一次一个复制出来的路径有17层产品在页面顶部加了个公告栏17层全废。而手写相对路径瞄准的是元素的特征不是它家在几楼几号页面怎么改只要那个特征还在路径就还能用。1.2 手写XPath解决什么实际问题手写XPath解决的是三个层面的问题一是定位稳健性通过特征匹配而不是位置匹配页面局部改动不影响二是定位精度很多元素没有id和class或者class是动态生成的随机串只有XPath能通过文本、属性组合、层级关系精准锁定三是批量提取能力爬虫场景下要抓取结构相似的一组元素用XPath的轴和谓词可以一次性取完不需要写一堆循环判断。1.3 手写XPath的基础前置知识写XPath之前脑子里要有两个模型。第一个是节点树模型HTML是一个树状结构每个标签是一个节点标签里的属性、文本、注释都是节点的一部分。第二个是上下文模型XPath的每一段都是相对当前节点来解析的当前节点变了同样的路径表达式指向的元素可能完全不同。浏览器开发者工具是验证XPath最方便的场所。在Elements面板按Ctrl F会弹出搜索框输入XPath后匹配的元素会高亮按回车可以切换匹配项。这个操作建议养成肌肉记忆写任何XPath之前先在这里验证一遍比写完了去跑脚本效率高十倍。下面所有示例在Chrome的Elements搜索框里都能直接跑建议一边看一边试。2. XPath的节点关系与语法骨架2.1 节点和节点关系的体系化梳理XPath把文档抽象成节点树节点之间有几种关系父节点、子节点、祖先节点、后代节点、兄弟节点。拿一段HTML举例div idnav ul lia href/home首页/a/li lia href/about关于我们/a/li /ul /diva标签是li的子节点同时是ul的孙节点、div的后代节点li之间是兄弟节点a的父节点是lili的父节点是ul。XPath里的路径表达式就是靠这几种关系一层层走。理解了节点关系XPath的路径表达式就不难拆解了。斜杠/表示从根节点开始绝对路径选择双斜杠//表示从任意位置开始相对路径选择点.表示当前节点双点..表示当前节点的父节点。这些符号组合起来基本就能完成大部分定位需求。2.2 路径表达式的核心结构路径表达式由定位步骤组成每个定位步骤包含三部分轴、节点测试、谓词。轴定义了节点之间的关系方向节点测试指定了节点的类型或名称谓词用方括号[]包裹对节点进行筛选过滤。这部分新手不用死记先掌握常用的三种写法//div 选中文档中所有div元素 //div[idnav] 选中文档中id属性等于nav的div元素 //div[idnav]/ul/li 选中文档中nav下ul下的li元素第一个示例是元素名选择返回页面所有div第二个示例在元素名前加了属性筛选第三个示例把两层组合起来形成一段完整的路径。这三个示例连起来就是XPath从简单到复杂的演化路径。把这三个写熟了再往深处学轴的用法就轻松。2.3 通配符的三种用法通配符是手写XPath里性价比最高的知识点一共就三个*匹配任意元素节点、*匹配任意属性节点、node()匹配任意类型的节点。在需要模糊定位或者筛选结构未知的内容时这三个符号能省很多事情。//div[*] 选中有任意属性的div元素 //li/* 选中li下所有直接子元素 //div/node() 选中div下所有子节点包括文本节点我实际使用中*用得最多的是配合父级约束来定位子元素比如某个容器里只有一种类型的元素写//div[classlist]/*就直接把所有条目取出来了。3. 高频定位写法详解3.1 属性定位的灵活组合属性定位是XPath里最基础也最常用的手段核心语法是[属性名属性值]。最常用的是id和class但实战中光用这两个不够name、type、href、src、placeholder都可以用//input[nameusername] 通过name属性定位输入框 //input[typesubmit] 通过type属性定位按钮 //a[href/login] 通过href属性定位链接 //input[placeholder请输入手机号] 通过占位文本定位输入框属性可以叠加也可以局部匹配。叠加用and表示多个条件同时满足局部匹配用contains函数表示属性值包含某个字符串。叠加写法示例//input[typetext and nameusername]多属性叠加的优点是精度高缺点是写起来长。我一般只在目标元素没有唯一特征时才叠加能用一个属性解决的绝不写两个。3.2 文本定位与contains函数文本定位靠的是text()函数和contains函数组合。热搜词里问得最多的“xpath a元素文字包含定位怎么写”核心就是这一句//a[contains(text(),登录)]这行的含义是选中文档中所有文本内容包含“登录”二字的a标签。text()取的是当前节点的直接文本注意不是后代节点的文本。如果文本是嵌在子标签里的比如a href/login点击span登录/span/aa的直接文本是“点击”span里的“登录”不在text()的范围内。这时候两种解决思路一是用.代替text()点表示当前节点的全部文本内容包括子节点的文本二是直接用后代节点的文本定位//a[contains(.,登录)] //a[contains(span,登录)]第一行用点取a的完整文本内容做包含匹配第二行限定到某个span子元素。实际使用中.比text()更稳妥因为很多页面会在标签里套各种span、i、em之类的装饰性标签纯用text()匹配容易漏。但.也有代价匹配范围变大误匹配概率增加所以能用text()精确匹配的地方还是优先用text()。3.3 文本精确匹配与扩展用法有些场景需要文本完全相等而不是包含这时候用等号//button[text()立即购买] //span[.新品]精确匹配的坑在于文本中的空格和换行。HTML源码里如果标签之间有换行缩进text()拿到的文本可能带着空白字符。比如这样一段span 新品 /span用text()新品是匹配不到的因为实际文本是“\n 新品\n”。这种场景下我会用contains来宽容处理或者配合normalize-space函数去掉首尾空白//span[normalize-space()新品]normalize-space会把文本里的连续空白折叠成一个空格同时去除首尾空白精确匹配时特别好用。缺点是写法长一些但对那些源码格式乱、空白字符多的页面它就是救命的。除了contains和normalize-space还有starts-with和ends-with可以用分别表示以某字符串开头和结尾//input[starts-with(id,user-)] id以user-开头的输入框 //a[ends-with(href,.pdf)] href以.pdf结尾的链接ends-with在XPath 1.0里不支持部分旧环境会报错使用前先确认运行环境的XPath版本。2.0和3.0的新特性后面单独说。3.4 索引定位与位置函数索引定位是定位列表元素时的常用手段语法是方括号里直接写数字。XPath的索引是1开始的写div[1]表示第一个div写div[last()]表示最后一个//ul[classlist]/li[1] 第一个列表项 //ul[classlist]/li[last()] 最后一个列表项 //ul[classlist]/li[position()3] 前两个列表项这里有一个新人特别容易踩的坑//li[1]和//ul/li[1]的含义不一样。//li[1]在部分解析器里表示整体文档中第一个li但由于//的语义是按文档顺序遍历所有li节点有说法认为是“每个路径下的第一个”实际结果因解析器实现而异。稳妥的写法是加上明确的父级约束比如//ul[classlist]/li[1]这样语义才没有歧义。last()和position()是XPath的节点集函数last()表示当前节点集合中的最后一个索引position()返回当前节点在集合中的位置。两者配合可以处理很多复杂筛选比如取倒数第二个//ul[classlist]/li[last()-1]3.5 轴定位从前向后用兄弟节点定位轴定位是XPath里威力最大也最容易被忽视的部分。通俗理解轴就是从一个节点出发沿某个方向去找相关节点。最常用的三个轴是following-sibling、preceding-sibling、parent。场景是这样的页面上某个元素没有id和class但它前面或后面的兄弟节点有。//label[text()用户名]/following-sibling::input这个写法的含义是先定位文本为“用户名”的label然后找它后面的兄弟节点中的input元素。表单页面里label和输入框经常是这样的相邻结构label上有稳定文本输入框上却啥特征都没有这招百试百灵。反过来找前面的兄弟节点用preceding-sibling//span[classerror]/preceding-sibling::input找父节点用parent轴或者直接写..。举个例子定位某个包含特定文本的div的父级容器//div[contains(text(),热门推荐)]/..两个点取父节点的写法比写parent::div更简洁但可读性稍差。我的习惯是脚本里用parent::div调试时用..因为调试阶段速度快更重要。4. 浏览器自带调试工具用起来4.1 Elements面板搜索框的妙用每次看到有人为了验证XPath专门写脚本跑一遍我都觉得太绕了。Chrome开发者工具的Elements面板里按Ctrl F呼出搜索框输入XPath就能实时高亮匹配元素这是官方提供的XPath调试器免费且好用。搜索框支持三种匹配模式文本字符串、CSS选择器、XPath。默认是文本字符串模式输入//开头的才自动识别为XPath。匹配到的元素会在页面上高亮黄色搜索框里会显示匹配总数按回车可以在多个匹配项之间循环切换。写XPath的时候写完一段立即回车确认匹配数量——匹配数是1才是理想状态等于0说明写错了大于1说明定位不够精确都需要调整。4.2 XPath Helper插件的选型与替代方案网上关于XPath Helper的下载和使用教程非常多但它已经很多年没更新了。如果你用的Chrome版本比较新装上去可能直接提示“此扩展程序已停用”或者干脆不显示。我的建议是能不装就不装浏览器自带功能完全够用。如果你确实需要一种在页面上直接划选元素、自动生成XPath的工具可以试试以下替代方案一是Chrome扩展商店里搜索XPath Finder界面更现代维护更活跃二是用Puppeteer或Playwright的脚本调试在Node环境里用page.$x或page.locator来验证三是不装任何插件用DevTools的Elements面板手动调试把搜索框当成XPath控制台用。实际体验下来DevTools搜索框的响应速度和准确性反而是最高的。4.3 Console里用JavaScript封装验证函数Elements搜索框能解决大部分验证需求但有一个场景它做不到连续调试多个XPath或者把结果做进一步操作。这时候可以用Console面板配合JavaScriptfunction xpathCheck(expression, context) { const result document.evaluate( expression, context || document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null ); const nodes []; for (let i 0; i result.snapshotLength; i) { nodes.push(result.snapshotItem(i)); } return nodes; }把这段代码粘贴到Console里执行之后就能随时调用xpathCheck(//a[contains(text(),登录)])来测试XPath返回的是匹配的元素数组可以在Console里直接展开查看属性。这套方案不依赖任何第三方工具比装插件靠谱得多。5. 手写XPath的避坑手册5.1 动态属性与动态class的处理思路现代前端框架流行把class和id做成动态值比如React的css-module、Vue的scoped样式生成的class像“btn_3jF2k”这种带随机后缀的。用完整class值去匹配一旦项目重新构建后缀变了XPath就失效。处理思路有两条一是用contains匹配稳定的前缀或关键字比如//button[contains(class,btn) and contains(text(),确认)]二是放弃属性改用文本、层级结构、兄弟关系来做特征定位。第二种这里给一个完整例子。某系统导航菜单每一项都长这样div classmenu-item>//div[contains(class,menu-item) and contains(.,订单管理)]这个写法有两个点需要注意。contains(.,订单管理)匹配的是div的完整文本内容包括所有子元素的文本如果菜单项里面有隐藏的计数数字只要还包含“订单管理”四个字仍然能匹配上。另外如果页面上有多个menu-item包含“订单管理”这个路径会匹配多个需要结合父级再限定。5.2 iframe内的元素为什么定位不到元素明明在页面上显示着XPath却定位不到十有八九是iframe在作怪。iframe是内嵌的独立文档主文档的XPath搜索不到iframe内部的元素。Chrome的Elements面板里会显示iframe的独立文档树搜索框默认只在当前选中的文档里搜索所以要先点击进入iframe对应的document节点再搜索。自动化脚本里的处理方式Selenium是先switch_to.framePlaywright是frameLocator爬虫场景则要单独请求iframe的src拿内容。XPath本身没有跨iframe定位的语法必须通过框架的API先切进iframe再执行XPath。这里分享一个iframe定位的完整示例Python Seleniumdriver.switch_to.frame(mainFrame) driver.find_element_by_xpath(//input[nameusername])如果iframe没有id或name可以先定位iframe元素再切入iframe driver.find_element_by_xpath(//iframe[contains(src,login)]) driver.switch_to.frame(iframe)5.3 文本节点与空白字符的干扰文本定位最大的干扰源是空白字符——换行、空格、制表符。尤其在自动化脚本里很多人用text()完整文本匹配失败不是写错了是被源码里的换行坑了。button 提交订单 /button这段HTML渲染出来显示“提交订单”但text()拿到的值是“\n 提交订单\n”。两种处理办法//button[contains(text(),提交订单)] //button[normalize-space()提交订单]第一种用contains做子串匹配绕开空白问题第二种用normalize-space把空白折叠后再精确匹配。我个人的经验是normalize-space配合等号基本不会出错它在做精确文本匹配时的稳定性比裸等号高很多。5.4 动态加载元素需要等待配合XPath定位的是静态DOM结构如果元素是接口返回后动态渲染的XPath写对了也定位不到。这不是XPath本身的问题是时序问题。解决方案是显式等待WebDriver里用WebDriverWaitPlaywright里用wait_for_selector。这里不展开讲等待的细节只提示一个原则XPath只负责“怎么找”元素“什么时候存在”是框架层的等待逻辑负责的两者配合而不是对立。5.5 常见报错信息速查表我把日常工作中最常见的报错整理成了表格方便定位时快速排查报错信息常见原因解决方向The expression is not a valid XPath expression语法错误比如方括号没闭合、引号用了中文全角检查括号配对、引号类型Unable to locate element元素不存在或XPath匹配不到先确认元素是否在iframe、是否动态加载Expected [object Undefined]路径写错或者匹配到了undefined在DevTools搜索框里逐段验证Timed out waiting for element元素出现太慢或XPath条件过严加等待时间或放宽contains约束NoSuchElementException元素不存在路径断了用DevTools重新定位加粗一行重点XPath调试必须逐段验证。路径写长了之后定位失败很难一眼看出哪一段出了问题。我的做法是把路径一段段拆开先验证最外层定位是否正确再依次往内层加每次加一段都确认匹配数符合预期。这样排错比整体瞎猜快得多。6. XPath版本差异与扩展能力6.1 XPath 1.0和3.0在实际中的差异目前绝大多数浏览器和自动化库的默认XPath实现是1.0Python的lxml默认也是1.0Selenium的find_element_by_xpath同样基于1.0。1.0能用的函数有限contains、starts-with、normalize-space、text、last、position、count、string、concat这些是够用的。XPath 2.0和3.0引入了更强大的能力比如ends-with、math、正则表达式匹配等但在自动化测试领域普及率很低——底层引擎不升级写了也白写。有一个例外是Playwright的locator它底层没有直接用XPath 3.0但支持CSS选择器与XPath的组合写法灵活性高一些。判断运行环境支持什么版本的方法很简单在对应工具里跑一句//element[ends-with(id,abc)]如果报错就说明只支持1.0。6.2 逻辑运算符与复杂条件组合多条件组合是手写XPath的日常。and在XPath里集成了复杂条件组合时注意运算符优先级//input[typetext and nameusername or placeholder邮箱] //button[contains(class,btn) and not(contains(class,disabled))]and的优先级高于or所以要避免写出含义模糊的表达式。实践里我用最稳妥的组合方式把or条件用括号包起来or条件内部再用and连接//button[(contains(class,btn-primary) or contains(class,btn-success)) and not(disabled)]这样语义清晰别人接手也容易看懂。not()函数在XPath 1.0里很常用作用是取反配合contains可以排除某些干扰元素。6.3 XPath在Selenium和Playwright中的写法差异同一条XPath在不同工具里的写法风格有一定差异。Selenium的WebDriver提供了find_element_by_xpath和find_elements_by_xpath两个方法直接传入XPath字符串driver.find_element(By.XPATH, //a[contains(text(),登录)])Playwright的风格是链式调用XPath作为选择器传入page.locator(xpath//a[contains(text(),登录)])playwright的locator还支持组合写法比如先CSS定位容器内部再用XPath定位目标元素page.locator(.nav-bar).locator(xpath//a[contains(text(),登录)])这个组合技巧很实用先用CSS锁定小范围再用XPath在范围内精确定位既能少写路径段数又能利用CSS处理class的高效性。6.4 结构化分支父级定位、兄弟定位的综合案例下面给一个完整的综合案例把之前的语法串联起来。页面结构如下div classorder-list div classorder-item>//div[contains(class,order-item) and contains(.,华为Mate60 Pro)]第二步在这个div里找button//div[contains(class,order-item) and contains(.,华为Mate60 Pro)]//button[contains(text(),去支付)]这个路径完整、稳健页面里其他“去支付”按钮不会干扰。更进一步如果订单标题是通过变量动态变化的可以写成拼接方式这在爬虫和自动化测试里更通用。这是我开发里用得最多的一类写法先定位一个包含特征的大容器再在大容器内部缩小范围。路径是长了点但稳定性极高推荐作为默认策略。7. XPath的完整实操案例复盘7.1 从零定位一个电商页面的完整过程用一个电商搜索页举例目标是抓取搜索结果中所有商品名称和价格并处理分页导航。页面结构大致如下div classsearch-result div classgoods-item a classgoods-name href/goods/101无线蓝牙耳机/a span classprice¥199/span /div div classgoods-item a classgoods-name href/goods/102机械键盘/a span classprice¥399/span /div /div div classpagination a classnext href/search?page2下一页/a /div第一步验证商品项定位//div[contains(class,goods-item)]在DevTools里输入匹配数应该是商品的个数。第二步定位商品名称和价格的相对路径//div[contains(class,goods-item)]/a[contains(class,goods-name)]/text() //div[contains(class,goods-item)]/span[contains(class,price)]/text()注意第三段用text()是因为要取节点文本这个在爬虫的lxml里是常见操作。如果拿不到文本检查一下lxml里用的xpath函数版本和写法。第三步处理翻页。下一页按钮//a[contains(class,next)]/href把这个href拿回来拼上URL继续请求就能循环抓完所有页。7.2 一段可以复用的Python爬虫示例配合lxml库完整的抓取逻辑长这样from lxml import etree import requests html requests.get(https://example.com/search?q耳机).text root etree.HTML(html) items root.xpath(//div[contains(class,goods-item)]) for item in items: name item.xpath(.//a[contains(class,goods-name)]/text()) price item.xpath(.//span[contains(class,price)]/text()) print(name, price)注意这里用的是相对路径item.xpath里的.不能省。点是相对当前节点进行选择不加点就是从整个文档重新找结果大概率不是想要的。这个.的习惯比XPath语法本身更值得养成。7.3 Puppeteer场景下的XPath使用示例前端自动化用Puppeteer时可以用$x方法执行XPathconst puppeteer require(puppeteer); (async () { const browser await puppeteer.launch(); const page await browser.newPage(); await page.goto(https://example.com); const [button] await page.$x(//button[contains(text(),确定)]); if (button) { await button.click(); } await browser.close(); })();注意$x返回的是ElementHandle数组所以第一个元素要用解构赋值来取。如果匹配不到返回的数组是空的直接取[0]会是undefined需要先判断再操作。8. 最后的几条经验XPath学到后面会发现真正难的不是语法是判断一个元素“怎么定位最稳”。我把自己的判断顺序写在这里供参考。优先用idid是页面唯一的其次用name和type组合再次用文本和contains组合最后才用索引和位置。这个优先级不是绝对的但大部分场景下都适用。还有一个所有自动化测试老手都会强调的通用原则定位器要能经受住页面变化的考验。你写完一条XPath可以问自己两个问题如果开发在页面上加了一行文字这个路径会不会断如果开发删掉了一个div这个路径会不会断如果会趁早换方案。最后提一个经常被忽略的小技巧。页面结构复杂时不要一次性写一个长路径先锁定一个稳定的父级容器再在容器内部用相对路径找目标元素。这个思路和代码里的作用域概念相通限定了范围路径的路径的稳定性和可读性都会大幅提升。我写的所有线上稳定运行的定位器几乎都是这个套路。

相关新闻

最新新闻

日新闻

周新闻

月新闻