복권 구매

복권 구매 자동화

이번 글에서는 복권 구매까지 진행을 해보겠습니다. 다음글에서는 마지막 코드 리펙토링을 진행하도록 하겠습니다.

이 글 끝까지 따라하면 구매 기능까지 구현하는데 진짜 구매하시려면 예치금을 충전하시면 가능합니다.

복권 구매는 단계를 나누어 보면 아래와 같습니다.

  1. 메인 페이지에서 복권 구매 버튼 클릭 (클릭 시 구매 팝업 열림)
  2. 복권 구매 번호 랜덤으로 발행(자동번호 발행 기능이 이미 있지만 코드 예제를 위해 수동으로 진행)
  3. 구매 버튼 클릭

element 조회, 클릭 기능으로 구현되지만 추가적으로 알아야할 것은 복권 구매 화면이 iframe 형식으로 구성이 되어있다는 것입니다. iframe 으로 되어있는 경우 selenium 을 통해 현재 frame 을 이동시켜줘야 원하는 element 등에 접근 가능합니다.

팝업 닫을 때 window 를 옮겨야했듯 frame 이 여러개인 경우 frame 또한 옮겨주어야 해당 frame 안에서의 조작이 가능합니다

우선 몇 개의 기능을 먼저 작성하겠습니다.

script 실행하기

def execute_script(script):
    driver.execute_script(script)

윈도우 중에서 마지막에 열린 윈도우로 현재 윈도우 변경하기

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

현재 프레임을 iframe 으로 변경하기

def switch_to_iframe(iframe):
    WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.TAG_NAME, "iframe"))
    )

    driver.switch_to.frame(iframe)

    WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.TAG_NAME, "iframe"))
    )

WebDriverWait 코드가 필요한 이유는 iframe 이 나중에 로드가 되어 약간의 딜레이를 주지 않고 바로 find~ 등을 진행할 경우 때때로 실패하는 것을 확인할 수 있습니다. 그렇기에 넉넉히 10초간 iframe tag 가 나오기를 기다리는 코드를 작성한 것입니다.

로또 번호 6개 발행하기

def pick_numbers():
    picks = set([])
    while 6 != len(picks):
        n = random.randint(LOTTO_MIN_NUM, LOTTO_MAX_NUM)
    picks.add(n)

    return picks

화면 캡처하기

def save_screenshot():
    driver.save_screenshot(f'{datetime.now().strftime("%Y%m%d_%H%M%S")}.png')


이제 기능 구현을 해보겠습니다.

1. 팝업 닫기

이전 글에서 진행했던 로그인 기능을 실행하면 로그인 완료 후 다시 팝업이 나타납니다. 그렇기에 로그인 완료 후에도 팝업 닫기 함수를 호출해야합니다.

sleep(1)
close_windows_if_not_main_url('common.do?method=main')

sleep(1) 을 추가한 이유는 팝업이 여러개 나타날 경우 팝업이 제대로 안 닫히는 경우가 있습니다. 그러므로 1초 동안 팝업이 다 열리기를 기다리는 것 입니다.

2. 복권 구매 버튼 클릭 시 발생되는 script 실행

execute_script('goLottoBuy(2);')

Alt text

저는 개인적으로 가능하다면 script 을 실행하는 편입니다. 그 이유는 html 구조 변경보다 js 변경이 덜 빈번하기 때문입니다. 또한 element 가 화면에 보이지 않을 경우 find 나 click 을 하려하면 에러를 발생시키는데 script 실행은 그에 비해 유연하기 때문입니다. 물론 상황에 따라서 적합한게 다를 것 입니다.

3. 복권 구매 팝업 윈도우로 윈도우 이동

switch_to_last_opened_window()

4. 복권 번호 클릭을 위해 현재 frame 이동

switch_to_iframe(driver.find_elements(By.TAG_NAME, 'iframe')[0])

Alt text

5. 복권 번호 6개 클릭 후 번호 선택 확인 버튼 클릭

for num in pick_numbers():
    execute_script(f'$("#check645num{num}").prop("checked", true);')

click(driver.find_element(By.ID, 'btnSelectNum'))

