Selenium 4.10.0 异常抛出机制分析

发布于:2025-07-02 ⋅ 阅读:(20) ⋅ 点赞:(0)

Selenium 4.10.0 异常抛出机制分析

1. 异常体系结构

Selenium的异常体系以 WebDriverException 为基类,位于 selenium4.10.0/common/exceptions.py

class WebDriverException(Exception):
    """Base webdriver exception."""

    def __init__(
        self, msg: Optional[str] = None, screen: Optional[str] = None, stacktrace: Optional[Sequence[str]] = None
    ) -> None:
        super().__init__()
        self.msg = msg
        self.screen = screen
        self.stacktrace = stacktrace

    def __str__(self) -> str:
        exception_msg = f"Message: {self.msg}\n"
        if self.screen:
            exception_msg += "Screenshot: available via screen\n"
        if self.stacktrace:
            stacktrace = "\n".join(self.stacktrace)
            exception_msg += f"Stacktrace:\n{stacktrace}"
        return exception_msg

2. 核心异常处理流程

2.1 命令执行流程

当执行WebDriver命令时,异常抛出的主要流程如下:

  1. 命令发送WebDriver.execute() 方法发送命令
  2. 响应接收RemoteConnection.execute() 接收服务器响应
  3. 异常检查ErrorHandler.check_response() 检查响应中的错误
  4. 异常抛出:根据错误代码抛出相应的异常
2.2 错误处理器核心逻辑

ErrorHandler.check_response() 方法是异常处理的核心:

