한라산에 가고 싶어

건우조·2024년 1월 8일

2024년 신년을 맞아 한라산의 기운을 받으러 가게 되었다.
그런데 큰 문제가 생겼다. 당일 한라산 등반 예약의 정원이 가득 차서 예약에 실패했다는 것.

혹여나 취소표가 뜰까봐 3일 동안 새로고침이나 하고 있었으나, 나올 턱이 없었다.
포기할까 하던 도중, '매크로나 만들어보자' 하는 마음으로 친구와 chatGPT의 도움을 받아 매크로를 짜보았다.

분명히 너무나 간단하고 깔끔하지도 않은 비기너 수준의 코드이다. 하지만 현재 어떤 생각으로 코드를 작성했는지, 지금 이 수준에서는 문제와 부딪쳤을 때 어떻게 해결했는지를 기록해 두고 추후 비교 및 리뷰를 위해 기록해두고자 한다.


필요한 Settings

Modules

  • Selenium
    - 동적으로 변하는 웹 페이지의 html 요소들에 접근하여 데이터를 가져올 수 있다.
    • 마우스 클릭, 키보드 입력 등의 시행도 가능하기 때문에 페이지 이동 등을 위해 사용
  • Smtplib, Email
    - 이메일을 보낼 때 필요한 모듈
    • gmail을 통해 알림을 받기 위해 사용
  • Time
    - sleep() 함수를 이용하여 페이지의 응답 대기

ChromeDriver

Selenium을 이용하여 웹 크롤링 등을 진행할 때 Chrome브라우저를 이용하기 위해 필요한 웹드라이버
chromedriver 다운로드
본인의 크롬 버전 및 운영체제에 맞는 버전을 다운받아 사용할 수 있다.


Problems

0. 보안문자

보안문자를 입력해야 예약을 진행할 수 있기 때문에, (적어도 지금 내 수준에서는) 자동 예약 매크로를 만들지 못하게 되었다. 따라서 조금 방향을 바꾸어 예약이 가능하면 나에게 메일을 통해 알림을 받을 수 있도록 작성하였다. email과 smtplib 모듈을 사용하게 된 이유이기도 하다.

1. SNS 인증

가장 먼저 마주한 문제는 바로 SNS 인증을 해야만 예약이 가능하다는 부분이었다. 따라서 카카오 로그인 버튼을 클릭한 후, 아이디와 비밀번호를 key로 보내주어 자동으로 로그인이 가능하도록 하였다. 이후 switch_to.window() 함수를 통해 기존 예약 페이지를 다시 탐색하였다.

def login():
    driver.get('https://visithalla.jeju.go.kr/login/login.do')

    login_btn = driver.find_element(By.CLASS_NAME,'btn-kakao')
    login_btn.click()
    
    time.sleep(3)
    
    ...
    
    driver.switch_to.window(driver.window_handles[-1])

    kakao_id_input = driver.find_element(by='name', value='loginId')
    kakao_pw_input = driver.find_element(by='name', value='password')

    kakao_id_input.send_keys('my kakaoID')
    kakao_pw_input.send_keys('my kakaoPW')

    kakao_login_btn = driver.find_element(By.CLASS_NAME,'btn_g')
    kakao_login_btn.click()

    time.sleep(3)

    driver.switch_to.window(driver.window_handles[0])

추가적으로 이미 로그인이 되어있는 경우, 카카오 로그인 버튼을 클릭하면 예약 페이지가 아닌 메인 페이지로 자동으로 이동해버리기 때문에, 현재 페이지 URL을 감지하여 로그인 유무를 판단한 후 로그인을 진행하는 함수를 작성했다.

	current_url = driver.current_url
    if current_url == 'https://visithalla.jeju.go.kr/main/main.do':
        return

    else:
    ...

2. 날짜 및 코스 선택

