[Python] 파이썬 Selenium으로 웹 브라우저 자동화하기

반응형

누구나 한 번쯤 반복적인 브라우저 작업에 지쳐본 경험이 있을 겁니다. 로그인하고, 버튼 클릭하고, 데이터 입력하고... 이런 작업을 매일 반복한다? 솔직히 정신병 걸릴 노릇입니다. 저 역시 웹 테스트를 위해 같은 작업을 수백 번 반복하다 결국 자동화의 길을 찾게 됐고, 그 해결책이 바로 Selenium이었습니다. 웹 브라우저 자동화의 최강자 Selenium을 파이썬과 함께 사용하는 방법, 제대로 파헤쳐 보겠습니다.

Selenium이란 무엇인가? (그리고 왜 써야 하는가)

Selenium은 웹 브라우저를 프로그래밍으로 제어할 수 있게 해주는 도구입니다. 원래는 웹 애플리케이션 테스트 자동화를 위해 만들어졌지만, 지금은 웹 스크래핑, 데이터 추출, 반복 작업 자동화 등 다양한 용도로 활용되고 있죠. 특히 파이썬과의 조합은 코드 몇 줄로 복잡한 브라우저 작업을 자동화할 수 있어 개발자들에게 사랑받고 있습니다.

그럼 왜 Selenium을 사용해야 할까요? 단순히 HTTP 요청을 보내는 requests 같은 라이브러리도 있는데 말이죠. 이유는 간단합니다. 현대 웹사이트는 자바스크립트로 가득 차 있고, 동적으로 콘텐츠를 로드합니다. requests는 정적 HTML만 가져올 수 있지만, Selenium은 실제 브라우저를 띄워 자바스크립트까지 실행한 최종 상태의 웹페이지를 다룰 수 있습니다. 또한 클릭, 스크롤, 키보드 입력 같은 사용자 상호작용도 자동화할 수 있죠.

개인적으로도 Selenium은 저의 일상적인 업무 자동화에 없어서는 안 될 도구입니다. 웹사이트 테스트, 대량의 데이터 수집, 심지어 특정 제품의 재고 알림까지... 한 번 능숙해지면 그 활용도는 무한합니다.

Python과 Selenium 환경 구축하기

Selenium을 사용하기 위해서는 몇 가지 준비물이 필요합니다. 파이썬 설치부터 시작해서 Selenium 라이브러리, 그리고 웹 브라우저를 제어하기 위한 WebDriver까지 필요합니다. 하나씩 설치 과정을 살펴보겠습니다.

우선 파이썬이 설치되어 있지 않다면 Python 공식 사이트에서 다운로드하여 설치합니다. 그 다음 pip를 이용해 Selenium 패키지를 설치합니다.

pip install selenium

다음으로 브라우저 드라이버가 필요합니다. 각 브라우저별로 WebDriver가 다르며, 사용하려는 브라우저에 맞는 드라이버를 설치해야 합니다. 가장 많이 사용되는 Chrome, Firefox, Edge 드라이버의 비교 정보를 아래 표에 정리했습니다.

브라우저 드라이버 다운로드 위치 호환성 특징
Chrome ChromeDriver Chrome 버전과 일치해야 함 가장 광범위하게 사용, 높은 안정성
Firefox GeckoDriver 대부분 버전 호환 오픈소스 철학에 부합, 설치 간편
Edge EdgeDriver Edge 버전과 일치해야 함 Windows 환경에서 우수한 성능
Safari 내장(macOS) 활성화 필요 macOS 필수, 기능 제한적

Selenium 4.0부터는 드라이버 자동 설치 기능도 지원하므로, 수동으로 드라이버를 관리하는 번거로움을 줄일 수 있습니다. 아래 코드를 사용하면 별도의 드라이버 다운로드 없이 자동으로 처리됩니다.

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

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))

웹 브라우저 조작의 기본

