[Selenium, Tkinter] 최종 완성

강승묵·2024년 5월 10일

AutoCourseProgram

목록 보기
4/4
post-thumbnail

1. 로그인부터 강의를 보기까지 과정 정리

그동안 블로그에 적었던 내용들을 다시 보니, 챕터와 강의 등등 용어들을 헷갈리게 적어 다시 정리한다. 크기 순서로 나열하면 [교육 과정] > [챕터] > [강의 및 퀴즈] 순서이다. 어떤 '교육 과정' 안에는 7개 정도의 '챕터'들이 있고, 그 챕터 안에는 20개 정도의 '강의 및 퀴즈'로 구성돼있다.
강의 관련 용어 정리
강의를 듣기 위해서는 'n 번 째 교육 과정'을 선택하고, 'n 번 째 챕터'를 선택한 다음에 수강할 수 있는 것이다.

다음은 강의를 듣기까지 어떤 창들이 뜨는지, 그 과정을 정리하였다.
frame 0
(1) 홈페이지에 접속한 뒤, 로그인을 하고 '교육 과정'을 선택까지 이 창(Frame 0)에서 한다. 교육 과정을 선택해 클릭하면 팝업창이 하나 뜨는데, 이때 '동의하기' 버튼을 눌러주면 두 번째 창이 뜬다.
frame 1
(2) 두 번째 창(Frame 1)은 앞선 글들에서 '교육 정보 페이지'라고 설명했던 창이다. 여기에서는 진도율, 공지사항 등 선택한 교육 과정에 대한 정보들을 볼 수 있다. 또한 '챕터'를 선택할 수 있다. 챕터를 선택해 클릭하면 세 번째 창이 뜬다.
frame2
(3) 세 번째 창(Frame 2)은 앞서 선택한 챕터에 해당하는 강의나 퀴즈를 볼 수 있는 실질적인 강의실 페이지이다. 유튜브처럼 강의 영상이 나오거나, 퀴즈를 푸는 화면이 나온다. 여기에 있는 강의를 모두 수강하면 한 개의 챕터를 들은 것이다.

2. 프로그램 동작 흐름도

처음에 프로그램이 시작되면, ID와 PW를 입력하는 GUI가 뜬다. 여기에 정보를 입력하고 '입력'버튼을 누르면 코드가 실행된다.
Tkinter GUI

강의 홈페이지에 접속해 입력받은 ID와 PW로 로그인을 하고, 아래의 흐름도와 같이 동작하여 강의 화면을 띄워준다.
프로그램 동작 순서도

원래는 프로그램의 모든 코드를 함수 없이 구현했다. 하지만 Tkinter을 이용해 GUI를 구현하려다 보니 함수를 이용해 코드를 정리하는 게 좋을 것 같아 frame 0, frame 1, frame 2에서의 동작으로 구분 지어 함수를 만들었다. 각각 login(), go_chapter(), learn_class() 함수로 구현하였다.

처음에 프로그램을 시작하자마자 뜨는 GUI 부분에 ID와 PW를 입력하면, 바로 세 함수가 순서대로 실행되도록 해야 했다. 이 구현을 쉽게 하기 위해 세 함수를 순차적으로 실행하는 all_task() 함수를 만들고, 이를 ID, PW 입력 버튼의 콜백 함수로 사용하였다.

그리고 강의 중간에 퀴즈가 나오는 예외를 처리하기 위해 quiz() 함수를 만들었는데, learn_class() 함수가 강의를 실행하다가 퀴즈가 나오면 quiz() 함수가 tkinter의 message box를 띄워 사용자가 퀴즈를 모두 풀 때까지 프로그램을 정지하도록 했다.

3. 전체 코드

Github 주소

https://github.com/mukmukmukmuk/auto_course_program-with_Selenium.git

이 코드에서 options.add_argument("--mute-audio") 옵션만 지우면 정상적으로 작동한다. 이 option은 영상에서의 소리를 안 나게 하는 기능인데, 코드를 짤 때는 강의 소리를 끄고 작업하는 게 편해서 이 옵션을 추가했다.

4. 수강 완료 전에 강의 꺼짐 이슈 해결