탐방로와 탐방 시간대는 콤보박스 형태로 되어 있어서 쉽게 입력이 가능했으나, 날짜의 경우 달력에서 선택하는 형태였기 때문에 html에 쉽게 접근이 어려웠다. 따라서 코드를 짠 당시(12월)을 기준으로 2024년 1월 4일을 선택하기 위해 '달력을 클릭 > 다음 달로 넘어감 > 4일을 클릭' 하는 상당히 비효율적인 방법을 사용하였다. CSS 선택자로 잘 접근이 되지 않아 XPATH 구문을 사용해 보았다.

	# 탐방로 선택 - 관음사 코스
    course_select = Select(driver.find_element(by='name', value="courseSeq"))
    course_select.select_by_visible_text("관음사 코스")

    # 탐방 날짜 설정 - 2024년 1월 4일
    # datepicker(달력) 클릭
    date_input = driver.find_element(By.CLASS_NAME, 'ui-datepicker-trigger')
    date_input.click()
    time.sleep(1)

	# 다음 달로 이동
    next_month = driver.find_element(By.CLASS_NAME, 'ui-icon-circle-triangle-e')
    next_month.click()

	# 4일을 찾아 클릭
    xpath_expression = f"//a[text()='4']"
    day4 = driver.find_element(By.XPATH, xpath_expression)
    day4.click()

    # 탐방 시작 시간 설정 - 06:00 ~ 08:00
    start_time_select = Select(driver.find_element(by='name', value="visitTm"))
    start_time_select.select_by_visible_text("06:00 ~ 08:00")

2.5 예약 인원 확인

2번과 비슷한 상황으로, 예약 인원을 나타내는 span 요소가 분명한 class나 id 값을 갖지 않아 역시 접근하는데 어려웠다. 따라서 부모 > 부모 요소의 id를 이용한 CSS 선택자를 통해 접근하여 텍스트를 추출해낼 수 있었다.

	# 'current_num' id를 갖는 span 태그의 자식 요소를 선택
    current_reservation_element = driver.find_element(By.CSS_SELECTOR, '#current_num>strong>span')

    # span 태그 내의 전체 텍스트 추출
    current_reservation_text = current_reservation_element.text.strip()
    print(current_reservation_text)

이후 이 텍스트를 통해 정원이 꽉 찬 상태가 아니라면, 나에게 메일을 보내도록 설정하였다.

	# 현재 예약 인원이 400명보다 작을 때 알림
    if current_reservation_text != '현재예약인원 400':
        # 변경되었다면 이메일로 알림 보내기
        send_email("예약 가능 여부 변경 알림", current_reservation_text)

3. Google의 보안

구글 ID와 비밀번호를 입력받아 메일을 보내도록 작성했었는데, 예기치 못한 오류 메세지를 받았다.

계정을 안전하게 보호하기 위해 2022년 5월 30일부터 ​​Google은 사용자 이름과 비밀번호만 사용하여 Google 계정에 로그인하도록 요청하는 서드 파티 앱 또는 기기의 사용을 더 이상 지원하지 않습니다.

작성한 매크로가 무용지물이 되나 했는데, 다행히 방법은 있었다. 2단계 인증을 통해 현재 기기에서 사용 가능한 앱 비밀번호를 발급 받으면 email 모듈을 통해서도 Gmail에 로그인 후 사용할 수 있었다.

def send_email(subject, body):
    # Gmail 서버에 연결
    server = smtplib.SMTP('smtp.gmail.com', 587)
    server.starttls()
    server.login(gmail_username, gmail_app_password)

    # 이메일 메시지 작성
    msg = MIMEText(body)
    msg['Subject'] = subject
    msg['From'] = gmail_username
    msg['To'] = recipient_email

    # 이메일 보내기
    server.sendmail(gmail_username, recipient_email, msg.as_string())

    # 서버 연결 종료
    server.quit()

Code

코드 전문은 다음과 같다.

import time
from selenium import webdriver
from selenium.webdriver.support.ui import Select
from selenium.webdriver.common.by import By
import smtplib
from email.mime.text import MIMEText

# 크롬 드라이버 경로 설정
chrome_driver_path = "C:\\Program Files\\ChromeDriver\\chromedriver.exe"

# 크롬 드라이버 옵션 설정
chrome_options = webdriver.ChromeOptions()
# chrome_options.add_argument("--headless")  # 주석 처리

# 크롬 드라이버 실행
driver = webdriver.Chrome(options=chrome_options)

# 예약 사이트 URL 설정
reservation_url = "https://visithalla.jeju.go.kr/reservation/firstComeStep.do"

# Gmail 계정 정보 설정
gmail_username = "my email"
gmail_app_password = 'my app_password'

# 수신자 이메일 주소
recipient_email = "my email"

# 취소표 확인 간격 (초)
check_interval = 300

