目录
使用selenium做web页面自动化时,最重要的工作的就是进行元素的定位,只有定位到元素才能进行后续的操作,例如点击一个按钮前,你必须先要知道这个按钮在哪里。
元素定位接口
Python
selenium为单个元素定位,提供了8个API:
接口名称 | 功能说明 | 返回值 |
find_element_by_id | 根据页面元素的Id,寻找元素 |
能找到则返回一个代表这个元素的WebElement对象,否则抛出NoSuchElementException异常。 即使有多一个元素符合条件,仅返回最先发现的那个元素的对象。 |
find_element_by_name | 根据页面元素的name,寻找元素 | 同上 |
find_element_by_class_name | 根据页面元素的class name,寻找元素 | 同上 |
find_element_by_xpath | 根据页面元素的xpath,寻找元素 | 同上 |
find_element_by_css_selector | 根据页面元素的css,寻找元素 | 同上 |
find_element_by_tag_name | 根据页面元素的tag名字,寻找元素。很少用,同一类元素都有相同的唐名字 | 同上 |
find_element_by_link_text | 根据页面元素的完整的链接字符串,寻找元素 | 同上 |
find_element_by_partial_link_text | 根据页面元素的部分链接字符串,寻找元素,模糊查找 | 同上 |
上面的接口返回的是单个元素,还有一组类似的接口,返回的是元素符合条件的元素对象列表:
find_elements_by_id | 返回值是一个列表或空列表 |
find_elements_by_name | 同上 |
find_elements_by_name | 同上 |
find_elements_by_xpath | 同上 |
find_elements_by_css_selector | 同上 |
find_elements_by_tag_name | 同上 |
find_elements_by_link_text | 同上 |
find_elements_by_partial_link_text | 同上 |
实际上上面2组接口都是调用find_element和find_elements实现的,这2个接口是私有的,不过你也可以调用的(因为python中没啥东西真的私有滴)
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome("C:\\selenium\\chromedriver.exe")
driver.get("http://www.baidu.com")
pp = driver.find_elements(By.ID, "adsfasd")
JAVA
Java中定位接口更简练些,只有find_element和find_elements
xpath和css定位技术
通过对上面元素定位接口的研究,我们知道我们可以用来进行定位的有 id,name, class name, tag name, link text, partial link text, xpath 和 css。
在实际使用中,有时候id,name, class name, tag name, link text, partial link text,可能不存在,或者可能会发生变化,又或者肯能存在重复的问题,这时候我们就需要更复杂的定位手段了。 他们就是xpath和css,但是相对的这个2个定位方法学习起来更难一些,所以在这里对这2中方法进行详细的介绍。
xpath
xpath是XML中的一种定位语言,因为HTML可以说是XML规范的一种实现,所以xpath也可以用在HTML中元素的定位。除了通过绝对路径进行定位,xpath还支持相对定位等待。
工具:
通过Firefox的FirePath插件可以方便的获取到xpath值. 由于最新版火狐不在支持FireBug等开发工具,可以通过https://ftp.mozilla.org/pub/firefox/releases/ 下载49版本以下的火狐就可以增加Firebug等扩展了。
我使用的chrome版本是75.0.3770.100,通过右击页面,选择 检查, 或通过 更多工具-开发者工具, 都可以打开debug工具,点中工具左上角的箭头图标,然后选择要定位的元素,在工具里会显示元素的信息,右击元素 -》 copy- 》 copy xpath
xpath基础知识
术语解释
<?xml version="1.0" encoding="UTF-8"?>
<bookstore>
<book>
<title lang="en">Harry Potter</title>
<author>J K. Rowling</author>
<year>2005</year>
<price>29.99</price>
</book>
</bookstore>
上面例子中:
<bookstore> 为文档节点,也称为根节点
<author>J K. Rowling</author> 为元素节点
lang=“en” 为属性节点
“en” 和J K. Rowling 被称为基本值
book 是 title,author, year,price的父节点-----parent
title,author, year,price 是 book的子节点-----Children
title,author, year,price 是兄弟节点------Sibling
title的先辈是book和bookstore--------Ancestor
bookstore的后代则是 book, title,author, year,price ------ Descendant
xpath语法
1. 使用路径表达式定位节点
路径分为绝对路径和相对路径:
绝对路径从根节点开始,使用 / 开头, 代表从根一直到当前节点
相对路径以 // 开头 根据要定位的元素的特征来进行定位
节点之间关系的表示:
当前节点用单个点 . 表示
当前节点的父节点用 2个点 .. 表示
2. 使用谓语来定位节点
当单纯的路径表达式无法准确定位时,我们可以使用谓语来进一步定位。 谓语被置于中括号内。
//*[@id="u1"]/a[2] 根据索引进行元素定位(下标从1开始)
//*[@id="u1"]/a[last()] 根据索引进行元素定位,我们可以获得最后一个元素
//*[@id="u1"]/a[last() -1] 根据索引进行元素定位,我们可以获得倒数第二个元素
//*[@id="u1"]/a[position()<3] 根据索引进行元素定位,我们可以获得前2个元素
//*[@id="u1"]/a[@name] 目标元素要具有指定的属性
//*[@id="u1"]/a[@name="news"] 目标元素不仅需要包含属性name,而且属性的值必须是news
//*[@id="u1"]/a[text()="news"] 通过标签的文本来定义节点 也可以写成//*[@id="u1"]/a[.="news"], 用这个方法时,标签必须有文本才行
//*[@id="u1"]/a[contains(text(),"news")] 这是一种模糊查找, 只要那个a标签的文本包含news,就符合条件
//*[@id="u1"]/a[contains(@name,"news")] 与上面相同,这是一种模糊查找, 只要那个a标签的name属性的值包含news,就符合条件
通过标签的文本来定义节点 也可以写成//*[@id="u1"]/a[.="news"]
3. 通配符
用来获取哪些未知的节点元素
* 可以用来代表任何元素节点 //*[@id="head"]/div/*
@* 可以用来标识任何属性 //*[@id="head"]/div/div[@*] 代表目标div必须有至少有一个属性
4. 一次获取多个节点元素
这多个元素通常没有什么关联性
//*[@id="head"]/div | //*[@id="form"]/span[1] 可以定位2个不相关的元素
5. 轴定位
就是靠节点间的父子关系来定位,适合用于相对位置较为固定的节点结构
语法规则: 当前节点/关系类型::目标节点特征
关系类型包括:父节点(parent)、子节点(child)、祖先节点(ancestor)、子孙节点(descendant)、后节点(following)前节点(preceding)、后兄弟节点(following-sibling)、前兄弟节点(preceding-sibling)
<html><head></head><body>
<div> #div1
<input>
<a>big</a>
<img name="haha">
<button></button>
<div>1</div> #div4
</div>
<div> #div2
<input>
<a>small</a>
<img name="haha2">
<button></button>
<div>2</div> #div5
</div>
<div> #div3
<input>
<a>large</a>
<img name="haha3">
<button></button>
<div>3</div> #div6
</div>
</body></html>
参考上图,举例说明各个关系是怎么运用的:
/html/body/div[1]/img/parent::*/input 以img为当前节点,获取它的parent(因为只有一个parent,所以可以用星号),然后定位到parent下的input节点
/html/body/div[1]/child::img 以div1为当前节点,获取它的所有子节点,从这些子节点中找到img节点
/html/body/div[1]/img/ancestor::body 以img为当前节点, 获取它的所有先辈节点(div1,body 和html),然后定位到body节点
/html/body/descendant::img[@name="haha"] 以body为当前节点,获取所有的子孙节点,从中定位到name属性为哈哈的img节点
/html/body/div[1]/following::div[2] 以div1为当前节点,获取与div1同级的后面所有节点及其所有子孙节点,从中定位第二个div。主要注意的是如果后面节点中存在多层结构,每层结构中都有div, 索引是从上到下,而与层级没有关系,本例中第二个div是上图中那个div5
/html/body/div[3]/preceding::div[1] 以div3为当前节点,获取与div3同级的前面所有节点及其所有子孙节点,从中定位我们要定位的元素。 主要最后的div[1]定位的是div5,索引是从下到上的
/html/body/div[1]/following-sibling::div[1] 以div1位当前节点, 获取与div1同级的后面的所有兄弟节点(不包含兄弟节点的子孙节点), 从中定位一个我们要的元素。 注意最后一个div[1] 定位的是div2
/html/body/div[3]/preceding-sibling::div[1] 以div3位当前节点,获取与div2同级的前面的所有兄弟节点,从中定位div2, 注意此处的索引是从下向上的。
6. 使用辅助函数进行定位
其实我们前边已经用过了,就是那个contains函数,能够支持的函数如下:
starts-with: 定位属性或文本以指定字符开头的节点
/html/body/div/a[starts-with(text(),"b")] 定位到 <a>big</a>
/html/body/div/img[starts-with(@name,"haha")] 会定位到3个img节点
contains: 定位属性或文本中包含指定字符串的节点
/html/body/div/img[contains(@name,"haha")] 会定位到3个img节点
text:获取节点的文本,可以用 . 代替
last:获取定位到一组节点的最后一个节点的索引
not:取反操作
/html/body/div/a[ not(contains(.,"s"))] 会选中big和large 这个2个节点
7. 运算符
这些运算符用于谓语中,构建过滤条件
+, -, *, div,mod: 算数运算, 加减乘除和取余,主要用来计算索引值
=, !=, >=, <=, >, <: 比较运算符,建立过滤条件 //div[. > 1] 就会定位到div5
or, and: 逻辑运算符,连接多个条件构成过滤条件 //div[. = 1 or . = 2] 会选定div4 和div5
CSS选择器定位
其实如果会用xpath,也可以不学css了,不过技多不压身嘛! 但是,我如果太久不用定位,就经常弄混css和xpath的语法,哎
先弄一个例子,所有的操作都是针对这个例子的
<html><head></head><body>
<div> #div1
<input id="test1" class="sweet1">
<a>big</a>
<img name="haha">
<button></button>
<div>1</div> #div4
</div>
<div> #div2
<input id="test2" class="sweet2">
<a>small</a>
<img name="haha2">
<button class="abc efg hhh" name="ddd eee fff"></button>
<div>2</div> #div5
</div>
<div> #div3
<input id="test3" class="sweet3">
<a>large</a>
<img name="haha3">
<button class="abc-efg-hhh" name="ddd-eee-fff"></button>
<div>3</div> #div6
</div>
</body></html>
我们从简单到复杂,一步一步来
一:使用单个属性进行定位
1. 使用id进行定位
#test1 定位到第一个input节点, 这里#有特殊意义,其后是id的值,作用类似于xpath中的 [@id="test1"]
.sweet3 定位到第三个input节点, 注意第一个字符是个点(.), 其后是class属性的值, 作用类似xpath中 [@class="sweet3"]
input 会搜索到3个input节点, 这个就是仅仅根据标签名进行定位
[name] 仅仅id和class属性有上面的特殊书写方式,其他属性就没这个权利了。 这个会搜索到所有包含name属性的节点
[name=haha] 或者 [name="haha"] 会定位到第一个input节点,注意haha可以带引号也可以不带,但xpath是需要给属性值加引号的,另外xpath中属性名前要加@
二. 使用多个属性组合来进行定位
input.sweet3 代表要找的是,class属性是sweet3的input标签
input#test1 代表要找的是,id属性是test1的input标签
input[name=haha] 代表要找的是,name属性是haha的input标签
input[name] 代表要找的是,拥有name属性的input标签
input[id=test1][class=sweet1] 代表要找的是,id是test1, class是sweet1的input标签, 后边2个中括号是and的关系
三. 使用模糊匹配来进行定位
当属性的值中包含多个由空格隔开的字符串时,可以用其中一个字符串进行定位
button[name~=ddd] button[name~=eee] button[name~=fff]
属性的值中包含多个由空格隔开的字符串时,无法用整个属性值定位,通常只有class的值才会有空格间隔
button[name=”ddd eee fff”]
当属性的值中包含多个由空格隔开的字符串时,可以用来搜索class中开始位置是abc的节点
button[class^=abc] button[class^=ab] button[class^=a]
当属性的值中包含多个由空格隔开的字符串时,可以用来搜索class中结尾位置是hhh的节点
button[class$=hhh] button[class$=hh] button[class$=h]
当属性值中包含 -(短横杠)时,可以匹配其中一部分,但是每一部分都要精确填写
button[class|=abc-efg-hhh] button[class|=abc-efg] button[class|=abc]
四. 多个class值进行定位
button.abc.efg.hhh 当一个节点的class值为多个由空格分隔的字符串时,可以用这种方式进行定位
五. 通过层级进行定位
1. 父元素>子元素
div>input[id=test1] div>input会搜索到所有div下的input节点,然后再通过id进行限定
2. 父元素 子孙元素
body * 代表body的所有子元素以及子元素的子元素。。。。
body input 代表body的所有子孙元素中的标签为input的元素
body input[id=test3] 代表body的所有子孙元素中的标签为input,且id是test3的元素
3. 通过下标定位
:nth-child(n) 理解了这个,下面的就好理解了
:nth-last-child(n) 当n为1时,表示最后一个child
:first-child 相当于nth-child(1)
:last-child 相当于nth-last-child(1)
:only-child 定位到的元素的父节点只有一个子节点时,才符合要求,后面有个例子
这些用法很难理解,查了很多地方,没搞明白,通过做实验观察结果,来总结它的用法。我们重写一个html来做实验
<html><head></head><body>
<div> #1
<input id="test" class="sweet"> #num1
<input id="test" class="sweet"> #num2
<a>big</a>
<img name="haha">
<button></button>
<p>
<a>test</a>
</p>
</div>
<div> #2
<input id="test" class="sweet"> #num3
<a>small</a>
<img></img>
<button></button>
<div>2</div> #4
<p>
<a>test</a>
<a>test</a>
</p>
</div>
<div> #3
<input id="test" class="sweet"> #num4
<a>small</a>
<img></img>
<button></button>
<div>3</div> #5
</div>
</body></html>
谁的孩子??
.sweet:nth-child(1)
我们选中的是什么呢? 选中的是#num1,#num3,#num4, 这个不是很好理解,.sweet:nth-child(2)选中的是#num2, 可以理解。 但是.sweet:nth-child(3) 或者比3更大的值,就选不到东西了,明明.sweet能搜到4个节点,这是为什么呢?
我的理解是:.sweet:nth-child(n) 这个表达式的意思是, .sweet所定位的所有元素中,属于同一个父节点的子元素中的第n个元素。
这样,上面的结果我们是不是就可以理解了呢。 不过这是我的推测,如果不正确的地方请指正。
男孩还是女孩??
body:nth-child(1)
能理解上面那个表达式了,但是马上就遇到问题了,body:nth-child(1) 这个选不到任何元素。 但是用head:nth-child(1)可以定位到head元素,为什么呢?
我的理解是:body定位到元素body,body节点的父节点是html,html的第一个子元素是head,但是head节点的标签并不是我们所需的body标签,所以没有定位的任何元素。 这里的关键点是,body节点的父节点的子节点可能是各种类型的,但我们最终要定位的是body类型。
为了验证我的猜测,body:nth-child(2) 这个表达式时可以定位的body标签的。
不同辈分的孩子怎么寻找父亲??
div:nth-child(1)
我们已经知道了,理解的关键是要找到目标元素的父节点。 但是这里div能定位到的节点在不同的层级中,它能搜索到5个div节点。div:nth-child(1) 对一个的是#1, div:nth-child(2)对应的是#2, div:nth-child(3)对应的是#3, div:nth-child(4)就定位不到任何元素,div:nth-child(5)对应到了#4和#5。
我的理解是:除了处于不同节点集的元素有不同的父节点, 在同一节点集中不同层次的元素也会有不同的父节点。 所以这就好解释了。
当n为1时,
对于父节点body来说,它定位到的是div是#1
对于父节点是#2来说,它定位到的将是#num3,但这个元素是个input,不是我们要找的div
对于父节点是#3来说,它定位到的将是#num4,但这个元素是个input,不是我们要找的div
所以当n为1时,我们能定位一个元素,他就是#1
n为2和3时,处理过程与1相似。
当n为4时,
对于父节点body来说,它没有第四个child
对于父节点是#2来说,它定位到的这个元素是个button,不是我们要找的div
对于父节点是#3来说,它定位到的这个元素是个button,不是我们要找的div
当n为5时,
对于父节点body来说,它没有第5个child
对于父节点是#2来说,它定位到的这个元素是#4
对于父节点是#3来说,它定位到的这个元素是#5
寻找要独生子女
div>p>a:only-child
这个仅仅会定位到#1中p下面的test,因为#2中p下面有2个child,不符合独生子女的要求。
参考文档:
https://selenium-python.readthedocs.io
https://www.runoob.com/xpath/xpath-syntax.html