Files
schedule_converter/parser/score.go

297 lines
6.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package parser
import (
"fmt"
"log/slog"
"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
IsRetake bool
}
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.Get().BaseURL + "/cjcx/queryQzcj"
default:
gradeURL = config.Get().BaseURL + "/cjcx/queryQmcj"
}
log := slog.With("exam_type", string(examType))
log.Info("获取成绩数据")
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()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("获取成绩失败, 状态码: %d", resp.StatusCode)
}
html, err := decodeResponseBody(resp)
if err != nil {
return nil, err
}
grades, err := parseGradesHTML(html)
if err != nil {
return nil, err
}
log.Info("成绩获取完成", "grade_count", len(grades))
return grades, nil
}
func FetchGPA(httpClient *http.Client) (*GPASummary, []GradeEntry, error) {
gpaURL := config.Get().BaseURL + "/xfj/queryListXfj"
slog.Info("获取学分绩数据")
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()
if resp.StatusCode != http.StatusOK {
return nil, nil, fmt.Errorf("获取学分绩失败, 状态码: %d", resp.StatusCode)
}
html, err := decodeResponseBody(resp)
if err != nil {
return nil, nil, err
}
summary, grades, err := parseGPAHTML(html)
if err != nil {
return nil, nil, err
}
slog.Info("学分绩获取完成", "grade_count", 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, isRetake := getCellTextsAndRetake(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),
IsRetake: isRetake,
}
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, isRetake := getCellTextsAndRetake(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),
IsRetake: isRetake,
}
if entry.Name != "" && len(entry.Name) >= 2 {
grades = append(grades, entry)
}
})
})
return summary, grades, nil
}
// getCellTextsAndRetake 解析单元格文本,同时检测是否包含补考标记
// 补考标记在HTML中表现为 <font color="#0066ff">补考</font>
// 解析后该单元格文本为"补考"需要从texts中移除并记录isRetake标志
func getCellTextsAndRetake(cells *goquery.Selection) ([]string, bool) {
var texts []string
isRetake := false
retakeIndex := -1
cells.Each(func(i int, cell *goquery.Selection) {
text := strings.TrimSpace(cell.Text())
// 检测补考标记:<font color="#0066ff">补考</font>
if text == "补考" {
isRetake = true
retakeIndex = i
}
texts = append(texts, text)
})
// 移除"补考"这一列,使后续列索引与正常行一致
if isRetake && retakeIndex >= 0 {
texts = append(texts[:retakeIndex], texts[retakeIndex+1:]...)
}
return texts, isRetake
}
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 ""
}