본문 바로가기

WEB&서버

[웹 스크래핑] 셀레니움 사용법

https://www.selenium.dev/

 셀레니움은 웹 브라우저에 대한 테스팅 자동화 목적으로 등장한 프레임워크로, DOM 상의 엘리먼트들에 접근해서 명령을 내리거나, 여러가지 정보를 가져올 수 있다. 특히 동적으로 동작하는 웹 브라우저에서 아직 등장하지 않은 엘리먼트를 기다리거나, 특정 엘리먼트에 직접 접근하여 자바스크립트 기반 명령을 내릴 수도 있는 등 다양한 기능을 가지고 있기 때문에 동적 웹사이트의 정보를 읽어오는 기능이 탁월하다.

 위와 같은 특징들에 기반하여 셀레니움은 웹 스크래핑에도 자주 사용된다. 이때 유사한 용어로 웹 크롤링이라는 말도 많이 사용하는데, 약간의 뉘앙스 차이가 있다.


웹 크롤링

https://en.wikipedia.org/wiki/Web_crawler

 봇을 이용하여 보관 혹은 인덱싱의 목적으로 웹사이트의 모든 컨텐츠를 읽고 저장하는 프로세스로, 검색엔진이 웹사이트에서 정보를 추출, 색인을 생성하는 과정이 웹 크롤링을 사용한다.

 따라서 크롤링은 특정 타겟 사이트보다는 다수의 사이트에 대한 색인 정보를 생성하는 것이 주 목적이라고 볼 수 있다. 크롤러 프로그램은 거미가 거미줄(웹) 을 타고 경로를 방문하는 것 같다고 해서 '스파이더'라고도 불린다.

 이때 검색엔진의 노출을 원하지 않는 경우도 존재할 것이다. 이 경우 해당 웹페이지에 대한 서버의 루트 폴더에 robots.txt 파일을 지정하면 웹 크롤러에게 자신의 페이지에 대한 크롤링 범위를 알려줄 수 있다. 현재까지는 권고안 수준이라 꼭 지킬 필요는 없으나, 일종의 약속이므로 지키는 편이 좋을 것 같다.

웹 스크래핑

https://en.wikipedia.org/wiki/Data_scraping#Web_scraping

 데이터 스크래핑의 소분야로, 특정 웹사이트에 존재하는 데이터를 스크랩하는 목적으로 사용된다. 크롤링이 다수 페이지에 대한 인덱싱에 집중한다면, 특정 사이트에서 발생하는 데이터를 추출하는데 집중한다.

 위 두 개념을 구분하기도 하고 굳이 구분하지 않기도 한다. 다만 크롤링이라는 개념 자체는 인덱싱에서 출발했으므로 특정 웹사이트의 특정 정보를 추출하는 과정을 크롤링에 포함하기에는 조금 거리가 있어 보인다.


셀레니움

 셀레니움을 사용하는 경우 웹 상의 엘리먼트에 직접 접근하거나, 조건을 부여하여 정보를 좀 더 직접적으로 가져올 수 있다. 동적 웹사이트의 동작을 대비하여 여러가지 방식의 Wait가 존재하며, DOM과 직접 소통할 수 있으므로 웹 기반 지식을 가지고 있다면 사용하기 더 편할듯 싶다.

시작하기

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome.webdriver import WebDriver

browser: WebDriver
browser = webdriver.Chrome(service=Service(ChromeDriverManager().install()))

browser.set_window_size(800, 960)  # 화면 사이즈 지정

 최신 셀레니움 버전에서는 디바이스 매니저를 수동으로 설치하고 해당 경로를 지정해줄 필요가 없다. webdriver_manager 패키지를 추가적으로 설치하면 위와 같이 코드가 간편화된다.

 이후 웹 브라우저에 대한 접근은 browser 객체를 통해 수행할 수 있다. 이때 brower 객체는 WebDriver 클래스에 대한 인스턴스로, find_element을 통해 특정 사이트 상의 엘리먼트를 직접 가리킬 수 있다.

 set_window.size(x, y) 을 이용하면 웹 브라우저의 크기를 설정할 수 있다. 미디어 쿼리 등에 의해 모바일 기기와 컴퓨터에서 보여주는 정보가 다른 경우가 있는데, 특정 크기를 지정함으로써 이를 해결할 수 있다.

특정 사이트로 이동

browser.get("https://www.google.com")

get 메서드를 이용하면 특정 사이트로 이동할 수 있다.

특정 엘리먼트 추출하기

 DOM상의 각 엘리먼트들은 다양한 방법을 이용하여 추출할 수 있다. 최신 버전의 셀레니움에서는 find_element 및 find_elements 을 이용하는 방식을 기본으로 설명하고 있다. 

from selenium.webdriver.common.by import By

id = browser.find_element(By.NAME, "userid")
times : list[WebElement] = browser.find_elements(By.XPATH, "//div[@class='hour']")

 find_element은 해당 엘리먼트를 선택할 조건인 By 및 문자열을 받는다. 이 부분은 웹과 거의 동일하게 대응되므로, 필요한 방식으로 선택하면 된다. 

 By.XPATH는 xml 문서를 탐색하기 위한 일종의 표현식으로, 웹 상의 요소를 좀 더 구체적으로 검색하기 위해 사용될 수 있다. By에서 제공하는 방법 중 가장 구체적인 탐색이 가능하다.

 사용 방법은 다음을 참고하자. : https://devhints.io/xpath

브라우저에서도 Xpath을 이용하여 요소를 검색할 수 있다. 원하는 사이트의 요소를 아래와 같이 검색하면서 찾아보자.

Xpath을 이용하여 네이버 로고를 검색한 모습