환경 설정이 완료되었다면, 이제 Selenium으로 실제 브라우저를 제어하는 방법을 알아봅시다. 웹 브라우저 자동화의 기본적인 작업들을 단계별로 살펴보겠습니다.

  1. 브라우저 시작하기

    웹드라이버를 초기화하고 브라우저 창을 여는 것부터 시작합니다. 이 때 다양한 옵션을 설정할 수 있습니다.

    from selenium import webdriver
    from selenium.webdriver.chrome.options import Options
    
    # 브라우저 옵션 설정
    chrome_options = Options()
    chrome_options.add_argument("--start-maximized")  # 브라우저 최대화
    chrome_options.add_argument("--incognito")  # 시크릿 모드
    
    # 브라우저 시작
    driver = webdriver.Chrome(options=chrome_options)
    driver.get("https://www.example.com")  # 웹사이트 접속
  2. 요소 찾기

    웹페이지에서 상호작용할 요소를 찾는 방법입니다. ID, 클래스, XPath 등 다양한 방법으로 요소를 특정할 수 있습니다.

    from selenium.webdriver.common.by import By
    
    # ID로 요소 찾기
    element_by_id = driver.find_element(By.ID, "login-button")
    
    # 클래스로 요소 찾기
    elements_by_class = driver.find_elements(By.CLASS_NAME, "product-item")
    
    # XPath로 요소 찾기
    element_by_xpath = driver.find_element(By.XPATH, "//button[contains(text(), '로그인')]")
    
    # CSS 선택자로 요소 찾기
    element_by_css = driver.find_element(By.CSS_SELECTOR, "div.main-content > p")
  3. 기본 상호작용

    요소를 찾았으면 이제 클릭하거나 텍스트를 입력하는 등의 상호작용이 가능합니다.

    # 버튼 클릭하기
    login_button = driver.find_element(By.ID, "login-button")
    login_button.click()
    
    # 텍스트 입력하기
    username_field = driver.find_element(By.NAME, "username")
    username_field.send_keys("사용자이름")
    
    # 텍스트 지우기
    username_field.clear()
    
    # 드롭다운 선택하기
    from selenium.webdriver.support.ui import Select
    dropdown = Select(driver.find_element(By.ID, "dropdown-menu"))
    dropdown.select_by_visible_text("옵션 1")  # 텍스트로 선택
    dropdown.select_by_index(2)  # 인덱스로 선택
    dropdown.select_by_value("value1")  # 값으로 선택
  4. 페이지 탐색

    브라우저의 기본 탐색 기능을 사용할 수 있습니다.

    # 다른 페이지로 이동
    driver.get("https://www.another-example.com")
    
    # 뒤로 가기
    driver.back()
    
    # 앞으로 가기
    driver.forward()
    
    # 페이지 새로고침
    driver.refresh()
  5. 브라우저 종료

    작업이 완료되면 브라우저를 종료해야 합니다.

    # 현재 탭만 닫기
    driver.close()
    
    # 브라우저 전체 종료
    driver.quit()
📝 TIP: 요소를 찾을 수 없을 때의 대처법

Selenium을 사용하다 보면 "No such element" 에러를 자주 만나게 됩니다. 이는 여러 이유로 발생할 수 있는데, 가장 흔한 원인은 다음과 같습니다.

  • 페이지가 완전히 로드되지 않아 요소가 아직 DOM에 없을 때
  • 요소가 iframe 내부에 있어 접근이 필요할 때
  • 동적으로 생성된 ID나 클래스를 사용하는 경우 (세션마다 변경됨)

이를 해결하기 위해 Explicit Wait(명시적 대기)를 사용하거나, 더 안정적인 XPath/CSS 선택자를 구성해보세요. 또한 개발자 도구에서 요소를 직접 확인하는 습관을 들이면 많은 도움이 됩니다.

고급 자동화 테크닉

기본적인 브라우저 조작을 넘어서, Selenium을 더 효율적으로 사용하기 위한 고급 기술들을 알아보겠습니다. 실제 프로젝트에서 자주 직면하는 문제들을 해결하는 방법들이죠.

대기(Wait) 전략

