2022.09.08

[Python, 웹 스크래핑] 서버의 문제로 로그인에 실패하였을 때 내 맘대로 해결해보기 (Feat. Selenium)

 

 

 

 

 

Python에서 Selenium 라이브러리와 Requests 라이브러리를 사용하여 '쿠팡 윙'에서 데이터 수집을 자동화하고 있었습니다.

Selenium 라이브러리를 사용하여 로그인을 진행해서 쿠키를 얻은 후, Requests 라이브러리를 사용하여 데이터를 수집하는 방식이었습니다.

그런데 저번 주까지는 문제 없이 돌아가던 코드가 오늘 다시 실행하니까 로그인 과정 중에 에러가 발생하는 것을 확인하였습니다.

 

 

로그인 과정 중 발생한 에러 화면

 

 

확인해보니 로그인 과정에서 다음과 같은 문제가 생기는 것을 알 수 있었습니다.

 

  1. 로그인 자체를 실패
  2. 로그인에는 성공하였지만 메인 화면이 아닌 에러 화면으로 튕김

 

ID/PW는 그대로이기 때문에 이러한 문제들은 해당 서버의 에러로 판단되었습니다.

 

 

(문제를 해결하는 과정 중에 시행착오를 겪은 내용들도 모두 글에 같이 담아내었습니다.

그렇기에 글을 읽으면서 제가 겪은 과정들을 따라하시기보다는 그냥 끝까지 한 번 쭉 읽어보시기를 추천드립니다.)

 

 

 

Selenium Documentation

https://selenium-python.readthedocs.io/

 

Selenium with Python — Selenium Python Bindings 2 documentation

Note This is not an official documentation. If you would like to contribute to this documentation, you can fork this project in GitHub and send pull requests. You can also send your feedback to my email: baiju.m.mail AT gmail DOT com. So far 50+ community

selenium-python.readthedocs.io

 

 

 

 

 


 

 

 

 

 

Selenium 라이브러리를 사용하여 WebDriver 띄우기

 

 

 

일단 python에서 selenium 라이브러리를 사용하여 webdriver를 띄워주었습니다.

 

import selenium.webdriver as webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
url = '[url address]'
driver.get(url)

 

제가 실행한 코드는 다음과 같습니다.

[url address]에는 여러분이 띄워주고 싶은 페이지의 주소를 넣어주시면 됩니다!

 

 

 

 

저는 다음과 같이 '쿠팡 윙'의 로그인 화면을 띄워주었습니다.

 

 

 

 

 


 

 

 

 

 

Selenium 라이브러리의 find_element()와 is_displayed() 함수 사용하기

 

 

 

처음 생각해 낸 해결책은 selenium 라이브러리의 함수를 사용하여 해당 화면에 해당 원소가 있는지 확인한 후

각각의 case에 대한 실행 코드를 작성하는 것이었습니다.

 

당연히 selenium에 해당 기능을 가진 함수가 있을 것이라 생각하였고,

구글링을 통해 find_element()와 is_displayed()를 이용하면 해당 기능을 구현할 수 있다는 정보를 얻었습니다.

 

el = driver.find_element(By.CSS_SELECTOR, '[element name]').is_displayed()

 

코드 내용을 간단히 설명하면 다음과 같습니다.

find_element()로 원소를 찾은 후 is_displayed()로 원소의 존재 여부를 'True'와 'False'로 return 받는 것입니다.

 

참고로 find_element()에는 CSS_SELECTOR 말고도 여러 종류의 parameter를 전달하여 원소를 찾을 수 있지만,

저는 코드 간결성과 보편성을 이유로 다음과 같이 CSS_SELECTOR를 사용하였습니다.

 

# 해당 page에 존재하는 [element name]으로 검색하였을 때
el = driver.find_element(By.CSS_SELECTOR, '[element name]').is_displayed()
print(el) # 'True' 출력
 
# 해당 page에 존재하지 않는 [element name]으로 검색하였을 때
el2 = driver.find_element(By.CSS_SELECTOR, '[element name]').is_displayed() # error 출력
print(el2) # 'el2'를 찾을 수 없다는 error 출력

 

그런데 find_element() 함수에는 문제점이 있었습니다.

 

해당 페이지에 존재하는 원소를 검색할 때에는