각 숫자는 checkbox 의 형식으로 되어있습니다. id 의 규칙을 보면 check645num 뒤에 1~45가 붙어있는 것임을 알 수 있습니다. 그렇기에 랜덤으로 6개의 숫자를 단순히 check645num 뒤에 붙여 checked 속성을 true 로 부여하였습니다. Alt text Alt text

6. 구매하기 버튼 클릭

click(driver.find_element(By.ID, 'btnBuy'))
execute_script('closepopupLayerConfirm(true);')

구매하기 버튼을 클릭하면 한 번 더 묻는 컨펌 다이얼로그가 나옵니다. 여기서 저는 해당 확인 버튼을 클릭하기 보다 js script 을 실행시키도록 하였습니다. Alt text

위 코드까지 실행하면 아래와 같이 구매 완료 화면이 나옵니다. Alt text

7. 구매내역 확인 캡처

구매만 하고 끝낼 수 있지만 저 구매내역 확인을 캡처해서 저장하고 싶을 수 있습니다. 그렇기에 캡처하고 브라우저 종료하도록 하겠습니다.

sleep(2)
save_screenshot()

driver.quit()

전체 코드

from datetime import datetime
import random
from time import sleep

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By

from dotenv import load_dotenv
import os

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

load_dotenv()

LOTTO_MIN_NUM = 1
LOTTO_MAX_NUM = 45


def create_driver():
    return webdriver.Chrome(service=Service('./driver/chromedriver_mac64_m1_97'))


driver = create_driver()
driver.get('https://dhlottery.co.kr/common.do?method=main')


def get_main_handle(main_url):
    for window_handle in driver.window_handles:
        driver.switch_to.window(window_handle)

        print(f'현재 핸들러 name : {driver.current_window_handle}')
        print(f'현재 url :  {driver.current_url}')
        print('')

        if main_url in driver.current_url:
            return window_handle
    raise '메인 핸들러 찾지 못함'


def close_windows_if_not_main_url(main_url):
    main_handle = get_main_handle(main_url)

    for window_handle in driver.window_handles:
        if main_handle != window_handle:
            driver.switch_to.window(window_handle)
            driver.close()

    driver.switch_to.window(main_handle)


def send_keys(element, text):
    element.send_keys(text)


def click(element):
    element.click()


def execute_script(script):
    driver.execute_script(script)


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


def switch_to_iframe(iframe):
    WebDriverWait(driver, 10).until(
        EC.presence_of_element_located((By.TAG_NAME, "iframe"))
    )

    driver.switch_to.frame(iframe)


def pick_numbers():
    picks = set([])
    while 6 != len(picks):
        n = random.randint(LOTTO_MIN_NUM, LOTTO_MAX_NUM)
        picks.add(n)

    return picks


def save_screenshot():
    driver.save_screenshot(f'{datetime.now().strftime("%Y%m%d_%H%M%S")}.png')


close_windows_if_not_main_url('common.do?method=main')

click(driver.find_element(By.XPATH, '/html/body/div[1]/header/div[2]/div[2]/form/div/ul/li[1]/a'))

send_keys(driver.find_element(By.ID, 'userId'), os.environ.get('ID'))
send_keys(driver.find_element(By.NAME, 'password'), os.environ.get('PW'))
send_keys(driver.find_element(By.NAME, 'password'), Keys.ENTER)

sleep(1)

close_windows_if_not_main_url('common.do?method=main')

execute_script('goLottoBuy(2);')

sleep(1)

switch_to_last_opened_window()

switch_to_iframe(driver.find_elements(By.TAG_NAME, 'iframe')[0])

for num in pick_numbers():
    execute_script(f'$("#check645num{num}").prop("checked", true);')

click(driver.find_element(By.ID, 'btnSelectNum'))

click(driver.find_element(By.ID, 'btnBuy'))
execute_script('closepopupLayerConfirm(true);')

sleep(2)
save_screenshot()

driver.quit()


로그인 기능
리펙토링
NCloud LB & SourcePipeline 구축하기
tech collection 서비스 성능 개선하기
Selenium 복권 구매 자동화 만들어보기
디자인 패턴
책 리뷰
블로그 챌린지