웹 자동화의 가장 큰 골칫거리 중 하나는 '타이밍'입니다. 페이지가 완전히 로드되기 전에 요소를 찾으려고 하면 당연히 실패합니다. 이런 문제를 해결하기 위해 Selenium은 여러 대기 전략을 제공합니다.

1. 암시적 대기(Implicit Wait)

드라이버 전체에 적용되는 기본 대기 시간을 설정합니다. 모든 find_element 명령에 영향을 주며, 지정된 시간 동안 요소를 찾기 위해 주기적으로 DOM을 확인합니다.

driver.implicitly_wait(10)  # 최대 10초 동안 대기

2. 명시적 대기(Explicit Wait)

특정 조건이 충족될 때까지 기다리는 방식입니다. 더 유연하고 세부적인 제어가 가능해 대부분의 상황에서 암시적 대기보다 권장됩니다.

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

# 요소가 클릭 가능할 때까지 최대 10초 대기
element = WebDriverWait(driver, 10).until(
    EC.element_to_be_clickable((By.ID, "myButton"))
)
element.click()

# 특정 텍스트가 페이지에 나타날 때까지 대기
WebDriverWait(driver, 10).until(
    EC.text_to_be_present_in_element((By.ID, "message"), "성공적으로 제출되었습니다")
)

3. 명시적 대기의 유용한 조건들

expected_conditions 모듈은 다양한 대기 조건을 제공합니다. 자주 사용되는 몇 가지를 살펴보겠습니다.

from selenium.webdriver.support import expected_conditions as EC

# 요소가 존재할 때까지 대기
EC.presence_of_element_located((By.ID, "myElement"))

# 요소가 보일 때까지 대기
EC.visibility_of_element_located((By.ID, "myElement"))

# 요소가 보이지 않을 때까지 대기
EC.invisibility_of_element_located((By.ID, "loadingIndicator"))

# 페이지 제목이 특정 값을 포함할 때까지 대기
EC.title_contains("완료")

# 특정 알림창(alert)이 나타날 때까지 대기
EC.alert_is_present()

고급 상호작용

단순한 클릭이나 텍스트 입력을 넘어서는 복잡한 상호작용을 처리해야 할 때가 있습니다. 키보드 조합, 드래그 앤 드롭, 스크롤 등의 작업을 위한 방법을 알아보겠습니다.

1. 키보드 액션

from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains

# Ctrl+A (전체 선택) 후 텍스트 입력
element = driver.find_element(By.ID, "editor")
element.send_keys(Keys.CONTROL + "a")  # 전체 선택
element.send_keys("새로운 텍스트")  # 텍스트 입력

# 여러 키 조합을 순차적으로 사용
actions = ActionChains(driver)
actions.key_down(Keys.CONTROL)  # Ctrl 키 누름
actions.send_keys('c')  # 'c' 키 누름 (복사)
actions.key_up(Keys.CONTROL)  # Ctrl 키 뗌
actions.perform()  # 액션 실행

2. 드래그 앤 드롭

from selenium.webdriver.common.action_chains import ActionChains

# 요소를 드래그 앤 드롭
source = driver.find_element(By.ID, "draggable")
target = driver.find_element(By.ID, "droppable")

actions = ActionChains(driver)
actions.drag_and_drop(source, target).perform()

# 또는 좀 더 세부적으로 제어하고 싶다면
actions = ActionChains(driver)
actions.click_and_hold(source)
actions.move_to_element(target)
actions.release()
actions.perform()

3. 스크롤 및 마우스 움직임

# JavaScript로 스크롤 제어
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")  # 페이지 맨 아래로 스크롤
driver.execute_script("window.scrollTo(0, 0);")  # 페이지 맨 위로 스크롤

# 특정 요소로 스크롤
element = driver.find_element(By.ID, "my-element")
driver.execute_script("arguments[0].scrollIntoView(true);", element)

# 마우스 호버 (마우스 올리기)
from selenium.webdriver.common.action_chains import ActionChains

element = driver.find_element(By.CLASS_NAME, "dropdown-toggle")
hover = ActionChains(driver).move_to_element(element)
hover.perform()

