diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..067b30c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 SourceFree + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 2d7e0a8..a4c6149 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,99 @@ -# KlasCroller -OSS +# KlasCroller (가제) + +*version 1.0.1 +
+ + + + + ### KlasCroller + #### -광운대학교 재학생 전용 PC 프로그램입니다.- + + + + + + + +
+ + +## 프로젝트 설명 +광운대학교 학부생들이 학업 성취도에 대한 진단을 스스로 할 수 있도록 Window 기반 GUI 프로그램을 만들었습니다. + +사용자의 요청에 따라 파라미터를 기준으로 단일 학기 분석, 두 개 학기 비교, 전체 학기(완료된 학기 기준) 진단 등의 기능을 수행 합니다. + +klas 로그인 시 사용하는 id,pw를 입력받아, 학부생의 학업 성취 데이터를 추출하여 아래와 같은 파라미터를 구성합니다. + + +### [프로젝트 기간] +- (2022-09-12 ~ 2022-11-26) + + +### [기능] +- KLAS (광운대학교 웹어플리케이션, https://klas.kw.ac.kr) 접근 및 로그인 +- 학기별 출석율, 과제제출율, 성적 등의 학생 Data Scraping 및 파라미터 값 추출 + + [파라미터] + + - 의지력 = (출석 / (출석+지각+결석)) X 100 + + - 지능 = ((실제 취득 학점) / (취득 가능 학점) ) X 100 + + - 생존력 = (*수강과목 갯수 에 따른 점수) + 총 과제,퀴즈 갯수(50개 이상 이면 50점) + + - 근명성 = (제출한 과제 수 + 제출한 퀴즈 수) / (총 과제 수 + 총 퀴즈 수) X 100 + + - 가성비 = (지능 /의지력) + + + *수강과목 갯수 에 따른 점수: + 6 개 이상: 50점 + 5 개 : 40점 + 4 개 : 32점 + 3 개 : 24점 + 2 개 : 16점 + 1 개 : 8점 + +- 로컬 PC에 cache file 생성 후 파라미터 값 저장 (이후 프로그램 구동 시, Scraping 과정 생략) +- 파라미터를 근거로 사용자가 원하는 학기에 대해 그래프 출력 + + 단일 학기 분석 시, 오각형 방사 그래프(Radiation Graph Of A Pentagon) 출력 + + 두개 학기 분석 시, 막대 그래프(Bar Graph) 출력 + +- 완료된 학기에 분석하여 사용자의 학업 성취 스타일 판단 + + 1. 5개의 파라미터 각각의 중앙값으로 가장 높은 값, 두번째로 높은 값을 가지는 파라미터를 확인 + + 2. 가장 높은 값을 가지는 파라미터로 적절한 동물 이미지를 매치 + + 3. 두번째로 높은 값을 가지는 파라미터로 적절한 배경 이미지를 매치 + + 4. 2,3 번 으로 장점이 부각된 사용자의 학업 성취 스타일 출력 + + 5. 5개의 파라미터 각각의 중앙값으로 가장 낮은 값, 두 번째로 낮은 값을 가지는 파라미터를 확인 + + 6. 가장 낮은 값을 가지는 파라미터로 적절한 동물 이미지를 매치 + + 7. 두번째로 낮은 값을 가지는 파라미터로 적절한 배경 이미지를 매치 + + 8. 6,7 번 으로 단점이 부각된 사용자의 학업 성취 스타일 출력 + +- 사용자 요청에 따른 cache file 초기화 + + +### [사용법] +- KLAS 로그인 시 사용하는 id,pw로 프로그램에 로그인 해주세요. +- 프로그램이 정상 작동이 되었다면 아래와 같은 화면에 사용자의 학번이 출력됩니다. + +- <단일 학기 분석> 버튼을 클릭한다면, 해당 기능에 대한 설명과 학기 선택 콤보 박스가 있는 학기선택 창을 보실 수 있습니다. +선택된 학기에 대해서 사용자의 파라미터 값으로 오각형의 방사그래프(Radiation Graph Of A Pentagon)가 출력됩니다. + +- <두개 학기 비교> 버튼을 클릭한다면, 해당 기능에 대한 설명과 학기 선택 콤보 박스가 있는 학기선택 창을 보실 수 있습니다. +선택된 두개 학기에 대해서 사용자의 파라미터 값으로 막대 그래프(Bar Graph)가 출력됩니다. + +- <종합 평가> 버튼을 클릭한다면, 해당 기능에 대한 설명 창을 보실 수 있습니다. + +- 새 학기를 업데이트 하려거나, KLAS 정보를 local PC에 저장하고 싶지 않을 경우, <내 정보 초기화> 버튼을 클릭 해주세요. + diff --git a/__pycache__/caching.cpython-310.pyc b/__pycache__/caching.cpython-310.pyc new file mode 100644 index 0000000..30bc37a Binary files /dev/null and b/__pycache__/caching.cpython-310.pyc differ diff --git a/__pycache__/displaying.cpython-310.pyc b/__pycache__/displaying.cpython-310.pyc new file mode 100644 index 0000000..df58318 Binary files /dev/null and b/__pycache__/displaying.cpython-310.pyc differ diff --git a/__pycache__/scraping.cpython-310.pyc b/__pycache__/scraping.cpython-310.pyc new file mode 100644 index 0000000..0981704 Binary files /dev/null and b/__pycache__/scraping.cpython-310.pyc differ diff --git a/caching.py b/caching.py new file mode 100644 index 0000000..dc94399 --- /dev/null +++ b/caching.py @@ -0,0 +1,31 @@ +import pickle +import gzip +import os + +class CacheManager: + def __init__(self,path,id): + # 파일이름 + self.file_path = os.path.join(path,id+".plk") + + def SaveCache(self, data): + # 저장, 압축 + with gzip.open(self.file_path, "wb") as f: + pickle.dump(data, f) + + # 저장된 데이터 초기화 + def InitCache(self): + empty_dic = {} + with gzip.open(self.file_path, "wb") as f: + pickle.dump(empty_dic, f) + + # 저장된 데이터 가져오기 + def GetCache(self): + dic = {} + with gzip.open(self.file_path,'rb') as f: + dic = pickle.load(f) + return dic + + # 파일 삭제 + def DestoryFile(self): + if os.path.exists(self.file_path): + os.remove(self.file_path) \ No newline at end of file diff --git a/displaying.py b/displaying.py new file mode 100644 index 0000000..75c1cbc --- /dev/null +++ b/displaying.py @@ -0,0 +1,838 @@ +from tkinter import * +from tkinter import ttk + +import os +import sys +import tkinter.font + +import webbrowser + +from math import pi + +import numpy as np +import matplotlib +import matplotlib.pyplot as plt +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +matplotlib.rcParams['font.family']='NanumGothic' +matplotlib.rcParams['axes.unicode_minus'] = False + +from caching import CacheManager + +# 상대 경로 -> 절대 경로 +def resource_path(relative_path): + """ Get absolute path to resource, works for dev and for PyInstaller """ + try: + # PyInstaller creates a temp folder and stores path in _MEIPASS + base_path = sys._MEIPASS + except Exception: + base_path = os.path.abspath(".") + + return os.path.join(base_path, relative_path) + +class WindowManager(): + def __init__(self, user_info = {}, id = ''): + self.id = id + self.pw = '' + + self.category = ['의지력', '사고력' , '생존력' , '근면성' , '가성비'] + # 유저 정보 ( 의지력 , 사고력 , 생존력 , 근명성 , 가성비) + self.user_info = user_info + self.user_copy = user_info + # 전체(완료+현재) 학기 이름 (ex) 2022년 1학기) + self.seme_list = list(user_info.keys()) + # 전체(완료+현재) 학기 수 + self.cnt_seme = len(user_info) + + # 계정을 삭제하고 종료하는지, 계정을 저장한 상태로 종료하는지 + self.close_type = 0 + + + # 로그인 용 Window 열기 + def OpenWindow_Login(self): + + # 창 설정 + self.win_lo = Tk() + self.win_lo.title("Klas Log-in") + self.win_lo.geometry("400x500-100+50") + + self.win_lo.protocol("WM_DELETE_WINDOW", self.CloseWindow_Login) + + # 폰트들 설정 + self.font1=tkinter.font.Font(family="나눔스퀘어라운드 ExtraBold", size=20, weight="bold") + self.font2=tkinter.font.Font(family="나눔스퀘어라운드 ExtraBold", size=10, weight="bold") + + self.can_1 = Canvas(self.win_lo, width=400, height=500, background='#7C1B0F', highlightthickness = 0) + self.can_1.pack(padx=0, pady=0) + self.can_1.place(x=0, y=0) + + self.can_2 = Canvas(self.win_lo, width=300, height=85, background='#7C1B0F') + self.can_2.pack(padx=0, pady=0) + self.can_2.place(x=50, y=55) + + # 배경 이미지 + img = PhotoImage(file=resource_path("KlasCroller\\img\\back.png")) + self.can_1.create_image(200, 250, image=img) + + file_img2 = resource_path("KlasCroller\\img\\campus_logo.png") + img2 = PhotoImage(file=file_img2) + self.can_2.create_image(157, 45, image=img2) + + # id 입력창 + self.ent1 = Entry(self.win_lo, relief="groove") + self.ent1.insert(0, "ID : 학번을 입력하세요") + def clear(event): + # 좌클릭 했을 때 입력창의 내용 다 지우기 + if self.ent1.get() == "ID : 학번을 입력하세요": + self.ent1.delete(0, len(self.ent1.get())) + self.ent1.bind("", clear) + self.ent1.place(x=50,y=200,width=300,height=35) + + # pw 입력창 + self.ent2 = Entry(self.win_lo) + self.ent2.config(show="*") + self.ent2.place(x=50,y=270,width=300,height=35) + + btn = Button(self.win_lo, font=self.font2, bg="white", fg="black") + btn.config(text = "로그인") + btn.place(x=150,y=350,width=100,height=40) + btn.config(command=self.EventHandler_Login) + + self.win_lo.mainloop() + + def CloseWindow_Login(self): + self.win_lo.destroy() + exit(0) + + def GetIdPw(self): + self.OpenWindow_Login() + return self.id,self.pw + + # 로그인 이벤트 핸들러 + def EventHandler_Login(self): + self.id = self.ent1.get() + self.pw = self.ent2.get() + + self.can_1.destroy() + self.can_2.destroy() + + self.win_lo.destroy() + + def Run_Main(self): + self.OpenWindow_MainMenu() + path_user_file = resource_path("KlasCroller\\usr") + CacheManager(path_user_file, self.id).DestoryFile() + SubBoxManager().MessageBox("KlasCroller가 수집한\n 이용자님의 정보가 삭제되었습니다.") + + exit(0) + + # 메인메뉴 창 열기 + def OpenWindow_MainMenu(self): + + # 하나의 기능 창만 열릴 수 있도록 + # boolean 형 변수 초기화 + self.is_opend_win_func1 = False + self.is_opend_win_func2 = False + self.is_opend_win_func3 = False + self.is_opend_win_func4 = False + + # 알림 창 선언 + self.win_notice_func1 = -1 + self.win_notice_func2 = -1 + self.win_notice_func3 = -1 + self.win_notice_func4 = -1 + + # 하위 창 선언 + self.win_func1 = -1 + self.win_func2 = -1 + self.win_func3 = -1 + + # 창 설정 + self.win_main = Tk() + self.win_main.title("Main Menu") + self.win_main.geometry("500x560-100+50") # 가로 세로 + self.win_main.resizable(True, True) + + self.win_main.protocol("WM_DELETE_WINDOW", self.CloseWindow_MainMenu) + + can = Canvas(self.win_main, width=500, height=230,background='red', highlightthickness = 0) + can.pack(padx=0, pady=0) + can.place(x=0, y=0) + + # 바탕 광운대 사진 삽입 + file_back = resource_path("KlasCroller\\img\\back.png") + back = PhotoImage(file=file_back) + can.create_image(0,0,image=back) + + # 각 기능 버튼에 해당하는 이미지들 + img1 = PhotoImage(file=resource_path("KlasCroller\img\img_analysis_black.png")) + + img2 = PhotoImage(file=resource_path("KlasCroller\img\img_compare_black.png")) + + img3 = PhotoImage(file=resource_path("KlasCroller\img\img_mbti_black.png")) + + img4 = PhotoImage(file=resource_path("KlasCroller\img\img_search_black.png")) + + # 폰트들 설정 + self.font0=tkinter.font.Font(family="나눔스퀘어라운드 ExtraBold", size=30, weight="bold") + self.font1=tkinter.font.Font(family="나눔스퀘어라운드 ExtraBold", size=20, weight="bold") + self.font2=tkinter.font.Font(family="나눔스퀘어라운드 ExtraBold", size=10, weight="bold") + + lab1 = Label(self.win_main) + lab1.config(text = "KlasCroller", font=self.font0, foreground='white', background='#7C1B0F') + lab1.place(x=25,y=25) + + # 사용자 학번 + lab2 = Label(self.win_main) + lab2.config(text = str(self.id) + " 님, 반갑습니다.", font=self.font1, foreground='white', background='#7C1B0F') + lab2.place(x=25,y=170) + + b1 = Button(self.win_main, text = "단일 학기\n 분석",font=self.font2, bg="white", fg="black",image=img1, compound="left",command=self.OpenWindow_Notice_Function1) + b2 = Button(self.win_main, text = "두개 학기\n 비교", font=self.font2, bg="white", fg="black",image=img2, compound="left",command=self.OpenWindow_Notice_Function2) + b3 = Button(self.win_main, text = "SF\nMBTI", font=self.font2, bg="white", fg="black",image=img3, compound="left",command=self.OpenWindow_Notice_Function3) + b4 = Button(self.win_main, text = "취업정보 확인", font=self.font2, bg="white", fg="black",image=img4, compound="left",command=self.OpenWindow_Notice_Function4) + b1.place(x=0,y=220,width=250, height=170) + b2.place(x=250,y=220,width=250, height=170) + b3.place(x=0,y=390,width=250, height=170) + b4.place(x=250,y=390,width=250, height=170); + + # 캐시파일 삭제 + Button(self.win_main, + text="계정 초기화", + bg='#7C1B0F', + fg='snow', + font=self.font2, + command = self.Delete_userInfo + ).place(x=400,y=20,width=95,height=25) + + self.win_main.mainloop() + + def Delete_userInfo(self): + self.close_type = 1 + self.win_main.destroy() + + def CloseWindow_MainMenu(self): + self.close_type = 0 + self.win_main.destroy() + exit(0) + + # 첫번째 기능 알림 창 open + def OpenWindow_Notice_Function1(self): + # main 창 제외하고 열린 창은 모두 닫기 + if self.is_opend_win_func1 : + self.win_notice_func1.destroy() + self.win_notice_func1 = -1 + self.is_opend_win_func1 = False + if self.is_opend_win_func2 : + self.win_notice_func2.destroy() + self.win_notice_func2 = -1 + self.is_opend_win_func2 = False + if self.is_opend_win_func3 : + self.win_notice_func3.destroy() + self.win_notice_func3 = -1 + self.is_opend_win_func3 = False + if self.is_opend_win_func4 : + self.win_notice_func4.destroy() + self.win_notice_func4 = -1 + self.is_opend_win_func4 = False + + # func1 창이 열림 + self.is_opend_win_func1 = True + + # 하위 창 초기화 + if self.win_func1 != -1: + self.win_func1.destroy() + self.win_func1 = -1 + if self.win_func2 != -1: + self.win_func2.destroy() + self.win_func2 = -1 + if self.win_func3 != -1: + self.win_func3.destroy() + self.win_func3 = -1 + + # 창 설정 + self.win_notice_func1 = Toplevel(self.win_main) + self.win_notice_func1.title("한 한기 분석 알림") + self.win_notice_func1.geometry("400x450+100+50") + self.win_notice_func1.resizable(width = FALSE, height = FALSE) + + # 배경 + canvas = Canvas(self.win_notice_func1, + width=400, + height=450, + background='#7C1B0F', + highlightthickness = 0) + canvas.pack(padx=0, pady=0) + canvas.place(x=0, y=0) + img1 = PhotoImage(file=resource_path("KlasCroller\\img\\back.png")) + canvas.create_image(200, 230, image=img1) + + # 제목 옆에 이미지 + img2 = PhotoImage(file=resource_path("KlasCroller\\img\\img_analysis_white.png")) + Label(self.win_notice_func1, image=img2, background='#7C1B0F').place(x=70, y=95) + + + # 창 Title + Label(master=self.win_notice_func1,text = "단일 학기 분석", + font=self.font1,foreground='white', + background='#7C1B0F', + justify= CENTER).place(x=130,y=100) + + # 설명글 + Label(master=self.win_notice_func1, + text = "아래의 드롭다운 메뉴에서\n\n분석하고 싶은 학기를 선택해주세요.\n\n\n 선택된 학기에 대해 \n\n오각 방사 그래프를 출력해드립니다.", + font=self.font2,foreground='white', + background='#7C1B0F', + justify= CENTER).place(x=95,y=150) + + # 비교 학기 선택창에 콤보박스 생성 + self.combobox_1_1 = ttk.Combobox(self.win_notice_func1) + self.combobox_1_1.config(value = self.seme_list) + self.combobox_1_1.current(0) + self.combobox_1_1.place(x=70,y=300,width=260,height=30) + + + # 분석하기 버튼 + Button(self.win_notice_func1, + text="분석하기", + bg='#7C1B0F', + fg='snow', + font=self.font2, + command = self.OpenWindow_OneSemesterAnalysis).place(x=160,y=400,width=80,height=25) + + + self.win_notice_func1.mainloop() + + def OpenWindow_OneSemesterAnalysis(self): + self.sem = self.combobox_1_1.get() + + self.data_list = self.user_info[self.sem].copy() + self.data_list += self.data_list[:1] + + angles = [n/float(5) * 2 *pi for n in range(5)] + angles += angles[:1] + + fig,ax = plt.subplots(1,1,figsize=(3,5),subplot_kw=dict(polar=True)) + ax.patch.set_facecolor('#7C1B0F') + fig.patch.set_facecolor('#7C1B0F') + ax.set_theta_offset(pi / 2) ## 시작점 + ax.set_theta_direction(-1) ## 그려지는 방향 시계방향 + plt.title(self.sem, size=15, color='white') + + plt.xticks(angles[:-1],self.category,color='white',size=10) + ax.tick_params(axis='x', which='major', pad=5) + plt.yticks([20,40,60,80],['','','',''],color='white',size=10) + plt.ylim(0,100) + ax.set_rlabel_position(100) + + ax.plot(angles,self.data_list,linewidth=3,linestyle='solid',color='#FFFF00') + ax.fill(angles,self.data_list,'#FFFF00',alpha=0.5) + + self.win_func1 = Toplevel(self.win_notice_func1) + self.win_func1.config(bg='#7C1B0F') + self.win_func1.title(self.sem+" 학기 분석") + self.win_func1.geometry("650x400+150+100") + self.win_func1.resizable(width = FALSE, height = FALSE) + + parameter_label = [Label(self.win_func1) for _ in range(5)] + value_label = [Label(self.win_func1) for _ in range(5)] + + + for i in range(5): + parameter_label[i].config(text = self.category[i], font = self.font1, bg='#69180D', fg = 'yellow') + parameter_label[i].place(x=420, y= 20 + 80*i) + + value_label[i].config(text = self.user_info[self.sem][i], font = self.font1, bg='#69180D', fg = 'snow') + value_label[i].place(x=550, y= 20 + 80*i) + + canvas = FigureCanvasTkAgg(fig, master=self.win_func1) + canvas.get_tk_widget().pack(anchor='w') + + + + + + + + + + + + # 두번째 기능 알림 창 open + def OpenWindow_Notice_Function2(self): + # main 창 제외하고 열린 창은 모두 닫기 + if self.is_opend_win_func1 : + self.win_notice_func1.destroy() + self.win_notice_func1 = -1 + self.is_opend_win_func1 = False + if self.is_opend_win_func2 : + self.win_notice_func2.destroy() + self.win_notice_func2 = -1 + self.is_opend_win_func2 = False + if self.is_opend_win_func3 : + self.win_notice_func3.destroy() + self.win_notice_func3 = -1 + self.is_opend_win_func3 = False + if self.is_opend_win_func4 : + self.win_notice_func4.destroy() + self.win_notice_func4 = -1 + self.is_opend_win_func4 = False + + # func2 창이 열림 + self.is_opend_win_func2 = True + + # 하위 창 초기화 + if self.win_func1 != -1: + self.win_func1.destroy() + self.win_func1 = -1 + if self.win_func2 != -1: + self.win_func2.destroy() + self.win_func2 = -1 + if self.win_func3 != -1: + self.win_func3.destroy() + self.win_func3 = -1 + + # 창 설정 + self.win_notice_func2 = Toplevel(self.win_main) + self.win_notice_func2.title("두 학기 비교 알림") + self.win_notice_func2.geometry("400x450+100+50") + self.win_notice_func2.resizable(width = FALSE, height = FALSE) + + # 배경 + canvas = Canvas(self.win_notice_func2, + width=400, + height=450, + background='#7C1B0F', + highlightthickness = 0) + canvas.pack(padx=0, pady=0) + canvas.place(x=0, y=0) + img1 = PhotoImage(file=resource_path("KlasCroller\\img\\back.png")) + canvas.create_image(200, 230, image=img1) + + # 제목 옆에 이미지 + img2 = PhotoImage(file=resource_path("KlasCroller\\img\\img_compare_white.png")) + Label(self.win_notice_func2, image=img2, background='#7C1B0F').place(x=70, y=95) + + # 창 Title + Label(master=self.win_notice_func2, + text = "두 개 학기 비교 ", + font=self.font1, + foreground='white', + background='#7C1B0F', + justify= CENTER).place(x=130,y=100) + + # 설명글 + Label(master=self.win_notice_func2, + text = "아래의 드롭다운 메뉴에서\n\n비교하고 싶은 학기를 선택해주세요.\n\n\n 선택된 학기에 대해 \n\n막대 그래프를 출력해드립니다.", + font=self.font2,foreground='white', + background='#7C1B0F', + justify= CENTER).place(x=95,y=150) + + # 비교 학기 선택창에 콤보박스 생성 + self.combobox_2_1 = ttk.Combobox(self.win_notice_func2) + self.combobox_2_2 = ttk.Combobox(self.win_notice_func2) + + self.combobox_2_1.config(value = self.seme_list) + self.combobox_2_1.current(0) + self.combobox_2_1.place(x=70,y=300,width=260,height=30) + + self.combobox_2_2.config(value = self.seme_list) + self.combobox_2_2.current(0) + self.combobox_2_2.place(x=70,y=350,width=260,height=30) + + # 비교하기 버튼 + Button(self.win_notice_func2, + text="비교하기", + bg='#7C1B0F', + fg='snow', + font=self.font2, + command = self.OpenWindow_TwoSemesterCompare).place(x=160,y=400,width=80,height=25) + + self.win_notice_func2.mainloop() + + def OpenWindow_TwoSemesterCompare(self): + self.sem1 = self.combobox_2_1.get() + self.sem2 = self.combobox_2_2.get() + self.list_1 = self.user_info[self.sem1].copy() + self.list_2 = self.user_info[self.sem2].copy() + + x = np.arange(len(self.category)) + width = 0.3 + + fig, ax =plt.subplots(figsize=(4,5)) + ax.patch.set_facecolor('#7C1B0F') + fig.patch.set_facecolor('#7C1B0F') + plt.bar(x, self.list_1, width, color='#FFFFFF',alpha = 0.5, edgecolor = '#FFFFFF', linewidth = 2) + plt.bar(x + width+0.1, self.list_2, width, color='#FFFF00',alpha = 0.5, edgecolor = '#FFFF00', linewidth = 2) + plt.xticks(x+width, self.category, color ='white') + plt.yticks(color='white') + + plt.title(self.sem1+' vs '+self.sem2,color='white') + + self.win_func2 = Toplevel(self.win_notice_func2) + self.win_func2.config(bg='#7C1B0F') + self.win_func2.title(self.sem1+','+self.sem2+" 학기 비교") + self.win_func2.geometry("650x400+150+100") + self.win_func2.resizable(width = FALSE, height = FALSE) + + + + + parameter_label = [Label(self.win_func2) for _ in range(5)] + # seme_1_label = [Label(window_func_2) for _ in range(5)] + # seme_1_label = [Label(window_func_2) for _ in range(5)] + value_1_label = [Label(self.win_func2) for _ in range(5)] + value_2_label = [Label(self.win_func2) for _ in range(5)] + + for i in range(5): + parameter_label[i].config(text = self.category[i], font = self.font1, bg='snow', fg = 'black') + parameter_label[i].place(x=420, y= 22 + 80*i) + + string_1 = self.sem1+" "+str(self.list_1[i]) + value_1_label[i].config(text = string_1, font = self.font2, bg='#69180D', fg = 'snow') + value_1_label[i].place(x=505, y= 15 + 80*i) + + string_2 = self.sem2+" "+ str(self.list_2[i]) + value_2_label[i].config(text = string_2, font = self.font2, bg='#69180D', fg = 'yellow') + value_2_label[i].place(x=505, y= 40 + 80*i) + canvas = FigureCanvasTkAgg(fig, master=self.win_func2) + canvas.get_tk_widget().pack(anchor='w') + + + + + + # 세번째 기능 알림 창 open + def OpenWindow_Notice_Function3(self): + # main 창 제외하고 열린 창은 모두 닫기 + if self.is_opend_win_func1 : + self.win_notice_func1.destroy() + self.win_notice_func1 = -1 + self.is_opend_win_func1 = False + if self.is_opend_win_func2 : + self.win_notice_func2.destroy() + self.win_notice_func2 = -1 + self.is_opend_win_func2 = False + if self.is_opend_win_func3 : + self.win_notice_func3.destroy() + self.win_notice_func3 = -1 + self.is_opend_win_func3 = False + if self.is_opend_win_func4 : + self.win_notice_func4.destroy() + self.win_notice_func4 = -1 + self.is_opend_win_func4 = False + + # func3 창이 열림 + self.is_opend_win_func3 = True + + # 하위 창 초기화 + if self.win_func1 != -1: + self.win_func1.destroy() + self.win_func1 = -1 + if self.win_func2 != -1: + self.win_func2.destroy() + self.win_func2 = -1 + if self.win_func3 != -1: + self.win_func3.destroy() + self.win_func3 = -1 + + # 창 설정 + self.win_notice_func3 = Toplevel(self.win_main) + self.win_notice_func3.title("학업 스타일 분석") + self.win_notice_func3.geometry("400x450+100+50") + self.win_notice_func3.resizable(width = FALSE, height = FALSE) + + # 배경 + canvas = Canvas(self.win_notice_func3, + width=400, + height=450, + background='#7C1B0F', + highlightthickness = 0) + canvas.pack(padx=0, pady=0) + canvas.place(x=0, y=0) + img1 = PhotoImage(file=resource_path("KlasCroller\\img\\back.png")) + canvas.create_image(200, 230, image=img1) + + # 제목 옆에 이미지 + img2 = PhotoImage(file=resource_path("KlasCroller\\img\\img_mbti_white.png")) + Label(self.win_notice_func3, image=img2, background='#7C1B0F').place(x=80, y=85) + + # 창 Title + Label(master=self.win_notice_func3, + text = "SF MBTI", + font=self.font1, + foreground='white', + background='#7C1B0F', + justify= CENTER).place(x=140,y=100) + + # 설명글 + Label(master=self.win_notice_func3, + text = " 지난 학기동안 당신은 어떻게\n\n학업을 수행하셨을까요?\n\n\n당신의 학업 성취 스타일과 맞는\n\n동물 사진을 출력해드립니다.", + font=self.font2, + foreground='white', + background='#7C1B0F', + justify= CENTER).place(x=95,y=150) + + # 확인하기 버튼 + Button(self.win_notice_func3, + text="확인하기", + bg='#7C1B0F', + fg='snow', + font=self.font2, + command = self.OpenWindow_PrintUserStyle).place(x=150,y=300,width=80,height=25) + + self.win_notice_func3.mainloop() + + def OpenWindow_PrintUserStyle(self): + + self.win_func3 = Toplevel(self.win_notice_func3) + self.win_func3.config(bg='#7C1B0F') + self.win_func3.title('SF MBTI') + self.win_func3.geometry("650x400+150+100") + self.win_func3.resizable(width = FALSE, height = FALSE) + + Label(self.win_func3,text="학업 스타일 분석",font=self.font1,bg='#7C1B0F',fg='snow').place(x=220,y=10) + + + + # 사용자 style 뽑아내는 로직 + good_adjective_list = ['열정많은','저명한','끈질긴','부지런한','운 좋은'] + good_noun_list = ['소나무','돌고래','쥐','개미','네잎클로버'] + + bad_adjective_list = ['노력상실','우둔한','포기 빠른','게으른','불운한'] + bad_noun_list = ['베짱이','금붕어','게복치','나무늘보','까마귀'] + + good_back_imgs = ['KlasCroller\\img\\back_p_1.png', + 'KlasCroller\\img\\back_p_2.png', + 'KlasCroller\\img\\back_p_3.png', + 'KlasCroller\\img\\back_p_4.png', + 'KlasCroller\\img\\back_p_5.png'] + bad_back_imgs = ['KlasCroller\\img\\back_n_1.png', + 'KlasCroller\\img\\back_n_2.png', + 'KlasCroller\\img\\back_n_3.png', + 'KlasCroller\\img\\back_n_4.png', + 'KlasCroller\\img\\back_n_5.png'] + + good_ani_imgs = ['KlasCroller\\img\\img_p_1.png', + 'KlasCroller\\img\\img_p_2.png', + 'KlasCroller\\img\\img_p_3.png', + 'KlasCroller\\img\\img_p_4.png', + 'KlasCroller\\img\\img_p_5.png'] + bad_ani_imgs = ['KlasCroller\\img\\img_n_1.png', + 'KlasCroller\\img\\img_n_2.png', + 'KlasCroller\\img\\img_n_3.png', + 'KlasCroller\\img\\img_n_4.png', + 'KlasCroller\\img\\img_n_5.png'] + + list_a =[] # 의지력 list + list_b =[] # 사고력 list + list_c =[] # 생존력 list + list_d =[] # 근명성 list + list_e =[] # 가성비 list + + + for seme in self.seme_list: + list_a.append(self.user_info[seme][0]) + list_b.append(self.user_info[seme][1]) + list_c.append(self.user_info[seme][2]) + list_d.append(self.user_info[seme][3]) + list_e.append(self.user_info[seme][4]) + + list_median = [] + list_median.append(np.median(list_a)) + list_median.append(np.median(list_b)) + list_median.append(np.median(list_c)) + list_median.append(np.median(list_d)) + list_median.append(np.median(list_e)) + del list_a, list_b, list_c, list_d, list_e + + # (best, second_best): list_median에서 최고, 차고 값의 인덱스 + # (worst, second_worst): list_median에서 최저, 차저 값의 인덱스 + best = second_best = worst = second_worst = 0 + # tmp: 차고, 차저 값의 인덱스 추출시 최고,최저 값을 잠시 저장하기 위해 쓰는 변수 + tmp = 0 + + # (best, second_best): list_median에서 최고, 차고 값의 인덱스 + best = list_median.index(max(list_median)) + tmp = list_median[best] + list_median[best] = -1 + second_best = list_median.index(max(list_median)) + list_median[best] = tmp + + # (worst, second_worst): list_median에서 최저, 차저 값의 인덱스 + worst = list_median.index(min(list_median)) + tmp = list_median[worst] + list_median[worst] = 101 + second_worst = list_median.index(min(list_median)) + list_median[worst] = tmp + del list_median + + str_good = good_adjective_list[second_best] +' '+ good_noun_list[best] + str_bad = bad_adjective_list[second_worst] +' '+ bad_noun_list[worst] + + + img1 = PhotoImage(file=resource_path("KlasCroller\\img\\img_board.png")) + + canvas_card1 = Canvas(self.win_func3, width=246, height=328,background='#7C1B0F', highlightthickness = 0) + canvas_card1.place(x=50, y=65) + canvas_card1.create_image(123, 164, image=img1) + + canvas_card2 = Canvas(self.win_func3, width=246, height=328,background='#7C1B0F', highlightthickness = 0) + canvas_card2.place(x=365, y=65) + canvas_card2.create_image(123, 164, image=img1) + + canvas_pos_animal = Canvas(self.win_func3, width=185, height=185 ,background='white', highlightthickness = 0) + canvas_pos_animal.place(x=80, y=185) + background_img_1 = PhotoImage(file=resource_path(good_back_imgs[best])) + foreground_img_1 = PhotoImage(file=resource_path(good_ani_imgs[best])) + canvas_pos_animal.create_image(185//2,185//2,image=background_img_1) + canvas_pos_animal.create_image(185//2,185//2,image=foreground_img_1) + + canvas_neg_animal = Canvas(self.win_func3, width=185, height=185 ,background='white', highlightthickness = 0) + canvas_neg_animal.place(x=396, y=185) + background_img_2 = PhotoImage(file=resource_path(bad_back_imgs[worst])) + foreground_img_2 = PhotoImage(file=resource_path(bad_ani_imgs[worst])) + canvas_neg_animal.create_image(185//2,185//2,image=background_img_2) + canvas_neg_animal.create_image(185//2,185//2,image=foreground_img_2) + + Label(self.win_func3,text=str_good,font=self.font2,bg='white',fg='black').place(x=80,y=125) + Label(self.win_func3,text="- "+self.category[best] + " 이/가 제일 훌륭합니다.",font=self.font2,bg='white',fg='black').place(x=80,y=145) + Label(self.win_func3,text="- "+self.category[second_best] + " 이/가 뛰어납니다.",font=self.font2,bg='white',fg='black').place(x=80,y=165) + + Label(self.win_func3,text=str_bad ,font=self.font2,bg='white',fg='black').place(x=395,y=125) + Label(self.win_func3,text="- "+self.category[worst] + " 이/가 제일 필요합니다.",font=self.font2,bg='white',fg='black').place(x=395,y=145) + Label(self.win_func3,text="- "+self.category[second_worst] + " 이/가 부족합니다.",font=self.font2,bg='white',fg='black').place(x=395,y=165) + + + self.win_func3.mainloop() + + + + + + + + +# 네번째 기능 알림 창 open + def OpenWindow_Notice_Function4(self): + # main 창 제외하고 열린 창은 모두 닫기 + if self.is_opend_win_func1 : + self.win_notice_func1.destroy() + self.win_notice_func1 = -1 + self.is_opend_win_func1 = False + if self.is_opend_win_func2 : + self.win_notice_func2.destroy() + self.win_notice_func2 = -1 + self.is_opend_win_func2 = False + if self.is_opend_win_func3 : + self.win_notice_func3.destroy() + self.win_notice_func3 = -1 + self.is_opend_win_func3 = False + if self.is_opend_win_func4 : + self.win_notice_func4.destroy() + self.win_notice_func4 = -1 + self.is_opend_win_func4 = False + + # func4 창이 열림 + self.is_opend_win_func4 = True + + # 창 설정 + self.win_notice_func4 = Toplevel(self.win_main) + self.win_notice_func4.title("학업 스타일 분석") + self.win_notice_func4.geometry("400x450+100+50") + self.win_notice_func4.resizable(width = FALSE, height = FALSE) + + # 배경 + canvas = Canvas(self.win_notice_func4, + width=400, + height=450, + background='#7C1B0F', + highlightthickness = 0) + canvas.pack(padx=0, pady=0) + canvas.place(x=0, y=0) + img1 = PhotoImage(file=resource_path("KlasCroller\\img\\back.png")) + canvas.create_image(200, 230, image=img1) + + # 제목 옆에 이미지 + img2 = PhotoImage(file=resource_path("KlasCroller\\img\\img_search_white.png")) + Label(self.win_notice_func4, image=img2, background='#7C1B0F').place(x=80, y=85) + + # 창 Title + Label(master=self.win_notice_func4, + text = "취업 공고 분야", + font=self.font1, + foreground='white', + background='#7C1B0F', + justify= CENTER).place(x=140,y=100) + + # 설명글 + Label(master=self.win_notice_func4, + text = "\n\n입력하신 분야/직업\n\n잡코리아 취업공고가 출력됩니다.", + font=self.font2, + foreground='white', + background='#7C1B0F', + justify= CENTER).place(x=95,y=150) + + # 입력창 + self.entry_search = Entry(master=self.win_notice_func4, relief = "groove") + self.entry_search.insert(0,"예: 웹 개발, 앱 개발 ..") + def clear(event): + # 좌클릭 했을 때 입력차의 내용 다 지우기 + if self.entry_search.get() == "예: 웹 개발, 앱 개발 ..": + self.entry_search.delete(0,len(self.entry_search.get())) + self.entry_search.bind("",clear) + self.entry_search.place(x=85,y=300,width=210,height=40) + + # 검색하기 버튼 + Button(self.win_notice_func4, + text="검색하기", + bg='#7C1B0F', + fg='snow', + font=self.font2, + command = self.OpenWebsite).place(x=150,y=360,width=80,height=25) + + self.win_notice_func4.mainloop() + + def OpenWebsite(self): + keyword = self.entry_search.get() + url = 'https://www.jobkorea.co.kr/Search/?stext={}&tabType=recruit&Page_No=1'.format(keyword) + webbrowser.open(url) + + +class SubBoxManager(): + def __init__(self): + print() + def __del__(self): + print() + def MessageBox(self,string): + self.win_message = Tk() + self.win_message.title("Message") + self.win_message.geometry("384x128") + self.win_message.option_add("*Font", "맑은고딕 12") + + label_message = Label(master=self.win_message) + button_message = Button(master=self.win_message) + + label_message.config(text = string,justify= CENTER, wraplength= 300) + button_message.config(text="확인",command=self.Check_Ok) + + label_message.pack(expand=True, fill="both") + button_message.pack(side="bottom", anchor="s",pady=10) + self.win_message.mainloop() + + def LoadingBox(self ,str="...", max_min = "5"): + self.win_loading = Tk() + self.win_loading.title("Loading") + self.win_loading.geometry("384x128") + self.win_loading.option_add("*Font", "맑은고딕 12") + + Label(self.win_loading,text=str).pack(fill="both",pady=10) + Label(self.win_loading,text="( 최대 소요시간: "+max_min+"분 )").pack(fill="both") + + progress = ttk.Progressbar(self.win_loading,orient=HORIZONTAL,length=300,mode='determinate') + progress.pack(pady=10) + progress.start(10) + self.win_loading.mainloop() + + def Check_Ok(self): + self.win_message.destroy() + \ No newline at end of file diff --git a/img/back.png b/img/back.png new file mode 100644 index 0000000..104f173 Binary files /dev/null and b/img/back.png differ diff --git a/img/back_n_1.png b/img/back_n_1.png new file mode 100644 index 0000000..b785f3d Binary files /dev/null and b/img/back_n_1.png differ diff --git a/img/back_n_2.png b/img/back_n_2.png new file mode 100644 index 0000000..906a07a Binary files /dev/null and b/img/back_n_2.png differ diff --git a/img/back_n_3.png b/img/back_n_3.png new file mode 100644 index 0000000..088dd99 Binary files /dev/null and b/img/back_n_3.png differ diff --git a/img/back_n_4.png b/img/back_n_4.png new file mode 100644 index 0000000..d055e21 Binary files /dev/null and b/img/back_n_4.png differ diff --git a/img/back_n_5.png b/img/back_n_5.png new file mode 100644 index 0000000..4e561bb Binary files /dev/null and b/img/back_n_5.png differ diff --git a/img/back_p_1.png b/img/back_p_1.png new file mode 100644 index 0000000..e8fe03a Binary files /dev/null and b/img/back_p_1.png differ diff --git a/img/back_p_2.png b/img/back_p_2.png new file mode 100644 index 0000000..df90106 Binary files /dev/null and b/img/back_p_2.png differ diff --git a/img/back_p_3.png b/img/back_p_3.png new file mode 100644 index 0000000..ffc8c04 Binary files /dev/null and b/img/back_p_3.png differ diff --git a/img/back_p_4.png b/img/back_p_4.png new file mode 100644 index 0000000..18dc132 Binary files /dev/null and b/img/back_p_4.png differ diff --git a/img/back_p_5.png b/img/back_p_5.png new file mode 100644 index 0000000..798f358 Binary files /dev/null and b/img/back_p_5.png differ diff --git a/img/campus_logo.png b/img/campus_logo.png new file mode 100644 index 0000000..f3ea7cc Binary files /dev/null and b/img/campus_logo.png differ diff --git a/img/img_analysis_black.png b/img/img_analysis_black.png new file mode 100644 index 0000000..3b3beb2 Binary files /dev/null and b/img/img_analysis_black.png differ diff --git a/img/img_analysis_white.png b/img/img_analysis_white.png new file mode 100644 index 0000000..6a64bcf Binary files /dev/null and b/img/img_analysis_white.png differ diff --git a/img/img_board.png b/img/img_board.png new file mode 100644 index 0000000..965c768 Binary files /dev/null and b/img/img_board.png differ diff --git a/img/img_compare_black.png b/img/img_compare_black.png new file mode 100644 index 0000000..dd61e78 Binary files /dev/null and b/img/img_compare_black.png differ diff --git a/img/img_compare_white.png b/img/img_compare_white.png new file mode 100644 index 0000000..d4ef642 Binary files /dev/null and b/img/img_compare_white.png differ diff --git a/img/img_mbti_black.png b/img/img_mbti_black.png new file mode 100644 index 0000000..edca13b Binary files /dev/null and b/img/img_mbti_black.png differ diff --git a/img/img_mbti_white.png b/img/img_mbti_white.png new file mode 100644 index 0000000..f3e0186 Binary files /dev/null and b/img/img_mbti_white.png differ diff --git a/img/img_n_1.png b/img/img_n_1.png new file mode 100644 index 0000000..aa7d073 Binary files /dev/null and b/img/img_n_1.png differ diff --git a/img/img_n_2.png b/img/img_n_2.png new file mode 100644 index 0000000..451f2ac Binary files /dev/null and b/img/img_n_2.png differ diff --git a/img/img_n_3.png b/img/img_n_3.png new file mode 100644 index 0000000..adf383d Binary files /dev/null and b/img/img_n_3.png differ diff --git a/img/img_n_4.png b/img/img_n_4.png new file mode 100644 index 0000000..452334c Binary files /dev/null and b/img/img_n_4.png differ diff --git a/img/img_n_5.png b/img/img_n_5.png new file mode 100644 index 0000000..8a5b830 Binary files /dev/null and b/img/img_n_5.png differ diff --git a/img/img_p_1.png b/img/img_p_1.png new file mode 100644 index 0000000..766332e Binary files /dev/null and b/img/img_p_1.png differ diff --git a/img/img_p_2.png b/img/img_p_2.png new file mode 100644 index 0000000..9ec89be Binary files /dev/null and b/img/img_p_2.png differ diff --git a/img/img_p_3.png b/img/img_p_3.png new file mode 100644 index 0000000..b25d1f4 Binary files /dev/null and b/img/img_p_3.png differ diff --git a/img/img_p_4.png b/img/img_p_4.png new file mode 100644 index 0000000..e4433e7 Binary files /dev/null and b/img/img_p_4.png differ diff --git a/img/img_p_5.png b/img/img_p_5.png new file mode 100644 index 0000000..d5e4475 Binary files /dev/null and b/img/img_p_5.png differ diff --git a/img/img_search_black.png b/img/img_search_black.png new file mode 100644 index 0000000..416baba Binary files /dev/null and b/img/img_search_black.png differ diff --git a/img/img_search_white.png b/img/img_search_white.png new file mode 100644 index 0000000..473d147 Binary files /dev/null and b/img/img_search_white.png differ diff --git a/img/kwang.png b/img/kwang.png new file mode 100644 index 0000000..c297f9e Binary files /dev/null and b/img/kwang.png differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..5a3b9de --- /dev/null +++ b/main.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +import sys +import io +import os +sys.stdout = io.TextIOWrapper(sys.stdout.detach(), encoding = 'utf-8') +sys.stderr = io.TextIOWrapper(sys.stderr.detach(), encoding = 'utf-8') + +import multiprocessing as mp + +# 버전정보 +# python 3.10.7 +# chrome 105.0.5195.127 + +from scraping import Scraper +from displaying import * +from caching import CacheManager + +# 상대 경로 -> 절대 경로 +def resource_path(relative_path): + """ Get absolute path to resource, works for dev and for PyInstaller """ + try: + # PyInstaller creates a temp folder and stores path in _MEIPASS + base_path = sys._MEIPASS + except Exception: + base_path = os.path.abspath(".") + + return os.path.join(base_path, relative_path) + +if __name__ == "__main__": + + # 유저 정보가 담긴 파일 위치 + path_user_file = resource_path("KlasCroller\\usr") + + loginWin = WindowManager() + alert = SubBoxManager() + + # 사용자로부터 id,pw를 받아온다. + id,pw = loginWin.GetIdPw() + del loginWin + + # loading box 출력(SubProcess, 동시실행을 위해서) + proc = mp.Process(target=alert.LoadingBox, args=("ID,PW 유효성 검사 중 입니다..","1",), daemon=True) + proc.start() + + # klas에 접속해서 id, pw 가 유효한지 먼저 확인 + scraper = Scraper(True) + cache = CacheManager(path_user_file,id) + file_name = os.path.join(path_user_file,str(id)+".plk") + + # 유효성 검사 중, 네트웨크에 제대로 연결되지 않았다면 강제 종료 + if(scraper.AcceseKlas(id,pw) == -1): + proc.kill() + del scraper, cache + alert.MessageBox("인터넷 연결 불안으로 비정상 종료되었습니다. 다시 시도해주세요.") + del alert + os._exit(0) + + # id,pw가 유효하지 않다면 강제 종료 + if(scraper.AcceseKlas(id,pw) == -2): + proc.kill() + del scraper, cache + alert.MessageBox("ID 혹은 Passward가 유효하지 않습니다. 다시 접속해주세요.") + del alert + os._exit(0) + + # 유효하다면 id로 캐시파일이 있는지 확인 + else: + proc.kill() + del scraper + + proc1 = mp.Process(target=alert.LoadingBox, args=("Klas에서 정보를 가져옵니다..","5",), daemon=True) + proc1.start() + scraper = Scraper() + + # (학번).plk 파일 이 없다면, 스크래핑 후 파일을 만든다. + if not os.path.isfile(file_name): + + # 로그인 중, 네트웨크에 제대로 연결되지 않았다면 강제 종료 + if(scraper.AcceseKlas(id,pw) == -1): + proc.kill() + del scraper, cache + alert.MessageBox("인터넷 연결 불안으로 비정상 종료되었습니다. 다시 시도해주세요.") + del alert + os._exit(0) + + user_info = scraper.ProcessingUserData() + proc1.kill() + # 정보를 가져오는 동안, 네트웨크에 제대로 연결되지 않았다면 강제 종료 + if(user_info == -1): + del scraper,cache + alert.MessageBox("인터넷 연결 불안으로 비정상 종료되었습니다. 다시 시도해주세요.") + del alert + os._exit(0) + # 정상적으로 읽어왔다면 캐시파일 생성 + del scraper + cache.SaveCache(user_info) + + # (학번).plk 파일 이 있다면, 로딩 창 끄기. + else: + del scraper + proc1.kill() + + user_info = cache.GetCache() + display_main = WindowManager(user_info,id) + display_main.Run_Main() + + + + diff --git a/scraping.py b/scraping.py new file mode 100644 index 0000000..9d03272 --- /dev/null +++ b/scraping.py @@ -0,0 +1,355 @@ +# -*- coding: utf-8 -*- +import sys +import io +sys.stdout = io.TextIOWrapper(sys.stdout.detach(), encoding = 'utf-8') +sys.stderr = io.TextIOWrapper(sys.stderr.detach(), encoding = 'utf-8') + +import time +import re + +from selenium import webdriver +from webdriver_manager.chrome import ChromeDriverManager +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import Select +from bs4 import BeautifulSoup + + +class Scraper: + def __init__(self,headless = False): + self.url = "https://klas.kw.ac.kr/" + ''' + 창을 띄우지 않으려면 아래 # options.add_argument('headless') 코드의 주석을 해제하면 됩니다. + ''' + options = webdriver.ChromeOptions() + if headless: + options.add_argument('headless') + self.browser = webdriver.Chrome(ChromeDriverManager().install(),options=options) + + # 화면 전환시 데이터가 바로 로드되지 않아 scraping 되지 않는 문제를 막기위해 + # scaping 대상 페이지에 도착시 sleep() 사용, 아래 변수는 sleep()에 전달할 인자 + self.sleeping_time = 0.8 + + # self.num_semester: (진행 + 완료) 학기 갯수 + self.num_semester = 0 + + def __del__(self): + print() + # id와 pw를 입력받아 klas에서 로그인 실행 + def AcceseKlas(self,id,pw): + + try: + # 1. chrome창을 띄어, klas로 이동 + self.browser.get(self.url) + # 2. id, pw 입력 + self.browser.find_element(By.ID,"loginId").send_keys(id) + self.browser.find_element(By.ID,"loginPwd").send_keys(pw) + # 3. 로그인 버튼 클릭 + self.browser.find_element(By.XPATH,"/html/body/div[1]/div/div/div[2]/form/div[2]/button").click() + + try: + WebDriverWait(self.browser,3).until(EC.presence_of_all_elements_located((By.CLASS_NAME,'ax-dialog-header'))) + return -2 + except: + print() + except: + return -1 + + # Klas에서 사용자의 Data를 가져와 가공한다. + def ProcessingUserData(self): + try: + try: + WebDriverWait(self.browser,8).until(EC.presence_of_all_elements_located((By.CLASS_NAME,'scheduletitle'))) + time.sleep(self.sleeping_time) + except: + return 0 + + html = self.browser.page_source + soup = BeautifulSoup(html, 'lxml') #html.parser + semesters = soup.find("select",{"class":"form-control form-control-sm"}).findAll("option") + + # res: 최종결과(dictionary) + # - key : 학기 이름 + # - value : [팀플,과제,퀴즈,출석정보,성적] (list) + self.num_semester = len(semesters) + res = {} + for sem in semesters: + res.update({sem.text:[]}) + # print(res) + + # academic_participation: 학기별 학업 참여도 (팀플,과제,퀴즈,출석율) + # grade_information: 성적정보 + academic_participation = list() + grade_information = list() + for semester_i in range(self.num_semester): + academic_participation.append(self.ScrapingSubjectData(semester_i)) + # print('+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++') + # print("학업참여정보",academic_participation) + grade_information = self.ScrapingGradeData() + # print('+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++') + # print("성적정보",grade_information) + + seme_name = list(res.keys()) + for i in range(self.num_semester): + # res[seme_name[i]] += grade_information[i] + + # 의지력 = (출석 / (출석+지각+결석)) * 100 + will_power = (academic_participation[i][6] * 100)// (academic_participation[i][6] + academic_participation[i][7] + academic_participation[i][8]) + res[seme_name[i]].append(int(will_power)) + + # 지능 = ((실제 취득 학점) / (취득 가능 학점) ) * 100 + intellect = grade_information[i][1] + res[seme_name[i]].append(int(intellect)) + + # 생존력 = (수강과목 갯수 에 따른 점수) + 총 과제,퀴즈 갯수(50개 이상 이면 50점) + # 수강 과목 갯수: + # ( + # 6 개 이상: 50점 + # 5 개 : 40점 + # 4 개 : 32점 + # 3 개 : 24점 + # 2 개 : 16점 + # 1 개 : 8점 + # ) + viability = academic_participation[i][1] + academic_participation[i][3] + academic_participation[i][5] + if viability > 50: + viability = 50 + if grade_information[i][0] >= 6: + viability += 50 + elif grade_information[i][0] == 5: + viability += 40 + elif grade_information[i][0] == 4: + viability += 32 + elif grade_information[i][0] == 3: + viability += 24 + elif grade_information[i][0] == 2: + viability += 16 + else: + viability += 8 + res[seme_name[i]].append(int(viability)) + + # 근명성 = (제출한 과제 수 + 제출한 퀴즈 수) / (총 과제 수 + 총 퀴즈 수) * 100 + diligence = 100*(academic_participation[i][0] + academic_participation[i][2] + academic_participation[i][4]) + diligence = diligence // (academic_participation[i][1] + academic_participation[i][3] + academic_participation[i][5]) + res[seme_name[i]].append(int(diligence)) + + # 가성비 = (지능 / 의지력) + # 100 이상이면 100으로 넣는다. + luck = (intellect*100) // will_power + if luck > 100 : luck = 100 + res[seme_name[i]].append(int(luck)) + + # 파라미터 중, 음수 값이 하나라도 있다면 성적처리가 아직 안됐거나, 잘못 기입된 된 것 + # 해당 학기 정보는 삭제한다. + if (viability<0 or intellect<0 or viability<0 or diligence<0 or luck<0 ) : + del(res[seme_name[i]]) + return res + except: + return -1 + + # 특정학기를 입력받아 학업 참여도를 가져온다. + def ScrapingSubjectData(self,semester_idx): + self.ChangeSemester(semester_idx) + + html = self.browser.page_source + soup = BeautifulSoup(html, 'lxml') #html.parser + + # each_subject_list: 과목별 정보 + # [첫번째 과목정보, 두번째 과목정보,…] 순으로 정리되어있다. + each_subject_list = list() + + # res: 반환할 결과 + # [수행한 팀플과제 수, 총 팀플과제 수, + # 제출한 개인과제 수, 총 개인과제 수, + # 제출한 퀴즈 수, 총 퀴즈 수, + # 출석횟수, 결석횟수, 지각횟수] 순으로 정리되어있다. + res = [0 for _ in range(9)] + + # subjects: 특정 학기에 들은 과목들에 정보가 들어있다. + subjects = soup.find("ul",{"class":"subjectlist listbox"}).findAll("li") + + for i in range(1,len(subjects)+1): + try: + WebDriverWait(self.browser,8).until(EC.presence_of_all_elements_located((By.CLASS_NAME,'scheduletitle'))) + time.sleep(self.sleeping_time) + except: + return 0 + self.ChangeSemester(semester_idx) + xpath = "/html/body/main/div/div/div/div/div[1]/div[2]/ul/li["+str(i)+"]" + each_subject_list.append(self.GetEachSubjectsData(xpath)) + + for i in range(len(subjects)): + if each_subject_list[i] == -1: #오류가 반환되었을 시, 일단 그냥 패쓰 + continue + for j in range(9): + res[j] += each_subject_list[i][j] + + return res + + # 학기별 학업 참여도를 추출할 때, 학기 인덱스를 입력받아 해당 학기 페이지로 이동 + def ChangeSemester(self, semester_idx): + try: + WebDriverWait(self.browser,8).until(EC.presence_of_all_elements_located((By.CLASS_NAME,'scheduletitle'))) + time.sleep(self.sleeping_time) + except: + return 0 + element_dropdown = self.browser.find_element(By.CLASS_NAME,"form-control.form-control-sm") + select_semster = Select(element_dropdown) + select_semster.select_by_index(semester_idx) + try: + WebDriverWait(self.browser,8).until(EC.presence_of_all_elements_located((By.CLASS_NAME,'scheduletitle'))) + time.sleep(self.sleeping_time) + except: + return 0 + + # 특정 한 학기의 특정 한 과목의 학업 참여도를 종합하여 반환한다. (팀플, 과제, 퀴즈, 출석율) + # 가장 최근 학기부터 첫 학기 순으로 반환한다. + def GetEachSubjectsData(self, xpath): + + # # 만약 e-learing이면 0000000 반환하고 걍 패쓰 + # if (self.WaitPageChange_Xpath(xpath+"/div[2]/button")): + # return [0,0,0,0,0,0,0,0,0] + + self.browser.find_element(By.XPATH,xpath).click() + + try: + WebDriverWait(self.browser,8).until(EC.presence_of_all_elements_located((By.CLASS_NAME,'oval'))) + time.sleep(self.sleeping_time) + except: + return 0 + + html = self.browser.page_source + soup = BeautifulSoup(html, 'lxml') + + res = list() # 최종 결과를 담을 변수 + + # 학업참여정보 + present = soup.select('a > span.oval') + team_project = re.findall(r'\d+', str(present[1])) # 팀플 + assignment = re.findall(r'\d+', str(present[5])) # 과제 + quiz = re.findall(r'\d+', str(present[6])) # 퀴즈 + + # 출결 정보 + attendance = len(soup.select('table > tbody > tr > td > span.memberft01')) # 출석 + absent = len(soup.select('table > tbody > tr > td > span.memberft04')) # 결석 + late = len(soup.select('table > tbody > tr > td > span.memberft02')) # 지각 + vacancy = len(soup.select('table > tbody > tr > td > span.memberft05')) # 공결 + + self.browser.get(self.url) + + res = [int(x) for x in team_project] + res += [int(x) for x in assignment] + res += [int(x) for x in quiz] + res.append((attendance + vacancy)) # 공결은 출석으로 포함 + res.append(absent) + res.append(late) + + return res + + # 성적정보를 가져온다. + # 가장 최근 학기부터 첫 학기 순으로 반환한다. + def ScrapingGradeData(self): + try: + WebDriverWait(self.browser,8).until(EC.presence_of_all_elements_located((By.CLASS_NAME,"scheduletitle"))) + time.sleep(self.sleeping_time) + except: + return 0 + + self.browser.find_element(By.XPATH,"/html/body/header/div[1]/div/div[1]/button").click() + time.sleep(self.sleeping_time) + self.browser.find_element(By.XPATH,"/html/body/header/div[2]/div/div/div[1]/ul/li[2]/ul/li[2]/a").click() + time.sleep(self.sleeping_time) + + html = self.browser.page_source + soup = BeautifulSoup(html, 'lxml') #html.parser + + tables = soup.select('div.tablelistbox > div#hakbu > table.AType') + + # 학점 가져오기 + credit = [[] for _ in range(len(tables))] + for ta_idx,table in enumerate(tables): + tbodies = table.select('td') + for tbody in tbodies: + if (' 3' in tbody.text): + credit[ta_idx].append(3) + elif (' 2' in tbody.text): + credit[ta_idx].append(2) + elif (' 1' in tbody.text): + credit[ta_idx].append(1) + + # 등급 가져오기 + grade = [[] for _ in range(len(tables))] + for ta_idx,table in enumerate(tables): + tbodies = table.select('td') + for tbody in tbodies: + if ('A+' in tbody.text): + grade[ta_idx].append(4.5) + elif ('A0' in tbody.text): + grade[ta_idx].append(4.0) + elif ('B+' in tbody.text): + grade[ta_idx].append(3.5) + elif ('B0' in tbody.text): + grade[ta_idx].append(3.0) + elif ('C+' in tbody.text): + grade[ta_idx].append(2.5) + elif ('C0' in tbody.text): + grade[ta_idx].append(2.0) + elif ('D+' in tbody.text): + grade[ta_idx].append(1.5) + elif ('D0' in tbody.text): + grade[ta_idx].append(1.0) + elif ('F' in tbody.text): + grade[ta_idx].append(0) + elif ('P' in tbody.text): + grade[ta_idx].append(4.5) + elif ('NP' in tbody.text): + grade[ta_idx].append(0) + + # res_0: 수강 과목 갯수 + res_0 = [0 for _ in range(self.num_semester)] + for i in range(self.num_semester): + res_0[i] = len(credit[i]) + + # res_1: 학기별 취득 가능 학점 + res_1 = [0 for _ in range(self.num_semester)] + for i in range(self.num_semester): + res_1[i] = sum(credit[i]) * 4.5 + + # res_2: 학기별 실제 취득 학점 + res_2 = [0 for _ in range(self.num_semester)] + for i in range(self.num_semester): + # 성적처리가 안되었을 경우, -1 삽입 + if (len(grade[i]) != len(credit[i])): + res_2[i] = -1 + continue + for j in range(len(grade[i])): + res_2[i] += (grade[i][j] * credit[i][j]) + + # res : res_0 , res_1 , res_2 종합 + # 학기별로 (수강과목 갯수 , 100 * (실제 취득 학점) / (취득 가능 학점) ) 출력 + res = [[] for _ in range(self.num_semester)] + for i in range(self.num_semester): + res[i].append( res_0[i] ) + res[i].append( (res_2[i]* 100) // res_1[i]) + + return res + + # 어떤 class 이름을 입력으로 주면 해당 class 이름이 현재 url 페이지에 나타날 때까지 기다린다. + # 10 초 안에 나타나면 데이터 로드할 시간을 좀 더 주고 True 반환, 10 초 이상 안나타나며 False 반환 + def WaitPageChange(self,class_name,term=8): + try: + WebDriverWait(self.browser,term).until(EC.presence_of_all_elements_located((By.CLASS_NAME,class_name))) + time.sleep(self.sleeping_time) + except: + return False + return True + + def WaitPageChange_Xpath(self,xpath_name,term=8): + try: + WebDriverWait(self.browser,term).until(EC.presence_of_all_elements_located((By.XPATH,xpath_name))) + time.sleep(self.sleeping_time) + except: + return False + return True + \ No newline at end of file diff --git a/usr/2017203088.plk b/usr/2017203088.plk new file mode 100644 index 0000000..546c46b Binary files /dev/null and b/usr/2017203088.plk differ