Web 自动化稳定性:六类真实异常与修复代码¶
UI 用例“有时过有时不过”,90% 能归到这六类异常。每类给出原始报错、根因和可直接用的修复。
1. 异常对照表¶
| 异常 | 典型消息 | 根因 |
|---|---|---|
NoSuchElementException |
Unable to locate element: {...} |
未加载、iframe、Shadow DOM、新窗口 |
ElementNotInteractableException |
element not interactable |
元素隐藏、宽度为 0、被禁用 |
ElementClickInterceptedException |
element click intercepted: Other element would receive the click |
遮罩、动画、吸底栏 |
StaleElementReferenceException |
element is not attached to the page document |
DOM 重渲染 |
TimeoutException |
Expected condition failed: waiting for visibility of element |
等待条件不对、异步未完成 |
WebDriverException |
session deleted because of page crash |
容器 /dev/shm 太小 |
2. 等待:不要用 sleep 堆¶
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class BasePage:
def __init__(self, driver, timeout=10):
self.driver = driver
self.wait = WebDriverWait(driver, timeout, poll_frequency=0.2,
ignored_exceptions=(StaleElementReferenceException,))
def visible(self, locator):
return self.wait.until(EC.visibility_of_element_located(locator))
def clickable(self, locator):
return self.wait.until(EC.element_to_be_clickable(locator))
要点:
poll_frequency=0.2比默认 0.5 反应更快,又不会空转太多ignored_exceptions忽略 stale,等元素重新挂载- 不要同时开隐式等待:
implicitly_wait与显式等待叠加,实际等待时间会变成两者之和,超时时间不可控。统一用显式等待,隐式设为 0
3. StaleElement:不要缓存 WebElement¶
# 错误:列表刷新后 item 失效
items = driver.find_elements(By.CSS_SELECTOR, ".order-item")
for item in items:
item.click() # 第二次点击可能 Stale
# 正确:每次重新定位
for i in range(len(items)):
driver.find_elements(By.CSS_SELECTOR, ".order-item")[i].click()
更通用的做法是封装重试:
import functools, time
from selenium.common.exceptions import StaleElementReferenceException
def retry_stale(times=3, delay=0.3):
def deco(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
last = None
for _ in range(times):
try:
return fn(*args, **kwargs)
except StaleElementReferenceException as e:
last = e
time.sleep(delay)
raise last
return wrapper
return deco
@retry_stale()
def click_first_item(page):
page.clickable((By.CSS_SELECTOR, ".order-item")).click()
4. ElementClickIntercepted:先等遮挡物消失¶
def safe_click(driver, locator, overlay=(By.CSS_SELECTOR, ".loading-mask")):
wait = WebDriverWait(driver, 10)
try:
wait.until(EC.invisibility_of_element_located(overlay))
except TimeoutException:
pass
el = wait.until(EC.element_to_be_clickable(locator))
driver.execute_script("arguments[0].scrollIntoView({block:'center'})", el)
try:
el.click()
except ElementClickInterceptedException:
# 最后手段:JS 点击(会绕过部分前端校验,慎用)
driver.execute_script("arguments[0].click()", el)
注意:JS 点击不会触发真实鼠标事件,某些组件(如基于 mousedown 的日期选择器)会失效。能修页面定位就优先修定位。
5. iframe 与 Shadow DOM¶
iframe:
wait.until(EC.frame_to_be_available_and_switch_to_it((By.ID, "pay-frame")))
driver.find_element(By.ID, "card-no").send_keys("6222...")
driver.switch_to.default_content()
Shadow DOM(Selenium 4 支持直接穿透部分场景,但嵌套 shadow root 仍需 JS):
host = driver.find_element(By.CSS_SELECTOR, "order-list")
inner = driver.execute_script(
"return arguments[0].shadowRoot.querySelector('button.submit')", host)
inner.click()
6. 新窗口与下载¶
# 新标签页
main = driver.current_window_handle
driver.find_element(By.LINK_TEXT, "发票").click()
WebDriverWait(driver, 10).until(lambda d: len(d.window_handles) == 2)
new = [h for h in driver.window_handles if h != main][0]
driver.switch_to.window(new)
# 操作完关掉并切回
driver.close()
driver.switch_to.window(main)
无头模式下载:
prefs = {
"download.default_directory": "/tmp/downloads",
"download.prompt_for_download": False,
"plugins.always_open_pdf_externally": True,
}
o.add_experimental_option("prefs", prefs)
无头 Chrome 下载有时需要 CDP:
driver.execute_cdp_cmd("Page.setDownloadBehavior", {
"behavior": "allow", "downloadPath": "/tmp/downloads"})
7. CI 容器参数¶
o.add_argument("--headless=new")
o.add_argument("--no-sandbox")
o.add_argument("--disable-dev-shm-usage")
o.add_argument("--disable-gpu")
o.add_argument("--window-size=1440,900")
| 报错 | 对应参数 |
|---|---|
Chrome failed to start: crashed |
--no-sandbox |
session deleted because of page crash |
--disable-dev-shm-usage 或 --shm-size=2g |
| 元素位置偏移、点击落空 | --window-size=1440,900(默认 800×600 会触发移动布局) |
| 中文变方块 | 镜像安装 fonts-noto-cjk |
8. 用接口准备数据,不要用 UI 铺路¶
# 错误:为了测“取消订单”,用 UI 下 5 单,耗时 40s 且不稳定
# 正确:接口造数,UI 只验证取消动作
order_id = api.create_order(sku_id=1001)["data"]["orderId"]
page.open_order(order_id).cancel()
UI 用例只覆盖“必须通过界面验证”的部分,其余用接口准备与断言。
9. 重试策略¶
原则:
- 只对已知偶发用例加重试,不要全局开
- 重试次数 ≤ 2,延迟 ≥ 1s
- 记录重试率;重试率 > 3% 时停下来修根因,而不是继续加次数