Selenium으로 웹 스크래핑 구현하기

Selenium은 단순한 테스트 도구를 넘어 웹 스크래핑 분야에서도 강력한 성능을 발휘합니다. 특히 자바스크립트로 동적으로 콘텐츠를 불러오는 사이트에서는 필수적인 도구라고 할 수 있습니다. 이번 섹션에서는 Selenium을 활용한 효과적인 웹 스크래핑 방법을 알아보겠습니다.

Selenium의 웹 스크래핑 장점

Selenium이 웹 스크래핑에서 특히 빛을 발하는 이유는 다음과 같습니다:

기능 설명 requests/BeautifulSoup 대비 장점
자바스크립트 처리 동적으로 로드되는 컨텐츠를 처리 가능 정적 HTML만 가져오는 것과 달리 JS로 생성된 콘텐츠도 수집 가능
사용자 상호작용 클릭, 스크롤 등 사용자 행동 시뮬레이션 "더 보기" 버튼 클릭, 무한 스크롤 등 처리 가능
로그인 처리 로그인이 필요한 사이트 접근 가능 복잡한 인증 프로세스를 시뮬레이션할 수 있음
대기 기능 요소가 로드될 때까지 대기 가능 비동기적으로 로드되는 데이터도 안정적으로 수집
Ajax 요청 처리 Ajax로 로드되는 데이터 수집 직접 네트워크 요청을 분석할 필요 없음

웹 스크래핑 실전 예제

실제 스크래핑 시나리오를 통해 Selenium의 활용법을 알아보겠습니다. 다음은 무한 스크롤 페이지에서 데이터를 추출하는 예제입니다.

import time
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from webdriver_manager.chrome import ChromeDriverManager
import pandas as pd

# 브라우저 설정
chrome_options = Options()
chrome_options.add_argument("--headless")  # 헤드리스 모드 (화면 표시 없음)
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=chrome_options)

# 웹사이트 접속
url = "https://example.com/products"
driver.get(url)

# 데이터를 저장할 리스트
products = []

# 이전 항목 수
prev_item_count = 0
max_scrolls = 10  # 최대 스크롤 횟수 제한
scroll_count = 0

# 무한 스크롤 처리
while scroll_count < max_scrolls:
    # 현재 상품 항목 수집
    items = driver.find_elements(By.CSS_SELECTOR, ".product-item")
    
    # 새로운 항목만 처리
    for item in items[prev_item_count:]:
        try:
            name = item.find_element(By.CSS_SELECTOR, ".product-name").text
            price = item.find_element(By.CSS_SELECTOR, ".product-price").text
            rating = item.find_element(By.CSS_SELECTOR, ".product-rating").text
            
            products.append({
                "name": name,
                "price": price,
                "rating": rating
            })
        except Exception as e:
            print(f"항목 처리 중 오류 발생: {e}")
    
    # 현재 항목 수 업데이트
    prev_item_count = len(items)
    
    # 페이지 맨 아래로 스크롤
    driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
    
    # 새 콘텐츠 로딩 대기
    time.sleep(2)
    
    # 새 항목이 로드되었는지 확인
    new_items = driver.find_elements(By.CSS_SELECTOR, ".product-item")
    if len(new_items) == prev_item_count:
        # 더 이상 새 항목이 로드되지 않으면 종료
        break
    
    scroll_count += 1

# 수집한 데이터 처리
df = pd.DataFrame(products)
df.to_csv("products.csv", index=False)

# 브라우저 종료
driver.quit()

print(f"총 {len(products)}개의 상품 정보가 수집되었습니다. 'products.csv' 파일로 저장되었습니다.")

실전 프로젝트와 모범 사례

Selenium을 프로덕션 환경에서 효율적으로 사용하기 위한 모범 사례와 팁을 알아보겠습니다. 실제 프로젝트에서는 단순히 코드가 작동하는 것을 넘어 안정성, 유지보수성, 성능 등을 고려해야 합니다.