find_element() 함수가 그 원소를 잘 찾아주고 is_displayed() 함수도 'True'를 출력하며 잘 실행되었습니다.

 

그러나 해당 페이지에 존재하지 않는 원소를 검색할 때에는

find_element() 함수에서 에러가 뜨기 때문에 코드가 실행되지 않았습니다.

 

# ‘el2 = driver.find_element(By.CSS_SELECTOR, '[element name]').is_displayed()’에 대한 error
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Users\smpar\anaconda3\envs\boostrawler\lib\site-packages\selenium\webdriver\remote\webdriver.py", line 855, in find_element
    return self.execute(Command.FIND_ELEMENT, {
  File "C:\Users\smpar\anaconda3\envs\boostrawler\lib\site-packages\selenium\webdriver\remote\webdriver.py", line 428, in execute
    self.error_handler.check_response(response)
  File "C:\Users\smpar\anaconda3\envs\boostrawler\lib\site-packages\selenium\webdriver\remote\errorhandler.py", line 243, in check_response
    raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"css selector","selector":"#container-wing-v2"}
  (Session info: chrome=104.0.5112.102)
Stacktrace:
Backtrace:
        Ordinal0 [0x009A78B3+2193587]
        Ordinal0 [0x00940681+1771137] ~

# ‘print(el2)’에 대한 error ('el2'를 찾을 수 없다는 내용)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'el2' is not defined

 

에러 내용은 다음과 같았습니다.

에러에 대한 해결책도 구글링을 통해 찾아보았지만 마땅한 해결책이 없었습니다.

 

아니 이럴 거면 애초에 is_displayed() 함수는 어디에 어떻게 사용하라고 만들어 놓은 것인지 의문이 들었습니다. ㅂㄷㅂㄷ

 

 

 

 

 


 

 

 

 

 

is_displayed() 함수의 용도

 

 

 

그래서 is_displayed() 함수의 용도에 대해 찾아보았습니다.

 

 

Stackoverflow에 올라온 is_displayed() 함수의 용도에 대한 질문

https://stackoverflow.com/questions/48514580/isdisplayed-never-returns-false-why-in-selenium-webdriver

 

.isDisplayed()--> never returns False why? in Selenium Webdriver

.isDisplayed()--> never returns False why? it always give No Such Element Exception though i am using try catch block. try { boolean status_second= Appointment_Booking_page.

stackoverflow.com

 

비슷한 질문을 하는 사람들이 많은 걸 보면 저처럼 생각하는 사람이 많았나 봅니다.

 

stackoverflow에 올라온 답변을 보니 is_displayed()는 'True'만 return 해줄 수 있는 쓸데없는 함수가 맞았습니다.

find_element() 함수를 사용하기 전에 해당 페이지에 해당 원소가 있는지 먼저 확인한 후

is_displayed()가 항상 'True'를 return할 수 있는 상황에서만 사용할 수 있다네요.

해당 페이지에 해당 원소가 없으면 find_element()에서 에러가 뜬답니다. 저처럼요.

 

(혹시 is_displayed()를 다른 방법으로 사용하는 방법을 아시는 분이 계시다면 댓글 부탁드릴게요..!)

 

 

 

 

 


 

 

 

 

 

Selenium 라이브러리의 presence_of_element_located()와 element_to_be_clickable() 함수 사용하기

 

 

 

분명 비슷한 기능을 하는 다른 함수가 있을 것 같아 구글링과 documentation을 통해 다른 함수를 찾아보았습니다.

그리고 presence_of_element_located()와 element_to_be_clickable()가 비슷한 기능을 한다는 것을 발견하였습니다.

 

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# timeout의 시간 동안 값을 찾지 못하면 timeout이 뜨게 함
wait = WebDriverWait(driver, timeout = 1)

# presence_of_element_located(): 원소가 존재하는지 확인
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '#username')))

# element_to_be_clickable (): 원소가 클릭 가능한 상태인지 확인
wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, '#username')))

 

코드 내용을 간단히 설명하면 다음과 같습니다.

 

먼저 timeout의 시간을 지정해줍니다.

그리고 presence_of_element_located() 함수로 원소를 찾게 하고,

원소를 찾지 못한 채 timeout 만큼의 시간이 지나면 에러 대신 timeout의 결과를 받는 것입니다.

 

