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 检测成绩行是否包含补考标记。 // 教务系统在成绩单元格内以 补考 标记补考, // 也会在整行文本中出现"补考"字样。 func detectRetake(row *goquery.Selection) bool { // 方式1:查找包含"补考"文字的 标签 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 "" }