실전 자동화 모범 사례

  • 페이지 객체 패턴(Page Object Model) 사용하기

    웹페이지의 요소와 그 동작을 캡슐화하여 유지보수성을 높이는 디자인 패턴입니다.

    # 페이지 객체 예시
    class LoginPage:
        def __init__(self, driver):
            self.driver = driver
            self.username_input = (By.ID, "username")
            self.password_input = (By.ID, "password")
            self.login_button = (By.ID, "login-btn")
        
        def navigate(self):
            self.driver.get("https://example.com/login")
        
        def enter_username(self, username):
            username_field = self.driver.find_element(*self.username_input)
            username_field.clear()
            username_field.send_keys(username)
        
        def enter_password(self, password):
            password_field = self.driver.find_element(*self.password_input)
            password_field.clear()
            password_field.send_keys(password)
        
        def click_login(self):
            self.driver.find_element(*self.login_button).click()
        
        def login(self, username, password):
            self.enter_username(username)
            self.enter_password(password)
            self.click_login()
    
    # 사용 예시
    login_page = LoginPage(driver)
    login_page.navigate()
    login_page.login("user123", "password123")
  • 헤드리스 모드 활용하기

    리소스가 제한된 환경이나 서버에서 실행할 때는 화면 없이 실행되는 헤드리스 모드를 사용하면 효율적입니다.

    from selenium.webdriver.chrome.options import Options
    
    chrome_options = Options()
    chrome_options.add_argument("--headless")
    chrome_options.add_argument("--disable-gpu")  # Windows에서 필요
    chrome_options.add_argument("--window-size=1920,1080")  # 해상도 설정
    
    driver = webdriver.Chrome(options=chrome_options)
  • 예외 처리 철저히 하기

    자동화 스크립트는 예상치 못한 상황에 쉽게 실패할 수 있습니다. 적절한 예외 처리로 견고성을 높이세요.

    from selenium.common.exceptions import NoSuchElementException, TimeoutException
    
    try:
        element = WebDriverWait(driver, 10).until(
            EC.presence_of_element_located((By.ID, "myElement"))
        )
        element.click()
    except TimeoutException:
        print("요소를 찾을 수 없습니다: 타임아웃 발생")
        # 로그 기록 또는 대체 작업 수행
    except NoSuchElementException:
        print("요소가 존재하지 않습니다")
        # 로그 기록 또는 대체 작업 수행
    finally:
        # 실패 여부와 관계없이 반드시 수행해야 할 작업
        driver.save_screenshot("error.png")  # 오류 발생 화면 캡처
  • ID 선택자 우선 사용하기

    요소를, ID > Name > CSS 선택자 > XPath 순으로 선택하면 성능과 안정성이 향상됩니다. XPath는 가장 느리고 취약한 방법이므로 다른 방법이 없을 때만 사용하세요.

  • 불필요한 브라우저 기능 비활성화

    이미지 로딩 비활성화, 자바스크립트 최적화 등으로 성능을 향상시킬 수 있습니다.

    chrome_options = Options()
    chrome_options.add_argument("--disable-extensions")  # 확장 기능 비활성화
    chrome_options.add_argument("--disable-images")  # 이미지 로딩 비활성화
    chrome_options.add_argument("--disable-notifications")  # 알림 비활성화
    chrome_options.add_argument("--disable-infobars")  # 정보 표시줄 비활성화
  • 자원 관리 최적화

    Python의 context manager를 사용하여 리소스 누수를 방지하는 것이 좋습니다..

    from contextlib import contextmanager
    
    @contextmanager
    def use_driver():
        driver = webdriver.Chrome()
        try:
            yield driver
        finally:
            driver.quit()
    
    # 사용 예시
    with use_driver() as driver:
        driver.get("https://www.example.com")
        # 작업 수행
    # with 블록을 벗어나면 자동으로 driver.quit() 호출
📝 TIP: 웹사이트 차단 우회하기