wait = WebDriverWait(driver, timeout = 5) # timeout 시간은 넉넉히 5초를 주었음

# 해당 page에 존재하는 [element name]으로 검색하였을 때
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[element name]'))) # 정상적인 결과 출력: <selenium.webdriver.remote.webelement.WebElement (session="c7f8a367146534522ef55306906e00d8", element="a874994f-d92c-46c9-a1b2-63ca77e79366")>
wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, '[element name]'))) # 정상적인 결과 출력: <selenium.webdriver.remote.webelement.WebElement (session="c7f8a367146534522ef55306906e00d8", element="a874994f-d92c-46c9-a1b2-63ca77e79366")>


# 해당 page에 존재하지 않는 [element name]으로 검색하였을 때
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '#container-wing-v2'))) # error
wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, '#container-wing-v2'))) # error

 

에러 대신 timeout의 결과를 받을 수 있을 줄 알았는데, 똑같이 에러를 출력하네요.

timeout 시간을 넉넉히 5초로 주니 5초 후에 에러 코드를 출력하더군요.

 

 

 

 

 


 

 

 

 

 

Selenium 라이브러리의 current_url을 사용하기

 

 

 

이쯤되니 selenium 라이브러리를 사용하여 해당 페이지에 해당 원소가 존재하는지의 여부를 확인하는 방법은 포기하게 되었습니다.

대신 3가지 방법을 생각해 보았습니다.

 

  1. Try ~ Catch 문 사용하기
  2. requests 라이브러리를 사용하여 현재 url의 response를 받기 => response.text에서 해당 원소가 존재하는지 확인하기
  3. selenium 라이브러리에서 current_url을 사용하여 현재 url 값을 받기 => url을 분류하여 어떤 페이지로 넘어갔는지 파악하기

 

셋 중에 가장 간단한 방법은 3번 방법이었습니다.

그런데 2번 방법을 사용하려면 각 url에 유의미한 차이가 있어야 하였습니다.

 

 

 

case를 다음과 같이 나누었고, 이에 따라 바뀌는 url은 다음과 같았습니다.

 

  1. 로그인 페이지 (처음 화면 or 로그인 실패): https://xauth.coupang.com/auth/realms/seller ~
  2. 에러 페이지 (로그인에는 성공하였으나 에러 페이지로 튕김): https://wing.coupang.com/sso/login ~
  3. 메인 페이지 (로그인 성공): https://wing.coupang.com/ ~

 

다행히 각 url에 유의미한 차이가 있었습니다.

그래서 이 방법을 사용하여 문제를 해결해보기로 하였고, 분류한 case에 맞추어 다음과 같이 코드를 작성하였습니다.

 

import selenium.webdriver as webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
url = 'https://wing.coupang.com/'
driver.get(url)

# Error often displayed when login. So divide cases.
while True:
    curr_url = driver.current_url
    # login page
    if curr_url.startswith('https://xauth.coupang.com/auth/realms/seller'):
    	el = driver.find_element(By.CSS_SELECTOR, '#username')
    	el.send_keys('[coupang_wing_id]')
    	el = driver.find_element(By.CSS_SELECTOR, '#password')
    	el.send_keys('[coupang_wing_pw]')
    	el = driver.find_element(By.CSS_SELECTOR, '#kc-login').click()
    # error page (login successed but error page appears)
    elif curr_url.startswith('https://wing.coupang.com/sso/login'):
    	el = driver.find_element(By.CSS_SELECTOR, '#popup-main-content').click()
    # main page (login successed)
    else:
    	break

 

성공입니다!

로그인 과정에서 문제가 생겨도 로그인이 최종적으로 성공할 때까지 코드가 잘 돌아가는 것을 확인하였습니다!

 

 

 

 

 


 

 

 

 

 

요약

 

1. Selenium 라이브러리에서 해당 페이지에 해당 원소가 존재하는지를 찾을 수 있는 함수는 사용할 수 없다. (True만 출력할 수 있고 False는 출력해주지 못한다.)

2. 그래서 Selenium 라이브러리의 currrent_url을 사용하여 해당 화면의 url로 case를 분류하여 문제를 해결했다.

 

 

 

 

복사했습니다!