[Python] 파이썬으로 SRT 예매 프로그램 만들기 (2) 기능 업데이트

 

안녕하세요? 2편이 많이 늦었네요.

1편에 이어 일부 기능을 업데이트하고 조금 더 완성도 있는 프로그램으로 만들고자 합니다.

 

게시물 작성 이후 추가 업데이트된 코드는 github를 참조해주세요.

https://github.com/kminito/srt_reservation

 

     

    1편 링크

    https://kminito.tistory.com/79

     

    1. 프로그램 구조 바꾸기

    이후의 기능 추가를 편리하게 하도록 하기 위하여 1편의 프로그램을 함수를 사용하는 형태로 바꾸겠습니다. 주석을 최소화하여 1편을 기준으로 수정된 부분 위주로 주석을 남기겠습니다. 각 코드의 기능에 대한 설명은 1편을 참조해주세요.

     

    <1편에서 구조만 조금 바꾼 코드>

    # -*- coding: utf-8 -*-
    import time
    from random import randint
    from selenium import webdriver
    from selenium.webdriver.common.by import By
    from selenium.webdriver.support.select import Select
    
    
    def open_brower():
        driver = webdriver.Chrome("chromedriver")
        return driver
    
    
    def login(driver, login_id, login_psw):
        driver.get('https://etk.srail.co.kr/cmc/01/selectLoginForm.do')
        driver.implicitly_wait(15)
        driver.find_element(By.ID, 'srchDvNm01').send_keys(str(login_id))
        driver.find_element(By.ID, 'hmpgPwdCphd01').send_keys(str(login_psw))
        driver.find_element(By.XPATH, '//*[@id="login-form"]/fieldset/div[1]/div[1]/div[2]/div/div[2]/input').click()
        driver.implicitly_wait(5)
        return driver
    
    
    def search_train(driver, dpt_stn, arr_stn, dpt_dt, dpt_tm, num_trains_to_check=2, want_reserve=False):
        is_booked = False # 예약 완료 되었는지 확인용
        cnt_refresh = 0 # 새로고침 회수 기록
    
        driver.get('https://etk.srail.kr/hpg/hra/01/selectScheduleList.do') # 기차 조회 페이지로 이동
        driver.implicitly_wait(5)
        # 출발지/도착지/출발날짜/출발시간 입력
        elm_dpt_stn = driver.find_element(By.ID, 'dptRsStnCdNm')
        elm_dpt_stn.clear()
        elm_dpt_stn.send_keys(dpt_stn) # 출발지
        elm_arr_stn = driver.find_element(By.ID, 'arvRsStnCdNm')
        elm_arr_stn.clear()
        elm_arr_stn.send_keys(arr_stn) # 도착지
        elm_dptDt = driver.find_element(By.ID, "dptDt")
        driver.execute_script("arguments[0].setAttribute('style','display: True;')", elm_dptDt)
        Select(driver.find_element(By.ID,"dptDt")).select_by_value(dpt_dt) # 출발날짜
        elm_dptTm = driver.find_element(By.ID, "dptTm")
        driver.execute_script("arguments[0].setAttribute('style','display: True;')", elm_dptTm)
        Select(driver.find_element(By.ID, "dptTm")).select_by_visible_text(dpt_tm) # 출발시간
    
        print("기차를 조회합니다")
        print(f"출발역:{dpt_stn} , 도착역:{arr_stn}\n날짜:{dpt_dt}, 시간: {dpt_tm}시 이후\n{num_trains_to_check}개의 기차 중 예약")
        print(f"예약 대기 사용: {want_reserve}")
    
        driver.find_element(By.XPATH, "//input[@value='조회하기']").click() # 조회하기 버튼 클릭
        driver.implicitly_wait(5)
        time.sleep(1)
    
        while True:
            for i in range(1, num_trains_to_check+1):
                standard_seat = driver.find_element(By.CSS_SELECTOR, f"#result-form > fieldset > div.tbl_wrap.th_thead > table > tbody > tr:nth-child({i}) > td:nth-child(7)").text
    
                if "예약하기" in standard_seat:
                    print("예약 가능")
                    driver.find_element(By.CSS_SELECTOR, f"#result-form > fieldset > div.tbl_wrap.th_thead > table > tbody > tr:nth-child({i}) > td:nth-child(7) > a").click() #CSS Selector로 변경
                    is_booked = True
                    break
    
            if not is_booked:
                time.sleep(randint(2, 4)) #2~4초 랜덤으로 기다리기
    
                # 다시 조회하기
                submit = driver.find_element(By.XPATH, "//input[@value='조회하기']")
                driver.execute_script("arguments[0].click();", submit)
                cnt_refresh += 1
                print(f"새로고침 {cnt_refresh}회")
                driver.implicitly_wait(10)
                time.sleep(0.5)
            else:
                break
        return driver
    
    
    if __name__ == "__main__":
        driver = open_brower()
        driver = login(driver, '1234567890', '1111111111!') # 회원 번호, 비밀번호
        search_train(driver, "동탄", "동대구", "20220115", "08", want_reserve=True)

     

    1편 코드를 총 세개의 함수로 쪼갰습니다. 

    - 크롬 실행(open_brower)

    - 로그인 (login)

    - 기차 검색 및 예약 (search_train)

     -> 예약 가능 여부를 체크할 기차의 숫자를 설정할 변수를 지정했습니다. (변수명 num_trains_to_check). 기본값은 2이며, 검색 결과 상위 2개의 기차의 예약 가능 여부를 체크한다는 의미입니다.

     -> 새로고침 회수를 나타내는 변수 cnt_refresh와 예약대기 선택 여부를 나타내는 변수 want_reserve를 만들었습니다.

     -> 일부 변수명 변경 및 아래와 같이 검색하는 기차 명도 표시하도록 했습니다. 

     

     

     

    2. 예약 대기 기능 추가

     

    기차의 예약 가능 여부 확인과 동일한 방식으로 구현합니다. 예약 대기 버튼이 있는 칸에 신청하기 버튼이 활성화 되어 있을 경우 해당 버튼을 클릭합니다.

     

     

    추가할 코드는 다음과 같습니다. 

     

    <코드>

    예약대기 Column 확인

    reservation = driver.find_element(By.CSS_SELECTOR, f"#result-form > fieldset > div.tbl_wrap.th_thead > table > tbody > tr:nth-child({i}) > td:nth-child(8)").text

    <코드>

    예약대기가 설정되어 있고, 예약대기의 신청하기 버튼이 활성화 되어 있을 경우 클릭

    if want_reserve:
        if "신청하기" in reservation:
            print("예약 대기 완료")
            driver.find_element(By.CSS_SELECTOR, f"#result-form > fieldset > div.tbl_wrap.th_thead > table > tbody > tr:nth-child({i}) > td:nth-child(8) > a").click()
            is_booked = True
            break

     

    두 코드를 합쳐서 두어도 되지만, 구조의 통일성을 위하여 아래와 같이 배치했습니다.

    # 이전 생략
    while True:
            for i in range(1, num_trains_to_check+1):
                standard_seat = driver.find_element(By.CSS_SELECTOR, f"#result-form > fieldset > div.tbl_wrap.th_thead > table > tbody > tr:nth-child({i}) > td:nth-child(7)").text
                reservation = driver.find_element(By.CSS_SELECTOR, f"#result-form > fieldset > div.tbl_wrap.th_thead > table > tbody > tr:nth-child({i}) > td:nth-child(8)").text
    
                if "예약하기" in standard_seat:
    			# 중략
                if want_reserve:
    			# 중략
    # 이후 생략

     

     

    3. 예약 버튼을 눌렀으나 실제 잔여석이 없는 경우 처리

     

    아래 스크린 샷과 같이 예약 버튼을 눌렀으나, 다른 사람이 먼저 눌러서 '잔여석없음'이라고 뜨는 경우가 있습니다. 이 경우를 처리하기 위해서 '잔여석없음' 페이지가 뜨는 지를 확인하는 것이 아니라, 정상적으로 처리되어서 '결제하기' 버튼이나 '다시계산' 버튼이 뜨는 경우를 가지고 조건문을 걸어서 처리하겠습니다.

     

     

    잔여석없음

     

    3-1. 소스 확인

    정상적으로 예약이 완료된 경우, 페이지에 보이는 '다시계산'버튼의 코드를 확인합니다. 해당 버튼은 isFalseGotoMain 라는 id를 고유 값으로 가지고 있습니다. 앞서 한 것처럼 이 id 값을 가지고 해당 버튼을 특정합니다.

     

    정상 예약 완료 화면

     

    <코드> '다시계산' 버튼을 찾기

    driver.find_element(By.ID, 'isFalseGotoMain')

    이 버튼이 있으면 예약이 제대로 되었다는 뜻입니다만, 버튼이 없을 경우 해당 코드는 예외를 발생시킵니다. 따라서 '다시계산'버튼이 없어도 예외가 발생되지 않도록 find_elements 메서드를 사용합니다

     

    <코드> - find_elements를 사용하여 해당 버튼이 없으면 빈 리스트를 리턴 

    driver.find_elements(By.ID, 'isFalseGotoMain')

     ->해당 요소가 빈 리스트일 경우 해당 버튼이 없는 것이고, 따라서 '잔여석없음'으로 넘어갔음을 알 수 있습니다.

     

    3-2. 조건문 만들기

     예약 버튼을 클릭한 후에,

     - "다시계산"버튼이 화면에 나타나면 예약이 완료된 것으로 생각하고 종료

     - "다시계산"버튼이 화면에 나타나지 않으면 뒤로가서 다시 검색

     

    해당 부분의 코드는 아래와 같습니다.

    <코드> - 잔여석 없는 경우 다시 검색

     "예약하기" 버튼이 있으면 클릭, 그 후 "다시계산" 버튼이 있으면 예약 성공. 버튼이 없으면 다시 뒤로 돌아가서 기차 스케쥴을 검색하는 코드 부분

    # 앞부분 생략
                if "예약하기" in standard_seat:
                    print("예약 가능 클릭")
                    driver.find_element(By.CSS_SELECTOR, f"#result-form > fieldset > div.tbl_wrap.th_thead > table > tbody > tr:nth-child({i}) > td:nth-child(7) > a").click()
                    driver.implicitly_wait(3)
                    
                    if driver.find_elements(By.ID, 'isFalseGotoMain'):
                        is_booked = True
                        print("예약 성공")
                        break
                    else:
                        print("잔여석 없음. 다시 검색")
                        driver.back() # 뒤로가기
                        driver.implicitly_wait(5)

     

    위의 코드를 업데이트하여 프로그램을 실행하면 아래와 같은 결과를 얻을 수 있습니다.

    잔여석이 없으면 다시 검색을 합니다.

     

     

     

     

     

    4. 전체 코드

    여기까지의 기능을 모두 추가한 전체 코드입니다. 

    # -*- coding: utf-8 -*-
    import time
    from random import randint
    from selenium import webdriver
    from selenium.webdriver.common.by import By
    from selenium.webdriver.support.select import Select
    
    def open_brower():
        driver = webdriver.Chrome("chromedriver")
        return driver
    
    
    def login(driver, login_id, login_psw):
        driver.get('https://etk.srail.co.kr/cmc/01/selectLoginForm.do')
        driver.implicitly_wait(15)
        driver.find_element(By.ID, 'srchDvNm01').send_keys(str(login_id))
        driver.find_element(By.ID, 'hmpgPwdCphd01').send_keys(str(login_psw))
        driver.find_element(By.XPATH, '//*[@id="login-form"]/fieldset/div[1]/div[1]/div[2]/div/div[2]/input').click()
        driver.implicitly_wait(5)
        return driver
    
    
    def search_train(driver, dpt_stn, arr_stn, dpt_dt, dpt_tm, num_trains_to_check=2, want_reserve=False):
        is_booked = False # 예약 완료 되었는지 확인용
        cnt_refresh = 0 # 새로고침 회수 기록
    
        driver.get('https://etk.srail.kr/hpg/hra/01/selectScheduleList.do') # 기차 조회 페이지로 이동
        driver.implicitly_wait(5)
        # 출발지/도착지/출발날짜/출발시간 입력
        elm_dpt_stn = driver.find_element(By.ID, 'dptRsStnCdNm')
        elm_dpt_stn.clear()
        elm_dpt_stn.send_keys(dpt_stn) # 출발지
        elm_arr_stn = driver.find_element(By.ID, 'arvRsStnCdNm')
        elm_arr_stn.clear()
        elm_arr_stn.send_keys(arr_stn) # 도착지
        elm_dptDt = driver.find_element(By.ID, "dptDt")
        driver.execute_script("arguments[0].setAttribute('style','display: True;')", elm_dptDt)
        Select(driver.find_element(By.ID,"dptDt")).select_by_value(dpt_dt) # 출발날짜
        elm_dptTm = driver.find_element(By.ID, "dptTm")
        driver.execute_script("arguments[0].setAttribute('style','display: True;')", elm_dptTm)
        Select(driver.find_element(By.ID, "dptTm")).select_by_visible_text(dpt_tm) # 출발시간
    
        print("기차를 조회합니다")
        print(f"출발역:{dpt_stn} , 도착역:{arr_stn}\n날짜:{dpt_dt}, 시간: {dpt_tm}시 이후\n{num_trains_to_check}개의 기차 중 예약")
        print(f"예약 대기 사용: {want_reserve}")
    
        driver.find_element(By.XPATH, "//input[@value='조회하기']").click() # 조회하기 버튼 클릭
        driver.implicitly_wait(5)
        time.sleep(1)
    
        while True:
            for i in range(1, num_trains_to_check+1):
                standard_seat = driver.find_element(By.CSS_SELECTOR, f"#result-form > fieldset > div.tbl_wrap.th_thead > table > tbody > tr:nth-child({i}) > td:nth-child(7)").text
                reservation = driver.find_element(By.CSS_SELECTOR, f"#result-form > fieldset > div.tbl_wrap.th_thead > table > tbody > tr:nth-child({i}) > td:nth-child(8)").text
    
                if "예약하기" in standard_seat:
                    print("예약 가능 클릭")
                    driver.find_element(By.CSS_SELECTOR, f"#result-form > fieldset > div.tbl_wrap.th_thead > table > tbody > tr:nth-child({i}) > td:nth-child(7) > a").click()
                    driver.implicitly_wait(3)
    
                    if driver.find_elements(By.ID, 'isFalseGotoMain'):
                        is_booked = True
                        print("예약 성공")
                        break
                    else:
                        print("잔여석 없음. 다시 검색")
                        driver.back()  # 뒤로가기
                        driver.implicitly_wait(5)
    
                if want_reserve:
                    if "신청하기" in reservation:
                        print("예약 대기 완료")
                        driver.find_element(By.CSS_SELECTOR, f"#result-form > fieldset > div.tbl_wrap.th_thead > table > tbody > tr:nth-child({i}) > td:nth-child(8) > a").click()
                        is_booked = True
                        break
    
            if not is_booked:
                time.sleep(randint(2, 4)) #2~4초 랜덤으로 기다리기
    
                # 다시 조회하기
                submit = driver.find_element(By.XPATH, "//input[@value='조회하기']")
                driver.execute_script("arguments[0].click();", submit)
                cnt_refresh += 1
                print(f"새로고침 {cnt_refresh}회")
                driver.implicitly_wait(10)
                time.sleep(0.5)
            else:
                break
        return driver
    
    
    if __name__ == "__main__":
        driver = open_brower()
        driver = login(driver, '1234567890', '1111111111')
        search_train(driver, "동탄", "동대구", "20220115", "08") #기차 출발 시간은 반드시 짝수

     

    만약 예약대기 기능을 이용하고 싶으시면, search_train을 실행할 때 want_reserve=True로,

    예약 가능 여부를 체크할 기차의 수를 바꾸고 싶으시면 num_trains_to_check=3 등과 같이 입력하시면 됩니다.

     

     

    예시 코드) 

    search_train(driver, "동탄", "동대구", "20220115", "08",num_trains_to_check=3, want_reserve=True)

     

    4. 후기

    이제 어느 정도 사용할 만한 프로그램이 된 것 같습니다. 

     

     

    내용 추가 (2022년 1월 15일)

     위의 게시물을 작성한 이후에 예외 처리, 구조 변경 등으로 코드를 업데이트 했습니다. 업데이트된 코드는 아래의 Github를 참고해주세요. 1편에서 설명한 접근 방법 및 파이썬의 클래스에 대한 이해가 있으면 변경된 코드를 이해하시는 데 문제가 없을 것으로 보입니다. 사용하시기에는 Github의 코드가 훨씬 더 편할 것으로 생각됩니다. 해당 Repository는 지속적으로 업데이트 예정입니다.

     

    감사합니다.

     

    https://github.com/kminito/srt_reservation

     

    GitHub - kminito/srt_reservation

    Contribute to kminito/srt_reservation development by creating an account on GitHub.

    github.com

     

     

    (추신) 텔레그램 알람은 아래 게시물 참고하시면 될 것 같아요.

     -> [Python] 파이썬으로 텔레그램 봇 사용하기

     

    댓글

    Designed by JB FACTORY