웹 스크래핑을 진행하다 보면 자연스럽게 웹사이트의 봇 차단 메커니즘을 만나게 됩니다. 정상적인 사용자처럼 행동하여 이를 우회하는 방법입니다.

  • User-Agent 헤더 설정하기
  • 요청 사이에 무작위 시간 간격 두기 (time.sleep(random.uniform(1, 5)))
  • 필요한 경우에만 자바스크립트 활성화하기
  • 프록시 서버 활용하기
  • 일관된 패턴보다는 약간의 무작위성 추가하기

항상 웹사이트의 이용약관과 robots.txt를 확인하고, 웹사이트에 과도한 부하를 주지 않도록 주의해야 합니다. 안 그러면 ... 짤리니까요

📝 TIP: Selenium 성능 최적화 전략

Selenium 스크립트가 느리게 실행된다면 다음 방법들을 확인해보시면 좋을 것 같습니다.

  • 암시적 대기 시간을 최소화하고 명시적 대기를 적절히 사용하세요.
  • XPath 대신 CSS 선택자를 사용하면 요소 찾기 속도가 향상됩니다.
  • 불필요한 JS와 이미지를 비활성화하여 페이지 로딩 시간을 줄이세요.
  • 스크롤이 필요한 경우 전체 페이지 대신 특정 요소만 스크롤하세요.
  • 병렬 처리와 멀티스레딩을 활용하여 여러 작업을 동시에 처리하세요.

성능 최적화는 작은 개선들이 모여 큰 차이를 만듭니다. 항상 변경사항을 측정하고 비교하세요.

자동 웹 모니터링 시스템 만들어보기.

이제 지금까지 배운 내용을 활용해 실제 유용한 프로젝트를 만들어 보겠습니다. 아래 코드는 특정 웹사이트의 중요 정보를 정기적으로 모니터링하고, 변경사항이 감지되면 이메일 알림을 보내는 시스템입니다. 이런 도구는 가격 변동, 재고 상태, 콘텐츠 업데이트 등을 추적하는 데 매우 유용합니다.

import time
import smtplib
import hashlib
import logging
import schedule
import pickle
import os.path
from email.message import EmailMessage
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException
from webdriver_manager.chrome import ChromeDriverManager

# 로깅 설정
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler("monitor.log"),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

