이제 Selenium 라이브러리를 이용, 웹브라우저를 열어 SRT 웹페이지를 원격 컨트롤 해 봅시다.

먼저 앞서 진행한 pyqt 코드에 별도 Selenium 컨트롤 할 수 있는 파일을 불러옵니다.

 

__init__.py

 

class Form(QtWidgets.QDialog):
    def __init__(self, parent=None):
        QtWidgets.QDialog.__init__(self, parent)
        self.ui = uic.loadUi("gui.ui")
        self.dep = self.ui.depCity                  #출발지
        self.arr = self.ui.arrCity                  #도착지
        self.dat = self.ui.depDate                  #출발일
        self.hou = self.ui.depHour                  #출발시간
        self.table = self.ui.resTable               #검색결과 표
        self.check_list = [self.ui.checkBox_01,     #체크박스 10개
                            self.ui.checkBox_02,
                            self.ui.checkBox_03,
                            self.ui.checkBox_04,
                            self.ui.checkBox_05,
                            self.ui.checkBox_06,
                            self.ui.checkBox_07,
                            self.ui.checkBox_08,
                            self.ui.checkBox_09,
                            self.ui.checkBox_10
        ]

        self.srt = SRT_page()                        # SRT_page Class를 호출, __init__(self) 함수 수행
        self.srt.login()                            # login(self) 함수 수행
        time.sleep(3)

        self.ui.show()

이어서 srt_book.py를 만들어 아래와 같이 작성합니다.

 

srt_book.py

 

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import time

class SRT_page():
    def __init__(self):
        print("__init__")
        super().__init__()

    def login(self):
        print("login")
        self.driver = webdriver.Firefox()
        self.driver.get("https://etk.srail.kr/cmc/01/selectLoginForm.do?pageId=TK0701000000")
        self.driver.find_element_by_id("srchDvNm01").send_keys("회원번호")
        self.driver.find_element_by_id("hmpgPwdCphd01").send_keys("비밀번호")
        self.driver.find_element_by_id("hmpgPwdCphd01").send_keys(Keys.RETURN)
        time.sleep(0.5)
        self.driver.get("https://etk.srail.kr/hpg/hra/01/selectScheduleList.do?pageId=TK0101010000")

 

먼저 파이어폭스 웹 브라우저 설치 및 geckodriver.exe 파일을 같은 폴더에 넣어놓는 것을 잊지 맙시다.

 

chromedriver.exe
10.2 MB

import 구문은 seleniumtime입니다. 그리고 SRT_page라는 이름의 클래스를 생성하고 login함수를 만들어 위와 같이 작성합니다.

 

로그인 함수를 한줄한줄 보면 다음과 같습니다.

 

  1. 'login'을 출력합니다.
  2. 파이어폭스 웹 구동 기능을 갖는 self.driver 변수로 만듭니다.
  3. SRT 홈페이지에 접속합니다.
  4. 회원 번호 입력 input에 회원번호를 입력합니다.
  5. 엔터키를 칩니다.
  6. 로그인 후 화면이 새로 뜰 때까지 0.5초정도 기다려줍니다.
  7. 예매화면으로 이동합니다.

 

다시 pyqt로 돌아갑시다.

 

__init__.py

from PyQt5 import QtWidgets
from PyQt5 import uic
from PyQt5.QtCore import *
from srt_book import SRT_page
import sys
import time
import playsound
import datetime


class Form(QtWidgets.QDialog):
    def __init__(self, parent=None):
        QtWidgets.QDialog.__init__(self, parent)
        self.ui = uic.loadUi("gui.ui")
        self.dep = self.ui.depCity                  #출발지
        self.arr = self.ui.arrCity                  #도착지
        self.dat = self.ui.depDate                  #출발일
        self.hou = self.ui.depHour                  #출발시간
        self.table = self.ui.resTable               #검색결과 표
        self.check_list = [self.ui.checkBox_01,     #체크박스 10개
                            self.ui.checkBox_02,
                            self.ui.checkBox_03,
                            self.ui.checkBox_04,
                            self.ui.checkBox_05,
                            self.ui.checkBox_06,
                            self.ui.checkBox_07,
                            self.ui.checkBox_08,
                            self.ui.checkBox_09,
                            self.ui.checkBox_10
        ]

        self.srt = SRT_page()
        self.srt.login()
        time.sleep(3)

        c = str(datetime.datetime.today().date()).replace('-', '/')
        self.dat.setDate(QDate.fromString(c, "yyyy/MM/dd"))

        self.ui.searchSeat.clicked.connect(self.find_seats)
        self.ui.tryReservation.clicked.connect(self.try_seat)

        self.ui.show()

 

