package parser import ( "fmt" "regexp" "strings" "github.com/PuerkitoBio/goquery" ) // CETScore 四六级成绩条目。 type CETScore struct { Level string // 等级:英语四级/英语六级 ExamTime string // 考试时间:如 2025年06月 TicketNum string // 准考证号 Score string // 成绩 } // CETStudentInfo 四六级查询中的学生信息。 type CETStudentInfo struct { StudentID string // 学号 Name string // 姓名 Gender string // 性别 IDCard string // 身份证号 Duration string // 学制 Grade string // 年级 College string // 学院 Major string // 专业 } // CETResult 四六级查询结果。 type CETResult struct { StudentInfo CETStudentInfo Scores []CETScore } // cetScoreRegex 匹配四六级成绩行。 // 示例:英语六级 时间 2025年06月 准考证号 372220251213105 成绩 445 var cetScoreRegex = regexp.MustCompile(`(英语[四六]级)\s+时间\s*([0-9]{4}年[0-9]{1,2}月)\s+准考证号\s*([0-9A-Za-z]+)\s+成绩\s*([0-9]+|缺考|合格|不合格|优秀|良好|通过|未通过)`) // FetchCETHTML 解析四六级成绩 HTML 页面。 func ParseCETHTML(html string) (*CETResult, error) { doc, err := goquery.NewDocumentFromReader(strings.NewReader(html)) if err != nil { return nil, err } result := &CETResult{} table := findCETTable(doc) if table.Length() == 0 { return nil, fmt.Errorf("未找到四六级成绩表格") } parseCETStudentInfo(table, &result.StudentInfo) result.Scores = parseCETScores(table) if len(result.Scores) == 0 { result.Scores = parseCETScoresFromText(doc.Find(".jdcx_area").Text()) } if len(result.Scores) == 0 { result.Scores = parseCETScoresFromText(doc.Find(".Contentbox").Text()) } if len(result.Scores) == 0 { result.Scores = parseCETScoresFromText(doc.Text()) } return result, nil } func findCETTable(doc *goquery.Document) *goquery.Selection { selectors := []string{ "table.addlist", ".jdcx_area table", "form#ff table", "table:contains('已有成绩')", } for _, selector := range selectors { table := doc.Find(selector).First() if table.Length() > 0 { return table } } return doc.Find("__not_found__") } func parseCETStudentInfo(table *goquery.Selection, info *CETStudentInfo) { table.Find("tr").Each(func(_ int, row *goquery.Selection) { var label string row.Find("th, td").Each(func(_ int, cell *goquery.Selection) { text := normalizeCETText(cell.Text()) if text == "" { return } if cell.Is("th") { label = normalizeCETLabel(text) return } assignCETStudentField(info, label, text) label = "" }) }) } func assignCETStudentField(info *CETStudentInfo, label, value string) { switch label { case "学号": info.StudentID = value case "姓名": info.Name = value case "性别": info.Gender = value case "身份证号": info.IDCard = value case "学制": info.Duration = value case "年级": info.Grade = value case "学院": info.College = value case "专业": info.Major = value } } func parseCETScores(table *goquery.Selection) []CETScore { var scores []CETScore table.Find("tr").Each(func(_ int, row *goquery.Selection) { rowText := normalizeCETText(row.Text()) if !strings.Contains(rowText, "英语四级") && !strings.Contains(rowText, "英语六级") { return } scores = append(scores, parseCETScoresFromText(rowText)...) }) return deduplicateCETScores(scores) } func parseCETScoresFromText(text string) []CETScore { text = normalizeCETText(text) matches := cetScoreRegex.FindAllStringSubmatch(text, -1) scores := make([]CETScore, 0, len(matches)) for _, match := range matches { if len(match) < 5 { continue } scores = append(scores, CETScore{ Level: strings.TrimSpace(match[1]), ExamTime: strings.TrimSpace(match[2]), TicketNum: strings.TrimSpace(match[3]), Score: strings.TrimSpace(match[4]), }) } return deduplicateCETScores(scores) } func deduplicateCETScores(scores []CETScore) []CETScore { if len(scores) <= 1 { return scores } seen := make(map[string]struct{}, len(scores)) result := make([]CETScore, 0, len(scores)) for _, score := range scores { key := strings.Join([]string{score.Level, score.ExamTime, score.TicketNum, score.Score}, "|") if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} result = append(result, score) } return result } func normalizeCETLabel(label string) string { label = normalizeCETText(label) label = strings.ReplaceAll(label, " ", "") label = strings.ReplaceAll(label, ":", "") label = strings.ReplaceAll(label, ":", "") return label } func normalizeCETText(text string) string { text = strings.ReplaceAll(text, "\u00a0", " ") return strings.Join(strings.Fields(strings.TrimSpace(text)), " ") }