일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
Tags
- 배낭 문제
- 모듈러 연산 분배법칙
- 클래스
- 일단 시도
- db replication
- LCS 알고리즘
- 최장공통부분수열
- 그래프탐색
- dfs
- 너비 우선 탐색
- 다이나믹 프로그래밍
- npm start
- bfs
- 수학
- 냅색 알고리즘
- error:0308010C:digital envelope routines::unsupported
- 깊이 우선 탐색
- lazy evaluation
- Container vs VM
- 파이썬
- Docker 원리
- 그래프 탐색
- 나는 바보야...
- Python
- 정처기 필기
- 구현
- 그래프 이론
- 문자열
- 최장공통부분문자열
- 동적 계획법
Archives
- Today
- Total
Save my data
[Python][Selenium 3.141.0] Chromedriver 실행시 잘못된 Timeout object가 전달되는 건에 대한 분석 본문
프로젝트/Python
[Python][Selenium 3.141.0] Chromedriver 실행시 잘못된 Timeout object가 전달되는 건에 대한 분석
양을 좋아하는 문씨 2024. 6. 20. 10:52개요 :
- 수주 프로젝트 코딩 중 Selenium 3.141.0 버전에서 Chromedriver와 구버전 Selenium간의 호환성 문제로 의심되는 ValueError가 발생하였음
- 이를 해결하기 위한 방법을 찾기 위해 코드를 상세히 분석함.
- 우선 사용 환경은 다음과 같다.
- python : 3.8
- Selenium : 3.141.0
- 에러 내용은 다음과 같다.
- 우선 사용 환경은 다음과 같다.
Timeout value connect was <object object at 0x00000201DBC64E50>, but it must be an int, float or None.
- 이것에 대해 가장 쉬운 해결 방법은 Selenium4를 사용하는 것이다.
- 그러나 의뢰주 측에서 3.141.0 버전에서 실행되는 프로그램을 원했음.
- 어쩔 수 없이 에러 로그를 하나씩 타고 올라가며 분석을 시작했다.
Traceback (most recent call last):
File "C:\Program Files\Python38\lib\concurrent\futures\process.py", line 239, in _process_worker
r = call_item.fn(*call_item.args, **call_item.kwargs)
File "f:\Github\projects\naver-kin\main.py", line 23, in make_scraper
driver: webdriver.Chrome = Driver(executable_path=CHROME_DRIVER).make_driver()
File "f:\Github\projects\naver-kin\modules\driver.py", line 28, in make_driver
driver = webdriver.Chrome(
File "f:\Github\projects\naver-kin\venv\lib\site-packages\selenium\webdriver\chrome\webdriver.py", line 76, in __init__
RemoteWebDriver.__init__(
File "f:\Github\projects\naver-kin\venv\lib\site-packages\selenium\webdriver\remote\webdriver.py", line 157, in __init__
self.start_session(capabilities, browser_profile)
File "f:\Github\projects\naver-kin\venv\lib\site-packages\selenium\webdriver\remote\webdriver.py", line 252, in start_session
response = self.execute(Command.NEW_SESSION, parameters)
File "f:\Github\projects\naver-kin\venv\lib\site-packages\selenium\webdriver\remote\webdriver.py", line 319, in execute
response = self.command_executor.execute(driver_command, params)
File "f:\Github\projects\naver-kin\venv\lib\site-packages\selenium\webdriver\remote\remote_connection.py", line 374, in execute
return self._request(command_info[0], url, body=data)
File "f:\Github\projects\naver-kin\venv\lib\site-packages\selenium\webdriver\remote\remote_connection.py", line 397, in _request
resp = self._conn.request(method, url, body=body, headers=headers)
File "f:\Github\projects\naver-kin\venv\lib\site-packages\urllib3\_request_methods.py", line 144, in request
return self.request_encode_body(
File "f:\Github\projects\naver-kin\venv\lib\site-packages\urllib3\_request_methods.py", line 279, in request_encode_body
return self.urlopen(method, url, **extra_kw)
File "f:\Github\projects\naver-kin\venv\lib\site-packages\urllib3\poolmanager.py", line 433, in urlopen
conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme)
File "f:\Github\projects\naver-kin\venv\lib\site-packages\urllib3\poolmanager.py", line 304, in connection_from_host
return self.connection_from_context(request_context)
File "f:\Github\projects\naver-kin\venv\lib\site-packages\urllib3\poolmanager.py", line 329, in connection_from_context
return self.connection_from_pool_key(pool_key, request_context=request_context)
File "f:\Github\projects\naver-kin\venv\lib\site-packages\urllib3\poolmanager.py", line 352, in connection_from_pool_key
pool = self._new_pool(scheme, host, port, request_context=request_context)
File "f:\Github\projects\naver-kin\venv\lib\site-packages\urllib3\poolmanager.py", line 266, in _new_pool
return pool_cls(host, port, **request_context)
File "f:\Github\projects\naver-kin\venv\lib\site-packages\urllib3\connectionpool.py", line 196, in __init__
timeout = Timeout.from_float(timeout)
File "f:\Github\projects\naver-kin\venv\lib\site-packages\urllib3\util\timeout.py", line 186, in from_float
return Timeout(read=timeout, connect=timeout)
File "f:\Github\projects\naver-kin\venv\lib\site-packages\urllib3\util\timeout.py", line 115, in __init__
self._connect = self._validate_timeout(connect, "connect")
File "f:\Github\projects\naver-kin\venv\lib\site-packages\urllib3\util\timeout.py", line 152, in _validate_timeout
raise ValueError(
ValueError: Timeout value connect was <object object at 0x00000201DBC64E50>, but it must be an int, float or None.
- 최초에 크롬 드라이버가 생성될 때 문제가 발생하고 있었다.
- 내용이 길어 보이지만 하나씩 타고 올라가는 수 밖에 없었다.
- 최종적인 문제는 `urllib3.util.timeout` 에서 `_validate_timeout` 로 인해 발생하는 것으로 보인다.
- `_validate_timeout` 를 보면 다음과 같다.
@classmethod
def _validate_timeout(cls, value: _TYPE_TIMEOUT, name: str) -> _TYPE_TIMEOUT:
"""Check that a timeout attribute is valid.
:param value: The timeout value to validate
:param name: The name of the timeout attribute to validate. This is
used to specify in error messages.
:return: The validated and casted version of the given value.
:raises ValueError: If it is a numeric value less than or equal to
zero, or the type is not an integer, float, or None.
"""
if value is None or value is _DEFAULT_TIMEOUT:
return value
if isinstance(value, bool):
raise ValueError(
"Timeout cannot be a boolean value. It must "
"be an int, float or None."
)
try:
float(value)
except (TypeError, ValueError):
raise ValueError(
"Timeout value %s was %s, but it must be an "
"int, float or None." % (name, value)
) from None
- 주어진 Timeout 객체의 timeout value가 `int`나 `float` 혹은 `None`이 아니면 에러를 일으키는 것으로 보인다.
- 여기서 한 가지 단서를 얻을 수 있었는데, 만약 Timeout 객체가 만들어지는 부분에서 value를 int나 float로 강제로 정해줄 수 있다면 몽키패치 비슷한 방법으로 고칠 수도 있겠다는 생각을 했다.
- 그러한 부분을 염두해 둔 상태에서,
- Timeout이 어디서 만들어지는지,
- 어떤 인자를 받아서 생성되는지 찾아보았다.
- Timeout 객체가 생성될 때의 모습을 찾아보았더니 아래와 같았다.
class Timeout:
...
DEFAULT_TIMEOUT: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT
def __init__(
self,
total: _TYPE_TIMEOUT = None,
connect: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT,
read: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT,
) -> None:
self._connect = self._validate_timeout(connect, "connect")
self._read = self._validate_timeout(read, "read")
self.total = self._validate_timeout(total, "total")
self._start_connect: float | None = None
def __repr__(self) -> str:
return f"{type(self).__name__}(connect={self._connect!r}, read={self._read!r}, total={self.total!r})"
- 객체가 생성될 때, 생성자에 의해서 `total`, `connect`, `read` 등의 값을 받고, 유효성 검사 후 프라이빗 멤버 변수로 가져간다.
- 이 때 값이 들어오지 않으면 `_DEFAULT_TIMEOUT` 을 기본 인수로 가져간다. `_DEFAULT_TIMEOUT` 에 대해 좀 찾아보면 다음과 같다.
class _TYPE_DEFAULT(Enum):
# This value should never be passed to socket.settimeout() so for safety we use a -1.
# socket.settimout() raises a ValueError for negative values.
token = -1
_DEFAULT_TIMEOUT: Final[_TYPE_DEFAULT] = _TYPE_DEFAULT.token
_TYPE_TIMEOUT = typing.Optional[typing.Union[float, _TYPE_DEFAULT]]
- `_DEFAULT_TIMEOUT` 은 `_TYPE_DEFAULT` 타입의 변수이고, `_TYPE_DEFAULT` 는 `Enum` 객체이다.
- 이 때 설명을 보면, 이 값은 `socket.settimeout()` 에 전달되어서는 안되므로, 안전을 위해 `-1` 을 할당하였다고 한다. 그리고 이 값이 전달될 경우 ValueError를 발생시킨다고 되어 있다.
- 근데 그러면 `_DEFAULT_TIMEOUT` 에서 `_TYPE_DEFAULT.token` 이 아니고 `_TYPE_DEFAULT.token.value` 로 작성해야 하는 것 아닌가?
- 어쨌든 문자열 값이던 음수 값이던 ValueError가 발생하는 것은 마찬가지라서, 지금 그걸 따지는 건 큰 의미는 없어 보이긴 했다.
- 다시 돌아와서, 어떤 Timeout value 가 만들어지길래 유효성 검사를 통과하지 못하는지, 확인하기 위해 Timeout 객체가 만들어지는 부분을 찾을 필요가 있어보였다.
- 그런데 그곳을 확인하기 전에, 위의 에러 로그를 보면 우리가 확인한 `__init__` 의 `self._validate_timeout` 가 호출되는 바로 전 단계에, `Timeout` 의 클래스메서드인 `from_float` 가 호출되는 것을 알 수 있다.
@classmethod
def from_float(cls, timeout: _TYPE_TIMEOUT) -> Timeout:
"""Create a new Timeout from a legacy timeout value.
The timeout value used by httplib.py sets the same timeout on the
connect(), and recv() socket requests. This creates a :class:`Timeout`
object that sets the individual timeouts to the ``timeout`` value
passed to this function.
:param timeout: The legacy timeout value.
:type timeout: integer, float, :attr:`urllib3.util.Timeout.DEFAULT_TIMEOUT`, or None
:return: Timeout object
:rtype: :class:`Timeout`
"""
return Timeout(read=timeout, connect=timeout)
- 이 부분은 문제 해결에 있어서 매우 중요해보였다. 새로운 Timeout 객체를 반환하는데, 해당 메서드의 주석을 읽어보면 핵심은 다음과 같다.
- 레거시 타임아웃 값으로부터 새로운 타임아웃 객체를 만든다.
- timeout 타입은 `int`, `float`, `urllib3.util.Timeout.DEFAULT_TIMEOUT`, `None` 타입을 가질 수 있다.
- `return Timeout(read=timeout, connect=timeout)` 부분을 주목하였다.
- 이 부분만 떼어다가 다음과 같이 수정하면 정상적인 작동을 하지 않을까 생각했다.
- 다음과 같이 수정하였다.
from urllib3.util.timeout import Timeout
class FixedTimeout(Timeout):
@classmethod
def from_float(cls, timeout) -> Timeout:
# Timeout은 urllib3.util.Timeout의 기본값을 따릅니다.
# >>> timeout = urllib3.util.Timeout(connect=2.0, read=7.0)
return Timeout(read=7.0, connect=2.0)
- 이제 이 수정된 코드를 chromedriver가 생성될 때 덮어씌우면 문제가 해결된다.
class Driver:
def __init__(self, *args: (str), **kwargs) -> None:
timeout.Timeout.from_float = FixedTimeout.from_float # 땜질한 부분.
naver_kin_logger.info(f"Driver generated.")
self.executable_path = kwargs["executable_path"]
self.options = Options()
if "debugpy" not in sys.modules: # VSCode 디버그 모드가 아닌 경우 args 추가
for option in args:
self.options.add_argument(option)
def make_driver(self) -> webdriver:
driver = webdriver.Chrome(
executable_path=self.executable_path,
options=self.options,
)
driver_version = driver.capabilities["chrome"]["chromedriverVersion"].split()[0]
logger.info(f"Chrome driver version {driver_version}")
return driver
- 이것으로 급한 불은 끌 수 있었다.
다만 근본적인 원인에 대해서는 모르고 끝난 상태라 좀 찝찝함이 있었다.
나머지 부분에 대한 분석은 이 문서에 링크로 연결할 예정이다.
추가 내용 :
[Python][Selenium 3.141.0] 이게 왜 되지? (import 작동 원리)
이전 포스팅 :https://mhd329.tistory.com/54 [Python][Selenium 3.141.0] Chromedriver 실행시 잘못된 Timeout object가 전달되는 건에 대한 분석개요 :수주 프로젝트 코딩 중 Selenium 3.141.0 버전에서 Chromedriver와 구버전 S
mhd329.tistory.com
'프로젝트 > Python' 카테고리의 다른 글
[Python][Selenium 3.141.0] 이게 왜 되지? (import 작동 원리) (0) | 2025.02.20 |
---|---|
[DjangoREST + React] Docker 사용 시 로컬에서의 변경사항을 다시 빌드하지 않고 컨테이너 내부에 바로 적용시키기 (0) | 2023.07.06 |
[DjangoREST + React] Django 기본 인증 클래스를 커스텀해보자. (0) | 2023.06.11 |
[DjangoREST + React] 2. backend 설정 (accounts) (0) | 2023.05.31 |
[DjangoREST + React] 1. 시작하기 (0) | 2023.05.31 |
Comments