로그인을 한 후 3초 딜레이 이후 오늘 날짜를 불러오고 년, 월, 일의 구분자를 '/'로 바꿔줍니다. (결과: 2021/03/28)


위의 형식이 프로그램 레이아웃에서 원하는 날짜 형식이기 때문입니다.

 

출발지/도착지/시간은 사실 큰 상관 없지만 날짜는 내가 입력하기 전에 미리 오늘날짜로 되어 있으면 좀더 편리할 수 있습니다.

 

그리고 이어서 '검색' 버튼과 '예약 시도'버튼에 클릭했을 때 각각 self.find_seatsself.try_seat 함수가 실행되도록 해 줍니다.

 

__init__.py find_seat 함수

    def find_seats(self):
        self.selected_dep = self.dep.currentText()    # 입력한 출발지 불러오기
        self.selected_arr = self.arr.currentText()    # 입력한 도착지 불러오기
        d = datetime.datetime(int(self.dat.date().toString("yyyy/MM/dd").split("/")[0]), 
            int(self.dat.date().toString("yyyy/M/dd").split("/")[1]),
            int(self.dat.date().toString("yyyy/M/dd").split("/")[2])).weekday()
        week_days = ["(월)","(화)","(수)","(목)","(금)","(토)","(일)"]
        self.selected_dat = self.dat.date().toString("yyyy/MM/dd")+week_days[d]
        self.selected_hou = self.hou.currentText()

        res = self.srt.plan(self.selected_dep, self.selected_arr, self.selected_dat, self.selected_hou)

        for num1 in range(10):
            for num2 in range(4):
                self.table.setItem(num1, num2, QtWidgets.QTableWidgetItem(res[num1][num2]))

 

이제 실제 원하는 출발지/도착지/날짜/시간을 입력하고 '검색' 버튼을 누르면 실행되는 좌석 찾기 입니다.


날짜의 경우 SRT 홈페이지에서 원하는 값은 다음과 같습니다.

 

 

2021/03/28(일)로 년/월/일(요일) 순서입니다. 그래서 datetime의 요일을 얻는 함수는 datetime.datetime(년,월,일).weekday()입니다. 여기서 년, 월, 일 데이터는 상수라서 int()로 묶어주었구요.

 

요일은 월,화,수,목,금,토,일이 아닌 0,1,2,3,4,5,6 으로 나오기 때문입니다.


그리고 week_days라는 리스트를 만들고 숫자에 대응하도록 하였습니다. week_days[d]

 

아마 예전이라면 if elif else를 사용했을텐데 발전하고 있습니다.

 

이어서 입력한 시간도 변수도 끄집어 내고 res값을 통하여 다시 SRT_page 클래스의 plan함수로 가서 검색을 실행해 봅시다.

 

