Selenium

Junha Kim·2021년 1월 3일
1

Crawling

목록 보기
2/4

웹은 크게 2가지로 나눌 수 있다.

웹의 종류

selenium은 가장 유명한 브라우저 자동화도구이다. 실제 웹브라우저를 켜는 과정을 거치기 때문에 selenium을 이용하면 동적 페이지에서도 데이터를 수집할 수 있다

브라우저를 직접 동작시킨다는 것은 JavaScript를 이용해 비동기적으로 혹은 뒤늦게 불러와지는 컨텐츠들을 가져올 수 있다는 것이다.

즉, ‘눈에 보이는’ 컨텐츠라면 모두 가져올 수 있다는 뜻이다.

requests에서 사용했던 .text의 경우 브라우저에서 ‘소스보기’를 한 것과 같이 동작하여, JS등을 통해 동적으로 DOM이 변화한 이후의 HTML을 보여주지 않는다.

반면 Selenium은 실제 웹 브라우저가 동작하기 때문에 JS로 렌더링이 완료된 후의 DOM결과물에 접근이 가능하다.


Selenium에서 흔히 겪는 문제 - NoSuchElementException

셀레니움을 사용하면 항상 마주하는 오류가 NoSuchElementException 이다.

AJAX를 사용해 동적으로 변화하는 html페이지의 경우 web element가 DOM로 나타나기까진 시간이 걸릴때가 있다

변화된 페이지 로딩이 되기 전에, 코드가 실행되어 요소를 찾지 못하는 것이다. 이를 해결하는 방법은 여러가지가 있다.

1. time 모듈

가장 쉬운 방법이다. time 모듈을 불러와 time.sleep(int)을 사용한다. int (초)만큼 코드 실행을 잠시 멈추며, 모든 시간을 소비하고 나서야 다음 코드를 실행한다.

하지만 이 방법은 비효율적이다. 네트워크에 따라서 페이지 로딩이 빠를수도, 느릴수도 있다.

명시한 시간을 소비하기 전에 페이지가 로딩이 된다면, 나머지 시간을 낭비하게 된다.

반대로 시간을 모두 소비하고도 페이지 로딩이 완벽히 되지 않은 경우가 있어 오류가 발생할 수 있다.

그래서 time 모듈을 사용하는 것은 권장되지 않는 방법이다.

2. Implicitly_wait 함수

두 번째 방법은 셀레니움 내장 함수인 **driver.implicitly_wait(int)를 사용**하는 것이다.

이는 driver 객체가 get(url)로 요청한 페이지 내용들이 모두 로딩이 완료될 때까지 int(초) 만큼 암묵적으로 기다리게 하는 것이다.

from selenium import webdriver

driver = webdriver.Chrome('chromedriver')

# driver를 만든 후 implicitly_wait 값(초단위)을 넣어주세요.
driver.implicitly_wait(3)

driver.get('https://www.kakaobank.com/')
..
..

time.sleep 과 다른 점은 페이지 로딩이 완료되었다면, 다음 코드를 바로 실행한다는 것이다.

Implicitly_wait()는 driver 객체 생성 후, 최초 1번만 선언을 해주면 global하게 설정이 된다. 즉, 다른 페이지로 옮겨도 페이지가 로딩될 때마다 기다린다는 것이다.

하지만 구글링을 해보면, 이 방법도 완전히 최적화가 되어 있다고 보지는 않는 것 같다.

웹이 ajax를 통해 HTML 구조를 동적으로 바꾸고 있다면 과연 처음에 설정한 시간이 적절한 값일지에 대해 의문이 든다는 것이다.

혹은 사용자가 원하는 요소가 이미 로딩이 되었는데도, 다른 페이지 요소때문에 무작정 일정시간을 기다리게 한다는 것이다.

그래서 조금 더 발전된 기다리는 방식인 Explicitly wait을 사용하게 된다.

3. Explicitly_wait

Explicitly wait은 명시적으로 어떤 조건이 성립했을 때까지 기다린다. 조건이 성립하지 않으면 timeout으로 설정된 시간만큼 최대한 기다리고, TimeoutExceptionthrow한다.

즉, 사용자가 찾으려는 특정 요소를 찾을 때까지 기다린다는 것이다.

Implicitly_wait페이지 전체 로딩을 기다렸다면, Explicitly wait특정 요소 로딩만을 기다리기에 시간 효율을 최적화할 수 있다.

WebDriverWait + By + expected_conditions(EC)의 조합으로 구현할 수 있다.

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

driver = webdriver.Chrome()
driver.get("http://somedomain/url_that_delays_loading")
try:
    element = WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.ID, "myDynamicElement"))
    )
finally:
    driver.quit()

WebDriverWait

