Files

204 lines
5.1 KiB
Go
Raw Permalink Normal View History

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 ""
}