srt_book.py plan 함수

    def plan(self, dep, arr, dat, hou):

        time.sleep(1)

        self.driver.find_element_by_xpath('//*[@id="dptRsStnCdNm"]').clear()        # 출발지 칸을 비우고
        self.driver.find_element_by_xpath('//*[@id="dptRsStnCdNm"]').send_keys(dep)    # 입력했던 도시명 입력
        self.driver.find_element_by_xpath('//*[@id="arvRsStnCdNm"]').clear()        # 도착지 칸을 비우고
        self.driver.find_element_by_xpath('//*[@id="arvRsStnCdNm"]').send_keys(arr)    # 입력했던 도시명 입력

        # 시간 입력하기
        self.driver.execute_script(f'arguments[0].innerText = {hou};', 
            self.driver.find_element_by_xpath(
                "/html/body/div[1]/div[4]/div/div[2]/form/fieldset/div[1]/div/div/div[3]/div[2]/a/span[2]"))

        # 날짜 입력하기
        self.driver.find_element_by_xpath(
            "/html/body/div[1]/div[4]/div/div[2]/form/fieldset/div[1]/div/div/div[3]/div[1]/a/span[2]").click()
        self.driver.find_elements_by_link_text(dat)[0].click()

        # 검색 버튼을 누르고 2초간 기다리기
        self.driver.find_element_by_xpath('/html/body/div[1]/div[4]/div/div[2]/form/fieldset/div[2]/input').click()
        time.sleep(2)

        self.seats = []
        for count in range(1, 11):
            seat_list = []
            try:
                seat_tr = self.driver.find_element_by_xpath(
                    f"/html/body/div[1]/div[4]/div/div[3]/div[1]/form/fieldset/div[6]/table/tbody/tr[{count}]/td[2]").text
                seat_dep = self.driver.find_element_by_xpath(
                    f"/html/body/div[1]/div[4]/div/div[3]/div[1]/form/fieldset/div[6]/table/tbody/tr[{count}]/td[4]").text
                seat_arr = self.driver.find_element_by_xpath(
                    f"/html/body/div[1]/div[4]/div/div[3]/div[1]/form/fieldset/div[6]/table/tbody/tr[{count}]/td[5]").text
                seat_ava = self.driver.find_element_by_xpath(
                    f"/html/body/div[1]/div[4]/div/div[3]/div[1]/form/fieldset/div[6]/table/tbody/tr[{count}]/td[7]/a").text

            except: 
                seat_tr = "없음"
                seat_dep = "없음"
                seat_arr = "없음"
                seat_ava = "없음"

            finally: 
                seat_list.append(seat_tr)
                seat_list.append(seat_dep)
                seat_list.append(seat_arr)
                seat_list.append(seat_ava)

            self.seats.append(seat_list)

        return self.seats

 

위의 코딩을 보면 html의 xpath 요소들을 변수로 저장하지 않고 그대로 썼습니다. 전혀 파이썬 답지도 않고 보기도 힘들다고 할 순 있겠지만 어차피 저만 사용하는 프로그램이고 어떨 땐 변수를 너무 남용해도 헷갈려서 그냥 위와 같이 사용했습니다. (결코 잘했다는 건 아니구요..😂)

 

plan함수는 당연히 인자를 4개(출발지, 도착지, 날짜, 시간) 네 개를 갖고 아래 화면의 빨간 동그라미 쳐 진 4군데에 입력합니다. 입력값이 원하는 형식이 아니면 프로그램이 튕깁니다.


나머지 여정경로, 인원정보, 좌석종류, 차종구분은 넣지 않았습니다. 생각해보면 프로그램 목적이 만석일 때 하나 자리 났을때 얼른 예약하는 건데 저런거 따지고 있을 필요가 없겠죠.😁

 

 

먼저 출발지와 목적지 부분을 지우고 pyqt에서 가져온 값으로 채웁니다.


그 다음 시간부터 입력합니다.(시간은 꼭 02와 같이 두자리인 것을 확인합니다.)

 

시간의 경우에는 self.driver.execute_script(f'arguments[0].innerText = {hou};', self.driver.find_element_by_xpath("주소"))를 이용하였습니다.

 

기본적으로 input을 채우는 방법은 위 출발지/목적지와 같이 .sendkeys()를 이용하는 경우가 통상적이면서 쉽지만 에러가 엄청 발생합니다.

 

그래서 위와 같이 execute_script()구문과 같이 자바스크립트 구문을 그대로 이용합니다. 사실 크롤링 좀 한다고 하면 필수입니다.

 

이제 날짜를 입력할 차례입니다. 사실 이 부분 하다가 짜증나서 그냥 접을까도 생각할정도로 삽질을 좀 했습니다.

(예전이라면 이정도 삽질은 아무것도 아니었는데 이제는 좀 했다고 건방져서 귀찮아 하는 제 자신을 질책하고 다시 열심히 삽질해서 해결했습니다.😫)

 

일단 아래 사진과 같이 select 선택 화면입니다.

 

 

하지만... html 태그가 요상합니다.

 

 

각각의 구문은 이해가 되지만 구조는 이해되지 않습니다.

 

