# 隨時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)
```
## 三、結果呈現
### 執行程式後會跳出視窗:首頁

### 在文字框內輸入使用者帳號及密碼

### 自動開啟Chrome瀏覽器、放大視窗並前往NTU e-portfolio

### 自動點選「登入ePo」並登入

### 自動點選「成績查詢」

### 爬取當學期成績資料後,將視窗最小化

### 視窗切換至計算結果頁面

## 四、優化方向
* **可改為網頁版,讓沒有安裝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')
```
          