class WebsiteMonitor:
    def __init__(self, config):
        self.config = config
        self.setup_driver()
        self.previous_state = self.load_previous_state()
    
    def setup_driver(self):
        """Selenium 웹드라이버 설정"""
        options = Options()
        options.add_argument("--headless")
        options.add_argument("--no-sandbox")
        options.add_argument("--disable-dev-shm-usage")
        options.add_argument("--disable-gpu")
        options.add_argument("--window-size=1920,1080")
        # 봇 감지 회피를 위한 User-Agent 설정
        options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36")
        
        self.driver = webdriver.Chrome(
            service=Service(ChromeDriverManager().install()),
            options=options
        )
        # 암시적 대기 설정
        self.driver.implicitly_wait(5)
    
    def load_previous_state(self):
        """이전 상태 로드, 없으면 빈 딕셔너리 반환"""
        if os.path.exists("previous_state.pkl"):
            try:
                with open("previous_state.pkl", "rb") as f:
                    return pickle.load(f)
            except Exception as e:
                logger.error(f"이전 상태 로드 실패: {e}")
        return {}
    
    def save_current_state(self, state):
        """현재 상태 저장"""
        try:
            with open("previous_state.pkl", "wb") as f:
                pickle.dump(state, f)
        except Exception as e:
            logger.error(f"현재 상태 저장 실패: {e}")
    
    def get_content_hash(self, content):
        """콘텐츠의 해시값 계산"""
        return hashlib.md5(content.encode()).hexdigest()
    
    def send_email_alert(self, subject, body):
        """이메일 알림 전송"""
        try:
            msg = EmailMessage()
            msg.set_content(body)
            msg['Subject'] = subject
            msg['From'] = self.config['email']['sender']
            msg['To'] = self.config['email']['recipient']
            
            server = smtplib.SMTP(self.config['email']['smtp_server'], self.config['email']['smtp_port'])
            server.starttls()
            server.login(self.config['email']['username'], self.config['email']['password'])
            server.send_message(msg)
            server.quit()
            
            logger.info(f"알림 이메일 전송됨: {subject}")
        except Exception as e:
            logger.error(f"이메일 전송 실패: {e}")
    
    def check_website(self, site_config):
        """지정된 웹사이트의 변경사항 확인"""
        url = site_config['url']
        elements = site_config['elements']
        site_name = site_config['name']
        
        logger.info(f"{site_name} 모니터링 시작...")
        
        try:
            self.driver.get(url)
            
            # 페이지 완전 로드 대기
            WebDriverWait(self.driver, 10).until(
                EC.presence_of_element_located((By.TAG_NAME, "body"))
            )
            
            current_state = {}
            changes_detected = False
            change_details = []
            
            # 지정된 요소들의 콘텐츠 확인
            for element_name, selector in elements.items():
                selector_type, selector_value = selector
                
                try:
                    # 명시적 대기를 통해 요소 확인
                    element = WebDriverWait(self.driver, 10).until(
                        EC.presence_of_element_located((getattr(By, selector_type), selector_value))
                    )
                    
                    # 요소 내용 추출
                    content = element.text or element.get_attribute("innerHTML")
                    content_hash = self.get_content_hash(content)
                    current_state[element_name] = {
                        "hash": content_hash,
                        "content": content
                    }
                    
                    # 이전 상태와 비교하여 변경 감지
                    site_previous = self.previous_state.get(site_name, {})
                    element_previous = site_previous.get(element_name, {})
                    
                    if element_previous and element_previous.get("hash") != content_hash:
                        changes_detected = True
                        change_details.append({
                            "element": element_name,
                            "previous": element_previous.get("content", ""),
                            "current": content
                        })
                        logger.info(f"{site_name}의 {element_name}에서 변경 감지됨")
                    
                except (TimeoutException, NoSuchElementException) as e:
                    logger.error(f"{site_name}의 {element_name} 요소 찾기 실패: {e}")
            
            # 변경사항이 감지되면 알림 전송
            if changes_detected:
                subject = f"{site_name} 웹사이트 변경 감지됨"
                body = f"{site_name} ({url})에서 다음 변경사항이 감지되었습니다:\n\n"
                
                for change in change_details:
                    body += f"요소: {change['element']}\n"
                    body += f"이전: {change['previous'][:100]}{'...' if len(change['previous']) > 100 else ''}\n"
                    body += f"현재: {change['current'][:100]}{'...' if len(change['current']) > 100 else ''}\n\n"
                
                self.send_email_alert(subject, body)
            
            # 현재 상태 저장
            self.previous_state[site_name] = current_state
            self.save_current_state(self.previous_state)
            
            logger.info(f"{site_name} 모니터링 완료")
            
        except Exception as e:
            logger.error(f"{site_name} 모니터링 중 오류 발생: {e}")
    
    def run_monitoring(self):
        """모든 구성된 웹사이트 모니터링"""
        try:
            for site_config in self.config['sites']:
                self.check_website(site_config)
        except Exception as e:
            logger.error(f"모니터링 실행 중 오류 발생: {e}")
        finally:
            # 브라우저 세션 유지 (메모리 누수 방지)
            self.driver.delete_all_cookies()
    
    def schedule_monitoring(self):
        """모니터링 작업 스케줄링"""
        schedule.every(self.config['check_interval']).minutes.do(self.run_monitoring)
        
        logger.info(f"모니터링 스케줄러 시작됨. 간격: {self.config['check_interval']}분")
        
        while True:
            schedule.run_pending()
            time.sleep(1)
    
    def cleanup(self):
        """리소스 정리"""
        if hasattr(self, 'driver'):
            self.driver.quit()
        logger.info("모니터링 종료, 리소스 정리 완료")


