XPath高级定位:利用兄弟节点和子节点关系查找目标元素
在做网页数据抓取或者自动化测试时,我们经常会遇到元素没有唯一id、class属性,或者属性值动态变化的情况,这时候基础的XPath定位方式往往不够用。这时候就可以利用XPath的轴定位能力,通过兄弟节点、子节点的关系来精准找到目标元素,解决很多复杂场景下的定位问题。
一、基本概念铺垫
在了解兄弟和子节点定位前,先明确两个基础概念:
- 子节点:某个元素内部直接嵌套的元素,就是该元素的子节点,子节点的子节点属于后代节点,不属于直接子节点。
- 兄弟节点:和当前元素拥有同一个父节点的元素,都互为兄弟节点,分为前面的哥哥节点和后面的弟弟节点。
XPath提供了专门的轴(axis)语法来描述节点之间的关系,常用的和兄弟、子节点相关的轴有:child(子节点)、following-sibling(后面的兄弟节点)、preceding-sibling(前面的兄弟节点)。
二、子节点定位实战
子节点定位是最常用的场景,比如我们要找某个<div>标签下第二个<p>标签,或者找某个列表下所有<li>子元素,都可以用子节点相关语法。
基础的子节点定位可以直接用/分隔,比如找<div id="content">下的所有<p>子节点,语法是//div[@id='content']/p,如果是找后代节点(不管嵌套多少层)就用//,比如//div[@id='content']//p。
如果需要更精准的控制,比如找第N个子节点,可以结合索引使用,注意XPath的索引是从1开始的,不是从0开始。下面是一个具体的示例,假设我们有如下HTML结构:
<div class="article">
<h3>文章标题</h3>
<p class="intro">文章简介内容</p>
<p class="content">文章正文第一段</p>
<p class="content">文章正文第二段</p>
<div class="footer">
<span>发布时间:2024-05-01</span>
<span>作者:张三</span>
</div>
</div>如果我们想定位到class为article的div下的第二个<p class="content">元素,就可以用下面的XPath表达式:
//div[@class='article']/p[@class='content'][2]
这里的/p[@class='content'][2]就是找当前div下的第二个class为content的p子节点,如果要找所有content类的p子节点,去掉索引即可:
//div[@class='article']/p[@class='content']
三、兄弟节点定位实战
兄弟节点定位在元素属性没有明显特征,但相邻元素有特征的时候非常好用,比如表单里<label>标签后面跟着的<input>输入框,或者表格里某个单元格后面的操作按钮,都可以通过兄弟节点关系定位。
1. 后面的兄弟节点(following-sibling)
following-sibling轴用来找当前节点之后的所有同级兄弟节点,语法是当前节点/following-sibling::目标节点类型[条件]。
还是用上面的HTML结构举例,如果我们要找到<h3>标题后面的第一个<p>标签,也就是简介的那个p,就可以用:
//h3[text()='文章标题']/following-sibling::p[1]
如果要找标题后面所有的p标签,就可以去掉索引:
//h3[text()='文章标题']/following-sibling::p
再举个表单的场景,假设HTML结构如下:
<form id="login-form"> <label for="username">用户名:</label> <input type="text" id="username" name="username"> <label for="password">密码:</label> <input type="password" id="password" name="password"> <button type="submit">登录</button> </form>
如果我们要通过label的文本找到对应的input输入框,就可以用following-sibling:
//label[text()='用户名:']/following-sibling::input[1] //label[text()='密码:']/following-sibling::input[1]
2. 前面的兄弟节点(preceding-sibling)
preceding-sibling轴和following-sibling相反,用来找当前节点之前的所有同级兄弟节点,语法是当前节点/preceding-sibling::目标节点类型[条件]。
还是用上面的登录表单举例,如果我们要通过input输入框找到对应的label标签,就可以用:
//input[@id='username']/preceding-sibling::label[1] //input[@id='password']/preceding-sibling::label[1]
这里的[1]表示找当前input前面最近的一个label兄弟节点,因为preceding-sibling返回的顺序是从近到远,所以第一个就是紧挨着的前面的label。
四、组合使用场景
实际场景中我们经常需要把子节点和兄弟节点的定位组合起来用,比如要找某个列表下,第二个<li>元素后面的所有<span>标签,就可以先定位到li,再用following-sibling找后面的span。
假设有如下列表结构:
<ul class="user-list">
<li>
<span class="name">张三</span>
<span class="age">20</span>
</li>
<li>
<span class="name">李四</span>
<span class="age">22</span>
</li>
<li>
<span class="name">王五</span>
<span class="age">25</span>
</li>
</ul>如果我们要找到第二个li(李四那一行)下的age的span,可以先定位到第二个li,再找子节点span:
//ul[@class='user-list']/li[2]/span[@class='age']
如果我们要找到李四的li后面,王五的li下的name的span,就可以组合兄弟和子节点:
//li[span[@class='name' and text()='李四']]/following-sibling::li[1]/span[@class='name']
这个表达式的逻辑是:先找到包含name为李四的span的li,然后找它后面的第一个li兄弟节点,再找这个li下的class为name的span子节点,就精准定位到了王五的名字span。
五、注意事项
- XPath的索引从1开始,和很多编程语言的数组从0开始不同,写索引的时候不要搞错。
- 使用兄弟节点轴的时候,要注意返回节点的顺序,following-sibling是从当前节点往后按顺序返回,preceding-sibling是从当前节点往前按从近到远的顺序返回。
- 如果页面结构可能变化,尽量不要用太固定的索引,比如
[3]这种,优先用属性条件来过滤,比如[@class='xxx'],这样定位更稳定。 - 写好的XPath可以在浏览器的开发者工具里验证,在Elements面板按Ctrl+F,输入XPath表达式,能匹配到元素就说明语法正确。