Files
schedule_converter/parser/score.go
WuYuuuub 4024f9eae0
Some checks failed
Build / build (push) Failing after 2m13s
删除了ai识别验证码部分,新增个人成绩补考标识和cet成绩查询
2026-06-17 14:48:27 +08:00

204 lines
5.1 KiB
Go
Raw Permalink 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 (
"strings"
"schedule_converter/models"
"github.com/PuerkitoBio/goquery"
)
// ParseGradesHTML 解析成绩页面 HTML返回成绩条目列表。
func ParseGradesHTML(html string) ([]models.GradeEntry, error) {
doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
if err != nil {
return nil, err
}
var grades []models.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 := parseGradeRowTexts(texts, detectRetake(row))
if isMidterm && entry.Score == "" {
entry.Score = safeGet(texts, 8)
if len(texts) > 9 {
entry.GradePoint = safeGet(texts, 9)
}
}
if entry.Name != "" && len(entry.Name) >= 2 {
if entry.Score == "" {
entry.Score = "-"
}
grades = append(grades, entry)
}
})
})
return grades, nil
}
// ParseGPAHTML 解析学分绩页面 HTML返回 GPA 概要与成绩条目列表。
func ParseGPAHTML(html string) (*models.GPASummary, []models.GradeEntry, error) {
doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
if err != nil {
return nil, nil, err
}
summary := &models.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 []models.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 := parseGradeRowTexts(texts, detectRetake(row))
if entry.Name != "" && len(entry.Name) >= 2 {
grades = append(grades, entry)
}
})
})
return summary, grades, nil
}
func parseGradeRowTexts(texts []string, isRetake bool) models.GradeEntry {
entry := models.GradeEntry{IsRetake: isRetake}
// 详细成绩/GPA 页面格式:
// 序号、学期、学院、课程代码、课程名称、课程性质、课程类别、学分、是否考试课、是否学位课、成绩标记、成绩、最终成绩、绩点1、绩点、录入时间、备注。
if len(texts) >= 15 && looksLikeDetailedGradeRow(texts) {
entry.Term = safeGet(texts, 1)
entry.Code = safeGet(texts, 3)
entry.Name = safeGet(texts, 4)
entry.Nature = safeGet(texts, 5)
entry.Category = safeGet(texts, 6)
entry.Credit = safeGet(texts, 7)
entry.IsExam = safeGet(texts, 8)
entry.Score = firstNonEmpty(safeGet(texts, 12), safeGet(texts, 11))
entry.GradePoint = firstNonEmpty(safeGet(texts, 14), safeGet(texts, 13))
return entry
}
// 常规成绩页面格式:学年、学期、课程代码、课程名称、课程性质、课程类别、学分、成绩、绩点、...
if len(texts) >= 10 {
entry.Term = safeGet(texts, 1)
entry.Code = safeGet(texts, 2)
entry.Name = safeGet(texts, 3)
entry.Nature = safeGet(texts, 4)
entry.Category = safeGet(texts, 5)
entry.Credit = safeGet(texts, 6)
entry.Score = safeGet(texts, 7)
entry.GradePoint = safeGet(texts, 8)
return entry
}
entry.Term = safeGet(texts, 1)
entry.Code = safeGet(texts, 3)
entry.Name = safeGet(texts, 4)
entry.Credit = safeGet(texts, 7)
entry.Score = safeGet(texts, 8)
if len(texts) > 9 {
entry.GradePoint = safeGet(texts, 9)
}
return entry
}
func looksLikeDetailedGradeRow(texts []string) bool {
return safeGet(texts, 3) != "" && safeGet(texts, 4) != "" && safeGet(texts, 7) != "" && (safeGet(texts, 8) == "是" || safeGet(texts, 8) == "否")
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return value
}
}
return ""
}
// detectRetake 检测成绩行是否包含补考标记。
// 教务系统在成绩单元格内以 <font color="#0066ff">补考</font> 标记补考,
// 也会在整行文本中出现"补考"字样。
func detectRetake(row *goquery.Selection) bool {
// 方式1查找包含"补考"文字的 <font> 标签
if row.Find("font:contains('补考')").Length() > 0 {
return true
}
// 方式2整行纯文本包含"补考"
rowText := strings.TrimSpace(row.Text())
return strings.Contains(rowText, "补考")
}
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 ""
}