一、为什么需要等待机制?

网页是动态加载的,元素出现的时间不确定。如果脚本在元素还没加载完成时就尝试操作它,就会抛出 NoSuchElementException 异常。

三种等待方式:

  1. 强制等待time.sleep() - 简单但低效

  2. 隐式等待driver.implicitly_wait() - 全局设置

  3. 显式等待WebDriverWait + expected_conditions - 最推荐的方式


二、强制等待 (不推荐但需了解)

import time

time.sleep(5) # 强制等待5秒
  • 优点:简单易用

  • 缺点

    • 效率低下(总是等待固定时间)

    • 不可靠(网络慢时可能不够,网络快时浪费時間)


三、隐式等待 (Implicit Wait)

设置一个全局的等待时间,对所有元素查找操作都生效。

from selenium import webdriver

driver = webdriver.Chrome()
driver.implicitly_wait(10) # 设置隐式等待时间为10秒

driver.get("https://example.com")
# 所有find_element操作都会最多等待10秒
element = driver.find_element(By.ID, "some-element")
  • 工作原理:在查找元素时,如果立即没找到,会轮询DOM直到找到元素或超时

  • 优点:一次设置,全局生效

  • 缺点

    • 不够灵活,无法针对特定条件等待

    • 可能会影响脚本性能


四、显式等待 (Explicit Wait) - 重点推荐

显式等待允许你设置特定条件,只在需要的地方等待,更加灵活和高效。

1. 基本用法

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By

# 创建WebDriverWait实例,设置最长等待时间10秒
wait = WebDriverWait(driver, 10)

# 等待直到元素可见
element = wait.until(EC.visibility_of_element_located((By.ID, "myDynamicElement")))

# 等待直到元素可点击
element = wait.until(EC.element_to_be_clickable((By.ID, "submit-btn")))

# 然后进行操作
element.click()

2. 常用的 Expected Conditions

Selenium 提供了许多预定义的条件,以下是最常用的几种:

元素存在与可见性
  • presence_of_element_located(locator) - 元素出现在DOM中(不一定可见)

  • visibility_of_element_located(locator) - 元素可见(有宽度和高度)

  • invisibility_of_element_located(locator) - 元素不可见或不存在

元素可交互性
  • element_to_be_clickable(locator) - 元素可见且可点击

  • element_to_be_selected(locator) - 复选框/单选框可被选中

文本内容
  • text_to_be_present_in_element(locator, text) - 元素包含特定文本

  • text_to_be_present_in_element_value(locator, text) - 元素的value属性包含特定文本

页面状态
  • title_is(title) - 页面标题完全匹配

  • title_contains(partial_title) - 页面标题包含特定文本

  • url_to_be(url) - URL完全匹配

  • url_contains(partial_url) - URL包含特定文本

元素选择状态
  • element_located_to_be_selected(locator) - 元素被选中

  • element_located_selection_state_to_be(locator, is_selected) - 元素选中状态符合预期

元素数量
  • presence_of_all_elements_located(locator) - 至少找到一个元素

  • number_of_elements_to_be(locator, number) - 找到特定数量的元素

  • number_of_elements_to_be_less_than(locator, number) - 元素数量少于指定值

  • number_of_elements_to_be_more_than(locator, number) - 元素数量多于指定值

3. 自定义等待条件

如果预定义条件不满足需求,你可以创建自定义等待条件:

from selenium.webdriver.support.ui import WebDriverWait

# 自定义等待函数 - 等待元素包含特定类名
def element_has_class(locator, class_name):
    def predicate(driver):
        element = driver.find_element(*locator)
        if class_name in element.get_attribute("class").split():
            return element
        return False
    return predicate

# 使用自定义等待条件
wait = WebDriverWait(driver, 10)
element = wait.until(element_has_class((By.ID, "my-element"), "active"))

4. 高级用法:设置轮询频率和忽略异常

from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import NoSuchElementException

# 设置等待时间10秒,每0.5秒检查一次,忽略NoSuchElementException异常
wait = WebDriverWait(
    driver, 
    timeout=10, 
    poll_frequency=0.5,
    ignored_exceptions=[NoSuchElementException]
)

element = wait.until(EC.visibility_of_element_located((By.ID, "my-element")))

五、混合使用等待策略

最佳实践是结合使用隐式等待和显式等待:

from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By

driver = webdriver.Chrome()
# 设置一个较短的隐式等待作为后备
driver.implicitly_wait(5)

driver.get("https://example.com")

try:
    # 使用显式等待处理关键元素
    wait = WebDriverWait(driver, 10)
    login_button = wait.until(EC.element_to_be_clickable((By.ID, "login-btn")))
    login_button.click()
    
    # 等待页面跳转完成(URL变化)
    wait.until(EC.url_contains("dashboard"))
    
    # 等待欢迎消息出现
    welcome_message = wait.until(
        EC.visibility_of_element_located((By.CLASS_NAME, "welcome-msg"))
    )
    print("登录成功:" + welcome_message.text)
    
finally:
    driver.quit()

六、实战示例:处理动态加载内容

假设有一个页面,点击按钮后通过AJAX加载内容:

from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys

driver = webdriver.Chrome()
wait = WebDriverWait(driver, 10)

driver.get("https://example.com/dynamic-content")

# 点击加载更多按钮
load_more_btn = wait.until(EC.element_to_be_clickable((By.ID, "load-more")))
load_more_btn.click()

# 等待新内容加载完成(等待特定元素出现)
# 方法1:等待新增的元素
new_item = wait.until(
    EC.presence_of_element_located((By.XPATH, "//div[@class='item'][last()]"))
)

# 方法2:等待加载指示器消失
wait.until(
    EC.invisibility_of_element_located((By.ID, "loading-spinner"))
)

# 方法3:等待特定数量的元素
wait.until(
    EC.number_of_elements_to_be((By.CLASS_NAME, "item"), 10)
)

print("新内容加载完成!")
driver.quit()

七、常见问题与解决方案

1. 等待超时怎么办?

from selenium.common.exceptions import TimeoutException

try:
    element = wait.until(EC.visibility_of_element_located((By.ID, "slow-element")))
except TimeoutException:
    print("元素加载超时,执行备用方案")
    # 执行其他操作或重新加载页面
    driver.refresh()
    # 再次尝试等待
    element = wait.until(EC.visibility_of_element_located((By.ID, "slow-element")))

2. 处理StaleElementReferenceException

当元素不再附加到DOM时会发生此异常,常见于页面刷新或AJAX更新后:

from selenium.common.exceptions import StaleElementReferenceException

def wait_for_non_stale_element(locator, timeout=10):
    wait = WebDriverWait(driver, timeout)
    return wait.until(lambda d: d.find_element(*locator))

element = wait_for_non_stale_element((By.ID, "dynamic-element"))

3. 等待多个条件

# 使用lambda表达式组合多个条件
wait.until(lambda d: 
    d.find_element(By.ID, "element1").is_displayed() and 
    d.find_element(By.ID, "element2").is_enabled()
)

八、最佳实践总结

  1. 优先使用显式等待:针对特定条件等待,更加精确和高效

  2. 合理设置超时时间:根据网络速度和页面复杂度设置

  3. 使用合适的预期条件:根据具体需求选择最匹配的条件

  4. 避免混合使用隐式和显式等待:可能导致不可预测的等待时间

  5. 处理异常:使用try-except块处理可能的超时异常

  6. 编写自定义等待条件:当内置条件不满足需求时

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