mirror of
https://github.com/KenanZhu/AutoLibrary.git
synced 2026-06-17 23:13:03 +08:00
feat(WebDriverManager): 新增浏览器管理类 WebDriverManager
- 新增浏览器管理类,支持下载和管理浏览器驱动
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
import platform
|
||||
import installed_browsers
|
||||
|
||||
from pathlib import Path
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class WebBrowserType(Enum):
|
||||
"""
|
||||
Web browser type
|
||||
"""
|
||||
|
||||
CHROME = "chrome"
|
||||
FIREFOX = "firefox"
|
||||
EDGE = "edge"
|
||||
|
||||
|
||||
class WebBrowserArch(Enum):
|
||||
"""
|
||||
Web browser architecture
|
||||
"""
|
||||
|
||||
WINX86_32 = 0
|
||||
WINX86_64 = 1
|
||||
WINARM = 2
|
||||
|
||||
LINUXX86_32 = 3
|
||||
LINUXX86_64 = 4
|
||||
LINUXARM = 5
|
||||
|
||||
MACX86_64 = 6
|
||||
MACARM = 7
|
||||
|
||||
@dataclass
|
||||
class WebBrowserInfo:
|
||||
"""
|
||||
Web browser information
|
||||
|
||||
Attributes:
|
||||
browser_arch (WebBrowserArch): Web browser architecture
|
||||
browser_type (WebBrowserType): Web browser type
|
||||
browser_version (str): Web browser version
|
||||
browser_path (Path): Web browser executable file path
|
||||
"""
|
||||
|
||||
browser_arch: WebBrowserArch
|
||||
browser_type: WebBrowserType
|
||||
browser_version: str
|
||||
browser_path: Path
|
||||
|
||||
|
||||
class WebBrowserArchDetector:
|
||||
"""
|
||||
Web browser architecture detector
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self
|
||||
):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def detect(
|
||||
self
|
||||
) -> WebBrowserArch:
|
||||
"""
|
||||
Detect system architecture
|
||||
|
||||
Returns:
|
||||
WebBrowserArch: System architecture
|
||||
"""
|
||||
|
||||
system = platform.system()
|
||||
machine = platform.machine().lower()
|
||||
if system == "Windows":
|
||||
if machine in ["amd64", "x86_64"]:
|
||||
return WebBrowserArch.WINX86_64
|
||||
elif machine in ["i386", "i686", "x86"]:
|
||||
return WebBrowserArch.WINX86_32
|
||||
elif machine in ["arm64", "aarch64"]:
|
||||
return WebBrowserArch.WINARM
|
||||
else:
|
||||
return WebBrowserArch.WINX86_64
|
||||
elif system == "Darwin":
|
||||
if machine in ["arm64", "aarch64"]:
|
||||
return WebBrowserArch.MACARM
|
||||
else:
|
||||
return WebBrowserArch.MACX86_64
|
||||
elif system == "Linux":
|
||||
if machine in ["amd64", "x86_64"]:
|
||||
return WebBrowserArch.LINUXX86_64
|
||||
elif machine in ["i386", "i686", "x86"]:
|
||||
return WebBrowserArch.LINUXX86_32
|
||||
elif machine in ["arm64", "aarch64"]:
|
||||
return WebBrowserArch.LINUXARM
|
||||
elif machine.startswith("arm"):
|
||||
return WebBrowserArch.LINUXARM
|
||||
else:
|
||||
return WebBrowserArch.LINUXX86_64
|
||||
raise ValueError(f"不支持的系统架构 : {system} {machine}")
|
||||
|
||||
|
||||
class WebBrowserDetector:
|
||||
"""
|
||||
Web browser detector
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self
|
||||
):
|
||||
|
||||
self.browser_arch = WebBrowserArchDetector().detect()
|
||||
self.browser_infos : list[WebBrowserInfo] = []
|
||||
|
||||
|
||||
def detect(
|
||||
self
|
||||
) -> list[WebBrowserInfo]:
|
||||
|
||||
"""
|
||||
Detect installed web browsers on the system.
|
||||
|
||||
Returns:
|
||||
list[WebBrowserInfo]: List of detected browser information objects.
|
||||
"""
|
||||
|
||||
self.browser_infos = []
|
||||
try:
|
||||
all_browsers = installed_browsers.browsers()
|
||||
except Exception as e:
|
||||
self.browser_infos = []
|
||||
return self.browser_infos
|
||||
|
||||
# Mapping from internal library name to our enum
|
||||
type_map = {
|
||||
'chrome': WebBrowserType.CHROME,
|
||||
'firefox': WebBrowserType.FIREFOX,
|
||||
'msedge': WebBrowserType.EDGE,
|
||||
}
|
||||
for browser in all_browsers:
|
||||
internal_name = browser.get('name', '').lower()
|
||||
if internal_name not in type_map:
|
||||
continue # Not one of the browsers we care about
|
||||
version = browser.get('version')
|
||||
if not version:
|
||||
# Skip browsers with no version info (unlikely, but defensive)
|
||||
continue
|
||||
exe_path = browser.get('location')
|
||||
if not exe_path:
|
||||
continue
|
||||
try:
|
||||
path = Path(exe_path)
|
||||
if not path.is_file():
|
||||
continue
|
||||
except Exception:
|
||||
continue # Invalid path
|
||||
info = WebBrowserInfo(
|
||||
browser_arch=self.browser_arch, # Use system architecture as fallback
|
||||
browser_type=type_map[internal_name],
|
||||
browser_version=version,
|
||||
browser_path=path,
|
||||
)
|
||||
self.browser_infos.append(info)
|
||||
return self.browser_infos
|
||||
@@ -0,0 +1,443 @@
|
||||
import os
|
||||
import time
|
||||
import shutil
|
||||
import requests
|
||||
import zipfile
|
||||
import tarfile
|
||||
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Optional, Callable
|
||||
|
||||
|
||||
class WebDriverType(Enum):
|
||||
"""
|
||||
Web driver type
|
||||
"""
|
||||
|
||||
CHROME = "chrome"
|
||||
FIREFOX = "firefox"
|
||||
EDGE = "edge"
|
||||
|
||||
|
||||
class WebDriverArch(Enum):
|
||||
"""
|
||||
Web driver architecture
|
||||
"""
|
||||
|
||||
class Chrome(Enum):
|
||||
"""
|
||||
Chrome web driver architecture
|
||||
"""
|
||||
|
||||
WINX86_32 = "win32"
|
||||
WINX86_64 = "win64"
|
||||
|
||||
# LINUX86_32 : no support for linux 32bit
|
||||
LINUX86_64 = "linux64"
|
||||
# LINUXARM : no support for linux arm64
|
||||
|
||||
MACX86_64 = "mac-x64"
|
||||
MACARM = "mac-arm64"
|
||||
|
||||
class Firefox(Enum):
|
||||
"""
|
||||
Firefox web driver architecture
|
||||
"""
|
||||
|
||||
WINX86_32 = "win32"
|
||||
WINX86_64 = "win64"
|
||||
WINARM = "win-aarch64"
|
||||
|
||||
LINUXX86_32 = "linux32"
|
||||
LINUXX86_64 = "linux64"
|
||||
LINUXARM = "linux-aarch64"
|
||||
|
||||
MACX86_64 = "macos"
|
||||
MACARM = "macos-aarch64"
|
||||
|
||||
class Edge(Enum):
|
||||
"""
|
||||
Edge web driver architecture
|
||||
"""
|
||||
|
||||
WINX86_32 = "win32"
|
||||
WINX86_64 = "win64"
|
||||
WINARM = "arm64"
|
||||
|
||||
# LINUX86_32 : no support for linux 32bit
|
||||
LINUXX86_64 = "linux64"
|
||||
# LINUXARM : no support for linux arm64
|
||||
|
||||
MACX86_64 = "mac64"
|
||||
MACARM = "mac64_m1"
|
||||
|
||||
|
||||
class WebDriverName:
|
||||
"""
|
||||
Web driver name
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
driver_type: WebDriverType
|
||||
):
|
||||
|
||||
self.driver_type = driver_type
|
||||
|
||||
|
||||
def __str__(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
match self.driver_type:
|
||||
case WebDriverType.CHROME:
|
||||
return "chromedriver"
|
||||
case WebDriverType.FIREFOX:
|
||||
return "geckodriver"
|
||||
case WebDriverType.EDGE:
|
||||
return "msedgedriver"
|
||||
case _:
|
||||
raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}")
|
||||
|
||||
|
||||
class WebDriverExecName:
|
||||
"""
|
||||
Web driver executable file name
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
driver_type: WebDriverType,
|
||||
arch: WebDriverArch
|
||||
):
|
||||
|
||||
self.driver_type = driver_type
|
||||
self.arch = arch
|
||||
|
||||
|
||||
def __str__(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
is_win = True if self.arch is WebDriverArch.Chrome.WINX86_32 or\
|
||||
self.arch is WebDriverArch.Chrome.WINX86_64 or\
|
||||
self.arch is WebDriverArch.Firefox.WINX86_32 or\
|
||||
self.arch is WebDriverArch.Firefox.WINX86_64 or\
|
||||
self.arch is WebDriverArch.Edge.WINX86_32 or\
|
||||
self.arch is WebDriverArch.Edge.WINX86_64 else False
|
||||
match self.driver_type:
|
||||
case WebDriverType.CHROME:
|
||||
return f"{WebDriverName(self.driver_type)}" + (".exe" if is_win else "")
|
||||
case WebDriverType.FIREFOX:
|
||||
return f"{WebDriverName(self.driver_type)}" + (".exe" if is_win else "")
|
||||
case WebDriverType.EDGE:
|
||||
return f"{WebDriverName(self.driver_type)}" + (".exe" if is_win else "")
|
||||
case _:
|
||||
raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}")
|
||||
|
||||
|
||||
class WebDriverFileName:
|
||||
"""\
|
||||
Web driver compressed file name
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
version: str,
|
||||
driver_type: WebDriverType,
|
||||
arch: WebDriverArch
|
||||
):
|
||||
|
||||
self.version = version
|
||||
self.driver_type = driver_type
|
||||
self.arch = arch
|
||||
|
||||
def __str__(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
match self.driver_type:
|
||||
case WebDriverType.CHROME:
|
||||
return f"{WebDriverName(self.driver_type)}-{self.arch.value}.zip"
|
||||
case WebDriverType.FIREFOX:
|
||||
if self.arch is WebDriverArch.Firefox.WINX86_32 or\
|
||||
self.arch is WebDriverArch.Firefox.WINX86_64:
|
||||
suffix = "zip"
|
||||
else:
|
||||
suffix = "tar.gz"
|
||||
return f"{WebDriverName(self.driver_type)}-v{self.version}-{self.arch.value}.{suffix}"
|
||||
case WebDriverType.EDGE:
|
||||
return f"edgedriver_{self.arch.value}.zip" # Edge web driver file name is different
|
||||
case _:
|
||||
raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}")
|
||||
|
||||
|
||||
class WebDriverURL:
|
||||
"""
|
||||
Web driver download URL
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
version: str,
|
||||
driver_type: WebDriverType,
|
||||
arch: WebDriverArch
|
||||
):
|
||||
|
||||
self.version = version
|
||||
self.driver_type = driver_type
|
||||
self.arch = arch
|
||||
self.file_name = str(WebDriverFileName(self.version, self.driver_type, self.arch))
|
||||
|
||||
|
||||
def __str__(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
match self.driver_type:
|
||||
case WebDriverType.CHROME:
|
||||
return f"https://storage.googleapis.com/chrome-for-testing-public/"\
|
||||
f"{self.version}/"\
|
||||
f"{self.arch.value}/"\
|
||||
f"{self.file_name}"
|
||||
case WebDriverType.FIREFOX:
|
||||
return f"https://github.com/mozilla/geckodriver/releases/download/"\
|
||||
f"v{self.version}/"\
|
||||
f"{self.file_name}"
|
||||
case WebDriverType.EDGE:
|
||||
return f"https://msedgedriver.microsoft.com/"\
|
||||
f"{self.version}/"\
|
||||
f"{self.file_name}"
|
||||
case _:
|
||||
raise ValueError(f"不受支持的 web driver 类型 : {self.driver_type}")
|
||||
|
||||
|
||||
class WebDriverDownloader:
|
||||
"""
|
||||
Base class for WebDriver downloaders
|
||||
|
||||
Args:
|
||||
driver_type (WebDriverType): Web driver type
|
||||
version (str): WebDriver version
|
||||
arch (WebDriverArch): WebDriver architecture
|
||||
download_dir (str): Download directory
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
driver_type: WebDriverType,
|
||||
driver_version: str,
|
||||
driver_arch: WebDriverArch,
|
||||
download_dir: str
|
||||
):
|
||||
|
||||
self.driver_type = driver_type
|
||||
self.arch = driver_arch
|
||||
self.version = driver_version
|
||||
self.download_url = str(WebDriverURL(self.version, self.driver_type, self.arch))
|
||||
self.download_dir = Path(download_dir)/self.driver_type.value/self.version/self.arch.value
|
||||
self.download_dir.mkdir(mode=0o0755, parents=True, exist_ok=True)
|
||||
self.download_path = self.download_dir/str(WebDriverFileName(self.version, self.driver_type, self.arch))
|
||||
|
||||
|
||||
def download(
|
||||
self,
|
||||
progress_callback: Optional[Callable[[int, int, float, str], None]] = None
|
||||
) -> Optional[Path]:
|
||||
|
||||
try:
|
||||
# downlaod file : 0% - 98%
|
||||
if not self._download(progress_callback):
|
||||
return None
|
||||
# verify file : 98% - 99%
|
||||
if not self._verify(progress_callback):
|
||||
return None
|
||||
# extract file : 99% - 100%
|
||||
driver_path = self._extract(progress_callback)
|
||||
if not driver_path:
|
||||
return None
|
||||
return driver_path
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _download(
|
||||
self,
|
||||
progress_callback: Optional[Callable[[int, int, float, str], None]] = None,
|
||||
max_retries: int = 3
|
||||
) -> bool:
|
||||
|
||||
CHUNK_SIZE = 8192*8 # 64KB chunk
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
}
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
# resume download if file exists
|
||||
if self.download_path.exists():
|
||||
downloaded_size = self.download_path.stat().st_size
|
||||
headers_ = headers.copy()
|
||||
headers_['Range'] = f"bytes={downloaded_size}-"
|
||||
mode = 'ab'
|
||||
else:
|
||||
downloaded_size = 0
|
||||
headers_ = headers
|
||||
mode = 'wb'
|
||||
# get response
|
||||
response = requests.get(str(self.download_url), headers=headers_, stream=True, timeout=120)
|
||||
if response.status_code not in [200, 206]:
|
||||
if self.download_path.exists():
|
||||
self.download_path.unlink()
|
||||
downloaded_size = 0
|
||||
mode = 'wb'
|
||||
response = requests.get(str(self.download_url), headers=headers, stream=True)
|
||||
response.raise_for_status()
|
||||
# get total size
|
||||
total_size = int(response.headers.get('Content-Length', 0))
|
||||
if response.status_code == 206: # Partial Content - server supports Range
|
||||
total_size += downloaded_size
|
||||
# download file with progress callback and speed calculation
|
||||
start_time = time.time()
|
||||
last_time = start_time
|
||||
last_size = downloaded_size
|
||||
last_progress = 0.0
|
||||
with open(self.download_path, mode) as f:
|
||||
for chunk in response.iter_content(CHUNK_SIZE):
|
||||
if not chunk:
|
||||
continue
|
||||
f.write(chunk)
|
||||
downloaded_size += len(chunk)
|
||||
if not progress_callback or total_size == 0:
|
||||
continue
|
||||
current_time = time.time()
|
||||
current_progress = (downloaded_size/total_size)*98.0
|
||||
if current_progress - last_progress >= 1.0 or current_progress == 98.0:
|
||||
elapsed = current_time - last_time
|
||||
if elapsed > 0:
|
||||
speed = (downloaded_size - last_size)/elapsed/1024.0 # KB/s
|
||||
else:
|
||||
speed = 0.0
|
||||
progress_callback(current_progress, 100, speed, "下载中...")
|
||||
last_progress = current_progress
|
||||
last_size = downloaded_size
|
||||
last_time = current_time
|
||||
if total_size > 0 and self.download_path.stat().st_size < total_size:
|
||||
raise Exception(f"下载不完整 : {self.download_path.stat().st_size}/{total_size} 字节")
|
||||
return True
|
||||
except Exception as e:
|
||||
if attempt < max_retries - 1:
|
||||
progress_callback(0, 100, 0.0, "准备重试...")
|
||||
time.sleep(1)
|
||||
continue
|
||||
raise e
|
||||
|
||||
|
||||
def _verify(
|
||||
self,
|
||||
progress_callback: Optional[Callable[[int, int, float, str], None]] = None
|
||||
) -> bool:
|
||||
|
||||
progress_callback(98, 100, 0.0, "验证完成")
|
||||
return True
|
||||
|
||||
|
||||
def _extract(
|
||||
self,
|
||||
progress_callback: Optional[Callable[[int, int, float, str], None]] = None
|
||||
) -> Optional[Path]:
|
||||
|
||||
try:
|
||||
progress_callback(98, 100, 0.0, "解压中...")
|
||||
file_path_str = str(self.download_path)
|
||||
if file_path_str.endswith('.tar.gz'):
|
||||
with tarfile.open(self.download_path, 'r:gz') as tar_ref:
|
||||
tar_ref.extractall(self.download_dir)
|
||||
else:
|
||||
with zipfile.ZipFile(self.download_path, 'r') as zip_ref:
|
||||
zip_ref.extractall(self.download_dir)
|
||||
driver_file = None
|
||||
for root, _, files in os.walk(self.download_dir):
|
||||
for file in files:
|
||||
expected_name = str(WebDriverExecName(self.driver_type, self.arch))
|
||||
if file == str(expected_name):
|
||||
src_path = Path(root, file)
|
||||
dst_path = self.download_dir/file
|
||||
src_path.rename(dst_path)
|
||||
driver_file = dst_path
|
||||
break
|
||||
if driver_file:
|
||||
break
|
||||
if not driver_file:
|
||||
raise FileNotFoundError(f"未找到 web driver 文件 : {expected_name}")
|
||||
progress_callback(100, 100, 0.0, "解压完成")
|
||||
self.download_path.unlink()
|
||||
self._cleanup(driver_file)
|
||||
return driver_file
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _cleanup(
|
||||
self,
|
||||
driver_file: Path
|
||||
) -> None:
|
||||
|
||||
for item in self.download_dir.iterdir():
|
||||
if item != driver_file:
|
||||
if item.is_dir():
|
||||
shutil.rmtree(item)
|
||||
else:
|
||||
item.unlink()
|
||||
|
||||
|
||||
class ChromeDriverDownloader(WebDriverDownloader):
|
||||
"""
|
||||
Chrome web driver downloader
|
||||
|
||||
Only support version higher than 114
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
version: str,
|
||||
arch: WebDriverArch,
|
||||
download_dir: str
|
||||
):
|
||||
|
||||
super().__init__(WebDriverType.CHROME, version, arch, download_dir)
|
||||
|
||||
|
||||
class FirefoxDriverDownloader(WebDriverDownloader):
|
||||
"""
|
||||
Firefox web driver downloader
|
||||
|
||||
This class do not resolve version mapping,
|
||||
only support driver version higher than 0.17.0
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
version: str,
|
||||
arch: WebDriverArch,
|
||||
download_dir: str
|
||||
):
|
||||
|
||||
super().__init__(WebDriverType.FIREFOX, version, arch, download_dir)
|
||||
|
||||
|
||||
class EdgeDriverDownloader(WebDriverDownloader):
|
||||
"""
|
||||
Edge web driver downloader
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
version: str,
|
||||
arch: WebDriverArch,
|
||||
download_dir: str
|
||||
):
|
||||
|
||||
super().__init__(WebDriverType.EDGE, version, arch, download_dir)
|
||||
@@ -0,0 +1,403 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) 2026 KenanZhu.
|
||||
All rights reserved.
|
||||
|
||||
This software is provided "as is", without any warranty of any kind.
|
||||
You may use, modify, and distribute this file under the terms of the MIT License.
|
||||
See the LICENSE file for details.
|
||||
"""
|
||||
import os
|
||||
import threading
|
||||
import packaging.version as ver
|
||||
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Optional, Callable
|
||||
|
||||
from managers.driver.WebBrowserDetector import (
|
||||
WebBrowserType, WebBrowserArch, WebBrowserInfo, WebBrowserDetector
|
||||
)
|
||||
from managers.driver.WebDriverDownloader import (
|
||||
WebDriverArch, WebDriverType,
|
||||
ChromeDriverDownloader, FirefoxDriverDownloader, EdgeDriverDownloader
|
||||
)
|
||||
|
||||
|
||||
class DriverStatus(Enum):
|
||||
"""
|
||||
Web driver status.
|
||||
"""
|
||||
|
||||
NOT_INSTALLED = 0
|
||||
INSTALLED = 1
|
||||
DOWNLOADING = 2
|
||||
ERROR = 3
|
||||
|
||||
|
||||
class WebDriverInfo:
|
||||
"""
|
||||
Web driver information.
|
||||
|
||||
Attributes:
|
||||
browser_info (WebBrowserInfo): Web browser information
|
||||
driver_type (WebDriverType): Web driver type
|
||||
driver_version (str): Web driver version
|
||||
driver_path (Optional[Path]): Web driver executable file path
|
||||
driver_status (DriverStatus): Web driver status
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
browser_info: WebBrowserInfo
|
||||
):
|
||||
|
||||
self.browser_info = browser_info
|
||||
self.driver_type = WebDriverType(browser_info.browser_type.value)
|
||||
self.driver_version = ""
|
||||
self.driver_path: Optional[Path] = None
|
||||
self.driver_status = DriverStatus.NOT_INSTALLED
|
||||
|
||||
|
||||
class WebDriverManager:
|
||||
"""
|
||||
Web Driver Manager Singleton Class
|
||||
|
||||
Args:
|
||||
driver_dir (str): The directory to store web drivers.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
driver_dir: str
|
||||
):
|
||||
|
||||
self.__driver_dir = os.path.abspath(driver_dir)
|
||||
self.__browser_detector = WebBrowserDetector()
|
||||
self.__driver_infos: list[WebDriverInfo] = []
|
||||
self.__initialized = False
|
||||
self.__lock = threading.Lock()
|
||||
|
||||
self.initialize()
|
||||
|
||||
|
||||
def initialize(
|
||||
self
|
||||
):
|
||||
|
||||
if self.__initialized:
|
||||
return
|
||||
os.makedirs(self.__driver_dir, exist_ok=True)
|
||||
self._detectBrowsers()
|
||||
self._checkDriverStatus()
|
||||
self.__initialized = True
|
||||
|
||||
|
||||
def _detectBrowsers(
|
||||
self
|
||||
):
|
||||
|
||||
with self.__lock:
|
||||
browser_infos = self.__browser_detector.detect()
|
||||
self.__driver_infos = [WebDriverInfo(info) for info in browser_infos]
|
||||
|
||||
|
||||
def _checkDriverStatus(
|
||||
self
|
||||
):
|
||||
|
||||
with self.__lock:
|
||||
for driver_info in self.__driver_infos:
|
||||
driver_arch = self._mapWebBrowserArch(
|
||||
driver_info.browser_info.browser_type,
|
||||
driver_info.browser_info.browser_arch
|
||||
)
|
||||
driver_path = self._getDriverPath(
|
||||
driver_info.driver_type,
|
||||
driver_arch
|
||||
)
|
||||
if driver_path and driver_path.exists() and driver_path.is_file():
|
||||
driver_info.driver_path = driver_path
|
||||
driver_info.driver_status = DriverStatus.INSTALLED
|
||||
try:
|
||||
driver_info.driver_version = self._getDriverVersion(
|
||||
driver_info.driver_type,
|
||||
driver_info.driver_info.browser_version
|
||||
)
|
||||
except Exception:
|
||||
driver_info.driver_status = DriverStatus.ERROR
|
||||
|
||||
|
||||
def _mapWebBrowserArch(
|
||||
self,
|
||||
browser_type: WebBrowserType,
|
||||
browser_arch: WebBrowserArch
|
||||
) -> WebDriverArch:
|
||||
|
||||
if browser_type == WebBrowserType.CHROME:
|
||||
if browser_arch == WebBrowserArch.WINX86_32:
|
||||
return WebDriverArch.Chrome.WINX86_32
|
||||
elif browser_arch == WebBrowserArch.WINX86_64:
|
||||
return WebDriverArch.Chrome.WINX86_64
|
||||
elif browser_arch == WebBrowserArch.WINARM:
|
||||
raise ValueError("Chrome 不支持 Windows ARM 架构")
|
||||
elif browser_arch == WebBrowserArch.LINUXX86_32:
|
||||
raise ValueError("Chrome 不支持 Linux x86_32 架构")
|
||||
elif browser_arch == WebBrowserArch.LINUXX86_64:
|
||||
return WebDriverArch.Chrome.LINUXX86_64
|
||||
elif browser_arch == WebBrowserArch.LINUXARM:
|
||||
raise ValueError("Chrome 不支持 Linux ARM 架构")
|
||||
elif browser_arch == WebBrowserArch.MACX86_64:
|
||||
return WebDriverArch.Chrome.MACX86_64
|
||||
elif browser_arch == WebBrowserArch.MACARM:
|
||||
return WebDriverArch.Chrome.MACARM
|
||||
else:
|
||||
raise ValueError(f"不支持的 Chrome 浏览器架构 : {browser_arch}")
|
||||
elif browser_type == WebBrowserType.FIREFOX:
|
||||
if browser_arch == WebBrowserArch.WINX86_32:
|
||||
return WebDriverArch.Firefox.WINX86_32
|
||||
elif browser_arch == WebBrowserArch.WINX86_64:
|
||||
return WebDriverArch.Firefox.WINX86_64
|
||||
elif browser_arch == WebBrowserArch.WINARM:
|
||||
return WebDriverArch.Firefox.WINARM
|
||||
elif browser_arch == WebBrowserArch.LINUXX86_32:
|
||||
return WebDriverArch.Firefox.LINUXX86_32
|
||||
elif browser_arch == WebBrowserArch.LINUXX86_64:
|
||||
return WebDriverArch.Firefox.LINUXX86_64
|
||||
elif browser_arch == WebBrowserArch.LINUXARM:
|
||||
return WebDriverArch.Firefox.LINUXARM
|
||||
elif browser_arch == WebBrowserArch.MACX86_64:
|
||||
return WebDriverArch.Firefox.MACX86_64
|
||||
elif browser_arch == WebBrowserArch.MACARM:
|
||||
return WebDriverArch.Firefox.MACARM
|
||||
else:
|
||||
raise ValueError(f"不支持的 Firefox 浏览器架构 : {browser_arch}")
|
||||
elif browser_type == WebBrowserType.EDGE:
|
||||
if browser_arch == WebBrowserArch.WINX86_32:
|
||||
return WebDriverArch.Edge.WINX86_32
|
||||
elif browser_arch == WebBrowserArch.WINX86_64:
|
||||
return WebDriverArch.Edge.WINX86_64
|
||||
elif browser_arch == WebBrowserArch.WINARM:
|
||||
return WebDriverArch.Edge.WINARM
|
||||
elif browser_arch == WebBrowserArch.LINUXX86_32:
|
||||
raise ValueError("Edge 不支持 Linux x86_32 架构")
|
||||
elif browser_arch == WebBrowserArch.LINUXX86_64:
|
||||
return WebDriverArch.Edge.LINUXX86_64
|
||||
elif browser_arch == WebBrowserArch.LINUXARM:
|
||||
raise ValueError("Edge 不支持 Linux ARM 架构")
|
||||
elif browser_arch == WebBrowserArch.MACX86_64:
|
||||
return WebDriverArch.Edge.MACX86_64
|
||||
elif browser_arch == WebBrowserArch.MACARM:
|
||||
return WebDriverArch.Edge.MACARM
|
||||
else:
|
||||
raise ValueError(f"不支持的 Edge 浏览器架构 : {browser_arch}")
|
||||
else:
|
||||
raise ValueError(f"不支持的 Web 浏览器类型 : {browser_type}")
|
||||
|
||||
|
||||
def _mapFirefoxDriverVersion(
|
||||
self,
|
||||
version: str
|
||||
) -> str:
|
||||
|
||||
version_mapping = [
|
||||
(ver.Version("128.0"), ver.Version("999.0"), "0.36.0"),
|
||||
(ver.Version("115.0"), ver.Version("127.0"), "0.35.0"),
|
||||
(ver.Version("91.0"), ver.Version("114.0"), "0.34.0"),
|
||||
(ver.Version("91.0"), ver.Version("120.0"), "0.33.0"),
|
||||
(ver.Version("91.0"), ver.Version("120.0"), "0.32.0"),
|
||||
(ver.Version("91.0"), ver.Version("120.0"), "0.31.0"),
|
||||
(ver.Version("78.0"), ver.Version("90.0"), "0.30.0"),
|
||||
(ver.Version("60.0"), ver.Version("90.0"), "0.29.0"),
|
||||
(ver.Version("60.0"), ver.Version("90.0"), "0.28.0"),
|
||||
(ver.Version("60.0"), ver.Version("90.0"), "0.27.0"),
|
||||
(ver.Version("57.0"), ver.Version("90.0"), "0.26.0"),
|
||||
(ver.Version("55.0"), ver.Version("62.0"), "0.25.0"),
|
||||
(ver.Version("55.0"), ver.Version("62.0"), "0.24.0"),
|
||||
(ver.Version("57.0"), ver.Version("79.0"), "0.23.0"),
|
||||
(ver.Version("57.0"), ver.Version("79.0"), "0.22.0"),
|
||||
(ver.Version("57.0"), ver.Version("79.0"), "0.21.0"),
|
||||
(ver.Version("55.0"), ver.Version("62.0"), "0.20.0"),
|
||||
(ver.Version("55.0"), ver.Version("62.0"), "0.19.0"),
|
||||
(ver.Version("53.0"), ver.Version("62.0"), "0.18.0"),
|
||||
(ver.Version("52.0"), ver.Version("62.0"), "0.17.0"),
|
||||
]
|
||||
|
||||
try:
|
||||
firefox_version = ver.Version(version)
|
||||
for min_ver, max_ver, gecko_ver in version_mapping:
|
||||
if min_ver <= firefox_version <= max_ver:
|
||||
return gecko_ver
|
||||
raise ValueError(
|
||||
f"不支持的 Firefox 版本 : {version}"
|
||||
f"Firefox 版本 52 及以上受支持"
|
||||
)
|
||||
except Exception as e:
|
||||
raise ValueError(f"无效的 Firefox 版本格式 : {version}") from e
|
||||
|
||||
|
||||
def _getDriverPath(
|
||||
self,
|
||||
driver_type: WebDriverType,
|
||||
driver_arch: WebDriverArch
|
||||
) -> Optional[Path]:
|
||||
|
||||
if driver_type == WebDriverType.CHROME:
|
||||
driver_name = "chromedriver"
|
||||
elif driver_type == WebDriverType.FIREFOX:
|
||||
driver_name = "geckodriver"
|
||||
elif driver_type == WebDriverType.EDGE:
|
||||
driver_name = "msedgedriver"
|
||||
else:
|
||||
return None
|
||||
is_win = driver_arch in [
|
||||
WebDriverArch.Chrome.WINX86_32,
|
||||
WebDriverArch.Chrome.WINX86_64,
|
||||
WebDriverArch.Firefox.WINX86_32,
|
||||
WebDriverArch.Firefox.WINX86_64,
|
||||
WebDriverArch.Edge.WINX86_32,
|
||||
WebDriverArch.Edge.WINX86_64,
|
||||
]
|
||||
exe_name = f"{driver_name}.exe" if is_win else driver_name
|
||||
driver_dir = Path(self.__driver_dir) / driver_type.value / driver_arch.value
|
||||
driver_path = driver_dir / exe_name
|
||||
if driver_path.exists() and driver_path.is_file():
|
||||
return driver_path
|
||||
return None
|
||||
|
||||
|
||||
def _getDriverVersion(
|
||||
self,
|
||||
driver_type: WebDriverType,
|
||||
browser_version: str
|
||||
) -> str:
|
||||
|
||||
if driver_type == WebDriverType.FIREFOX:
|
||||
return self._mapFirefoxDriverVersion(browser_version)
|
||||
return browser_version
|
||||
|
||||
|
||||
def refresh(
|
||||
self
|
||||
):
|
||||
|
||||
with self.__lock:
|
||||
self._detectBrowsers()
|
||||
self._checkDriverStatus()
|
||||
|
||||
|
||||
def getDriverInfos(
|
||||
self
|
||||
) -> list[WebDriverInfo]:
|
||||
|
||||
with self.__lock:
|
||||
return self.__driver_infos.copy()
|
||||
|
||||
|
||||
def getDriverInfo(
|
||||
self,
|
||||
driver_type: WebDriverType
|
||||
) -> Optional[WebDriverInfo]:
|
||||
|
||||
with self.__lock:
|
||||
for driver_info in self.__driver_infos:
|
||||
if driver_info.driver_type == driver_type:
|
||||
return driver_info
|
||||
return None
|
||||
|
||||
|
||||
def getDriverPath(
|
||||
self,
|
||||
driver_type: WebDriverType
|
||||
) -> Optional[Path]:
|
||||
|
||||
driver_info = self.getDriverInfo(driver_type)
|
||||
if driver_info and driver_info.driver_status == DriverStatus.INSTALLED:
|
||||
return driver_info.driver_path
|
||||
return None
|
||||
|
||||
|
||||
def installDriver(
|
||||
self,
|
||||
driver_type: WebDriverType,
|
||||
progress_callback: Optional[Callable[[int, int, float, str], None]] = None
|
||||
) -> Optional[Path]:
|
||||
|
||||
with self.__lock:
|
||||
driver_info = self.getDriverInfo(driver_type)
|
||||
if not driver_info:
|
||||
raise ValueError(f"未找到类型为 {driver_type} 的浏览器")
|
||||
if driver_info.driver_status == DriverStatus.DOWNLOADING:
|
||||
raise ValueError(f"{driver_type} 驱动正在下载中")
|
||||
driver_info.driver_status = DriverStatus.DOWNLOADING
|
||||
try:
|
||||
driver_arch = self._mapWebBrowserArch(
|
||||
driver_info.browser_info.browser_type,
|
||||
driver_info.browser_info.browser_arch
|
||||
)
|
||||
browser_version = driver_info.browser_info.browser_version
|
||||
driver_version = self._getDriverVersion(driver_type, browser_version)
|
||||
downloader = None
|
||||
if driver_type == WebDriverType.CHROME:
|
||||
downloader = ChromeDriverDownloader(
|
||||
version=driver_version,
|
||||
arch=driver_arch,
|
||||
download_dir=self.__driver_dir
|
||||
)
|
||||
elif driver_type == WebDriverType.FIREFOX:
|
||||
downloader = FirefoxDriverDownloader(
|
||||
version=driver_version,
|
||||
arch=driver_arch,
|
||||
download_dir=self.__driver_dir
|
||||
)
|
||||
elif driver_type == WebDriverType.EDGE:
|
||||
downloader = EdgeDriverDownloader(
|
||||
version=driver_version,
|
||||
arch=driver_arch,
|
||||
download_dir=self.__driver_dir
|
||||
)
|
||||
if downloader is None:
|
||||
raise ValueError(f"不支持的 Web Driver 类型 : {driver_type}")
|
||||
|
||||
driver_path = downloader.download(progress_callback=progress_callback)
|
||||
with self.__lock:
|
||||
if driver_path:
|
||||
driver_info.driver_path = driver_path
|
||||
driver_info.driver_version = driver_version
|
||||
driver_info.driver_status = DriverStatus.INSTALLED
|
||||
else:
|
||||
driver_info.driver_status = DriverStatus.ERROR
|
||||
return driver_path
|
||||
except Exception as e:
|
||||
with self.__lock:
|
||||
driver_info.driver_status = DriverStatus.ERROR
|
||||
raise
|
||||
|
||||
|
||||
def driverDir(
|
||||
self
|
||||
) -> str:
|
||||
|
||||
return self.__driver_dir
|
||||
|
||||
|
||||
# WebDriverManager singleton instance.
|
||||
_webdriver_manager_instance = None
|
||||
|
||||
# Singleton instance lock.
|
||||
_instance_lock = threading.Lock()
|
||||
|
||||
def instance(
|
||||
driver_dir: str = ""
|
||||
) -> WebDriverManager:
|
||||
|
||||
global _webdriver_manager_instance
|
||||
with _instance_lock:
|
||||
if _webdriver_manager_instance is None:
|
||||
if not driver_dir:
|
||||
raise ValueError("WebDriverManager 需要驱动目录参数")
|
||||
_webdriver_manager_instance = WebDriverManager(driver_dir)
|
||||
else:
|
||||
if driver_dir and _webdriver_manager_instance.driverDir() != os.path.abspath(driver_dir):
|
||||
raise ValueError("WebDriverManager 的实例已初始化,不能使用不同的驱动目录")
|
||||
return _webdriver_manager_instance
|
||||
Reference in New Issue
Block a user