# 隨時GPA計算機 ## 一、發想與目的   每學期結束後,同學們都會好奇自己的成績表現,然而在台大的NTU e-portfolio網站上,只有在所有修課成績都到齊後才能顯示當學期的GPA。由於每一科的成績不定期公布,同學們若要估算自己的GPA,只能機械式的常常查詢有哪些成績更新並進行人工計算。因此,想設計可以==自動查詢成績並計算GPA的簡易計算機== ## 二、實作架構 ### 使用者介面 - 須取得使用者的帳號及密碼才能登入NTU e-portfolio,而程式進行計算後也須列出結果,因此需要設計GUI(圖形使用者介面) - 使用Python的內建套件==tkinter== ```python= import tkinter as tk ``` - 建立root window ```python=60 # root window window = tk.Tk() window.title('隨時GPA計算機') window.geometry('500x500') window.resizable(True, True) ``` - 分為兩個分頁:首頁與計算結果頁面 ```python=66 # 切換視窗 def switchFrame(frame1, frame2): frame2.pack(fill = "both", expand = True) frame1.pack_forget() ``` - 建立首頁 - 「計算」按鍵的事件處理函式:因為要傳入參數,所以使用匿名函式lambda呼叫計算GPA的函式,並傳入使用者帳號與密碼 - 在tkinter中有三種排版方式:pack, grid, place,在此選用place並以相對位置的方式進行視窗元件排版,避免使用者將視窗放大後跑版 ```python=72 # 首頁 LogIn_window = tk.Frame(window) LogIn_window.pack(fill = "both", expand = True) # 首頁視窗元件 account_label = tk.Label(LogIn_window, text="帳號") account = tk.Entry(LogIn_window) password_label = tk.Label(LogIn_window, text="密碼") password = tk.Entry(LogIn_window, show= '*') calculate_btn = tk.Button(LogIn_window, text="計算", command= lambda: calculate(account.get(), password.get())) # 首頁視窗元件排版 account_label.place(relx=0.25, rely=0.2) account.place(relx=0.32, rely=0.2) password_label.place(relx=0.25, rely=0.3) password.place(relx=0.32, rely=0.3) calculate_btn.place(relx=0.45, rely=0.5) ``` - 建立計算結果頁面 - 「回到首頁」按鍵的事件處理函式:因為要傳入參數,所以使用匿名函式lambda呼叫切換分頁的函式,並傳入目前的視窗與要切換的視窗 - 因計算結果頁面的視窗元件較單純(無並排的元件),因此直接使用pack的方式進行排版 ```python=90 # 計算結果頁面 ShowGpa_window = tk.Frame(window) # 計算結果頁面視窗元件 homepage_btn = tk.Button(ShowGpa_window, text= "回到首頁", command= lambda: switchFrame(ShowGpa_window, LogIn_window)) homepage_btn.pack() ``` - 使程式常駐執行,在最後使用mainloop()函式 ```python=98 window.mainloop() ``` ### 查詢成績:動態爬蟲 - 使用==selenium==函式庫所提供的API模擬使用者操作瀏覽器 ```python=4 from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.common.by import By ``` - calculate為計算GPA的函式: - 第一步驟為登入NTU e-portfolio - 若使用者輸入的帳號或密碼為空字串則不會繼續運算 - 若使用者輸入的帳號或密碼錯誤,仍會開啟網站的登入頁面,因此使用者可以直接在瀏覽器上重新登入,登入成功後,程式可繼續執行自動化操作 - ``import time``:使用time.sleep()等待瀏覽器加載,避免因為加載未完成而無法找到網頁元素 - 使用XPATH定位網頁元素(速度較快) - 第二步驟為取得成績資料 - 使用pandas函式庫中的read_html讀取頁面中所有的table - 取出當學期成績的table並轉換成DataFrame - 等待爬取資料後,將視窗最小化 ```python=9 def calculate(account, password): # 登入epo if (account == '' or password == ''): return driver = webdriver.Chrome(service=Service(ChromeDriverManager().install())) driver.get("https://if163.aca.ntu.edu.tw/eportfolio/") driver.maximize_window() time.sleep(2) # 輸入帳號、密碼 driver.find_element(By.XPATH, "/html/body/table/tbody/tr[1]/td/table/tbody/tr/td/table/tbody/tr[2]/td[2]/table/tbody/tr/td/a[1]/img").click() driver.find_element(By.XPATH, "/html/body/center/div/div[4]/form/table/tbody/tr[1]/td/input").send_keys(account) driver.find_element(By.XPATH, "/html/body/center/div/div[4]/form/table/tbody/tr[2]/td/input").send_keys(password) driver.find_element(By.XPATH, "/html/body/center/div/div[4]/form/table/tbody/tr[3]/td[2]/input").click() time.sleep(2) # 進入成績查詢 driver.find_element(By.XPATH, "/html/body/table[2]/tbody/tr/td[1]/div[11]/a").click() time.sleep(3) driver.minimize_window() # 取得當學期成績 all_table_list = pd.read_html(driver.page_source) table = all_table_list[4] table.head() ``` ### 計算GPA:資料處理 - 使用==pandas==函式庫 ```python=3 import pandas as pd ``` - calculate: - 第三步驟為計算GPA與取得「成績未到」及「未通過」的課程名稱 - 已停修的課程不列入「成績未到」或「未通過」 - 使用dictionary取得等第對應分數 - 浮點數精確度處理:每次計算分數加權後都進行一次四捨五入至小數點後一位 ```python=31 # 計算GPA score = {'A+': 4.3, 'A': 4.0, 'A-': 3.7, 'B+': 3.3, 'B': 3.0, 'B-': 2.7, 'C+': 2.3, 'C': 2.0, 'C-': 1.7, 'Pass': 0} no_score = [] # 成績未到 fail = [] # 未通過 tot_score = 0 tot_credit = 0 # 成績處理 for row in table.index: grade = table.at[row, "成績"] if (pd.isnull(grade)): if (table.at[row, "備註"] == "停修"): continue no_score.append(table.at[row, "課程名稱"]) elif (grade == "Fail"): fail.append(table.at[row, "課程名稱"]) else: tot_score += score[grade] * table.at[row, "學分"] tot_score = round(tot_score, 1) tot_credit += table.at[row, "學分"] ``` - calculate: - 第四步驟為結果輸出 - 使用fstring將計算結果串接為一字串 - 將結果呈現於計算結果頁面的多行文字框 - 將分頁切換至計算結果頁面 ```python=+ # 輸出處理 if len(no_score) == 0: no_score = ["無"] if len(fail) == 0: fail = ["無"] result = f"本學期目前的GPA: {round((tot_score / tot_credit), 2)}\n尚未公布成績的課程: {'、'.join(no_score)}\n未通過的課程: {'、'.join(fail)}" result_widget = tk.Text(ShowGpa_window, font=("Ubuntu", 28)) result_widget.insert(tk.END, result) result_widget.pack() switchFrame(LogIn_window, ShowGpa_window) ``` ## 三、結果呈現 ### 執行程式後會跳出視窗:首頁 ![](https://i.imgur.com/cQysUCJ.jpg) ### 在文字框內輸入使用者帳號及密碼 ![](https://i.imgur.com/9rFukgD.jpg) ### 自動開啟Chrome瀏覽器、放大視窗並前往NTU e-portfolio ![](https://i.imgur.com/gqv1QAZ.jpg) ### 自動點選「登入ePo」並登入 ![](https://i.imgur.com/aGQkDZP.jpg) ### 自動點選「成績查詢」 ![](https://i.imgur.com/mF9omrv.jpg) ### 爬取當學期成績資料後,將視窗最小化 ![](https://i.imgur.com/9EZLibH.png) ### 視窗切換至計算結果頁面 ![](https://i.imgur.com/2pMwIgo.jpg) ## 四、優化方向 * **可改為網頁版,讓沒有安裝Python的同學們也能方便使用** * **可擴充功能,例如:爬取歷年成績,並搭配==matplotlib==繪製成折線圖** ```python= import matplotlib.pyplot as plt plt.axes().set_facecolor("white") plt.xlabel("Semester", color = 'green') plt.ylabel("GPA", color = 'green') plt.ylim(2, 4.3) plt.plot(semester, grades, color = 'green', marker = 'o') ```           ![](https://i.imgur.com/vEmsNJv.png "歷年成績折線圖")