def check_response(self, response: Dict[str, Any]) -> None:
    """Checks that a JSON response from the WebDriver does not have an
    error.

    :Args:
     - response - The JSON response from the WebDriver server as a dictionary
       object.

    :Raises: If the response contains an error message.
    """
    status = response.get("status", None)
    if not status or status == ErrorCode.SUCCESS:
        return
    value = None
    message = response.get("message", "")
    screen: str = response.get("screen", "")
    stacktrace = None
    if isinstance(status, int):
        value_json = response.get("value", None)
        if value_json and isinstance(value_json, str):
            import json

            try:
                value = json.loads(value_json)
                if len(value) == 1:
                    value = value["value"]
                status = value.get("error", None)
                if not status:
                    status = value.get("status", ErrorCode.UNKNOWN_ERROR)
                    message = value.get("value") or value.get("message")
                    if not isinstance(message, str):
                        value = message
                        message = message.get("message")
                else:
                    message = value.get("message", None)
            except ValueError:
                pass

    exception_class: Type[WebDriverException]
    if status in ErrorCode.NO_SUCH_ELEMENT:
        exception_class = NoSuchElementException
    elif status in ErrorCode.NO_SUCH_FRAME:
        exception_class = NoSuchFrameException
    elif status in ErrorCode.NO_SUCH_SHADOW_ROOT:
        exception_class = NoSuchShadowRootException
    elif status in ErrorCode.NO_SUCH_WINDOW:
        exception_class = NoSuchWindowException
    elif status in ErrorCode.STALE_ELEMENT_REFERENCE:
        exception_class = StaleElementReferenceException
    elif status in ErrorCode.ELEMENT_NOT_VISIBLE:
        exception_class = ElementNotVisibleException
    elif status in ErrorCode.INVALID_ELEMENT_STATE:
        exception_class = InvalidElementStateException
    elif (
        status in ErrorCode.INVALID_SELECTOR
        or status in ErrorCode.INVALID_XPATH_SELECTOR
        or status in ErrorCode.INVALID_XPATH_SELECTOR_RETURN_TYPER
    ):
        exception_class = InvalidSelectorException
    elif status in ErrorCode.ELEMENT_IS_NOT_SELECTABLE:
        exception_class = ElementNotSelectableException
    elif status in ErrorCode.ELEMENT_NOT_INTERACTABLE:
        exception_class = ElementNotInteractableException
    elif status in ErrorCode.INVALID_COOKIE_DOMAIN:
        exception_class = InvalidCookieDomainException
    elif status in ErrorCode.UNABLE_TO_SET_COOKIE:
        exception_class = UnableToSetCookieException
    elif status in ErrorCode.TIMEOUT:
        exception_class = TimeoutException
    elif status in ErrorCode.SCRIPT_TIMEOUT:
        exception_class = TimeoutException
    elif status in ErrorCode.UNKNOWN_ERROR:
        exception_class = WebDriverException
    elif status in ErrorCode.UNEXPECTED_ALERT_OPEN:
        exception_class = UnexpectedAlertPresentException
    elif status in ErrorCode.NO_ALERT_OPEN:
        exception_class = NoAlertPresentException
    elif status in ErrorCode.IME_NOT_AVAILABLE:
        exception_class = ImeNotAvailableException
    elif status in ErrorCode.IME_ENGINE_ACTIVATION_FAILED:
        exception_class = ImeActivationFailedException
    elif status in ErrorCode.MOVE_TARGET_OUT_OF_BOUNDS:
        exception_class = MoveTargetOutOfBoundsException
    elif status in ErrorCode.JAVASCRIPT_ERROR:
        exception_class = JavascriptException
    elif status in ErrorCode.SESSION_NOT_CREATED:
        exception_class = SessionNotCreatedException
    elif status in ErrorCode.INVALID_ARGUMENT:
        exception_class = InvalidArgumentException
    elif status in ErrorCode.NO_SUCH_COOKIE:
        exception_class = NoSuchCookieException
    elif status in ErrorCode.UNABLE_TO_CAPTURE_SCREEN:
        exception_class = ScreenshotException
    elif status in ErrorCode.ELEMENT_CLICK_INTERCEPTED:
        exception_class = ElementClickInterceptedException
    elif status in ErrorCode.INSECURE_CERTIFICATE:
        exception_class = InsecureCertificateException
    elif status in ErrorCode.INVALID_COORDINATES:
        exception_class = InvalidCoordinatesException
    elif status in ErrorCode.INVALID_SESSION_ID:
        exception_class = InvalidSessionIdException
    elif status in ErrorCode.UNKNOWN_METHOD:
        exception_class = UnknownMethodException
    else:
        exception_class = WebDriverException
    if not value:
        value = response["value"]
    if isinstance(value, str):
        raise exception_class(value)
    if message == "" and "message" in value:
        message = value["message"]

    screen = None  # type: ignore[assignment]
    if "screen" in value:
        screen = value["screen"]

    stacktrace = None
    st_value = value.get("stackTrace") or value.get("stacktrace")
    if st_value:
        if isinstance(st_value, str):
            stacktrace = st_value.split("\n")
        else:
            stacktrace = []
            try:
                for frame in st_value:
                    line = frame.get("lineNumber", "")
                    file = frame.get("fileName", "<anonymous>")
                    if line:
                        file = f"{file}:{line}"
                    meth = frame.get("methodName", "<anonymous>")
                    if "className" in frame:
                        meth = f"{frame['className']}.{meth}"
                    msg = "    at %s (%s)"
                    msg = msg % (meth, file)
                    stacktrace.append(msg)
            except TypeError:
                pass
    if exception_class == UnexpectedAlertPresentException:
        alert_text = None
        if "data" in value:
            alert_text = value["data"].get("text")
        elif "alert" in value:
            alert_text = value["alert"].get("text")
        raise exception_class(message, screen, stacktrace, alert_text)  # type: ignore[call-arg]  # mypy is not smart enough here
    raise exception_class(message, screen, stacktrace)

3. 异常抛出的具体方法

3.1 错误代码映射

ErrorCode 类定义了各种错误代码与异常类型的映射关系:

54:95:selenium4.10.0/webdriver/remote/errorhandler.py

