feat(grpc): add support for fetching grades and exams
All checks were successful
Build / build (push) Successful in 2m14s
All checks were successful
Build / build (push) Successful in 2m14s
Implement new gRPC task handlers for retrieving student grades and exam schedules. This includes adding new parser logic for processing score and exam data, and updating the client capabilities to advertise support for these new task types. - Add `getGrades` and `getExams` handlers in `grpc/handler.go` - Add `parser/encoding.go`, `parser/exam.go`, and `parser/score.go` for data parsing - Update `grpc/client.go` to include `TASK_TYPE_GET_GRADES` and `TASK_TYPE_GET_EXAMS` in capabilities
This commit is contained in:
@@ -78,6 +78,8 @@ func (c *Client) register() error {
|
|||||||
Version: c.version,
|
Version: c.version,
|
||||||
Capabilities: map[string]string{
|
Capabilities: map[string]string{
|
||||||
"TASK_TYPE_GET_SCHEDULE": "true",
|
"TASK_TYPE_GET_SCHEDULE": "true",
|
||||||
|
"TASK_TYPE_GET_GRADES": "true",
|
||||||
|
"TASK_TYPE_GET_EXAMS": "true",
|
||||||
"TASK_TYPE_LOGIN": "true",
|
"TASK_TYPE_LOGIN": "true",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
142
grpc/handler.go
142
grpc/handler.go
@@ -8,6 +8,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -52,6 +54,10 @@ func (h *TaskHandler) Execute(task *pb.Task) *pb.TaskResult {
|
|||||||
switch task.Type {
|
switch task.Type {
|
||||||
case pb.TaskType_TASK_TYPE_GET_SCHEDULE:
|
case pb.TaskType_TASK_TYPE_GET_SCHEDULE:
|
||||||
data, err = h.getSchedule(context.Background(), task.Payload)
|
data, err = h.getSchedule(context.Background(), task.Payload)
|
||||||
|
case pb.TaskType_TASK_TYPE_GET_GRADES:
|
||||||
|
data, err = h.getGrades(context.Background(), task.Payload)
|
||||||
|
case pb.TaskType_TASK_TYPE_GET_EXAMS:
|
||||||
|
data, err = h.getExams(context.Background(), task.Payload)
|
||||||
case pb.TaskType_TASK_TYPE_LOGIN:
|
case pb.TaskType_TASK_TYPE_LOGIN:
|
||||||
data, err = h.login(context.Background(), task.Payload)
|
data, err = h.login(context.Background(), task.Payload)
|
||||||
default:
|
default:
|
||||||
@@ -220,6 +226,86 @@ func executeLoginAndFetchSchedule(ctx context.Context, httpClient *http.Client,
|
|||||||
return parser.FetchScheduleWithClient(httpClient, semester)
|
return parser.FetchScheduleWithClient(httpClient, semester)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *TaskHandler) getGrades(ctx context.Context, payload []byte) ([]byte, error) {
|
||||||
|
var req pb.GetGradesPayload
|
||||||
|
if err := json.Unmarshal(payload, &req); err != nil {
|
||||||
|
return nil, apperr.New("parse_payload", err, "解析任务载荷失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
log := slog.With("username", req.Username, "semester", req.Semester)
|
||||||
|
log.Info("获取成绩任务")
|
||||||
|
|
||||||
|
httpClient, err := createHTTPClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := config.Get()
|
||||||
|
setTWFIDCookie(httpClient, cfg.BaseURL, "")
|
||||||
|
|
||||||
|
if err := executeLogin(ctx, httpClient, req.Username, req.Password); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, grades, err := parser.FetchGPA(httpClient)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("获取GPA页面失败,尝试成绩页面", "error", err)
|
||||||
|
grades, err = parser.FetchGrades(httpClient, parser.ScoreTypeFinal)
|
||||||
|
if err != nil {
|
||||||
|
return nil, apperr.New("fetch_grades", err, "获取成绩失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pbGrades := convertGrades(grades)
|
||||||
|
|
||||||
|
result := &pb.GradesResultData{
|
||||||
|
StudentId: req.Username,
|
||||||
|
Semester: req.Semester,
|
||||||
|
Grades: pbGrades,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("成绩获取成功", "grade_count", len(pbGrades))
|
||||||
|
return json.Marshal(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *TaskHandler) getExams(ctx context.Context, payload []byte) ([]byte, error) {
|
||||||
|
var req pb.GetExamsPayload
|
||||||
|
if err := json.Unmarshal(payload, &req); err != nil {
|
||||||
|
return nil, apperr.New("parse_payload", err, "解析任务载荷失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
log := slog.With("username", req.Username, "semester", req.Semester)
|
||||||
|
log.Info("获取考试安排任务")
|
||||||
|
|
||||||
|
httpClient, err := createHTTPClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := config.Get()
|
||||||
|
setTWFIDCookie(httpClient, cfg.BaseURL, "")
|
||||||
|
|
||||||
|
if err := executeLogin(ctx, httpClient, req.Username, req.Password); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
exams, err := parser.FetchAllExams(httpClient)
|
||||||
|
if err != nil {
|
||||||
|
return nil, apperr.New("fetch_exams", err, "获取考试安排失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
pbExams := convertExams(exams)
|
||||||
|
|
||||||
|
result := &pb.ExamsResultData{
|
||||||
|
StudentId: req.Username,
|
||||||
|
Semester: req.Semester,
|
||||||
|
Exams: pbExams,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("考试安排获取成功", "exam_count", len(pbExams))
|
||||||
|
return json.Marshal(result)
|
||||||
|
}
|
||||||
|
|
||||||
func convertCourses(courses []models.Course) []*pb.Course {
|
func convertCourses(courses []models.Course) []*pb.Course {
|
||||||
result := make([]*pb.Course, len(courses))
|
result := make([]*pb.Course, len(courses))
|
||||||
for i, c := range courses {
|
for i, c := range courses {
|
||||||
@@ -248,3 +334,59 @@ func convertToInt32(arr []int) []int32 {
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convertGrades(grades []parser.GradeEntry) []*pb.Grade {
|
||||||
|
result := make([]*pb.Grade, 0, len(grades))
|
||||||
|
for _, g := range grades {
|
||||||
|
var credit float32
|
||||||
|
if v, err := parseFloat(g.Credit); err == nil {
|
||||||
|
credit = v
|
||||||
|
}
|
||||||
|
var gradePoint float32
|
||||||
|
if v, err := parseFloat(g.GradePoint); err == nil {
|
||||||
|
gradePoint = v
|
||||||
|
}
|
||||||
|
result = append(result, &pb.Grade{
|
||||||
|
CourseCode: g.Code,
|
||||||
|
CourseName: g.Name,
|
||||||
|
Credit: credit,
|
||||||
|
Grade: g.Score,
|
||||||
|
GradePoint: gradePoint,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertExams(exams []parser.ExamEntry) []*pb.Exam {
|
||||||
|
result := make([]*pb.Exam, 0, len(exams))
|
||||||
|
for _, e := range exams {
|
||||||
|
examTypeStr := "期末"
|
||||||
|
switch e.Type {
|
||||||
|
case parser.ExamTypeMidterm:
|
||||||
|
examTypeStr = "期中"
|
||||||
|
case parser.ExamTypeMakeup:
|
||||||
|
examTypeStr = "补考"
|
||||||
|
}
|
||||||
|
result = append(result, &pb.Exam{
|
||||||
|
CourseName: e.Course,
|
||||||
|
ExamType: examTypeStr,
|
||||||
|
Date: e.Date,
|
||||||
|
Time: e.Time,
|
||||||
|
Room: e.Week,
|
||||||
|
Seat: e.Weekday,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFloat(s string) (float32, error) {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" || s == "-" {
|
||||||
|
return 0, fmt.Errorf("empty")
|
||||||
|
}
|
||||||
|
v, err := strconv.ParseFloat(s, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return float32(v), nil
|
||||||
|
}
|
||||||
|
|||||||
74
parser/encoding.go
Normal file
74
parser/encoding.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/text/encoding/simplifiedchinese"
|
||||||
|
"golang.org/x/text/transform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func decodeResponseBody(resp *http.Response) (string, error) {
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return decodeBody(body, resp.Header.Get("Content-Type")), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeBody(body []byte, contentType string) string {
|
||||||
|
lowerCT := strings.ToLower(contentType)
|
||||||
|
if strings.Contains(lowerCT, "charset=gb") || strings.Contains(lowerCT, "charset=gb2312") || strings.Contains(lowerCT, "charset=gbk") {
|
||||||
|
decoded, err := decodeGBK(body)
|
||||||
|
if err == nil {
|
||||||
|
return decoded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isValidUTF8(body) {
|
||||||
|
return string(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded, err := decodeGBK(body)
|
||||||
|
if err == nil {
|
||||||
|
return decoded
|
||||||
|
}
|
||||||
|
return string(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeGBK(data []byte) (string, error) {
|
||||||
|
reader := transform.NewReader(bytes.NewReader(data), simplifiedchinese.GBK.NewDecoder())
|
||||||
|
decoded, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(decoded), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidUTF8(data []byte) bool {
|
||||||
|
for i := 0; i < len(data); i++ {
|
||||||
|
if data[i]&0x80 != 0 {
|
||||||
|
if data[i]&0xE0 == 0xC0 {
|
||||||
|
if i+1 >= len(data) || data[i+1]&0xC0 != 0x80 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
} else if data[i]&0xF0 == 0xE0 {
|
||||||
|
if i+2 >= len(data) || data[i+1]&0xC0 != 0x80 || data[i+2]&0xC0 != 0x80 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
i += 2
|
||||||
|
} else if data[i]&0xF8 == 0xF0 {
|
||||||
|
if i+3 >= len(data) || data[i+1]&0xC0 != 0x80 || data[i+2]&0xC0 != 0x80 || data[i+3]&0xC0 != 0x80 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
i += 3
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
128
parser/exam.go
Normal file
128
parser/exam.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"schedule_converter/client"
|
||||||
|
"schedule_converter/config"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExamType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ExamTypeFinal ExamType = "final"
|
||||||
|
ExamTypeMidterm ExamType = "midterm"
|
||||||
|
ExamTypeMakeup ExamType = "makeup"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExamEntry struct {
|
||||||
|
Course string
|
||||||
|
Date string
|
||||||
|
Time string
|
||||||
|
Week string
|
||||||
|
Weekday string
|
||||||
|
Type ExamType
|
||||||
|
}
|
||||||
|
|
||||||
|
func examTypeToCode(t ExamType) string {
|
||||||
|
switch t {
|
||||||
|
case ExamTypeMidterm:
|
||||||
|
return "02"
|
||||||
|
case ExamTypeMakeup:
|
||||||
|
return "03"
|
||||||
|
default:
|
||||||
|
return "01"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchExams(httpClient *http.Client, examType ExamType) ([]ExamEntry, error) {
|
||||||
|
examURL := config.BaseURL + "/kscx/queryKsForXs"
|
||||||
|
fmt.Printf("[考试] 正在获取考试时间数据 (%s)...\n", examType)
|
||||||
|
|
||||||
|
data := fmt.Sprintf("xnxq=&kssjd=%s", examTypeToCode(examType))
|
||||||
|
req, err := http.NewRequest("POST", examURL, strings.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header = client.GetHeadersWithContentType("application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
html, err := decodeResponseBody(resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
exams, err := ParseExamTimeHTML(html, examType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("[考试] 共获取 %d 条考试记录 (%s)\n", len(exams), examType)
|
||||||
|
return exams, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchAllExams(httpClient *http.Client) ([]ExamEntry, error) {
|
||||||
|
var allExams []ExamEntry
|
||||||
|
for _, et := range []ExamType{ExamTypeFinal, ExamTypeMidterm, ExamTypeMakeup} {
|
||||||
|
exams, err := FetchExams(httpClient, et)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("[考试] 获取%s考试失败: %v\n", et, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
allExams = append(allExams, exams...)
|
||||||
|
}
|
||||||
|
return allExams, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseExamTimeHTML(html string, examType ExamType) ([]ExamEntry, error) {
|
||||||
|
doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var exams []ExamEntry
|
||||||
|
|
||||||
|
doc.Find("table.bot_line").Each(func(_ int, table *goquery.Selection) {
|
||||||
|
rows := table.Find("tr")
|
||||||
|
if rows.Length() < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.Each(func(idx int, row *goquery.Selection) {
|
||||||
|
if idx == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cells := row.Find("td")
|
||||||
|
if cells.Length() < 6 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
texts := getCellTexts(cells)
|
||||||
|
|
||||||
|
exam := ExamEntry{
|
||||||
|
Course: safeGet(texts, 1),
|
||||||
|
Date: safeGet(texts, 2),
|
||||||
|
Time: safeGet(texts, 3),
|
||||||
|
Week: safeGet(texts, 4),
|
||||||
|
Weekday: safeGet(texts, 5),
|
||||||
|
Type: examType,
|
||||||
|
}
|
||||||
|
|
||||||
|
if exam.Course != "" && len(exam.Course) >= 2 {
|
||||||
|
exams = append(exams, exam)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return exams, nil
|
||||||
|
}
|
||||||
257
parser/score.go
Normal file
257
parser/score.go
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"schedule_converter/client"
|
||||||
|
"schedule_converter/config"
|
||||||
|
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ScoreType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ScoreTypeFinal ScoreType = "final"
|
||||||
|
ScoreTypeMidterm ScoreType = "midterm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GradeEntry struct {
|
||||||
|
Term string
|
||||||
|
Code string
|
||||||
|
Name string
|
||||||
|
Nature string
|
||||||
|
Category string
|
||||||
|
Credit string
|
||||||
|
Score string
|
||||||
|
GradePoint string
|
||||||
|
IsExam string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GPASummary struct {
|
||||||
|
AverageGPA string
|
||||||
|
Rank string
|
||||||
|
ExamTotalGPA string
|
||||||
|
ExamTotalCredits string
|
||||||
|
FailCredits string
|
||||||
|
Semesters string
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchGrades(httpClient *http.Client,ExamType ScoreType) ([]GradeEntry, error) {
|
||||||
|
var gradeURL string
|
||||||
|
switch ExamType {
|
||||||
|
case ScoreTypeMidterm:
|
||||||
|
gradeURL = config.BaseURL + "/cjcx/queryQzcj"
|
||||||
|
default:
|
||||||
|
gradeURL = config.BaseURL + "/cjcx/queryQmcj"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("[成绩] 正在获取成绩数据 (%s)...\n", ExamType)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", gradeURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header = client.GetHeaders()
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
html, err := decodeResponseBody(resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
grades, err := ParseGradesHTML(html)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("[成绩] 共获取 %d 条成绩记录\n", len(grades))
|
||||||
|
return grades, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchGPA(httpClient *http.Client) (*GPASummary, []GradeEntry, error) {
|
||||||
|
gpaURL := config.BaseURL + "/xfj/queryListXfj"
|
||||||
|
fmt.Println("[学分绩] 正在获取学分绩数据...")
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", gpaURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
req.Header = client.GetHeaders()
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
html, err := decodeResponseBody(resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
summary, grades, err := ParseGPAHTML(html)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("[学分绩] 共获取 %d 门课程明细\n", len(grades))
|
||||||
|
return summary, grades, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseGradesHTML(html string) ([]GradeEntry, error) {
|
||||||
|
doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var grades []GradeEntry
|
||||||
|
|
||||||
|
doc.Find("table.bot_line").Each(func(_ int, table *goquery.Selection) {
|
||||||
|
rows := table.Find("tr")
|
||||||
|
if rows.Length() < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.Each(func(idx int, row *goquery.Selection) {
|
||||||
|
if idx == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cells := row.Find("td")
|
||||||
|
if cells.Length() < 8 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
texts := getCellTexts(cells)
|
||||||
|
if len(texts) < 9 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isMidterm := len(texts) == 10
|
||||||
|
|
||||||
|
entry := GradeEntry{
|
||||||
|
Term: safeGet(texts, 1),
|
||||||
|
Code: safeGet(texts, 3),
|
||||||
|
Name: safeGet(texts, 4),
|
||||||
|
Credit: safeGet(texts, 7),
|
||||||
|
}
|
||||||
|
|
||||||
|
if isMidterm {
|
||||||
|
entry.Score = safeGet(texts, 8)
|
||||||
|
if len(texts) > 9 {
|
||||||
|
entry.GradePoint = safeGet(texts, 9)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if len(texts) >= 13 {
|
||||||
|
entry.Nature = safeGet(texts, 3)
|
||||||
|
entry.Category = safeGet(texts, 4)
|
||||||
|
entry.Score = safeGet(texts, 12)
|
||||||
|
entry.GradePoint = safeGet(texts, 9)
|
||||||
|
entry.IsExam = safeGet(texts, 6)
|
||||||
|
} else if len(texts) >= 10 {
|
||||||
|
entry.Score = safeGet(texts, 8)
|
||||||
|
entry.GradePoint = safeGet(texts, 9)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.Name != "" && len(entry.Name) >= 2 {
|
||||||
|
if entry.Score == "" {
|
||||||
|
entry.Score = "-"
|
||||||
|
}
|
||||||
|
grades = append(grades, entry)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return grades, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseGPAHTML(html string) (*GPASummary, []GradeEntry, error) {
|
||||||
|
doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := &GPASummary{}
|
||||||
|
|
||||||
|
doc.Find("span").Each(func(_ int, span *goquery.Selection) {
|
||||||
|
id, _ := span.Attr("id")
|
||||||
|
text := strings.TrimSpace(span.Text())
|
||||||
|
switch id {
|
||||||
|
case "pjxfj":
|
||||||
|
summary.AverageGPA = text
|
||||||
|
case "zrs":
|
||||||
|
summary.Rank = text
|
||||||
|
case "kskxfj":
|
||||||
|
summary.ExamTotalGPA = text
|
||||||
|
case "kskxfs":
|
||||||
|
summary.ExamTotalCredits = text
|
||||||
|
case "kckxfj":
|
||||||
|
summary.FailCredits = text
|
||||||
|
case "xqs":
|
||||||
|
summary.Semesters = text
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
var grades []GradeEntry
|
||||||
|
|
||||||
|
doc.Find("table.bot_line").Each(func(_ int, table *goquery.Selection) {
|
||||||
|
rows := table.Find("tr")
|
||||||
|
if rows.Length() < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.Each(func(idx int, row *goquery.Selection) {
|
||||||
|
if idx == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cells := row.Find("td")
|
||||||
|
if cells.Length() < 10 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
texts := getCellTexts(cells)
|
||||||
|
|
||||||
|
entry := GradeEntry{
|
||||||
|
Term: safeGet(texts, 0),
|
||||||
|
Code: safeGet(texts, 1),
|
||||||
|
Name: safeGet(texts, 2),
|
||||||
|
Nature: safeGet(texts, 3),
|
||||||
|
Category: safeGet(texts, 4),
|
||||||
|
IsExam: safeGet(texts, 6),
|
||||||
|
Credit: safeGet(texts, 7),
|
||||||
|
Score: safeGet(texts, 8),
|
||||||
|
GradePoint: safeGet(texts, 9),
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.Name != "" && len(entry.Name) >= 2 {
|
||||||
|
grades = append(grades, entry)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return summary, grades, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCellTexts(cells *goquery.Selection) []string {
|
||||||
|
var texts []string
|
||||||
|
cells.Each(func(_ int, cell *goquery.Selection) {
|
||||||
|
text := strings.TrimSpace(cell.Text())
|
||||||
|
texts = append(texts, text)
|
||||||
|
})
|
||||||
|
return texts
|
||||||
|
}
|
||||||
|
|
||||||
|
func safeGet(arr []string, idx int) string {
|
||||||
|
if idx < len(arr) {
|
||||||
|
return arr[idx]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user