output.txt, output2.txt, error.txt 는 새로 만든다.input.txt 예시) 프로필 링크[tab]추출날짜
output2.txt 에 프로필주소[탭]답변없음 저장하고 다음 프로그램 진행한다.
주소[탭]답변번호[탭]등록일output.txt 저장 예시) https://kin.naver.com/qna/mydetail.naver?dirId=??????&docId=?????? 2 20240409error.txt 저장 (오류발생 input[탭]오류내용) 하고 다음 input 라인 실행한다.발단 :
Timeout value connect was <object object at 0x00000201DBC64E50>, but it must be an int, float or None.가장 쉬운 해결책은, Selenium 4를 사용하는 것.
어쩔 수 없이 에러 로그를 하나씩 타고 올라가며 분석을 시작했다.
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
int나 float 혹은 None이 아니면 에러를 일으키는 것으로 보인다.
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 로 작성해야 하는 것 아닌가?다시 돌아와서, 어떤 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)
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
이것으로 급한 불은 끌 수 있었다.
다만 근본적인 원인에 대해 확실하게 모르는 상태라서, 좀 더 파고들기로 하였다.
from_float 가 호출되는 부분 위를 보면 urllib3.poolmanager.py 이 보인다.
프라이빗 메서드 _new_pool 은 다음과 같다.
def _new_pool(
self,
scheme: str,
host: str,
port: int,
request_context: dict[str, typing.Any] | None = None,
) -> HTTPConnectionPool:
"""
Create a new :class:`urllib3.connectionpool.ConnectionPool` based on host, port, scheme, and
any additional pool keyword arguments.
If ``request_context`` is provided, it is provided as keyword arguments
to the pool class used. This method is used to actually create the
connection pools handed out by :meth:`connection_from_url` and
companion methods. It is intended to be overridden for customization.
"""
pool_cls: type[HTTPConnectionPool] = self.pool_classes_by_scheme[scheme]
if request_context is None:
request_context = self.connection_pool_kw.copy()
# Default blocksize to _DEFAULT_BLOCKSIZE if missing or explicitly
# set to 'None' in the request_context.
if request_context.get("blocksize") is None:
request_context["blocksize"] = _DEFAULT_BLOCKSIZE
# Although the context has everything necessary to create the pool,
# this function has historically only used the scheme, host, and port
# in the positional args. When an API change is acceptable these can
# be removed.
for key in ("scheme", "host", "port"):
request_context.pop(key, None)
if scheme == "http":
for kw in SSL_KEYWORDS:
request_context.pop(kw, None)
return pool_cls(host, port, **request_context)
pool_cls 를 반환하는데, 이것의 타입은 HTTPConnectionPool 의 인스턴스이다.
self.pool_classes_by_scheme[scheme] 와 같이 딕셔너리에서 일치하는 키에 해당하는 동작을 하는데, pool_classes_by_scheme 는 다음과 같다.
class PoolManager(RequestMethods):
...
def __init__(
...
) -> None:
...
self.pool_classes_by_scheme = pool_classes_by_scheme
self.key_fn_by_scheme = key_fn_by_scheme.copy()
꽤 많은 부분을 생략하긴 했는데, 어쨌든 뭔가 글로벌 네임스페이스 어딘가에 pool_classes_by_scheme 라는 변수가 정의되어 있는 듯 보였다.
pool_classes_by_scheme = {"http": HTTPConnectionPool, "https": HTTPSConnectionPool}
즉, 위의 _new_pool 에서는 self.pool_classes_by_scheme[scheme] 를 통해 host, port, **request_context 의 인자를 가지는 HTTPConnectionPool 인스턴스를 반환하고 있다.
그런데, 위에서는 Timeout에 대한 정보가 보이지 않는다. request_context 에 인자로 주어지는지 확인하기 위해 print를 찍어보았다.
def _new_pool(
...
) -> HTTPConnectionPool:
...
for key in ("scheme", "host", "port"):
print(request_context)
request_context.pop(key, None)
if scheme == "http":
for kw in SSL_KEYWORDS:
request_context.pop(kw, None)
return pool_cls(host, port, **request_context)
# >>> {'timeout': <object object at 0x0000020E8CCF4E50>, 'scheme': 'http', 'port': 12607, 'host': '127.0.0.1', 'blocksize': 16384}
# >>> {'timeout': <object object at 0x0000020E8CCF4E50>, 'port': 12607, 'host': '127.0.0.1', 'blocksize': 16384}
# >>> {'timeout': <object object at 0x0000020E8CCF4E50>, 'port': 12607, 'blocksize': 16384}
# >>> {'timeout': <object object at 0x0000020E8CCF4E50>, 'blocksize': 16384}
timeout 과 blocksize 만 남았다.request_context 가 어디서 주어지는지 즉, self._new_pool 이 어디서 호출되는지 찾아보았다.connection_from_pool_key 은 다시 아래와 같이 호출되고 있었으며,connection_from_host 메서드에서 생성되고 있었다.self._merge_pool_kwargs(pool_kwargs) 를 거쳐 request_context 가 만들어지는 것을 볼 수 있다._merge_pool_kwargs 메서드는 다음과 같고, 하던대로 중간에 print 찍어보면 이렇게 나온다.connection_pool_kw 로 어떤 timeout 오브젝트가 들어있다.self.connection_pool_kw 는 생성자에 의해 생성된 후 별도의 값 변경 없이 그대로 사용이 되고 있었다.PoolManager 인스턴스가 생성되는 곳이 어딘지 찾을 필요가 있어보였다.계속 찾아보면 urlopen 이라는 메서드에서 self.connection_from_host 을 호출하고 있었다.
어쨌든 request_encode_body 는 해당 모듈의 request 메서드에서 호출되고 있었으며, request 는, selenium\webdriver\remote\remote_connection.py 에서 호출되고 있었다.
클래스 생성자 호출하는 부분 바로 다음 로그는 이렇다.
set_timeout 메서드가 실행되지 않은건지는 잘 모르겠다.# utils.py
from selenium.webdriver.remote.remote_connection import RemoteConnection
class FixedRemoteConnection(RemoteConnection):
RemoteConnection.set_timeout(10)
# 별도의 땜질 없어도 import 되면서 적용되기 때문에 이대로만 작성해도 잘 작동한다.