WebDriverWait(driver, timeout, poll_frequency=0.5, ignored_exceptions=None) 객체를 생성한다.

  1. driver : 사용할 webdriver 객체를 전달
  2. timeout : 최대로 대기할 시간이며 단위는 .
  3. poll_frequency : 로딩이 완료될때까지 기다리는 동안 기다리는 element를 일정 시간 간격마다 호출해보게 되는데, 이때 얼마만큼의 시간 간격마다 호출할 것인지를 결정한다. 기본 값은 0.5초이다
  4. ignored_exceptions : 일정 간격마다 element를 호출하는동안 발생하는 Exception 중 무시할 것을 지정할 수 있다. 기본값으로는 NoSuchElementException만 무시한다. iterable한 형식으로 무시할 Exception들을 전달해주면 된다.

Until

  • until(method, message='') : method를 실행해서 False가 아닐 때 까지 기다리게 된다.
  • until_not(method, message='') : method를 실행해서 Return값이 False가 나오지 않을 때 까지 기다리게 된다.

method의 값Boolean 값을 반환하는 것이어야 한다.

Exception_conditions

Selenium은 until과 until_not에 condition으로 사용할 method 중 빈번하게 쓰이는 것들을 미리 정의하여 제공하고 있다.

  • 정의된 Methods

위의 method들을 적절히 사용하면 된다.


Selenium_class.py

아래의 클래스는 내가 자주 쓰는 함수들을 클래스로 만들어서, Explicitly Wait를 좀 더 간결하게 사용하려고 한 것이다.

  • options.add_argument('headless') : 크롤링 하면서 크롬을 백그라운드에서 실행
  • options.add_argument( "user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36") : 봇으로 인식하지 않게끔 설정
  • **ChromeDriverManager().install()** : 크롬 드라이버를 직접 설치하지 않아도 모듈이 사용자 환경에 맞게 설치, 설치된 chromedriver.exe는 cache에 저장됨.
  • def get_fra(self, name): : iframe 이동하는 함수, 하지만 iframe이 제대로 이동하지 못하는 경우가 있었다. 그래서 무한 루프를 돌면서 frame이 바뀔 때까지 실행되도록 커스터마이징 했다. ⇒ 위의 EC 모듈에 정의된 함수로 바꿀 수 있는지 찾아봐야겠다.
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By

class Driver:
    def __init__(self):
        options = webdriver.ChromeOptions()
        options.add_argument('headless')
        options.add_argument(
            "user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36")
        self.driver = webdriver.Chrome(ChromeDriverManager().install(), options=options)

    def __call__(self):
        return self.driver

    def get_url(self, url): # 새 창으로 url 열기
        self.driver.get(url)

    def get_default(self):  # default 프레임 이동
        while True:
            try:
                self.driver.switch_to_default_content()
                return
            except:
                print('default frame 이동')
                pass

    def get_fra(self, name):  # 특정 프레임 이동
        while True:
            try:
                self.driver.switch_to_frame(name)
                break
            except:
                self.get_default()
                print(name, 'frame 이동')
                continue

    def find_by_xpath(self, xpath): # Xpath로 단일 요소 찾기
        return WebDriverWait(self.driver, 10).until(
                        EC.presence_of_element_located(
                            (By.XPATH, xpath)))

    def find_by_class(self, class_name): # class name으로 단일 요소 찾기
        return WebDriverWait(self.driver, 10).until(
                        EC.presence_of_element_located(
                            (By.CLASS_NAME, class_name)))

    def find_by_tag(self, tag): # tag로 단일 요소 찾기
        return WebDriverWait(self.driver, 10).until(
            EC.presence_of_element_located(
                (By.TAG_NAME, tag)))

    def find_by_name(self, name): # name으로 단일 요소 찾기
        return WebDriverWait(self.driver, 10).until(
            EC.presence_of_element_located(
                (By.NAME, name)))

    def find_all_by_class(self, class_name): # class name으로 모든 요소 찾기
        return WebDriverWait(self.driver, 10).until(
            EC.presence_of_all_elements_located(
                (By.TAG_NAME, class_name)))

    def find_all_by_tag(self, tag): # tag로 모든 요소 찾기
        return WebDriverWait(self.driver, 10).until(
            EC.presence_of_all_elements_located(
                (By.TAG_NAME, tag)))

    def find_all_by_name(self, name): # name으로 모든 요소 찾기
        return WebDriverWait(self.driver, 10).until(
            EC.presence_of_all_elements_located(
                (By.NAME, name)))

    def find_all_by_tag_with_obj(self, obj, name): # tag으로 모든 요소 찾기
        return WebDriverWait(obj, 20).until(
            EC.presence_of_all_elements_located(
                (By.TAG_NAME, name)))

    def find_by_tag_with_obj(self, obj, name): # tag으로 요소 찾기
        return WebDriverWait(obj, 20).until(
            EC.presence_of_element_located(
                (By.TAG_NAME, name)))

    def find_by_link(self, text):
        return WebDriverWait(self.driver, 10).until(
            EC.presence_of_element_located(
                (By.LINK_TEXT, text)))

    def click(self, btn):
        self.driver.execute_script("arguments[0].click();", btn)

    def close(self):
        self.driver.close()

참고 자료

5. Waits - Selenium Python Bindings 2 documentation

Selenium - 페이지 로딩이 완료될 때까지 기다리기 (python)

Python Selenium Explicit Waits 사용하기

0개의 댓글