# 사용 예시
if __name__ == "__main__":
    # 모니터링 구성
    config = {
        "check_interval": 30,  # 분 단위
        "email": {
            "sender": "your-email@gmail.com",
            "recipient": "recipient@example.com",
            "smtp_server": "smtp.gmail.com",
            "smtp_port": 587,
            "username": "your-email@gmail.com",
            "password": "your-app-password"  # Gmail의 경우 앱 비밀번호 필요
        },
        "sites": [
            {
                "name": "상품 가격 모니터",
                "url": "https://www.example.com/product/12345",
                "elements": {
                    "price": ("CLASS_NAME", "product-price"),
                    "stock": ("ID", "stock-status"),
                    "rating": ("CSS_SELECTOR", ".rating-value span")
                }
            },
            {
                "name": "뉴스 헤드라인",
                "url": "https://www.example-news.com",
                "elements": {
                    "main_headline": ("XPATH", "//h1[@class='main-title']"),
                    "breaking_news": ("CSS_SELECTOR", ".breaking-news-container")
                }
            }
        ]
    }
    
    try:
        monitor = WebsiteMonitor(config)
        # 초기 실행 (즉시 모니터링)
        monitor.run_monitoring()
        # 스케줄링된 모니터링 시작
        monitor.schedule_monitoring()
    except KeyboardInterrupt:
        logger.info("사용자에 의해 프로그램 종료됨")
    except Exception as e:
        logger.error(f"프로그램 실행 중 오류 발생: {e}")
    finally:
        if 'monitor' in locals():
            monitor.cleanup()

이 코드는 다음과 같은 주요 기능을 포함하는데요.

  • 다중 웹사이트 및 요소 모니터링
  • 변경사항 감지를 위한 해시 기반 비교
  • 이메일 알림 시스템
  • 예외 처리 및 로깅
  • 정기적인 모니터링을 위한 스케줄링
  • 리소스 관리 및 메모리 누수 방지

이 코드를 실행하려면 다음 패키지가 필요합니다.

pip install selenium webdriver-manager schedule

실제 사용 전에 이메일 설정과 모니터링할 웹사이트 정보를 업데이트해야 합니다. Gmail을 사용하는 경우 계정에서 "앱 비밀번호"를 생성해야 합니다.

이 코드는 다양한 용도로 확장 가능합니다:

  • 제품 가격 추적기
  • 재고 알림 시스템
  • 뉴스 모니터링
  • 경쟁사 웹사이트 변경 추적
  • 구인 정보 알림

이 코드는 웹 자동화의 실용적인 응용 사례를 보여주며, 자신의 필요에 맞게 수정하여 사용할 수 있습니다. 특히 주기적으로 확인해야 하는 정보나 중요한 변경사항을 놓치고 싶지 않은 경우에 유용합니다.

Selenium 자동화의 무한한 가능성

지금까지 Selenium을 이용한 웹 브라우저 자동화에 대해 쭈욱 살펴봤습니다. 많은 개발자들이 단순 반복 작업에 시간을 낭비하고 있는데, Selenium을 마스터하면 그 시간을 보다 창의적인 작업에 투자할 수 있습니다. 자동화는 단순히 편리함을 넘어 일관성, 정확성, 그리고 확장성을 제공합니다.

물론 Selenium이 만능은 아닙니다. 성능 측면에서 한계가 있고, 웹사이트 구조가 변경되면 스크립트도 따라서 수정해야 합니다. 하지만 이런 단점을 감안하더라도, 그 활용 가치는 충분히 높습니다. 특히 웹 테스트 자동화, 데이터 수집, 모니터링 시스템 구축 등 다양한 영역에서 강력한 도구로 활용될 수 있습니다.

Selenium을 사용하면서 기억해야 할 중요한 점은 웹사이트의 이용약관과 robots.txt를 항상 존중해야 한다는 것입니다. 웹 자동화는 편리하지만, 웹사이트에 과도한 부하를 주거나 저작권을 침해하는 방식으로 사용해서는 안 됩니다.

"자동화는 불가능을 가능하게 만들고, 가능한 것을 쉽게 만들며, 쉬운 것을 우아하게 만든다." - Doug McIlroy

Designed by JB FACTORY