通过查看 selenium 源码中的 By 类,可以知道 selenium 中支持 8 种定位的方式
class By(object):
"""
Set of supported locator strategies.
"""
ID = "id"
XPATH = "xpath"
LINK_TEXT = "link text"
PARTIAL_LINK_TEXT = "partial link text"
NAME = "name"
TAG_NAME = "tag name"
CLASS_NAME = "class name"
CSS_SELECTOR = "css selector"
然后通过查看 WebDriver 类下的 find_element 方法,可以明确 ID、TAG_NAME、CLASS_NAME、NAME 最终对应的底层策略都是 CSS_SELECTOR
def find_element(self, by=By.ID, value=None):
"""
Find an element given a By strategy and locator. Prefer the find_element_by_* methods when
possible.
:Usage:
element = driver.find_element(By.ID, 'foo')
:rtype: WebElement
"""
if self.w3c:
if by == By.ID:
by = By.CSS_SELECTOR
value = '[id="%s"]' % value
elif by == By.TAG_NAME:
by = By.CSS_SELECTOR
elif by == By.CLASS_NAME:
by = By.CSS_SELECTOR
value = ".%s" % value
elif by == By.NAME:
by = By.CSS_SELECTOR
value = '[name="%s"]' % value
return self.execute(Command.FIND_ELEMENT, {
'using': by,
'value': value})['value']
定位速度比 xpath 快,但支持 web 和 webview 在原生的 app 上不支持
选择器 | 例子 | 描述 |
---|---|---|
.class | .intro | 所有 class='intro'的元素,等价于 [id='intro'] |
#id | #name | 所有 id='name'的元素,等价于 [class='name'] |
* | * | 所有元素 |
element | p | 所有<p>元素 |
element,element | div,p | 所有<div>和<p>元素 |
element element | div p | <div>元素内部的所有<p>元素,即在子孙节点 |
element>element | div>p | 父元素为<div>元素的所有<p>元素,即父子节点 |
element+element | div+p | 跟着<div>元素的所有<p>元素,即兄弟节点 |
[attribute] | [target] | 所有带有 target 属性的元素 |
[attribute=value] | [target=_blank] | 所有 target 属性值为_blank 的元素 |
:nth-child(n) | p:nth-child(2) | 位于父元素的第二个子元素的 元素 |
:nth-last-child(n) | p:nth-last-child(2) | 位于父元素的倒数第二个子元素的 元素 |
element1~element2 | p~ul | 选择前面有<p>元素的每个<ul>元素 |
表达式 | 结果 |
---|---|
nodename | 选取此节点的左右子节点 |
/ | 从根节点选取 |
// | 子孙节点 |
. | 当前节点 |
.. | 当前节点的父节点 |
@ | 选取属性 |
示例:
表达式 | 结果 |
---|---|
/bookstore/book[1] | 选取 document 下 bookstore 节点下的第一个 book 元素 |
/bookstore/book[last()] | 选取 document 下 bookstore 节点下的最后一个 book 元素 |
/bookstore/book[position()<3] | 选取 document 下 bookstore 节点下的前两个 book 元素 |
/bookstore/book[price>35.00] | 选取 document 下 bookstore 节点下的 price 元素大于 35 的 book 元素 |
//title[@lang='eng'] | 选取 doucumen 下所有子孙节点中属性 lang 为 eng 的 title 元素 |
与 java 语言一样,python 语言中也存在 sleep 函数,用于让当前线程休眠,不同的是 python 中的 sleep 函数的单位默认为秒
time.sleep(3) # 强制等待3秒
但是这个方法有个问题,即使一开始就找到元素了,仍然会等待 3 秒。
selenium 中提供了更高效的等待方式,隐式等待,它会设置一个等待时间,轮询查找(默认 0.5 秒)元素是否出现,如果没出现就抛出异常,单位默认为秒。
self.driver.implicitly_wait(3)
这是一个全局的等待时间,即一旦设置这个时间,每次进行元素查找的时候,都会在这个等待时间内进行轮询,直到找到元素。
但是这个方法也存在一定的问题,就是无法更加精确的对元素进行判断,列入有些元素可见但是不一定可点击。
在代码中定义等待条件,当条件发生时才继续执行代码
WebDriverWait 配合 until() 和 until_not() 方法,根据判断条件进行等待,程序每隔一段时间(默认为 0.5 秒)进行条件判断,如果条件成立,则执行下一步,否则继续等待,直到超过设置的等待时间,抛出异常,单位默认为秒。
wait = WebDriverWait(driver=self.driver, timeout=10)
wait.until(expected_conditions.visibility_of_element_located(
self.driver.find_elements_by_xpath('//*[@class="style__clients___iw5uL"]')))
上面代码中设置了一个超时时间 10 秒,然后会一直等待到该元素位置可见。
POLL_FREQUENCY = 0.5 # 轮询时间
IGNORED_EXCEPTIONS = (NoSuchElementException,) # 忽略哪些异常
class WebDriverWait(object):
def __init__(self, driver, timeout, poll_frequency=POLL_FREQUENCY, ignored_exceptions=None):
...
从 WebDriverWait 的源码中可以看到,其轮询的默认时间为 0.5s,可以通过参数 poll_frequency 进行设置;其默认是会抛出所有异常的,也可以通过 ignored_exceptions 参数去指定忽略哪些异常
WebDriverWait 会结合 expected_conditions 模块去对当前元素的状态进行检查,主要方法如下:
class url_contains(object): # url包含什么
class url_matches(object): # url匹配正则表达式
class url_to_be(object): # url是什么
class title_is(object): # 浏览器标题是什么
class title_contains(object): # 浏览器标题包含什么
class presence_of_element_located(object): # 元素在页面的DOM结构上是存在的,并不意味着可见
class visibility_of_element_located(object):# 元素在页面上是可见的,即元素的display属性不为none或者visibility不为hidden等隐藏元素,此外,该元素的长和宽的尺寸都大于0
class text_to_be_present_in_element(object): # 元素中出现具体的文字信息,即元素的text方法可以取到值
class text_to_be_present_in_element_value(object): # 元素的value属性中出现具体的文字信息,即元素的get_attribute("value")方法可以取到值
class invisibility_of_element_located(object): # 元素不可见,或不在DOM结构中
class element_to_be_clickable(object): # 元素可以被点击,意味着元素是可见的,并且是可用的,即元素的disabled属性为false,这样才能点击它
class element_to_be_selected(object): # 元素是被选中的,即单元框或者多选框元素的checked属性为true
class new_window_is_opened(object): # 新的标签页被打开,通过判断窗口句柄是否增多
class alert_is_present(object): # alert弹窗出现,通过switch_to.alert方法
执行 PC 端的鼠标点击,双击,右键,拖拽等事件
原理:调用 ActionChains 的方法时,不会立即执行,而是将所有的操作按顺序存放在一个队列里,当调用 perform() 方法时,队列中的事件会依次执行
def test_play(self):
self.driver.get("https://music.163.com/#/playlist?id=3232747189")
self.driver.switch_to.frame(self.driver.find_element_by_name("contentFrame"))
self.driver.find_element_by_xpath('//*[@id="content-operation"]//a[@data-res-action="play"]').click()
self.driver.switch_to.default_content()
# 点击后,确保当前用例播放栏不被隐藏
self.driver.find_element_by_xpath('//*[@data-action="lock"]').click()
progress_btn = self.driver.find_element_by_xpath('//*[@id="g_player"]//*[@class="btn f-tdn f-alpha"]')
progress_bar = self.driver.find_element_by_xpath('//*[@id="g_player"]//*[@class="m-pbar"]')
# 动作链
chains = ActionChains(self.driver)
chains.click_and_hold(progress_btn)
# 针对progress_btn而言进行移动,此时以progress_btn为原点,向左移动(x轴)到progress_bar的一半,纵坐标(y轴)不变
chains.move_by_offset(xoffset=progress_bar.size['width'] / 2, yoffset=0)
chains.release()
chains.perform()
上面的代码用于拖动进度条,构建了一个动作链:
click_and_hold 在某个元素上按下鼠标左键
move_by_offset 移动到什么坐标
release 松开鼠标左键
最后通过 perform 方法依次执行了动作链中的每个动作,效果如下:
模拟 PC 和移动端的点击,滑动,拖拽,多点触控等多种手势操作,原理于 ActionChains 类似,先生成动作链,然后通过 perform 方法去依次执行每个动作
当页面上某个可点击元素的 target 属性为"_blank"时,点击后就会打开一个新的标签页,例如网易云音乐工具栏上的 “商城” 按钮,点击后就会打开一个新的标签页 “云音乐商城”,此时如果相对新的标签页进行操作,就需要进行窗口的跳转。
from selenium import webdriver
class TestToolBar:
def setup_class(self):
self.driver = webdriver.Chrome(executable_path="D:\\87\\chromedriver.exe")
self.driver.maximize_window()
self.driver.implicitly_wait(10)
def teardown_class(self):
self.driver.quit() # 关闭所有的标签页,关闭浏览器
def setup_method(self):
self.driver.get("https://music.163.com/")
self.current_window = self.driver.current_window_handle # 获取当前窗口句柄
def teardown_method(self):
self.driver.close() # 关闭当前标签页
self.driver.switch_to.window(self.current_window) # 切换到首页窗口
def test_mart_link(self):
self.driver.find_element_by_xpath('//*[@data-module="store"]').click()
self.driver.switch_to.window(self.driver.window_handles[1]) # 获取所有的窗口句柄后,切换到第二个窗口句柄
mart_link_title = self.driver.title
assert "云音乐商城" in mart_link_title
def test_musician_link(self):
self.driver.find_element_by_xpath('//*[@data-module="musician"]').click()
self.driver.switch_to.window(self.driver.window_handles[1]) # 获取所有的窗口句柄后,切换到第二个窗口句柄
mart_link_title = self.driver.title
assert "网易音乐人" in mart_link_title
执行接口如下:
test_tool_bar.py::TestToolBar::test_mart_link
test_tool_bar.py::TestToolBar::test_musician_link
============================= 2 passed in 18.42s ==============================
有时候会存在这样的清空,刚打开页面的时候用$x(...)
无法定位元素,当右键点击元素,选择检查后,再次使用$x(...)
又可以定位成功了,然后在代码中通过 xpath 定位发现总是报错:NoSuchElementException。
这个时候就要看看这个元素的最外层的元素是否在<iframe>标签中,如网易云中的页面:
如果我们要定位的元素位于<iframe>标签内,就需要先切换到 iframe,然后再进行元素定位:
def setup_method(self):
self.driver.get("https://music.163.com/")
# 无法定位元素的原因是因为嵌套在iframe中,需要先进入iframe再进行元素定位
# 表现形式:通过$x('...')无法定位,当点击检查元素后,通过$x('...')又可以定位了
self.driver.switch_to.frame(self.driver.find_element_by_name("contentFrame"))
def test_banner(self):
self.driver.find_element_by_xpath('//*[@id="index-banner"]//img').click()
有些情况下,点击按钮后会弹出 alert 确认框,此时需要对 alert 框进行处理,主要有如下方法:
# 相等于点击alert框的确定按钮
self.driver.switch_to.alert.accept()
# 相等于点击alert框的取消按钮
self.driver.switch_to.alert.dismiss()