기존의 코드를 실행했을 때 learn_class() 함수에서 에러가 나면서 프로그램이 비정상 종료된 경우가 자주 발생했다. learn_class() 함수라면, frame 2에서 강의 및 퀴즈를 다룰 때 오류가 발생한 것이다. 프로그램을 실행하고 오류가 발생할 때까지 지켜본 결과, 프로그램이 하나의 강의가 끝나기 전에 다음 강의로 이동하는 버튼을 눌러서 뜬 오류였다. 이렇게 되면 '경고 창'이 뜨며 frame 2가 꺼지게 되고, 프로그램도 비정상 종료되는 것이다.

이 문제는 time.sleep() 함수가 호출될 시 발생하는 delay가 누적되어 발생되는 것으로 생각된다. 이를 해결하기 위해 경고 창이 뜨면 frame 1에서부터 다시 시작하는 동작을 넣었고, 경고 창이 뜨지 않는다면 frame 2로 전환하도록 했다.

try:
    #예상치 못한 강의 창 꺼짐에 대한 예외처리
    alert=Alert(driver)
    alert.accept()
    cur-=1 # cur := 현재 보고 있는 '챕터'의 번호
    continue
except: 
    driver.switch_to.window(driver.window_handles[2])
    driver.switch_to.frame(driver.find_element(By.XPATH,'/html/frameset/frame[2]'))

여기에서 cur은 현재 보고 있는 '챕터'의 번호로 챕터의 번호를 1 감소시고 continue를 하여, 반복 문의 처음부터(frame 1부터) 다시 시작하도록 했다.

5. quiz() 함수의 비동기 이슈 해결(?)

frame 2에서 퀴즈가 나왔을 때, quiz() 함수를 실행시켜 사용자가 퀴즈를 풀 때까지 프로그램이 정지하도록 했다고 했다. 기존의 코드에서 이 함수는 퀴즈가 나왔을 때 멈춰 있다가 python의 prompt로 키보드 입력을 주면, 다음 강의를 보여주도록 작동했다. 하지만 이는 사용자의 접근성이 떨어지므로, prompt에 입력을 주는 방식 대신 Tkinter을 이용해 GUI 창을 띄워 GUI의 입력 버튼을 누르면 넘어가는 것으로 대체하려고 했다.

따라서 처음에는 다음과 같이 코드를 작성했다.

def quiz():
    global driver
    global root
    font=tkinter.font.Font(family="맑은 고딕", size=40)
    quiz_page=Toplevel(root)
    quiz_page.geometry("700x300")
    quiz_page.lift()
    quiz_page.title('퀴즈!')
    quiz_text=Label(quiz_page, text = " 퀴즈를 모두 풀고, \n확인 버튼을 눌러주세요",font=font)
    quiz_text.grid(row=0,column=0)
    
    def next_page_button():
        driver.find_element(By.ID,'btn_nextPage').click()
        quiz_page.destroy()
    
    quiz_complete=Button(quiz_page,text='확인',width=10,font=font)
    quiz_complete.grid(row=1,column=0,pady=10)
    quiz_complete.configure(bg="orange")
    quiz_complete['command'] = next_page_button

퀴즈가 나오면 quiz() 함수가 실행되고 새로운 GUI 창이 뜬다. 그 뒤 사용자가 퀴즈 문제를 모두 푼 다음 GUI의 '확인' 버튼을 누르면 next_page_button() 함수가 실행되어 다음 강의로 넘어가는 버튼을 누르고 GUI 창을 닫는 동작을 수행한다.

하지만!!! 이 코드는 제대로 작동하지 않았다...

quiz() 함수가 실행된 다음에, 다음 줄의 코드가 순차적으로 실행돼야 하는데 다음 줄의 코드가 먼저 실행돼서 오류가 발생했다.

try:
	driver.find_element(By.XPATH,'/html/body/div/div[2]/div[2]/div[2]/div[3]/span').text[2:-2].split(sep=' / ')
except Exception as ec:
    quiz()
    
driver.find_element(By.ID,'player').click()
# quiz()보다 driver.find_element(By.ID,'player').click() 가 먼저 실행됨.