엘리먼트에서 엘리먼트 구하기

webdriver에서만 엘리먼트를 구할 수 있는게 아니다. 엘리먼트 내에서도 자식 엘리먼트를 구할 수 있다. 

elem_list : list[WebElement] = element.find_elements(By.TAG_NAME, 'tr')

이 경우 특정 엘리먼트에 대한 하위 엘리먼트들을 지정된 조건을 적용하여 구할 수 있게 된다. 이 경우에는 list[WebElement] 으로 타입을 명시하지 않으면 any 타입으로 반환되어 자동 완성이 제대로 작동하지 않으므로, 타입을 명시해서 사용해야 개발할 때 편하다.

엘리먼트의 특정 속성 가져오기

text = element.get_attribute('innerText')
style = element.get_attribute('style')

 엘리먼트가 지닌 속성 정보를 가져온다. 웹 상의 엘리먼트가 가진 속성과 대응되므로, 해당 엘리먼트가 어떤 속성을 가지고 있는지 검사하고 원하는 속성을 지정하여 가져오기만 하면 된다.

엘리먼트에 대한 동작 실행

button.click() # 버튼 클릭
id.send_keys("blaxsior") # 키보드 입력 전달
# 등등 ... 웹 상에서 실행 가능한 것은 다 된다.

 특정 엘리먼트에 대해 가능한 동작을 실행한다. 해당 동작들은 웹 상에서 엘리먼트들을 getElementBy~ 함수나 querySelector을 통해 선택한 엘리먼트에 대해 수행가능한 모든 함수가 대상이 된다.

코드 실행

browser.execute_script('document.body.scrollBy(0, document.body.scrollHeight);')
length = browser.execute_script('return document.body.scrollHeight;')

# 원하는 대상 명시하기
target = browser.find_element(By.XPATH, "//div[@class='main']")
height = browser.execute_script('return arguments[0].scrollHeight;', target)

 웹상에서 사용하고 싶은 자바스크립트 코드를 명시하면, 해당 코드를 실행한다. 만약 자바스크립트 코드 상에 return 값이 존재하면, 해당 값을 받을 수도 있다. 

 코드가 전역 환경에서 실행하고 싶은 것이 아니라, 특정 엘리먼트에 대해 실행하고 싶을 수 있다. 이 경우 자바스크립트 코드 이후 인자로 특정 엘리먼트를 전달하고, 코드 상에 해당 엘리먼트들이 전달된 순서를 arguments[ n ] 형태로 작성하여 자바스크립트 코드를 식별할 수 있도록 한다. 

 return 문이 동작하는 것을 보면 아마 어떤 함수의 형태를 지정해두고 자바스크립트 코드를 그대로 복사한 후 나머지 인자들은 arguments라는 이름의 배열에 담아 웹 상에서 해당 함수를 실행하는 방식으로 동작하지 않을까 예상해본다.

대기

https://www.selenium.dev/documentation/webdriver/waits/#expected-conditions

implicit wait, explicit wait 및 Fluent wait 이 있다.

  • implicit wait : 지정된 시간동안 암시적으로 대기하면서 요소를 찾는 동안 DOM을 검사한다.
  • explicit wait : 직접적으로 특정 조건이 만족될 때까지 대기한다.
  • Fluent wait : explicit wait에 빈도 지정 등의 기능이 추가된 방식

 대부분의 상황에서는 implicit wait을 사용하면 문제가 해결된다. 만약 대기에 특정 조건 (웹 상에 특정 엘리먼트의 속성의 변하는 등) 이 꼭 필요하다면 explicit wait을 사용하면 된다. 공식에서는 두가지 방식을 혼합하지 않는 것을 권장한다.

# 암시적 대기
driver = Firefox()
driver.implicitly_wait(10)
driver.get("http://somedomain/url_that_delays_loading")
my_dynamic_element = driver.find_element(By.ID, "myDynamicElement")

# 명시적 대기
from selenium.webdriver.support.ui import WebDriverWait

WebDriverWait(browser, timeout=10).until(lambda d: d.find_element(By.XPATH, '//div[@class="main"]'))

 웹 환경에 대한 지식이 있다면 셀레니움을 DOM에 접근하기 위한 도구처럼 사용할 수 있어 쉽게 웹상의 정보를 가져올 수 있다. find_element 및 find_elements 은 DOM상의 엘리먼트을 반환하는 것과 유사하며, 해당 엘리먼트에 대해서 자식 엘리먼트를 구하는 것도 동일한 방식으로 동작한다. 

 특정 웹사이트의 정보를 자동으로 가져오는 방법을 고려하고 있다면 셀레니움도 꽤 괜찮은 것 같다. 단순히 스크래핑 목적으로 쓰는 것이 아니더라도 꽤 괜찮아 보인다. 다양한 환경에서 지원하고 있으므로 한번 사용해보자.

 다음 링크는 학교 강의 프로젝트를 위해 만들었던 스크래퍼인데, 코드 작성할 때 참고할 수 있을 것 같다.

https://github.com/CSID-DGU/2022-1-OSSP2-NOW-1/tree/test-scrap-table-info/util 

 

GitHub - CSID-DGU/2022-1-OSSP2-NOW-1

Contribute to CSID-DGU/2022-1-OSSP2-NOW-1 development by creating an account on GitHub.

github.com

 

'WEB&서버' 카테고리의 다른 글

[WEB] 웹 상의 전역변수 length  (0) 2023.01.30
[WEB] 데이터 속성  (0) 2023.01.25
SSH  (0) 2022.04.14
[WEB] beforeunload 이벤트  (0) 2022.01.04
[WEB] URL & Location & URLSearchParams  (0) 2022.01.01