결국 제가 값을 입력해야 하는 부분은 맨 아래 <span class="ui-selectmenu-text">2021/03/30(화)</span> 이 부분인데 앞서 말한 .sendkeys().execute_script()가 작동을 안했습니다.

 

몇 번의 삽질 끝에 값을 넣는 개념이 아닌 실제로 사람들이 하는 것처럼 칸을 클릭하고 원하는 날짜를 클릭하는 두 동작으로 해결했습니다. 해결 방법은 결코 어렵지 않은데 참...

 

여기서 주의할 점은 self.driver.find_elements_by_link_text()는 결과 값이 개수를 떠나서 List로 나오니 [0]을 붙여주었습니다.

 

모든 정보를 입력 후 맨아래 검색 버튼을 누르고 2초 딜레이동안 이제 검색한 차편이 죽- 뜹니다.

 

 

먼저 최종 좌석정보를 위한 빈 self.seats 리스트를 만듭니다.


그리고 최대 10개의 기차편이 보여지기 때문에 for문을 1부터 10까지 10번 돌립니다.


그리고 빈 seat_list 리스트를 만들어 놓고 사실상 필요한 정보인 기차정보, 출발정보, 도착정보, 일반좌석정보를 각각 seat_tr, seat_dep, seat_arr, seat_ava로 변수 설정하고 html 태그의 각각 위치별로 크롤링하여 .text로 값을 얻습니다.

 

그리고 try except구문을 쓴 경우는 당일 남은 열차편이 10개 미만인 경우에는 10번 for문을 돌리다가 에러가 발생하기 때문에 값을 못찾는 경우에는 각각의 변수에 '없음'을 주었습니다.

 

그리고 try, except 어떠한 경우에도 앞서 만든 seat_list에 추가해주고 for문의 다음 회차를 시작하기 전에 self.seats 리스트에 개별 리스트를 넣어줍니다.

 

그리고 for문 종료 후 self.seats값을 return 해 줍니다.

 

마지막으로 __init__.py로 돌아와서 다시 코드를 보면 res값에 self.seats리스트가 들어가게 됩니다.

그리고 마지막으로 아래 구문의 맨 아래 부분과 같이 나의 pyqt 프로그램의 열차 테이블에 그 값들을 순차적으로 넣어줍니다.

 

__init__.py find_seat함수

    def find_seats(self):
        self.selected_dep = self.dep.currentText()
        self.selected_arr = self.arr.currentText()
        d = datetime.datetime(int(self.dat.date().toString("yyyy/MM/dd").split("/")[0]),
            int(self.dat.date().toString("yyyy/M/dd").split("/")[1]),
            int(self.dat.date().toString("yyyy/M/dd").split("/")[2])).weekday()
        print(d)
        week_days = ["(월)","(화)","(수)","(목)","(금)","(토)","(일)"]
        self.selected_dat = self.dat.date().toString("yyyy/MM/dd")+week_days[d]
        self.selected_hou = self.hou.currentText()
        print(self.selected_dep, self.selected_arr, self.selected_dat, self.selected_hou)

        res = self.srt.plan(self.selected_dep, self.selected_arr, self.selected_dat, self.selected_hou)
        print(res)

        for num1 in range(10):            # self.seats 리스트를 for문으로 돌려줍니다.
            for num2 in range(4):        # 각각의 값 안에 들어있는 기차정보/출발정보/도착정보/좌석정보를 돌려줍니다.
                self.table.setItem(num1, num2, QtWidgets.QTableWidgetItem(res[num1][num2]))

 

이번엔 쓸데없이 포스트 개수만 늘리지 않기 위해 길게 작성중입니다.

 

이제 다음번 포스트가 마지막입니다. 😎

 

Python, SRT 자동예매(매크로) 프로그램 만들기 (feat. PyQt5) - 1. 시작, 레이아웃 작성
Python, SRT 자동예매(매크로) 프로그램 만들기 (feat. PyQt5) - 2. Selenium 원격 컨트롤
Python, SRT 자동예매(매크로) 프로그램 만들기 (feat. PyQt5) - 3. 자동 예매, 매크로, 끝

+ Recent posts