def send_email(subject, body):
    # Gmail 서버에 연결
    server = smtplib.SMTP('smtp.gmail.com', 587)
    server.starttls()
    server.login(gmail_username, gmail_app_password)

    # 이메일 메시지 작성
    msg = MIMEText(body)
    msg['Subject'] = subject
    msg['From'] = gmail_username
    msg['To'] = recipient_email

    # 이메일 보내기
    server.sendmail(gmail_username, recipient_email, msg.as_string())

    # 서버 연결 종료
    server.quit()

def login():
    driver.get('https://visithalla.jeju.go.kr/login/login.do')

    login_btn = driver.find_element(By.CLASS_NAME,'btn-kakao')
    login_btn.click()
    
    time.sleep(3)

    # 현재 페이지의 주소 확인
    current_url = driver.current_url
    if current_url == 'https://visithalla.jeju.go.kr/main/main.do':
        return

    else:
        driver.switch_to.window(driver.window_handles[-1])

        kakao_id_input = driver.find_element(by='name', value='loginId')
        kakao_pw_input = driver.find_element(by='name', value='password')

        kakao_id_input.send_keys('my kakaoID')
        kakao_pw_input.send_keys('my kakaoPW')

        kakao_login_btn = driver.find_element(By.CLASS_NAME,'btn_g')
        kakao_login_btn.click()

        time.sleep(3)

        driver.switch_to.window(driver.window_handles[0])

def check_reservation_status():
    # 예약 사이트 접속
    driver.get(reservation_url)

    # 탐방로 선택 - 관음사 코스
    course_select = Select(driver.find_element(by='name', value="courseSeq"))
    course_select.select_by_visible_text("관음사 코스")

    # 탐방 날짜 설정 - 2024년 1월 4일
    date_input = driver.find_element(By.CLASS_NAME, 'ui-datepicker-trigger')
    date_input.click()
    time.sleep(1)

    next_month = driver.find_element(By.CLASS_NAME, 'ui-icon-circle-triangle-e')
    next_month.click()

    xpath_expression = f"//a[text()='4']"
    day4 = driver.find_element(By.XPATH, xpath_expression)
    day4.click()

    # 탐방 시작 시간 설정 - 06:00 ~ 08:00
    start_time_select = Select(driver.find_element(by='name', value="visitTm"))
    start_time_select.select_by_visible_text("06:00 ~ 08:00")

    time.sleep(1)

    # 'current_num' id를 갖는 span 태그의 자식 요소를 선택
    current_reservation_element = driver.find_element(By.CSS_SELECTOR, '#current_num>strong>span')

    # span 태그 내의 전체 텍스트 추출
    current_reservation_text = current_reservation_element.text.strip()
    print(current_reservation_text)

    # 현재 예약 인원이 400명보다 작을 때 알림
    if current_reservation_text != '현재예약인원 400':
        # 변경되었다면 이메일로 알림 보내기
        send_email("예약 가능 여부 변경 알림", current_reservation_text)

# 주기적으로 예약 인원 확인
while True:
    login()
    check_reservation_status()
    time.sleep(check_interval)

결과 및 이후

메일을 받고 후다닥 들어가 예약에 성공할 수 있었다!

날씨가 매우 좋아서 구름 한 점 없던 백록담. 예약에 실패해서 못 갔으면 정말 후회할 뻔 했다.

복학하기 전 한 번 쯤은 혼자서 크롤링에 대해 공부하고 직접 진행해 보고 싶었는데, 비록 별거 없는 내용이지만 생각지도 못한 계기로 하게 되었다. 그러나 아직 코드가 지저분한 부분도 있고, chatGPT의 도움을 받아 온전히 이해하지 못한 부분도 있다. 차차 공부해나가며 온전히 내것으로 만들어고, 이후 보완할 점을 찾으면 꾸준히 보완해 나가고자 한다.


생각해 볼 점

수강신청이라던지, 티켓팅이라던지. 사람들보다 빠르게 자리를 선점하고 이득을 보는 매크로에 대해 부정적인 시선이 많다. 기술 윤리적인 측면에서 보았을 때, 기술이 불평등을 초래할 수 있다는 것이다. 그러나 직접 매크로를 접하니 (매크로라고 부르기에도 부족하지만) 단지 자동화를 위한 한 프로그램에 과한 잣대를 들이미는 것이 아닌가 하는 생각도 든다. 아직은 경험도 부족하고 지식도 부족하나, 앞으로 웹과 데이터를 다루면서 자주 맞닥뜨릴 주제인 것 같아 시간을 가지고 충분히 고민해 볼 만 한 것 같다.

0개의 댓글