cet成绩查询
All checks were successful
Build / build (push) Successful in 9m51s

This commit is contained in:
WuYuuuub
2026-06-17 22:41:52 +08:00
parent 8915fca841
commit 04ad865bf2
2 changed files with 250 additions and 0 deletions

184
parser/cet.go Normal file
View File

@@ -0,0 +1,184 @@
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)), " ")
}

66
service/cet.go Normal file
View File

@@ -0,0 +1,66 @@
package service
import (
"context"
"log/slog"
"schedule_converter/client"
"schedule_converter/parser"
apperr "schedule_converter/pkg/errors"
pb "schedule_converter/proto/runner"
)
const cetQueryPath = "/sljcjcx/sljcjcxInit"
// GetCET 获取四六级成绩:登录 → 请求四六级查询结果页 → 解析 → 转换为 pb 结果。
func (s *Service) GetCET(ctx context.Context, req *pb.GetCetPayload) (*pb.CetResultData, error) {
log := slog.With("username", req.Username)
httpClient, err := s.newAuthenticatedClient(ctx, req.Username, req.Password, "")
if err != nil {
return nil, err
}
html, err := client.FetchHTML(ctx, httpClient, "GET", cetQueryPath, nil, "", "")
if err != nil {
return nil, apperr.New("fetch_cet", err, "获取四六级成绩页面失败")
}
cetResult, err := parser.ParseCETHTML(html)
if err != nil {
return nil, apperr.New("parse_cet", err, "解析四六级成绩失败")
}
log.Info("四六级成绩获取成功", "score_count", len(cetResult.Scores))
return &pb.CetResultData{
StudentInfo: convertCETStudentInfo(cetResult.StudentInfo),
Scores: convertCETScores(cetResult.Scores),
}, nil
}
func convertCETStudentInfo(info parser.CETStudentInfo) *pb.CetStudentInfo {
return &pb.CetStudentInfo{
StudentId: info.StudentID,
Name: info.Name,
Gender: info.Gender,
IdCard: info.IDCard,
Duration: info.Duration,
Grade: info.Grade,
College: info.College,
Major: info.Major,
}
}
func convertCETScores(scores []parser.CETScore) []*pb.CetScore {
result := make([]*pb.CetScore, 0, len(scores))
for _, score := range scores {
result = append(result, &pb.CetScore{
Level: score.Level,
ExamTime: score.ExamTime,
TicketNum: score.TicketNum,
Score: score.Score,
})
}
return result
}