class ErrorCode:
    """Error codes defined in the WebDriver wire protocol."""

    # Keep in sync with org.openqa.selenium.remote.ErrorCodes and errorcodes.h
    SUCCESS = 0
    NO_SUCH_ELEMENT = [7, "no such element"]
    NO_SUCH_FRAME = [8, "no such frame"]
    NO_SUCH_SHADOW_ROOT = ["no such shadow root"]
    UNKNOWN_COMMAND = [9, "unknown command"]
    STALE_ELEMENT_REFERENCE = [10, "stale element reference"]
    ELEMENT_NOT_VISIBLE = [11, "element not visible"]
    INVALID_ELEMENT_STATE = [12, "invalid element state"]
    UNKNOWN_ERROR = [13, "unknown error"]
    ELEMENT_IS_NOT_SELECTABLE = [15, "element not selectable"]
    JAVASCRIPT_ERROR = [17, "javascript error"]
    XPATH_LOOKUP_ERROR = [19, "invalid selector"]
    TIMEOUT = [21, "timeout"]
    NO_SUCH_WINDOW = [23, "no such window"]
    INVALID_COOKIE_DOMAIN = [24, "invalid cookie domain"]
    UNABLE_TO_SET_COOKIE = [25, "unable to set cookie"]
    UNEXPECTED_ALERT_OPEN = [26, "unexpected alert open"]
    NO_ALERT_OPEN = [27, "no such alert"]
    SCRIPT_TIMEOUT = [28, "script timeout"]
    INVALID_ELEMENT_COORDINATES = [29, "invalid element coordinates"]
    IME_NOT_AVAILABLE = [30, "ime not available"]
    IME_ENGINE_ACTIVATION_FAILED = [31, "ime engine activation failed"]
    INVALID_SELECTOR = [32, "invalid selector"]
    SESSION_NOT_CREATED = [33, "session not created"]
    MOVE_TARGET_OUT_OF_BOUNDS = [34, "move target out of bounds"]
    INVALID_XPATH_SELECTOR = [51, "invalid selector"]
    INVALID_XPATH_SELECTOR_RETURN_TYPER = [52, "invalid selector"]

    ELEMENT_NOT_INTERACTABLE = [60, "element not interactable"]
    INSECURE_CERTIFICATE = ["insecure certificate"]
    INVALID_ARGUMENT = [61, "invalid argument"]
    INVALID_COORDINATES = ["invalid coordinates"]
    INVALID_SESSION_ID = ["invalid session id"]
    NO_SUCH_COOKIE = [62, "no such cookie"]
    UNABLE_TO_CAPTURE_SCREEN = [63, "unable to capture screen"]
    ELEMENT_CLICK_INTERCEPTED = [64, "element click intercepted"]
    UNKNOWN_METHOD = ["unknown method exception"]

    METHOD_NOT_ALLOWED = [405, "unsupported operation"]
3.2 命令执行中的异常检查

WebDriver.execute() 方法中,每次命令执行后都会检查响应:

325:350:selenium4.10.0/webdriver/remote/webdriver.py

def execute(self, driver_command: str, params: dict = None) -> dict:
    """Sends a command to be executed by a command.CommandExecutor.

    :Args:
     - driver_command: The name of the command to execute as a string.
     - params: A dictionary of named parameters to send with the command.

    :Returns:
      The command's JSON response loaded into a dictionary object.
    """
    # 序列化操作
    params = self._wrap_value(params)

    if self.session_id:
        if not params:
            params = {"sessionId": self.session_id}
        elif "sessionId" not in params:
            params["sessionId"] = self.session_id

    response = self.command_executor.execute(driver_command, params)
    if response:
        self.error_handler.check_response(response)  # 这里检查异常
        #反序列化操作
        response["value"] = self._unwrap_value(response.get("value", None))
        return response
    # If the server doesn't send a response, assume the command was
    # a success
    return {"success": 0, "value": None, "sessionId": self.session_id}

4. 主要异常类型

Selenium定义了多种具体的异常类型,包括:

  • 元素相关异常NoSuchElementExceptionStaleElementReferenceExceptionElementNotVisibleException
  • 交互相关异常ElementNotInteractableExceptionElementClickInterceptedException
  • 选择器异常InvalidSelectorException
  • 超时异常TimeoutException
  • 会话异常SessionNotCreatedExceptionInvalidSessionIdException
  • JavaScript异常JavascriptException

5. 异常信息增强

某些异常类会增强错误信息,例如 NoSuchElementException

65:83:selenium4.10.0/common/exceptions.py

class NoSuchElementException(WebDriverException):
    """Thrown when element could not be found.

    If you encounter this exception, you may want to check the following:
        * Check your selector used in your find_by...
        * Element may not yet be on the screen at the time of the find operation,
          (webpage is still loading) see selenium.webdriver.support.wait.WebDriverWait()
          for how to write a wait wrapper to wait for an element to appear.
    """

    def __init__(
        self, msg: Optional[str] = None, screen: Optional[str] = None, stacktrace: Optional[Sequence[str]] = None
    ) -> None:
        with_support = f"{msg}; {SUPPORT_MSG} {ERROR_URL}#no-such-element-exception"

        super().__init__(with_support, screen, stacktrace)

总结

Selenium的异常抛出机制主要通过以下步骤实现:

  1. 命令执行服务器响应错误检查异常映射异常抛出
  2. 使用 ErrorHandler.check_response() 作为核心异常检查方法
  3. 通过 ErrorCode 类映射错误代码到具体异常类型
  4. 异常包含详细的信息(消息、截图、堆栈跟踪)
  5. 某些异常会增强错误信息,提供解决建议