원인을 생각해 봤는데, 아마 동기-비동기와 관련된 문제로 생각된다. quiz()가 비동기적으로 실행되어 다음 줄이 먼저 실행된 것으로 추측된다. 따라서 async/await 과 관련된 문서를 찾아보고, 코드도 여러 번 수정해 봤지만, 문제를 해결할 수 없었다. 심지어는 quiz() 함수 실행 전에 time.sleep()이나 Selenium의 WebDriverWait를 이용해 퀴즈를 푸는 시간 동안 프로그램을 멈추는 방법도 시도해 봤지만, 그렇게 되면 quiz()에서 띄워야 할 GUI 창도 정상적으로 띄워지지 않기 때문에 실패했다.

따라서 Tkinter의 message box 기능을 이용해 이를 대체했다.

def quiz():
    global driver
    result = tkinter.messagebox.showinfo("퀴즈!", "퀴즈를 모두 풀고, 확인 버튼을 눌러주세요")
    if result:
        driver.find_element(By.ID,'btn_nextPage').click()

message box의 확인 버튼을 누르면 다음 강의로 넘어가도록 하였다. 확실히 기존의 방법에 비해서는 퀴즈가 나왔을 때 정상적으로 작동하는 것처럼 보였다. 하지만 message box의 글씨나 버튼의 크기가 작고, '수강 완료 전에 꺼짐 이슈'처럼 또 어떤 오류가 발생할지 모르기 때문에 아직 완벽하게 해결하지는 못했다. 앞으로 동기-비동기에 대해 더 알게 되면 이 문제도 해결해 볼 것이다.

6. 실행파일(.exe) 파일로 만들기

pyinstaller를 설치하고, 여러 옵션을 통해 python 파일을 하나의 실행파일로 만들었다. pyinstaller에 대한 자세한 내용은 아래의 문서에서 알 수 있다.

Wikidocs 문서

https://wikidocs.net/21952

이 실행파일을 google drive나 다른 메일로 보내고, 그렇게 보내진 실행파일을 다시 다운로드할 때 windows 바이러스 검사가 뜨며 트로이 목마 코드로 인식되어 다운로드할 수 없다는 에러 가 떴다. 에러의 정확한 이유는 모르겠지만, pyinstaller를 이용해 만들어진 실행파일의 이름을 바꿔서 전송하면 해결할 수 있다.

7. 후기

그동안 코드를 짤 때에는 새로운 프로그래밍 언어를 연습하거나, 학교에서 과제가 나와서 했던 경우가 대부분이었다. 필요한 것을 만들기 위해 프로그래밍을 해본 적이 적어서도 뿌듯했지만, 나를 위해서가 아니라 다른 사람을 도와주기 위해 만들어서 더 보람 있었다.

말로만 들었던 웹 크롤링을 직접 해보고, python으로 GUI를 만들거나 실행파일을 만드는 방법도 익힐 수 있었다. 검색해 보고 공식 문서를 찾아가면서 학습하는 방법이 조금은 자연스러워진 것 같다. 특히 Selenium을 이용해서 웹을 조작하는 것은 웬만해서 다 할 수 있을 것 같다. 그리고 신기하게 코드를 짜는 시간보다 에러를 잡는 시간이 더 많았다. 한 5배는 더 고민하고 검색한 것 같다. 에러를 많이 접하고 처리하다 보니, 처음에는 생소했던 try-except 문도 이제는 익숙해졌다.

큰 프로젝트를 하게 되면 많은 오류들을 만날 것 같은데, 미리미리 친해져야겠다... 이때도 코드를 짜는 시간보다 에러를 수정하는 시간이 더 많이 걸릴지 의문이다..

아직은 프로그램이 완벽한 것 같지는 않다. 내가 찾지 못하고 수정하지 못한 오류들이 많을 것 같은데, 좀 더 성장해서 완벽한 프로그램으로 업데이트하고 싶다. 그리고 숙직 기사님이랑 청소 아주머님께서 유용하게 사용해 주셨으면 좋겠다.

블로그 이전 전 원글 주소

https://blog.naver.com/ksm010825/223241320903

profile
멋진 개발자 되기

0개의 댓글